Sagents.Extract (Sagents v0.8.0-rc.8)
Copy MarkdownStructured data extraction through an Sagents.Agent.
This is LangChain.Chains.DataExtractionChain lifted to the Agent layer.
Use it when you want a single agent run to return a structured map, and
you want the call to flow through your agent's middleware stack — token
usage attribution, tenancy/APM context propagation, scope, fallback models,
filesystem scope, and so on.
The trick is the same one DataExtractionChain uses: define a single tool
whose parameters_schema is the desired result shape, run the agent with
until_tool: "<that tool>", and read the LLM-supplied arguments back out.
The tool's body never has to do real work — it just hands args back as
processed_content.
Basic usage
schema = %{
type: "object",
properties: %{
"title" => %{type: "string"},
"summary" => %{type: "string"}
},
required: ["title", "summary"]
}
state = State.new!(%{messages: [Message.new_user!("Summarize: ...")]})
{:ok, result} = Sagents.Extract.run(agent, state, schema: schema)
# result is the LLM-supplied map: %{"title" => "...", "summary" => "..."}Options
:schema(required, unless:toolis given) — A JSON Schema map describing the result shape. Becomes theparameters_schemaof the submit tool.:tool(required, unless:schemais given) — A pre-builtLangChain.Function. Use this when you want full control over the tool's name, description,parse_argscallback, etc. When supplied,:schema,:tool_name, and:descriptionare ignored.:tool_name(default"submit_result") — Name of the submit tool the LLM will be required to call. Only meaningful when:schemais used.:description(default a generic string) — Description sent to the LLM for the submit tool. Worth writing well; the LLM reads it to decide what to put in the arguments.:max_runs(default5) — Maximum LLM calls. Enough for a single call plus a few retries if the tool body orparse_argsvalidation rejects malformed args. Increase for complex schemas.
What run/3 returns
{:ok, map()}— The arguments the LLM passed to the submit tool, with keys exactly as the provider returned them (or as theparse_argscallback processes them).{:error, term()}— ALangChainErrorif the LLM never successfully called the submit tool withinmax_runs, if a same-named tool already exists on the agent, or anything else thatAgent.execute/3would surface.
Provider-side tool_choice
Anthropic and OpenAI both let you require the model to call a specific tool
via tool_choice. Setting this on your ChatAnthropic / ChatOpenAI model
before passing it to Agent.new/2 materially improves single-call
reliability. The name in tool_choice must match the submit tool's name
— which is whatever you pass as :tool_name (or "submit_result" if you
don't). Mismatched names still work — until_tool catches whichever tool
the LLM calls — but you lose the provider-side guarantee.
Example:
model =
ChatAnthropic.new!(%{
model: "claude-sonnet-4-6",
tool_choice: %{"type" => "tool", "name" => "submit_result"}
})
{:ok, agent} = Sagents.Agent.new(%{model: model, ...})
Sagents.Extract.run(agent, state, schema: schema)Validation and retry
Two layers of validation are available, both caller-owned:
- JSON schema (provider-enforced) — defined by your
:schema. The provider rejects shape errors before the call reaches us. - Business rules (in your tool) — anything JSON Schema can't express.
Use
LangChain.Function's:parse_argscallback (Zoi schemas work well) or validate inside the tool body. Return{:error, "reason"}; the LLM sees the error and retries with corrected args until success or:max_runsis hit. Write error messages that are actionable — the LLM is the one reading them.
Nothing here needs to know about either layer. They fall out of how
until_tool already loops.
Middleware compatibility
Not every middleware is appropriate in a fire-and-wait run. run/3 calls
Sagents.Agent.execute/3 directly — Sagents.AgentServer is not involved.
Middleware that assumes a registered AgentServer will misbehave. In
particular:
- Middleware that broadcasts events over PubSub via
Sagents.AgentServer.publish_event_from/2. - Middleware that schedules or relies on an inactivity timer.
- Middleware that looks up an agent process by
agent_id.
Middleware that is purely state-shaping or context-propagating is fine: token-usage capture, tenancy/APM context propagation, request-scoped logging, scope-keyed persistence, etc. Composing a safe agent is the caller's responsibility.
Non-mutation
run/3 does not modify the agent you pass in. It builds a new agent value
with the submit tool appended to :tools and runs that. The submit tool
only exists for the duration of that call; the original agent is unchanged
and never sees it.
Summary
Functions
Run a structured extraction with the given agent and state.
Types
@type opts() :: [ schema: map(), tool: LangChain.Function.t(), tool_name: String.t(), description: String.t(), max_runs: pos_integer() ]
Options for run/3.
Functions
@spec run(Sagents.Agent.t(), Sagents.State.t(), opts()) :: {:ok, map()} | {:error, term()}
Run a structured extraction with the given agent and state.
The state carries the full conversation (system messages, user prompts,
any prior turns). See the module doc for option details.