defmodule BeamMePrompty.Tool do
  @moduledoc """
  Defines the behaviour and structure for tools that can be executed by Language Models (LLMs).

  A `BeamMePrompty.Tool` module acts as an interface that LLMs can understand and interact with
  to perform specific actions or retrieve information. Modules implementing this behaviour
  are typically created by using `use BeamMePrompty.Tool` which sets up the required
  `tool_info/0` callback based on provided options. Developers then implement the `run/1`
  function to execute the tool's logic.

  The `:parameters` option provided to `use BeamMePrompty.Tool` should be a map that
  defines the expected input arguments for the tool, structured according to the
  JSON Schema specification (e.g., describing types, properties for objects,
  and required fields). This schema is crucial for LLMs to correctly format
  their requests to the tool.

  ## Examples

  A simple tool module might look like this:

  ```elixir
  defmodule MyTool do
    use BeamMePrompty.Tool,
      name: :my_tool,
      description: "A simple example tool",
      parameters: %{
        "type" => "object", # Using strings for keys as per JSON schema
        "properties" => %{
          "input_string" => %{
            "type" => "string",
            "description" => "The string to process"
          }
        },
        "required" => ["input_string"]
      }

    @impl BeamMePrompty.Tool
    def run(%{"input_string" => input}) do
      # Prefer returning maps for structured data if the LLM benefits from it,
      # though strings are allowed.
      {:ok, %{output: "Processed: " <> input}}
      # Or simply: {:ok, "Processed: " <> input}
    end

    # Catch-all clause for invalid arguments
    def run(args) do
      {:error, %BeamMePrompty.LLM.Errors.ToolError{module: __MODULE__, cause: "Invalid arguments: " <> inspect(args)}}
    end
  end
  ```
  """
  @moduledoc section: :tools_and_capabilities

  use TypedStruct

  alias BeamMePrompty.LLM.Errors.ToolError

  typedstruct do
    field :name, atom()
    field :description, String.t()
    field :parameters, map()
    field :module, module()
  end

  @doc """
  Execute the tool with the provided arguments.

  ## Parameters

    - `args`: A map containing the arguments to be passed to the tool,
      typically matching the schema defined in `:parameters` of `tool_info/0`.
    - `context`: A map containing additional context that may be needed
      for the tool's execution. This can include information about the LLM
      making the request, such as its configuration or state.

  ## Returns

    - `{:ok, result}`: A tuple containing the result of the tool execution.
      The `result` can be a `String.t()` or a `map()`. While `String.t()` is permissible
      for simple outputs, returning a `map()` is often preferred for providing
      structured data that LLMs can more easily interpret and utilize, especially
      if the LLM expects a JSON-like object.
    - `{:error, reason}`: A tuple containing an error reason if the execution fails.
      The `reason` must be a `BeamMePrompty.LLM.Errors.ToolError.t()` struct.
  """
  @callback run(map(), map()) :: {:ok, map() | String.t()} | {:error, ToolError.t()}

  @doc """
  Provides information about the tool, used by LLMs to understand how to call it.

  This callback is typically generated by `use BeamMePrompty.Tool`.

  ## Returns

    - `%BeamMePrompty.Tool{}`: A struct containing the tool's:
        - `:name` (atom): The unique identifier for the tool.
        - `:description` (String.t): A human-readable description of what the tool does.
        - `:parameters` (map): A map structured according to JSON Schema (or a
          compatible subset of OpenAPI Specification) describing the expected
          arguments for the `run/1` function.
        - `:module` (module): The module implementing the tool.
  """
  @callback tool_info() :: __MODULE__.t()

  defmacro __using__(opts) do
    if not Keyword.has_key?(opts, :name) do
      raise CompileError, description: "BeamMePrompty.Tool requires a :name option"
    end

    if not Keyword.has_key?(opts, :description) do
      raise CompileError, description: "BeamMePrompty.Tool requires a :description option"
    end

    quote bind_quoted: [
            name: opts[:name],
            description: opts[:description],
            parameters: Macro.escape(opts[:parameters])
          ] do
      @behaviour BeamMePrompty.Tool

      @impl BeamMePrompty.Tool
      def tool_info do
        %BeamMePrompty.Tool{
          name: unquote(name),
          description: unquote(description),
          parameters: unquote(parameters),
          module: __MODULE__
        }
      end

      def name, do: unquote(name)

      defoverridable tool_info: 0
    end
  end
end
