LangEx.LLM.ChatModel (LangEx v0.11.3)

Copy Markdown View Source

Helper to create graph nodes that call an LLM.

Produces a node function that reads messages from state, sends them to the configured LLM provider, and appends the response to the messages list.

Summary

Functions

One-shot text completion outside a graph node.

Reducer that accumulates token usage maps by summing numeric fields.

Returns a node function that calls an LLM provider.

One-shot structured extraction outside a graph node.

Returns a node function that asks the LLM for a structured result.

Validate a decoded structured result against a JSON-schema's top-level required keys. Keys are compared as strings.

Functions

complete(messages, opts)

@spec complete(
  [LangEx.Message.t()],
  keyword()
) :: {:ok, LangEx.Message.AI.t(), LangEx.LLM.usage()} | {:error, term()}

One-shot text completion outside a graph node.

Resolves the provider, optionally routes through LangEx.LLM.Resilient (:resilient), and returns the raw assistant message with token usage — a primitive for auxiliary LLM calls (summarisation, critique, tool selection) that need usage accounting but no graph state.

Returns {:ok, %Message.AI{}, usage} or {:error, reason}.

merge_usage(current, new)

@spec merge_usage(map() | nil, map()) :: map()

Reducer that accumulates token usage maps by summing numeric fields.

Use as the schema reducer for the usage key:

Graph.new(llm_usage: {%{}, &ChatModel.merge_usage/2})

node(opts)

@spec node(keyword()) :: (map() -> map())

Returns a node function that calls an LLM provider.

Options

  • :provider - module implementing LangEx.LLM (explicit)
  • :model - model string like "gpt-4o" or "claude-sonnet-4-20250514" (auto-resolves provider)
  • :messages_key - state key holding the message list (default: :messages)
  • :usage_key - state key accumulating token usage (default: :llm_usage); only written when the key exists in the graph state schema
  • :tools - list of %LangEx.Tool{} definitions for function calling
  • :resilient - route calls through LangEx.LLM.Resilient for retries with backoff. true for defaults, or a keyword list of Resilient options (:max_retries, :retry_base_ms, :fallback, ...)
  • All other opts forwarded to provider.chat/2 (:api_key, :temperature, etc.). Any opt given as {:from_state, fn state -> value end} is resolved from the node's state on each call — useful for per-run callbacks like :on_thinking whose context isn't known when the graph is built.

Either :provider or :model must be given. When :model is a string and :provider is absent, the provider is resolved via LangEx.LLM.Registry.init_chat_model/2.

Tool execution is handled by a separate LangEx.Tool.Node in the graph, not by the LLM node itself.

Token usage accounting

When the provider implements chat_with_usage/2, token counts are attached to the [:lang_ex, :llm, :chat, :stop] telemetry event as :usage metadata. To also accumulate usage in graph state, declare the usage key in the schema with merge_usage/2 as the reducer:

Graph.new(
  messages: {[], &Message.add_messages/2},
  llm_usage: {%{}, &ChatModel.merge_usage/2}
)

Examples

Graph.add_node(:llm, ChatModel.node(model: "gpt-4o"))
Graph.add_node(:llm, ChatModel.node(model: "gpt-4o",
  tools: [%LangEx.Tool{name: "search", ...}]
))

structured(messages, opts)

@spec structured(
  [LangEx.Message.t()],
  keyword()
) :: {:ok, map()} | {:error, term()}

One-shot structured extraction outside a graph node.

Forces the provider to answer via a synthetic respond tool whose parameters are :schema, decodes the tool call (falling back to decoding JSON content), and validates that the schema's top-level required keys are present.

On a schema validation failure (:no_structured_output or a missing required key) the model is re-asked with the validation error appended as feedback, up to :max_retries times — turning intermittent malformed output into self-corrections. Provider/transport errors are returned immediately (use :resilient for those).

Options

  • :schema (required) - JSON-schema map describing the desired shape
  • :max_retries - validation-feedback retries (default 2; 0 disables)
  • :strategy - :tool (default; a synthetic respond tool, works with any provider) or :provider (forces the tool via the provider's native tool_choice, for adapters that support it)
  • :resilient - true or LangEx.LLM.Resilient options to retry on transient failures
  • :provider / :model and other options are forwarded to the provider

Returns {:ok, map} or {:error, reason}:

  • {:error, :no_structured_output} - the model returned nothing decodable
  • {:error, {:missing_required, keys}} - required keys were absent
  • {:error, term} - the provider call itself failed

structured_node(opts)

@spec structured_node(keyword()) :: (map() -> map())

Returns a node function that asks the LLM for a structured result.

The model is given a synthetic respond tool whose parameters are the provided JSON-schema; calling it yields the structured data, which is decoded and written to the :into state key (default :structured). A clean assistant message carrying the JSON is appended to the messages so the conversation stays valid. Works with any provider that supports tool calling — no provider-specific configuration required.

Options

  • :schema (required) - JSON-schema map describing the desired shape
  • :into - state key to write the decoded result to (default :structured)
  • :messages_key - state key holding the message list (default :messages)
  • :provider / :model and other options are forwarded to the provider, exactly like node/1

Example

Graph.add_node(:extract, ChatModel.structured_node(
  model: "gpt-4o",
  into: :analysis,
  schema: %{
    type: "object",
    properties: %{sentiment: %{type: "string"}, score: %{type: "number"}},
    required: ["sentiment", "score"]
  }
))

validate_structured(data, schema)

@spec validate_structured(map() | nil, map()) :: {:ok, map()} | {:error, term()}

Validate a decoded structured result against a JSON-schema's top-level required keys. Keys are compared as strings.