Skip to content

feat: support structured output parsing - #26

Open
rk042 wants to merge 2 commits into
Vedantgitbot:mainfrom
rk042:fix/issue-6-structured-output
Open

rk042 wants to merge 2 commits into
Vedantgitbot:mainfrom
rk042:fix/issue-6-structured-output

Conversation

@rk042

@rk042 rk042 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

What does this change?

Adds a structured-output escape hatch for check() and acheck().

Callers can now either:

  • Return a pre-parsed response dictionary from call_fn.
  • Supply a synchronous parse_fn for provider-specific string decoding.

Both paths use the existing Attempt, validation, retry, confidence, ambiguity, and BoothResult pipeline.

Fixes #6

Why?

Some providers already return structured data, so forcing those responses through regex and raw_decode string extraction is unnecessary and fragile.

This keeps the current architecture intact:

call_fn → parse/normalize → Attempt → existing evaluation → BoothResult

There are no prompt changes, AI repair behaviors, provider-specific integrations, or new runtime dependencies.

## Checklist

- [x] Full test suite passes: 390 passed, 6 skipped.
- [x] Mypy passes: no issues found in 2 source files.
- [x] Added sync and async structured-output tests.
- [x] Existing tests were updated only where dictionary returns are now intentionally supported.
- [x] Added tutorial documentation.
- [x] Added a CHANGELOG entry for the public API change.

## Anything else worth knowing?

parse_fn is synchronous for both check() and acheck(). Existing string responses continue using the default parser.

@Vedantgitbot

Copy link
Copy Markdown
Owner

I ran the branch: 403 tests pass and mypy is clean, so nothing is broken. The part I want to decide separately is dict returns from call_fn. On 0.5.6, lambda p: {"id": "x", "choices": [...]} gives call_failed=True ("expected str"). On this branch it gives UNCERTAIN with call_failed=False and "missing the required 'answer' or 'confidence' key". That changes what call_failed means, and the most common mistake (returning a whole API response) now looks like a model parse failure.

Could you split the PR?

  1. parse_fn only, as the issue describes, with ParseFn = Callable[[str], Optional[dict]].
  2. Dict returns as a follow-up, once I've settled the semantics (a dict missing the required keys, and a dict plus parse_fn).

@Vedantgitbot Vedantgitbot left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @rk042, good work overall. Details of the dict-return concern are in my comment above. Requested changes for the split-out parse_fn PR:

Scope

  • Remove the dict-return path (call_fn returning a dict); it goes in a follow-up PR once I've decided the semantics. Restore the v0.5.4 behavior and tests: a dict return is a call failure (call_failed=True, "expected str"), and {"answer": "x"} stays in the parametrized list in test_v054_regressions.py. The test_v048_regressions.py rename and rewrite should be reverted too.
  • ParseFn = Callable[[str], Optional[dict]]. A None result is a parse failure, as in the issue.

Tests (use import booth and pytest.raises, no sys.path.insert)

  • non-callable parse_fn raises TypeError before any call_fn call
  • acheck() with parse_fn
  • parse_fn returning None, and a non-dict value, is a parse failure (not call_failed)
  • parse_fn result that fails validation (for example confidence out of range)
  • "ambiguous": true from parse_fn gives AMBIGUOUS
  • parse_fn=None leaves default behavior unchanged

Docs

  • TUTORIAL: add parse_fn and ParseFn as new sub-sections without renumbering existing ones, and add parse_fn to the quick-reference table.
  • Say clearly that retry prompts are unchanged: after a failed parse_fn result the JSON-style retry prompt is still sent, so structured-output users may prefer max_retries=0.
  • CHANGELOG: one entry under [Unreleased], no version bump (this ships in a minor release).

Process

  • For changes to core.py, please wait for a decision on the issue before opening a PR, since #6 had an open design question.

Happy to re-review quickly once it's updated.

@rk042

rk042 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

hey @Vedantgitbot ,

Thanks for clarifying. I understand the design question is how direct dict returns from call_fn should behave.

Option A: keep PR #26 parse_fn-only. call_fn must return str, parse_fn converts it to dict, and direct dict returns keep the v0.5.4 behavior: call_failed=True, "expected str".

Option B: support direct dict returns in a follow-up, after defining the behavior for missing keys and dict + parse_fn.

I’ll proceed with Option A for PR #26 and leave direct dict returns out until Issue #6 is decided.

@Vedantgitbot

Copy link
Copy Markdown
Owner

Thanks @rk042, assigned you on #6. I've posted the design decision there: parse_fn only, call_fn stays str, retry prompts unchanged, no new dependencies, shipping in a minor release. Please follow the checklist from my earlier review, and ping me when it's updated.

@rk042

rk042 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, understood. I’ll update PR #26 according to the checklist and ping you once the changes, tests, mypy, and documentation are complete.

@rk042

rk042 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @rk042, assigned you on #6. I've posted the design decision there: parse_fn only, call_fn stays str, retry prompts unchanged, no new dependencies, shipping in a minor release. Please follow the checklist from my earlier review, and ping me when it's updated.

@Vedantgitbot Thanks for the design decision. I updated PR #26 according to the checklist:

  • Kept the change parse_fn-only; call_fn still returns str.
  • Restored direct dict returns as call failures with “expected str”.
  • Added the requested sync, async, validation, ambiguity, and retry tests.
  • Updated the tutorial and changelog without a version bump or new dependency.

Verification: 396 tests passed, 6 skipped, and mypy is clean. Ready for re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add parse_fn for structured output (bypass regex/raw_decode JSON scraping)

2 participants