lib/tools/calculator.ex

defmodule LangChain.Tools.Calculator do
  @moduledoc """
  Defines a Calculator tool for performing basic math calculations.

  This is an example of a pre-built `LangChain.Function` that is designed and
  configured for a specific purpose.

  This defines a function to expose to an LLM and provides an implementation for
  the `execute/2` function for evaluating when an LLM executes the function.

  When using the `Calculator` tool, you will either need to:

  * make repeated calls to run the chain as the tool is called and the results
  are then made available to the LLM before it returns the final result.
  * OR run the chain using the `while_needs_response: true` option like this:
    `LangChain.LLMChain.run(chain, while_needs_response: true)`


  ## Example

  The following is an example that uses a prompt where math is needed. What
  follows is the verbose log output.

      {:ok, updated_chain, %Message{} = message} =
        %{llm: ChatOpenAI.new!(%{temperature: 0}), verbose: true}
        |> LLMChain.new!()
        |> LLMChain.add_message(
          Message.new_user!("Answer the following math question: What is 100 + 300 - 200?")
        )
        |> LLMChain.add_functions(Calculator.new!())
        |> LLMChain.run(while_needs_response: true)

  Verbose log output:

      LLM: %LangChain.ChatModels.ChatOpenAI{
        endpoint: "https://api.openai.com/v1/chat/completions",
        model: "gpt-3.5-turbo",
        temperature: 0.0,
        frequency_penalty: 0.0,
        receive_timeout: 60000,
        n: 1,
        stream: false
      }
      MESSAGES: [
        %LangChain.Message{
          content: "Answer the following math question: What is 100 + 300 - 200?",
          index: nil,
          status: :complete,
          role: :user,
          function_name: nil,
          arguments: nil
        }
      ]
      FUNCTIONS: [
        %LangChain.Function{
          name: "calculator",
          description: "Perform basic math calculations",
          function: #Function<0.108164323/2 in LangChain.Tools.Calculator.execute>,
          parameters_schema: %{
            properties: %{
              expression: %{
                description: "A simple mathematical expression.",
                type: "string"
              }
            },
            required: ["expression"],
            type: "object"
          }
        }
      ]
      SINGLE MESSAGE RESPONSE: %LangChain.Message{
        content: nil,
        index: 0,
        status: :complete,
        role: :assistant,
        function_name: "calculator",
        arguments: %{"expression" => "100 + 300 - 200"}
      }
      EXECUTING FUNCTION: "calculator"
      FUNCTION RESULT: "200"
      SINGLE MESSAGE RESPONSE: %LangChain.Message{
        content: "The answer to the math question \"What is 100 + 300 - 200?\" is 200.",
        index: 0,
        status: :complete,
        role: :assistant,
        function_name: nil,
        arguments: nil
      }

  """
  require Logger
  alias LangChain.Function

  @doc """
  Define the "calculator" function. Returns a success/failure response.
  """
  @spec new() :: {:ok, Function.t()} | {:error, Ecto.Changeset.t()}
  def new() do
    Function.new(%{
      name: "calculator",
      description: "Perform basic math calculations",
      parameters_schema: %{
        type: "object",
        properties: %{
          expression: %{type: "string", description: "A simple mathematical expression."}
        },
        required: ["expression"]
      },
      function: &execute/2
    })
  end

  @doc """
  Define the "calculator" function. Raises an exception if function creation fails.
  """
  @spec new!() :: Function.t() | no_return()
  def new!() do
    case new() do
      {:ok, function} ->
        function

      {:error, changeset} ->
        raise LangChain.LangChainError, changeset
    end
  end

  # @doc """
  # Define the calculator tool using a JSON Schema.
  # """
  # def define do
  #   # JSON Schema definition of the function. The name, description, and the
  #   # parameters it takes.
  #   %{
  #     "name" => name(),
  #     "description" => "Perform basic math calculations",
  #     "parameters" => %{
  #       "type" => "object",
  #       "properties" => %{
  #         "expression" => %{
  #           "type" => "string",
  #           "description" => "A simple mathematical expression."
  #         }
  #       },
  #       "required" => ["expression"]
  #     }
  #   }
  # end

  @doc """
  Performs the calculation specified in the expression and returns the response
  to be used by the the LLM.
  """
  @spec execute(args :: %{String.t() => any()}, context :: map()) :: String.t()
  def execute(%{"expression" => expr} = _args, _context) do
    case Abacus.eval(expr) do
      {:ok, number} ->
        to_string(number)

      {:error, reason} ->
        Logger.warning(
          "Calculator tool errored in eval of #{inspect(expr)}. Reason: #{inspect(reason)}"
        )

        "ERROR"
    end
  end
end