Packages
langchain
0.8.13
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/chains/routing_chain.ex
defmodule LangChain.Chains.RoutingChain do
@moduledoc """
Run a router based on a user's initial prompt to determine what category best
matches from the given options. If there is no good match, the value "DEFAULT"
is returned.
Here's an example:
routes = [
PromptRoute.new!(%{
name: "marketing_email",
description: "Create a marketing focused email",
chain: marketing_email_chain
}),
PromptRoute.new!(%{
name: "blog_post",
description: "Create a blog post that will be linked from the company's landing page",
chain: blog_post_chain
}),
]
selected_route =
RoutingChain.new!(%{
llm: ChatOpenAI.new!(%{model: "gpt-40-mini", stream: false}),
input_text: "Let's create a marketing blog post about our new product 'Fuzzy Furies'",
routes: routes,
default_route: PromptRoute.new!(%{name: "DEFAULT", chain: fallback_chain})
})
|> RoutingChain.evaluate()
# The PromptRoute for the `blog_post` should be returned as the `selected_route`.
The `llm` is the model used to make the determination of which route is the
best match. A smaller, faster LLM may be a great choice for the routing
decision, then a more complex LLM may be used for a selected route.
The `default_route` is required and is used as a fallback if the user's prompt
doesn't match any of the specified routes. It may also be used in some
fallback error situations as well.
"""
use Ecto.Schema
import Ecto.Changeset
require Logger
alias LangChain.Routing.PromptRoute
alias LangChain.PromptTemplate
alias __MODULE__
alias LangChain.Chains.LLMChain
alias LangChain.LangChainError
alias LangChain.Message
alias LangChain.Utils
alias LangChain.Utils.ChainResult
@primary_key false
embedded_schema do
field :llm, :any, virtual: true
field :input_text, :string
field :routes, {:array, :any}, virtual: true
field :default_route, :any, virtual: true
field :verbose, :boolean, default: false
end
@type t :: %RoutingChain{}
@create_fields [:llm, :input_text, :routes, :default_route, :verbose]
@required_fields [:llm, :input_text, :routes, :default_route]
@doc """
Start a new RoutingChain.
"""
@spec new(attrs :: map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def new(attrs \\ %{}) do
%RoutingChain{}
|> cast(attrs, @create_fields)
|> common_validation()
|> apply_action(:insert)
end
@doc """
Start a new RoutingChain 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, chain} ->
chain
{:error, changeset} ->
raise LangChainError, changeset
end
end
defp common_validation(changeset) do
changeset
|> validate_required(@required_fields)
|> validate_default_route()
|> Utils.validate_llm_is_struct()
end
@doc """
Run a simple RoutingChain to analyze the input_text and determine which of the
given routes is the best match.
A simpler, faster LLM may be a great fit for running the analysis. If it fails
to find a good match, the `default_route` is used. The `default_route`'s name
is supplied to the LLM as well. The name "DEFAULT" is suggested for this
route.
"""
@spec run(t(), Keyword.t()) ::
{:ok, LLMChain.t()} | {:error, LLMChain.t(), LangChainError.t()}
def run(%RoutingChain{} = chain, opts \\ []) do
default_name = chain.default_route.name
messages =
[
Message.new_system!("""
You analyze the INPUT from the user to identify which category
it best applies to. If no category seems to be a good fit, assign
the category #{default_name}. Respond only with the category name.
REMEMBER: The category MUST be one of the candidate category names
specified below OR it can be "#{default_name}" if the input is not well
suited for any of the candidate categories.
"""),
PromptTemplate.new!(%{
role: :user,
text: """
<< CANDIDATE CATEGORIES >>
<%= for route <- @routes do %>- <%= route.name %><%= if route.description do %>: <%= route.description %><% end %>
<% end %>
<< INPUT >>
<%= @input %>
"""
})
]
|> PromptTemplate.to_messages!(%{input: chain.input_text, routes: chain.routes})
%{llm: chain.llm, verbose: chain.verbose}
|> LLMChain.new!()
|> LLMChain.add_messages(messages)
|> LLMChain.run(opts)
end
@doc """
Runs the RoutingChain and evaluates the result to return the selected chain.
"""
@spec evaluate(t(), Keyword.t()) :: PromptRoute.t()
def evaluate(%RoutingChain{} = chain, opts \\ []) do
selected_name =
chain
|> run(opts)
|> ChainResult.to_string()
|> case do
{:ok, name} ->
Logger.debug("RoutingChain selected #{inspect(name)}")
if chain.verbose, do: IO.inspect(name, label: "SELECTED ROUTE NAME")
name
{:error, _chain, reason} ->
Logger.warning("RoutingChain failed. Reason: #{inspect(reason)}")
if chain.verbose, do: IO.puts("RoutingChain FAILED IN EXECUTION - USING DEFAULT")
"DEFAULT"
end
# use selected route name to return the matching chain
if selected_name == "DEFAULT" do
chain.default_route
else
selected_name
|> PromptRoute.get_selected(chain.routes)
|> case do
%PromptRoute{} = route ->
route
nil ->
# log, verbose
Logger.warning("No matching route found. Returning default chain.")
if chain.verbose, do: IO.puts("NO MATCHING ROUTE FOUND: USING DEFAULT")
chain.default_route
end
end
end
defp validate_default_route(changeset) do
case get_field(changeset, :default_route) do
nil ->
changeset
%PromptRoute{} ->
changeset
_other ->
add_error(
changeset,
:default_route,
"must be a PromptRoute"
)
end
end
end