# {:ok, f} = LangChain.Function.new(%{name: "register_person", description: "Register a new person in the system", required: ["name"], parameters: [p_name, p_age]})
# NOTE: New in OpenAI - https://openai.com/blog/function-calling-and-other-api-updates
# - 13 June 2023
# NOTE: Pretty much takes the place of a LangChain "Tool".
defmodule LangChain.Function do
@moduledoc """
Defines a "function" that can be provided to an LLM for the LLM to optionally
execute and pass argument data to.
A function is defined using a schema.
* `name` - The name of the function given to the LLM.
* `description` - A description of the function provided to the LLM. This
should describe what the function is used for or what it returns. This
information is used by the LLM to decide which function to call and for what
purpose.
* ` parameters_schema` - A [JSONSchema
structure](https://json-schema.org/learn/getting-started-step-by-step.html)
that describes the required data structure format for how arguments are
passed to the function.
* `function` - An Elixir function to execute when an LLM requests to execute
the function.
## Example
This example defines a function that an LLM can execute for performing basic
math calculations. **NOTE:** This is a partial implementation of the
`LangChain.Tools.Calculator`.
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:
fn(%{"expression" => expr} = _args, _context) ->
"Uh... I don't know!"
end)
})
The `function` attribute is an Elixir function that can be executed when the
function is "called" by the LLM.
The `args` argument is the JSON data passed by the LLM after being parsed to a
map.
The `context` argument is passed through as the `context` on a
`LangChain.Chains.LLMChain`. This is whatever context data is needed for the
function to do it's work.
Context examples may be user_id, account_id, account struct, billing level,
etc.
The `parameters_schema` is an Elixir map that follows a
[JSONSchema](https://json-schema.org/learn/getting-started-step-by-step.html)
structure. It is used to define the required data structure format for
receiving data to the function from the LLM.
"""
use Ecto.Schema
import Ecto.Changeset
require Logger
alias __MODULE__
alias LangChain.LangChainError
@primary_key false
embedded_schema do
field :name, :string
field :description, :string
# flag if the function should be auto-evaluated. Defaults to `false`
# requiring an explicit step to perform the evaluation.
# field :auto_evaluate, :boolean, default: false
field :function, :any, virtual: true
# parameters is a map used to express a JSONSchema structure of inputs and what's required
field :parameters_schema, :map
end
@type t :: %Function{}
@create_fields [:name, :description, :parameters_schema, :function]
@required_fields [:name]
@doc """
Build a new function.
"""
@spec new(attrs :: map()) :: {:ok, t} | {:error, Ecto.Changeset.t()}
def new(attrs \\ %{}) do
%Function{}
|> cast(attrs, @create_fields)
|> common_validation()
|> apply_action(:insert)
end
@doc """
Build a new function and return it or raise an error if invalid.
"""
@spec new!(attrs :: map()) :: t() | no_return()
def new!(attrs \\ %{}) do
case new(attrs) do
{:ok, function} ->
function
{:error, changeset} ->
raise LangChainError, changeset
end
end
defp common_validation(changeset) do
changeset
|> validate_required(@required_fields)
|> validate_length(:name, max: 64)
end
@doc """
Execute the function passing in arguments and additional optional context.
This is called by a `LangChain.Chains.LLMChain` when a `Function` execution is
requested by the LLM.
"""
def execute(%Function{function: fun} = function, arguments, context) do
Logger.debug("Executing function #{inspect(function.name)}")
fun.(arguments, context)
end
end
defimpl LangChain.ForOpenAIApi, for: LangChain.Function do
alias LangChain.Function
def for_api(%Function{} = fun) do
%{
"name" => fun.name,
"description" => fun.description,
"parameters" => get_parameters(fun)
}
end
defp get_parameters(%Function{parameters_schema: nil} = _fun) do
%{
"type" => "object",
"properties" => %{}
}
end
defp get_parameters(%Function{parameters_schema: schema} = _fun) do
schema
end
end