lib/gen_ai/tool.ex

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