Skip to content

feat: add StructuredOutputHook to parse an Agent's final reply into structured output - #13240

Merged
sjrl merged 14 commits into
mainfrom
feat/structured-output-hook
Oct 15, 2026
Merged

sjrl merged 14 commits into
mainfrom
feat/structured-output-hook

Conversation

@sjrl

@sjrl sjrl commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Related Issues

Proposed Changes:

  • Add StructuredOutputHook (haystack.hooks.structured_output), an on_exit hook that parses the Agent's final text reply as JSON, validates it against a Pydantic model or JSON schema, and stores a dict under a State key (structured_output by default). Declaring the key in state_schema makes it an Agent output.
  • Optional max_retries: on invalid JSON or a schema mismatch the hook adds a user message with the error and schema and keeps the Agent running. Retries are counted from tagged correction messages in the current turn and stay bounded by max_agent_steps.
  • Non-text exits (e.g. length cutoffs) and exhausted retries store None and log a warning.
  • Add a Structured Output docs page, the hooks API reference entry, and a release note.

How did you test it?

Unit tests with MockChatGenerator (sync and async); ran the docs examples against gpt-5.4-nano.

Notes for the reviewer

  • On exhausted retries exit_reason stays "text" and only the output is None.

Checklist

  • I have read the contributors guidelines and the code of conduct.
  • I have updated the related issue with new insights and changes.
  • I have added unit tests and updated the docstrings.
  • I've used one of the conventional commit types for my PR title: fix:, feat:, build:, chore:, ci:, docs:, style:, refactor:, perf:, test: and added ! in case the PR includes breaking changes.
  • I have documented my code.
  • I have added a release note file, following the contributors guidelines.
  • I have run pre-commit hooks and fixed any issue.

@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
haystack-docs Ready Ready Preview Oct 15, 2026 1:01pm UTC

Request Review

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  haystack/core/pipeline
  pipeline.py
  haystack/hooks/structured_output
  __init__.py
  hooks.py 180
Project Total  

This report was generated by python-coverage-comment-action

@rautaditya2606

Copy link
Copy Markdown
Contributor

Thanks @sjrl, I tried the hook with a mock generator and live against OpenAI (gpt-4o-mini, and gpt-5.4-nano for the docs examples) and Groq (gpt-oss-20b/120b, qwen3.8-27b).

Works as described: the 18 unit tests pass. Both docs examples run unchanged. I also checked plain JSON, retries, retry exhaustion (None plus a warning), run_async, a tool-calling Agent, tool exit conditions, and the to_dict/from_dict round trip.

One thing I'd flag: replies wrapped in a markdown fence (```json ... ```) aren't parsed. With max_retries=0, gpt-4o-mini fenced all 20 of its replies for "answer as JSON" style prompts, so all gave None. With max_retries=2 it still ended with None in 4 to 6 of 10 runs. Other models fenced less (gpt-oss-120b and qwen3.8-27b between 10% and 100% depending on the prompt) and recovered with retries. Stripping a surrounding fence before json.loads would fix most of this and save the retry calls. These are 10 runs per case, so treat the rates as rough.

Happy to test anything else.

@sjrl

sjrl commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@rautaditya2606 Thanks for testing this! We've added support for replies wrapped in a markdown code fence, which is now removed before parsing.

@sjrl
sjrl marked this pull request as ready for review October 14, 2026 08:06
@sjrl
sjrl requested a review from a team as a code owner October 14, 2026 08:06
@sjrl
sjrl requested review from julian-risch and removed request for a team October 14, 2026 08:06

@julian-risch julian-risch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks quite good to me already in general.
Two edge cases I found with AI code review and we should talk about them:

  1. When the run ends through max_agent_steps or a stop_run hook, the on_exit hook does not run and therefore in those cases result["structured_output"] raises KeyError. What about some fallback? For example running for on_run_end too? Or alternatively at least document that behavior more explicitly?
  2. The hook parses messages[-1] but if another on_exit hook runs before it and adds a user message, the hook parses that message instead of the model's reply. I'd say we can live with that limitation for now but at least documenting this would be good or maybe you have an idea for a simple fix.

@sjrl

sjrl commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

@julian-risch Thanks! For 1, after_run doesn't work for this hook because setting continue_run has no effect there, so retries wouldn't work. I've updated the docstring and docs page to say the key is missing when the run ends through max_agent_steps or stop_run, and to read it with result.get(...).

For 2, the hook now skips parsing when an earlier on_exit hook has already set continue_run, and parses the reply at the next exit instead. Combined with the existing "register the hook last" note, messages[-1] is then the final message the run ends on, which matches last_message. I think this version is more intuitive b/c its possible a user purposefully edited the last message before this hook fires and would want this hook to see the last message in the current chat history.

@julian-risch julian-risch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@sjrl
sjrl enabled auto-merge (squash) October 15, 2026 13:06
@sjrl
sjrl merged commit c9a87a2 into main Oct 15, 2026
26 checks passed
@sjrl
sjrl deleted the feat/structured-output-hook branch October 15, 2026 13:09

This branch was successfully deployed

1 active deployment
Preview — 3d6ff9cb Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 High priority, add to the next sprint topic:tests type:documentation Improvements on the docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support schema-constrained structured outputs with automatic recovery in Agent

3 participants