Jidoka: Structured Output
Build a ticket classifier whose normal chat result is a validated Elixir map. The notebook tests the output contract without a provider first, then uses the same agent in an optional provider-backed chat cell.
Setup
local_checkout? = File.exists?(Path.expand("../mix.exs", __DIR__))
jidoka_dep =
if local_checkout? do
{:jidoka, path: Path.expand("..", __DIR__)}
else
{:jidoka, git: "https://github.com/agentjido/jidoka.git", branch: "main"}
end
Mix.install(
[
jidoka_dep,
{:kino, "~> 0.19.0"}
],
config: [
jidoka: [
model_aliases: %{fast: "anthropic:claude-haiku-4-5"}
]
],
force: !local_checkout?
)
Jidoka.Kino.setup()
Define A Typed Classifier
The output do block is part of the agent contract. Jidoka appends output
instructions to the model request, then validates the completed final answer
before returning it to the caller.
defmodule LivebookDemo.StructuredOutput.TicketClassifier do
use Jidoka.Agent
agent do
id :livebook_ticket_classifier
output do
schema Zoi.object(%{
category: Zoi.enum([:billing, :technical, :account]),
confidence: Zoi.float(),
summary: Zoi.string()
})
retries 1
on_validation_error :repair
end
end
defaults do
model :fast
instructions """
Classify support tickets for routing.
Keep summaries short and user-facing.
"""
end
end
Inspect the compiled output contract.
{:ok, definition} = Jidoka.inspect_agent(LivebookDemo.StructuredOutput.TicketClassifier)
%{
id: definition.id,
output: definition.output,
schema: LivebookDemo.StructuredOutput.TicketClassifier.output_schema()
}
Test The Contract Without A Provider
You can parse and validate a sample final answer directly. This is useful when you are designing the schema before making any model calls.
output = LivebookDemo.StructuredOutput.TicketClassifier.output()
Jidoka.Output.parse(
output,
~s({"category":"billing","confidence":0.94,"summary":"Customer reports a duplicate charge."})
)
The runtime finalizer is also testable without a provider by completing a
synthetic request and letting Jidoka.Output replace the raw answer.
request_id = "structured-output-demo"
agent =
LivebookDemo.StructuredOutput.TicketClassifier.runtime_module()
|> then(& &1.new(id: "structured-output-demo-agent"))
|> Jido.AI.Request.start_request(request_id, "Classify a billing issue")
|> Jido.AI.Request.complete_request(
request_id,
~s({"category":"billing","confidence":0.88,"summary":"Invoice was charged twice."})
)
agent = Jidoka.Output.finalize(agent, request_id, output)
Jido.AI.Request.get_result(agent, request_id)
Import The Same Shape
JSON and YAML imported agents cannot embed Zoi terms, so they use an object-shaped JSON Schema subset.
spec = %{
"agent" => %{"id" => "imported_ticket_classifier"},
"defaults" => %{
"model" => "fast",
"instructions" => "Classify support tickets for routing."
},
"output" => %{
"schema" => %{
"type" => "object",
"required" => ["category", "confidence", "summary"],
"properties" => %{
"category" => %{"type" => "string"},
"confidence" => %{"type" => "number"},
"summary" => %{"type" => "string"}
}
},
"retries" => 1,
"on_validation_error" => "repair"
}
}
{:ok, imported} = Jidoka.import_agent(spec)
Map.take(Jidoka.ImportedAgent.definition(imported), [:id, :output])
Optional Provider Chat
With an ANTHROPIC_API_KEY Livebook secret configured, the normal chat call
returns a parsed map. Use output: :raw when you want to inspect the unparsed
assistant answer during debugging.
{:ok, pid} =
Jidoka.Kino.start_or_reuse("livebook-ticket-classifier", fn ->
LivebookDemo.StructuredOutput.TicketClassifier.start_link(id: "livebook-ticket-classifier")
end)
Jidoka.Kino.chat("Structured classifier chat", fn ->
LivebookDemo.StructuredOutput.TicketClassifier.chat(
pid,
"I was double charged for my last invoice and need help."
)
end)
Jidoka.Kino.chat("Raw classifier answer", fn ->
LivebookDemo.StructuredOutput.TicketClassifier.chat(
pid,
"I cannot reset my password.",
output: :raw
)
end)