Packages
langchain
0.4.0-rc.0
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_processors/changeset_processor.ex
defmodule LangChain.MessageProcessors.ChangesetProcessor do
@moduledoc """
A built-in Message processor that processes a received Message into the
provided `Ecto.Changeset`.
`EctoChangesetProcessor`? `EctoProcessor`? `ChangesetProcessor`?
Don't want it to try an update this way. The ID issue. I want updates to go
through my context.
EctoProcessor.new!(%{repo: Repo})
EctoProcessor.new!(%{apply: true/false})
Return a changeset with or without errors or apply the changeset and return an
{:ok, Struct} | {:error, changeset}
TODO: PROBLEM. The JSONSchema still needs to be defined separately from the Ecto struct. They would need to be kept in sync but separate. Not ideal. That's what Instructor handles rather nicely.
The other approach to show is using a function to interact with a Phoenix context (business layer) and also through a real database-backed Ecto Schema.
When successful, the assistant message's JSON contents are processed into a
map and set on `processed_content`. No additional validation or processing of
the data is done in by this processor.
When JSON data is expected but not received, or the received JSON is invalid
or incomplete, a new user `Message` struct is returned with a text error
message for the LLM so it can try again to correct the error and return a
valid response.
There are multiple ways to extract JSON content.
When the JSON data is reliably returned as the only response, this extracts it
to an Elixir Map:
message = Message.new_assistant!(%{content: "{\"value\": 123}"})
# process the message for JSON content
{:cont, updated_chain, updated_message} =
JsonProcessor.run(chain, message)
The updated message will be an assistant message where content is a map:
updated_message.content
#=> %{"value" => 123}
Some models are unable to reliably return a JSON response without adding some
commentary. For that situation, instruct the model how to format the JSON
content. Depending on the model, one of these formats may work better than
another:
# bracketing the data with XML tags
<json>
{"value": 123}
</json>
# markdown style code fences with json language
```json
{"value": 123}
```
# markdown style code fences (no language)
```
{"value": 123}
```
When the LLM adds commentary with the data, it may appear like this:
The answer to your question in JSON is:
```json
{"value": 123}
```
We can still extract the JSON data in a situation like this. We provide a
Regex to use for extracting the data from whatever starting and ending text
the LLM was instructed to use.
Examples:
~r/<json>(.*?)<\/json>/s
~r/```json(.*?)```/s
~r/```(.*?)```/s
The `"```json"` formatted one is processed like this:
{:cont, updated_chain, updated_message} =
JsonProcessor.run(chain, message, ~r/```json(.*?)```/s)
"""
alias LangChain.Chains.LLMChain
alias LangChain.Message
@doc """
Returns a function for use in a `LangChain.Chains.LLMChain.message_processors/2`.
"""
def new!() do
# Doesn't need any currying.
&run/2
end
@doc """
Returns a wrapped (curried) function for use in a
`LangChain.Chains.LLMChain.message_processors/2` that includes the configured
Regex to use for extracting JSON content.
The Regex pattern is used with the `:all_but_first` capture option to extract
just the internal capture text.
"""
def new!(%Regex{} = regex) do
# Curry in the regex option
&run(&1, &2, regex)
end
@doc """
Run the JSON Processor on a message. The response indicates what should be
done with the message.
Response values:
- `{:cont, %Message{}}` - The returned message replaces the one being
processed and no additional processors are run.
- `{:halt, %Message{}}` - Future processors are skipped. The Message is
returned as a response to the LLM for reporting errors.
"""
@spec run(LLMChain.t(), Message.t()) ::
{:cont, Message.t()} | {:halt, Message.t()}
def run(%LLMChain{} = chain, %Message{} = message) do
case Jason.decode(message.processed_content) do
{:ok, parsed} ->
if chain.verbose, do: IO.puts("Parsed JSON text to a map")
{:cont, %Message{message | processed_content: parsed}}
{:error, %Jason.DecodeError{} = error} ->
error_message = Jason.DecodeError.message(error)
{:halt, Message.new_user!("ERROR: Invalid JSON data: #{error_message}")}
end
end
def run(%LLMChain{} = chain, %Message{} = message, regex_pattern) do
case Regex.run(regex_pattern, message.processed_content, capture: :all_but_first) do
[json] ->
if chain.verbose, do: IO.puts("Extracted JSON text from message")
# run recursive call on just the extracted JSON
run(chain, %Message{message | processed_content: json})
_ ->
{:halt, Message.new_user!("ERROR: No JSON found")}
end
end
end