Current section
Files
Jump to
Current section
Files
lib/agent_chain.ex
defmodule Magus.AgentChain do
@doc """
Define a chain for agents.
This is an extention of the LangChain.Chains.LLMChain chain that provides
a few helpers for the agent use case.
A `stream_handler` can be passed in when the AgentChain is created and then
used automatically when the chain is run. This is useful for the AgentExecutor
to listen to tokens as they're returned by the LLM.
The AgentChain can also be configured with a JSON schema that is used when
requesting content from the LLM and then used to validate that the LLM response
conforms to the schema.
"""
alias LangChain.PromptTemplate
alias Magus.AgentChain
alias LangChain.Message
alias LangChain.MessageDelta
alias LangChain.Chains.LLMChain
alias LangChain.ChatModels.ChatOpenAI
alias LangChain.ChatModels.ChatGoogleAI
defstruct [
:wrapped_chain,
:stream_handler,
:json_response_schema
]
@default_openai_model "gpt-3.5-turbo"
@default_gemini_model "gemini-1.5-flash"
@json_format_template PromptTemplate.from_template!(
~S|"The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Do not wrap the JSON with any Markdown.
Here is the output schema:
```
<%= @schema %>
```|
)
@type t() :: %AgentChain{}
@doc """
Start a new AgentChain configuration.
## Options
- `:verbose` - Runs the LLM in verbose mode if set to true. Defaults to `false`.
- `:stream_handler` - Handler that is called as the LLM returns messages.
"""
@spec new!(opts :: keyword()) :: t()
def new!(opts \\ []) do
default = [verbose: false, stream_handler: nil]
opts = Keyword.merge(default, opts)
stream_handler = opts[:stream_handler]
callback =
if stream_handler != nil, do: stream_handler, else: get_default_stream_handler()
# TODO: pull this default llm from a config
wrapped_chain =
%{
llm: get_default_llm(),
verbose: opts[:verbose]
}
|> LLMChain.new!()
|> LLMChain.add_callback(callback)
%AgentChain{wrapped_chain: wrapped_chain, stream_handler: stream_handler}
end
@doc """
Adds a JSON schema to the chain as the response format from the LLM.
A message is added to the chain that tells the LLM to return the response
content in JSON format. In the OpenAI case, the request will be made
in [JSON Mode](https://platform.openai.com/docs/guides/text-generation/json-mode)
"""
@spec ask_for_json_response(t(), map()) :: t()
def ask_for_json_response(chain, schema) do
# TODO: this is ugly and doesn't generalize well. Re-work this when we re-think LLM configuration
inner_llm =
case chain.wrapped_chain.llm do
%ChatOpenAI{} -> %ChatOpenAI{chain.wrapped_chain.llm | json_response: true}
_ -> chain.wrapped_chain.llm
end
wrapped_chain = %{chain.wrapped_chain | llm: inner_llm}
# Add a message to the chain that requests the schema
schema_json = Jason.encode!(schema)
wrapped_chain =
wrapped_chain
|> LLMChain.add_message(
PromptTemplate.to_message!(@json_format_template, %{
schema: schema_json
})
)
%AgentChain{chain | wrapped_chain: wrapped_chain, json_response_schema: schema}
end
@spec add_message(t(), LangChain.Message.t()) :: t()
def add_message(chain, message) do
wrapped_chain = chain.wrapped_chain |> LLMChain.add_message(message)
%AgentChain{chain | wrapped_chain: wrapped_chain}
end
@spec add_messages(t(), [LangChain.Message.t()]) :: t()
def add_messages(chain, messages) do
wrapped_chain = chain.wrapped_chain |> LLMChain.add_messages(messages)
%AgentChain{chain | wrapped_chain: wrapped_chain}
end
@spec add_tool(t(), LangChain.Function.t()) :: t()
def add_tool(chain, tool) do
wrapped_chain = chain.wrapped_chain |> LLMChain.add_tools([tool])
%AgentChain{chain | wrapped_chain: wrapped_chain}
end
@doc """
Run the AgentChain.
If a `stream_handler` was specified when the AgentChain was created,
it will be called as the LLM returns tokens.
If a JSON response was requested with `ask_for_json_response`, the response
will be validated against the schema and decoded to a struct.
"""
@spec run(t()) :: {:error, binary() | list()} | {:ok, :any, LangChain.Message.t()}
def run(%AgentChain{wrapped_chain: llm_chain} = chain) do
with {:ok, updated_llm_chain} <-
LLMChain.run(llm_chain),
content <- process_raw_content(updated_llm_chain.last_message.content),
{:ok, content} <- parse_content_to_schema(content, chain.json_response_schema) do
{:ok, content, updated_llm_chain.last_message}
end
end
defp process_raw_content(content) when is_list(content) do
content
|> Enum.map(fn part -> part.content end)
|> Enum.join(" ")
end
defp process_raw_content(content) do
content
end
defp parse_content_to_schema(content, nil) do
{:ok, content}
end
defp parse_content_to_schema(content, schema) do
# Strip Gemini's markdown
content =
content |> String.replace_leading("```json", "") |> String.replace_trailing("```", "")
with {:ok, parsed_content} <- Jason.decode(content),
:ok <- ExJsonSchema.Validator.validate(schema, parsed_content) do
{:ok, parsed_content}
end
end
defp get_default_llm() do
model_provider = Application.fetch_env!(:magus, :model_provider)
case model_provider do
"gemini_ai" -> get_default_gemini_llm()
"openai" -> get_default_openai_llm()
end
end
def get_default_gemini_llm() do
gemini_key = Application.fetch_env!(:magus, :gemini_key)
gemini_model = Application.get_env(:magus, :gemini_model) || @default_gemini_model
ChatGoogleAI.new!(%{
endpoint: "https://generativelanguage.googleapis.com",
model: gemini_model,
api_key: gemini_key,
stream: true
})
end
defp get_default_openai_llm() do
openai_key = Application.fetch_env!(:magus, :openai_key)
openai_model = Application.get_env(:magus, :openai_model) || @default_openai_model
ChatOpenAI.new!(%{model: openai_model, api_key: openai_key, stream: true})
end
defp get_default_stream_handler() do
%{
on_llm_new_delta: fn _model, %MessageDelta{} = _data ->
:ok
end,
on_message_processed: fn _chain, %Message{} = _data ->
:ok
end
}
end
end