Skip to content

Commit

Permalink
Merge pull request #35 from brainlid/me-function-param-structs
Browse files Browse the repository at this point in the history
created FunctionParam struct
  • Loading branch information
brainlid authored Nov 13, 2023
2 parents b7b5b61 + 3a81189 commit 476b03f
Show file tree
Hide file tree
Showing 9 changed files with 882 additions and 26 deletions.
18 changes: 17 additions & 1 deletion lib/chains/data_extraction_chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule LangChain.Chains.DataExtractionChain do
information is returned as an array.
Originally based on:
- https://github.com/hwchase17/langchainjs/blob/main/langchain/src/chains/openai_functions/extraction.ts#L42
- https://github.com/langchain-ai/langchainjs/blob/main/langchain/src/chains/openai_functions/extraction.ts#L43
## Example
Expand Down Expand Up @@ -51,6 +51,22 @@ defmodule LangChain.Chains.DataExtractionChain do
"person_name" => "Claudia"
}
]
The `schema_parameters` in the previous example can also be expressed using a
list of `LangChain.FunctionParam` structs. An equivalent version looks like
this:
alias LangChain.FunctionParam
schema_parameters = [
FunctionParam.new!(%{name: "person_name", type: :string}),
FunctionParam.new!(%{name: "person_age", type: :number}),
FunctionParam.new!(%{name: "person_hair_color", type: :string}),
FunctionParam.new!(%{name: "dog_name", type: :string}),
FunctionParam.new!(%{name: "dog_breed", type: :string})
]
|> FunctionParam.to_parameters_schema()
"""
use Ecto.Schema
require Logger
Expand Down
86 changes: 80 additions & 6 deletions lib/function.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ defmodule LangChain.Function do
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` - A list of `Function.FunctionParam` structs that are
converted to a JSONSchema format. (Use in place of `parameters_schema`)
* ` 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.
passed to the function. (Use if greater control or unsupported features are
needed.)
* `function` - An Elixir function to execute when an LLM requests to execute
the function.
When passing arguments from an LLM to a function, they go through a single
`map` argument. This allows for multiple keys or named parameters.
## Example
This example defines a function that an LLM can execute for performing basic
Expand Down Expand Up @@ -56,11 +62,54 @@ defmodule LangChain.Function do
Context examples may be user_id, account_id, account struct, billing level,
etc.
## Function Parameters
The `parameters` field is a list of `LangChain.FunctionParam` structs. This is
a convenience for defining the parameters to the function. If it does not work
for more complex use-cases, then use the `parameters_schema` to declare it as
needed.
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.
NOTE: Only use `parameters` or `parameters_schema`, not both.
## Expanded Parameter Examples
Function with no arguments:
alias LangChain.Function
Function.new!(%{name: "get_current_user_info"})
Function that takes a simple required argument:
alias LangChain.FunctionParam
Function.new!(%{name: "set_user_name", parameters: [
FunctionParam.new!(%{name: "user_name", type: :string, required: true})
]})
Function that takes an array of strings:
Function.new!(%{name: "set_tags", parameters: [
FunctionParam.new!(%{name: "tags", type: :array, item_type: "string"})
]})
Function that takes two arguments and one is an object/map:
Function.new!(%{name: "update_preferences", parameters: [
FunctionParam.new!(%{name: "unique_code", type: :string, required: true})
FunctionParam.new!(%{name: "data", type: :object, object_properties: [
FunctionParam.new!(%{name: "auto_complete_email", type: :boolean}),
FunctionParam.new!(%{name: "items_per_page", type: :integer}),
]})
]})
The `LangChain.FunctionParam` is nestable allowing for arrays of object and
objects with nested objects.
"""
use Ecto.Schema
import Ecto.Changeset
Expand All @@ -76,13 +125,16 @@ defmodule LangChain.Function do
# 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

# parameters_schema is a map used to express a JSONSchema structure of inputs and what's required
field :parameters_schema, :map
# parameters is a list of `LangChain.FunctionParam` structs.
field :parameters, {:array, :any}, default: []
end

@type t :: %Function{}

@create_fields [:name, :description, :parameters_schema, :function]
@create_fields [:name, :description, :parameters_schema, :parameters, :function]
@required_fields [:name]

@doc """
Expand Down Expand Up @@ -114,6 +166,7 @@ defmodule LangChain.Function do
changeset
|> validate_required(@required_fields)
|> validate_length(:name, max: 64)
|> ensure_single_parameter_option()
end

@doc """
Expand All @@ -125,27 +178,48 @@ defmodule LangChain.Function do
Logger.debug("Executing function #{inspect(function.name)}")
fun.(arguments, context)
end

defp ensure_single_parameter_option(changeset) do
params_list = get_field(changeset, :parameters)
schema_map = get_field(changeset, :parameters_schema)

cond do
# can't have both
is_map(schema_map) and !Enum.empty?(params_list) ->
add_error(changeset, :parameters, "Cannot use both parameters and parameters_schema")

true ->
changeset
end
end
end

defimpl LangChain.ForOpenAIApi, for: LangChain.Function do
alias LangChain.Function
alias LangChain.FunctionParam
alias LangChain.Utils

def for_api(%Function{} = fun) do
%{
"name" => fun.name,
"description" => fun.description,
"parameters" => get_parameters(fun)
}
|> Utils.conditionally_add_to_map("description", fun.description)
end

defp get_parameters(%Function{parameters_schema: nil} = _fun) do
defp get_parameters(%Function{parameters: [], parameters_schema: nil} = _fun) do
%{
"type" => "object",
"properties" => %{}
}
end

defp get_parameters(%Function{parameters_schema: schema} = _fun) do
defp get_parameters(%Function{parameters: [], parameters_schema: schema} = _fun)
when is_map(schema) do
schema
end

defp get_parameters(%Function{parameters: params} = _fun) do
FunctionParam.to_parameters_schema(params)
end
end
Loading

0 comments on commit 476b03f

Please sign in to comment.