feat: add support for structured output schemas and improve agent capabilities - #28
Conversation
…abilities - Integrated `StructuredOutputFormat` with workflows to enable JSON Schema-constrained outputs. - Updated LLM provider to enforce schema validation for agent replies. - Enhanced testing coverage for backward compatibility and schema behavior. - Added a new `structured_output_agent` example demonstrating schema-constrained workflows. - Migrated default LLM to OpenAI backend in tests for improved compatibility. - Incremented dependencies with `schemars` integration for schema generation.
WalkthroughThis PR adds optional structured output schema support to the agent framework, enabling LLMs to produce strictly validated JSON responses. It extends workflow and activity state with an ChangesStructured Output Schema Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/agent_workflow.rs (1)
200-216: ⚡ Quick winConsider a slightly looser tolerance than
f64::EPSILON.At magnitude ~15-20, one ULP is already ~3.55e-15, while
f64::EPSILON≈ 2.22e-16. The current bounds hold only if the model emits the exact JSON literals from the prompt (15.4,4.5,19.9) — which is the assumption stated in the comment, but it's fragile:
- The
addtool returnsparsed.a + parsed.b, serialized via ryu. If the model echoes the tool's computed sum verbatim instead of recomputing/printing19.9, the round-trip f64 can differ from19.9_f64by ~1 ULP, exceedingf64::EPSILON.- Small reasoning models also occasionally normalize numbers (e.g.,
19.90,19.9000000000000004).A tolerance like
1e-9(or even1e-6) preserves the "no hallucinated math" intent while making the assertion robust to a single rounding step. The schema/typed-shape check is what actually catches structural deviations.♻️ Proposed tolerance loosening
- // Tight epsilon — LLM emits JSON literals so values round-trip exactly, - // but clippy::float_cmp insists on `abs() < eps` over `==`. - assert!( - (parsed.left_operand - 15.4).abs() < f64::EPSILON, + // Allow one ULP of slack: the tool sums in f64 before serializing, so the + // model may echo the rounded sum rather than the literal. + const TOL: f64 = 1e-9; + assert!( + (parsed.left_operand - 15.4).abs() < TOL, "left_operand mismatch: got {}", parsed.left_operand ); assert!( - (parsed.right_operand - 4.5).abs() < f64::EPSILON, + (parsed.right_operand - 4.5).abs() < TOL, "right_operand mismatch: got {}", parsed.right_operand ); assert!( - (parsed.result - 19.9).abs() < f64::EPSILON, + (parsed.result - 19.9).abs() < TOL, "result mismatch: got {}", parsed.result );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/agent_workflow.rs` around lines 200 - 216, The assertions using f64::EPSILON are too strict for values ~15–20; replace the epsilon with a looser tolerance (e.g., const TOL: f64 = 1e-9) and update the three checks that compare parsed.left_operand, parsed.right_operand, and parsed.result to use (value - expected).abs() < TOL instead of f64::EPSILON so the test tolerates one ULP/serialization variance while still catching incorrect math or shape errors.examples/structured_output_agent/main.rs (1)
209-238: 💤 Low valueOptional: surface
stop_reasonand treat parse failure as a demo signal.
final_answeris only schema-constrained when the agent reachesStopReason::FinalAnswer. If the loop hitsMaxTurnsor the provider doesn't honor the schema, the deserialization will silently fall through to theErrbranch and the example exitsOk(())— easy to miss for someone running this as a smoke test of structured outputs. Consider gating the parse on the stop reason and/or returning an error on parse failure so the demo loudly distinguishes "model ignored schema" from "demo succeeded."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/structured_output_agent/main.rs` around lines 209 - 238, The example currently always attempts to parse out.final_answer into WeatherReport and treats parse failure as a non-fatal demo message; instead first gate parsing on out.stop_reason (compare against StopReason::FinalAnswer) and if the stop reason is not FinalAnswer, print/emit a clear warning including out.stop_reason and return an Err to fail the demo; if stop reason is FinalAnswer then attempt serde_json::from_str::<WeatherReport>(&out.final_answer) and on parse errors return an Err (with context about final_answer and the deserialize error) rather than silently exiting Ok, using the AgentOutput variable name out and the WeatherReport/StopReason symbols to locate the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@examples/structured_output_agent/main.rs`:
- Around line 209-238: The example currently always attempts to parse
out.final_answer into WeatherReport and treats parse failure as a non-fatal demo
message; instead first gate parsing on out.stop_reason (compare against
StopReason::FinalAnswer) and if the stop reason is not FinalAnswer, print/emit a
clear warning including out.stop_reason and return an Err to fail the demo; if
stop reason is FinalAnswer then attempt
serde_json::from_str::<WeatherReport>(&out.final_answer) and on parse errors
return an Err (with context about final_answer and the deserialize error) rather
than silently exiting Ok, using the AgentOutput variable name out and the
WeatherReport/StopReason symbols to locate the logic.
In `@tests/agent_workflow.rs`:
- Around line 200-216: The assertions using f64::EPSILON are too strict for
values ~15–20; replace the epsilon with a looser tolerance (e.g., const TOL: f64
= 1e-9) and update the three checks that compare parsed.left_operand,
parsed.right_operand, and parsed.result to use (value - expected).abs() < TOL
instead of f64::EPSILON so the test tolerates one ULP/serialization variance
while still catching incorrect math or shape errors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ff315663-b349-417d-9e1a-4216029d5b01
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
Cargo.tomlexamples/interactive_math_agent/main.rsexamples/pipelined_math_agent/main.rsexamples/simple_math_agent/main.rsexamples/structured_output_agent/main.rssrc/activities.rssrc/llm.rssrc/prelude.rssrc/state.rssrc/workflow.rstests/agent_workflow.rs
StructuredOutputFormatwith workflows to enable JSON Schema-constrained outputs.structured_output_agentexample demonstrating schema-constrained workflows.schemarsintegration for schema generation.Summary by CodeRabbit
New Features
Chores