Packages
langchain
0.1.6
0.9.2
0.9.1
0.9.0
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.4.0-rc.3
0.4.0-rc.2
0.4.0-rc.1
0.4.0-rc.0
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-rc.2
0.3.0-rc.1
0.3.0-rc.0
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Elixir implementation of a LangChain style framework that lets Elixir projects integrate with and leverage LLMs.
Current section
Files
Jump to
Current section
Files
lib/message.ex
defmodule LangChain.Message do
@moduledoc """
Models a complete `Message` for a chat LLM.
## Roles
- `:system` - a system message. Typically just one and it occurs first as a
primer for how the LLM should behave.
- `:user` - The user or application responses. Typically represents the
"human" element of the exchange.
- `:assistant` - Responses coming back from the LLM.
- `:arguments` - The `arguments` can be set as a map where each key is an
"argument". If set as a String, it is expected to be a JSON formatted string
and will be parsed to a map. If there is an error parsing the arguments to
JSON, it is considered an error.
An empty map `%{}` means no arguments are passed.
- `:function` - A message for returning the result of executing a
`function_call`.
## Functions
A `function_call` comes from the `:assistant` role. The `function_name`
identifies the named function to execute.
Create a message of role `:function` to provide the function response.
"""
use Ecto.Schema
import Ecto.Changeset
require Logger
alias __MODULE__
alias LangChain.LangChainError
@primary_key false
embedded_schema do
field :content, :string
field :index, :integer
field :status, Ecto.Enum, values: [:complete, :cancelled, :length], default: :complete
field :role, Ecto.Enum,
values: [:system, :user, :assistant, :function],
default: :user
field :function_name, :string
field :arguments, :any, virtual: true
end
@type t :: %Message{}
@type status :: :complete | :cancelled | :length
@create_fields [:role, :content, :status, :function_name, :arguments, :index]
@required_fields [:role]
@doc """
Build a new message and return an `:ok`/`:error` tuple with the result.
"""
@spec new(attrs :: map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def new(attrs \\ %{}) do
%Message{}
|> cast(attrs, @create_fields)
|> parse_arguments()
|> common_validations()
|> apply_action(:insert)
end
@doc """
Build a new message 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, message} ->
message
{:error, changeset} ->
raise LangChainError, changeset
end
end
defp changeset_is_function?(changeset) do
get_field(changeset, :role) == :assistant and
is_binary(get_field(changeset, :function_name)) and
get_field(changeset, :status) == :complete
end
defp parse_arguments(changeset) do
args = get_field(changeset, :arguments)
is_function = changeset_is_function?(changeset)
# only
cond do
is_function && is_binary(args) ->
# decode the arguments
case Jason.decode(args) do
{:ok, parsed} when is_map(parsed) ->
put_change(changeset, :arguments, parsed)
{:ok, parsed} ->
Logger.warning(
"Parsed unexpected function argument format. Expected a map but received: #{inspect(parsed)}"
)
add_error(changeset, :arguments, "unexpected JSON arguments format")
{:error, error} ->
Logger.warning("Received invalid argument JSON data. Error: #{inspect(error)}")
add_error(changeset, :arguments, "invalid JSON function arguments")
end
true ->
changeset
end
end
defp common_validations(changeset) do
changeset
|> validate_required(@required_fields)
|> validate_content()
|> validate_function_name_for_role()
|> validate_function_name_when_args()
end
# validate that a "user" and "system" message has content. Allow an
# "assistant" message to be created where we don't have content yet because it
# can be streamed in through deltas from an LLM and not yet receive the
# content.
defp validate_content(changeset) do
case fetch_field!(changeset, :role) do
role when role in [:system, :user] ->
validate_required(changeset, [:content])
_other ->
changeset
end
end
# validate that "function_name" can only be set if the role is "function_call"
# for requesting execution or "function" for the returning a function result.
#
# The function_name is required for those message types.
defp validate_function_name_for_role(changeset) do
case fetch_field!(changeset, :role) do
role when role in [:function] ->
validate_required(changeset, [:function_name])
role when role in [:system, :user] ->
if get_field(changeset, :function_name) == nil do
changeset
else
add_error(changeset, :function_name, "can't be set with role #{inspect(role)}")
end
_other ->
changeset
end
end
# validate that "function_name" is required if arguments are set.
defp validate_function_name_when_args(changeset) do
function_name = get_field(changeset, :function_name)
if get_field(changeset, :arguments) != nil && is_nil(function_name) do
add_error(changeset, :function_name, "is required when arguments are given")
else
changeset
end
end
@doc """
Create a new system message which can prime the AI/Assistant for how to
respond.
"""
@spec new_system(content :: String.t()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def new_system(content \\ "You are a helpful assistant.") do
new(%{role: :system, content: content, status: :complete})
end
@doc """
Create a new system message which can prime the AI/Assistant for how to
respond.
"""
@spec new_system!(content :: String.t()) :: t() | no_return()
def new_system!(content \\ "You are a helpful assistant.") do
case new_system(content) do
{:ok, msg} ->
msg
{:error, %Ecto.Changeset{} = changeset} ->
raise LangChainError, changeset
end
end
@doc """
Create a new user message which represents a human message or a message from
the application.
"""
@spec new_user(content :: String.t()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def new_user(content) do
new(%{role: :user, content: content, status: :complete})
end
@doc """
Create a new user message which represents a human message or a message from
the application.
"""
@spec new_user!(content :: String.t()) :: t() | no_return()
def new_user!(content) do
case new_user(content) do
{:ok, msg} ->
msg
{:error, %Ecto.Changeset{} = changeset} ->
raise LangChainError, changeset
end
end
@doc """
Create a new assistant message which represents a response from the AI or LLM.
"""
@spec new_assistant(content :: String.t(), status()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def new_assistant(content, status \\ :complete) do
new(%{role: :assistant, content: content, status: status})
end
@doc """
Create a new assistant message which represents a response from the AI or LLM.
"""
@spec new_assistant!(content :: String.t(), status()) :: t() | no_return()
def new_assistant!(content, status \\ :complete) do
case new_assistant(content, status) do
{:ok, msg} ->
msg
{:error, %Ecto.Changeset{} = changeset} ->
raise LangChainError, changeset
end
end
@doc """
Create a new function_call message to represent the request for a function to
be executed.
"""
@spec new_function_call(name :: String.t(), raw_args :: String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def new_function_call(name, raw_args) do
case Jason.decode(raw_args) do
{:ok, parsed} ->
new(%{role: :assistant, function_name: name, arguments: parsed, status: :complete})
{:error, %Jason.DecodeError{data: reason}} ->
{:error,
%Message{role: :assistant, function_name: name}
|> change()
|> add_error(:arguments, "Failed to parse arguments: #{inspect(reason)}")}
end
end
@doc """
Create a new function_call message to represent the request for a function to
be executed.
"""
@spec new_function_call!(name :: String.t(), raw_args :: String.t()) :: t() | no_return()
def new_function_call!(name, raw_args) do
case new_function_call(name, raw_args) do
{:ok, msg} ->
msg
{:error, %Ecto.Changeset{} = changeset} ->
raise LangChainError, changeset
end
end
@doc """
Create a new function message to represent the result of an executed
function.
"""
@spec new_function(name :: String.t(), result :: any()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def new_function(name, result) do
new(%{role: :function, function_name: name, content: result})
end
@doc """
Create a new function message to represent the result of an executed
function.
"""
@spec new_function!(name :: String.t(), result :: any()) :: t() | no_return()
def new_function!(name, result) do
case new_function(name, result) do
{:ok, msg} ->
msg
{:error, %Ecto.Changeset{} = changeset} ->
raise LangChainError, changeset
end
end
@doc """
Return if a Message is a function_call.
"""
def is_function_call?(%Message{role: :assistant, status: :complete, function_name: fun_name})
when is_binary(fun_name),
do: true
def is_function_call?(%Message{}), do: false
end
defimpl LangChain.ForOpenAIApi, for: LangChain.Message do
alias LangChain.Message
def for_api(%Message{role: :assistant, function_name: fun_name} = fun)
when is_binary(fun_name) do
%{
"role" => :assistant,
"function_call" => %{
"arguments" => Jason.encode!(fun.arguments),
"name" => fun.function_name
},
"content" => fun.content
}
end
def for_api(%Message{role: :function} = fun) do
%{
"role" => :function,
"name" => fun.function_name,
"content" => fun.content
}
end
def for_api(%Message{} = fun) do
%{
"role" => fun.role,
"content" => fun.content
}
end
end