defmodule GenAI.Tool.Function do @vsn 1.0 defstruct [ name: nil, description: nil, parameters: %{}, vsn: @vsn ] def from_json(json_string) when is_bitstring(json_string) do with {:ok, json} <- Jason.decode(json_string) do do_from_json(json) end end def from_yaml(yaml_string) when is_bitstring(yaml_string) do with {:ok, json} <- YamlElixir.read_from_string(yaml_string) do do_from_json(json) end end defp do_from_json(%{"type" => "function", "function" => json}) do do_from_json(json) end defp do_from_json(%{"name" => _} = json) do # parameters is json a standard object json schema entry so use it to build a map of parameters one implemented. parameters = with {:ok, x} <- json["parameters"] && GenAI.Tool.Schema.Type.from_json(json["parameters"]) do x end {:ok, %GenAI.Tool.Function{ name: json["name"], description: json["description"], parameters: parameters } } end end defmodule GenAI.Tool.Schema.Type do def from_json(json) do cond do GenAI.Tool.Schema.Object.is_type(json) -> GenAI.Tool.Schema.Object.from_json(json) GenAI.Tool.Schema.Enum.is_type(json) -> GenAI.Tool.Schema.Enum.from_json(json) GenAI.Tool.Schema.String.is_type(json) -> GenAI.Tool.Schema.String.from_json(json) GenAI.Tool.Schema.Number.is_type(json) -> GenAI.Tool.Schema.Number.from_json(json) GenAI.Tool.Schema.Integer.is_type(json) -> GenAI.Tool.Schema.Integer.from_json(json) GenAI.Tool.Schema.Null.is_type(json) -> GenAI.Tool.Schema.Null.from_json(json) GenAI.Tool.Schema.Bool.is_type(json) -> GenAI.Tool.Schema.Bool.from_json(json) :else -> {:error, :pending} end end end defmodule GenAI.Tool.Schema.TypeBehaviour do @callback is_type(map()) :: boolean @callback from_json(map()) :: {:ok, term} | {:error, term} end defmodule GenAI.Tool.Schema.Bool do @moduledoc """ Represents a schema for boolean types, converting JSON schema attributes to Elixir struct fields. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ description: nil ] @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "boolean"}), do: true def is_type(_), do: false @impl GenAI.Tool.Schema.TypeBehaviour def from_json(%{"type" => "boolean"} = json) do {:ok, %__MODULE__{description: json["description"]}} end def from_json(_), do: {:error, :unrecognized_type} end defmodule GenAI.Tool.Schema.Number do @moduledoc """ Represents a schema for number types, including integers and floating-point numbers. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ type: "number", description: nil, minimum: nil, maximum: nil, multiple_of: nil, exclusive_minimum: nil, exclusive_maximum: nil ] @type t :: %__MODULE__{ type: String.t(), description: String.t() | nil, minimum: float() | nil, maximum: float() | nil, multiple_of: float() | nil, exclusive_minimum: boolean() | nil, exclusive_maximum: boolean() | nil } @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "number"}), do: true def is_type(_), do: false @impl GenAI.Tool.Schema.TypeBehaviour def from_json(%{"type" => "number"} = json) do {:ok, %__MODULE__{ description: json["description"], minimum: json["minimum"], maximum: json["maximum"], multiple_of: json["multipleOf"], exclusive_minimum: json["exclusiveMinimum"], exclusive_maximum: json["exclusiveMaximum"] }} end def from_json(_), do: {:error, :unrecognized_type} end defmodule GenAI.Tool.Schema.Integer do @moduledoc """ Represents a schema for integer types, converting JSON schema attributes to Elixir struct fields. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ type: "integer", description: nil, minimum: nil, maximum: nil, multiple_of: nil, exclusive_minimum: nil, exclusive_maximum: nil ] @type t :: %__MODULE__{ type: String.t(), description: String.t() | nil, minimum: integer() | nil, maximum: integer() | nil, multiple_of: integer() | nil, exclusive_minimum: boolean() | nil, exclusive_maximum: boolean() | nil } @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "integer"}), do: true def is_type(_), do: false @impl GenAI.Tool.Schema.TypeBehaviour def from_json(%{"type" => "integer"} = json) do {:ok, %__MODULE__{ type: "integer", description: json["description"], minimum: json["minimum"], maximum: json["maximum"], multiple_of: json["multipleOf"], exclusive_minimum: json["exclusiveMinimum"], exclusive_maximum: json["exclusiveMaximum"] }} end def from_json(_), do: {:error, :unrecognized_type} end defmodule GenAI.Tool.Schema.Null do @moduledoc """ Represents a schema for null types. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ type: "null", description: nil ] @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "null"}), do: true def is_type(_), do: false @impl GenAI.Tool.Schema.TypeBehaviour def from_json(%{"type" => "null"} = json) do {:ok, %__MODULE__{ type: "null", description: json["description"]} } end def from_json(_), do: {:error, :unrecognized_type} end defmodule GenAI.Tool.Schema.Enum do @moduledoc """ Represents a schema for enum types, converting JSON schema attributes to Elixir struct fields. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ type: "string", description: nil, enum: nil ] @type t :: %__MODULE__{ type: String.t(), description: String.t() | nil, enum: [String.t()] } @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "string", "enum" => _}), do: true def is_type(_), do: false @impl GenAI.Tool.Schema.TypeBehaviour def from_json(%{"type" => "string", "enum" => _} = json) do %__MODULE__{ type: "string", description: json["description"], enum: json["enum"] } end def from_json(_), do: {:error, :pending} end defmodule GenAI.Tool.Schema.String do @moduledoc """ Represents a schema for string types, converting JSON schema attributes to Elixir struct fields. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ type: "string", description: nil, min_length: nil, max_length: nil, pattern: nil, format: nil, ] @type t :: %__MODULE__{ type: String.t(), description: String.t() | nil, min_length: integer() | nil, max_length: integer() | nil, pattern: String.t() | nil, format: String.t() | nil, } @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "string"}), do: true def is_type(_), do: false @doc """ Converts a JSON map to a `GenAI.Tool.Schema.String` struct, handling naming conventions. """ @spec from_json(map()) :: t() @impl GenAI.Tool.Schema.TypeBehaviour def from_json(attributes = %{"type" => "string"}) do a = attributes |> Enum.map( fn {"maxLength", value} -> {:max_length, value} {"minLength", value} -> {:min_length, value} {"pattern", value} -> {:pattern, value} {"description", value} -> {:description, value} {"format", value} -> {:format, value} {"type", value} -> {:type, value} end) |> Enum.reject(&is_nil(&1)) {:ok, struct(__MODULE__, a)} end def from_json(_), do: {:error, :pending} end defmodule GenAI.Tool.Schema.Object do @moduledoc """ Represents a schema for object types, converting JSON schema attributes to Elixir struct fields. """ @behaviour GenAI.Tool.Schema.TypeBehaviour defstruct [ type: "object", description: nil, properties: nil, min_properties: nil, max_properties: nil, property_names: nil, pattern_properties: nil, required: nil, additional_properties: nil, ] @type t :: %__MODULE__{ type: String.t(), description: String.t() | nil, properties: map() | nil, min_properties: integer() | nil, max_properties: integer() | nil, property_names: map() | nil, pattern_properties: map() | nil, required: [String.t()] | nil, additional_properties: boolean() | map() | nil, } @impl GenAI.Tool.Schema.TypeBehaviour def is_type(%{"type" => "object"}), do: true def is_type(_), do: false @doc """ Converts a JSON map to a `GenAI.Tool.Schema.String` struct, handling naming conventions. """ @spec from_json(map()) :: t() @impl GenAI.Tool.Schema.TypeBehaviour def from_json(attributes = %{"type" => "object"}) do a = attributes |> Enum.map( fn {"type", value} -> {:type, value} {"properties", nil} -> nil {"properties", value} -> x = Enum.map(value, fn {k,v} -> with {:ok, x} <- GenAI.Tool.Schema.Type.from_json(v) do {k, x} else error -> {k, error} end end) |> Map.new() {:properties, x} {"minProperties", value} -> {:min_properties, value} {"maxProperties", value} -> {:max_properties, value} {"propertyNames", value} -> {:property_names, value} {"patternProperties", nil} -> nil {"patternProperties", value} -> x = Enum.map(value, fn {k,v} -> with {:ok, x} <- GenAI.Tool.Schema.Type.from_json(v) do {k, x} else error -> {k, error} end end) |> Map.new() {:pattern_properties, x} {"required", nil} -> nil {"required", []} -> nil {"required", value} -> {:required, value} {"additionalProperties", nil} -> nil {"additionalProperties", true} -> {:additional_properties, true} {"additionalProperties", false} -> {:additional_properties, false} {"additionalProperties", value} -> x = with {:ok, x} <- GenAI.Tool.Schema.Type.from_json(value) do x end {:additional_properties, x} {"description", value} -> {:description, value} {"type", "object"} -> nil end ) |> Enum.reject(&is_nil(&1)) {:ok, struct(__MODULE__, a)} end def from_json(_), do: {:error, :pending} end defimpl Jason.Encoder, for: [ GenAI.Tool.Function, GenAI.Tool.Schema.Bool, GenAI.Tool.Schema.Enum, GenAI.Tool.Schema.Integer, GenAI.Tool.Schema.Null, GenAI.Tool.Schema.Number, GenAI.Tool.Schema.Object, GenAI.Tool.Schema.String, ] do def encode(s, opts) do s |> Map.from_struct() |> Enum.reject(fn {k, v} -> k == :vsn or is_nil(v) end) |> Map.new() |> Jason.Encode.map(opts) end end