-
Notifications
You must be signed in to change notification settings - Fork 56
refactor(providers): extract shared output schema builders into _schema.py #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Jason Robert (jrob5756)
merged 8 commits into
microsoft:main
from
hertznsk:refactor/emit-output-pr2a
Jul 20, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
10dd24b
refactor(providers): add shared output schema builder core
hertznsk d50d9b0
refactor(providers): wire Claude tool schema through shared builder
hertznsk c4a7df1
refactor(providers): wire Hermes prompt schema through shared legacy …
hertznsk 852b043
refactor(providers): wire Copilot prompt schema through shared builder
hertznsk bf63d9b
refactor(providers): wire Claude Agent SDK output format through shar…
hertznsk a231a88
test(providers): add golden regression tests for shared schema builder
hertznsk 7a6d1f5
fix(providers): restore legacy Hermes depth boundary for non-object a…
hertznsk e1cc0cd
chore(providers): drop dead schema wrappers and stale internal refere…
hertznsk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,288 @@ | ||
| """Shared, provider-neutral output-schema builders. | ||
|
|
||
| This private module contains the core logic for turning | ||
| :class:`~conductor.config.schema.OutputField` definitions into | ||
| JSON-Schema fragments and prompt-facing schema fragments. Each provider | ||
| wraps these helpers with its own error type and message formatting. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from conductor.config.schema import OutputField | ||
|
|
||
|
|
||
| class SchemaDepthError(Exception): | ||
| """Raised when output schema nesting exceeds the configured maximum depth.""" | ||
|
|
||
| def __init__(self, depth: int, max_depth: int) -> None: | ||
| """Initialize with the depth that was exceeded. | ||
|
|
||
| Args: | ||
| depth: The current nesting depth that triggered the limit. | ||
| max_depth: The maximum allowed nesting depth. | ||
| """ | ||
| super().__init__(f"Schema nesting depth {depth} exceeds maximum of {max_depth} levels") | ||
| self.depth = depth | ||
| self.max_depth = max_depth | ||
|
|
||
|
|
||
| def _check_depth(depth: int, max_depth: int) -> None: | ||
| """Raise :class:`SchemaDepthError` if ``depth > max_depth``. | ||
|
|
||
| Args: | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| if depth > max_depth: | ||
| raise SchemaDepthError(depth, max_depth) | ||
|
|
||
|
|
||
| def build_json_schema_field( | ||
| field: OutputField, *, depth: int = 0, max_depth: int = 10 | ||
| ) -> dict[str, Any]: | ||
| """Build a JSON-Schema fragment for a single ``OutputField``. | ||
|
|
||
| The fragment contains ``type`` and optionally ``description``, | ||
| ``properties`` + ``required`` (for objects), or ``items`` (for arrays). | ||
|
|
||
| Args: | ||
| field: The output field definition. | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
|
|
||
| Returns: | ||
| A JSON-Schema fragment dictionary. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| _check_depth(depth, max_depth) | ||
|
|
||
| schema: dict[str, Any] = {"type": field.type} | ||
|
|
||
| if field.description: | ||
| schema["description"] = field.description | ||
|
|
||
| if field.type == "object" and field.properties: | ||
| schema["properties"] = build_json_schema_properties( | ||
| field.properties, depth=depth + 1, max_depth=max_depth | ||
| ) | ||
| schema["required"] = list(field.properties.keys()) | ||
|
|
||
| if field.type == "array" and field.items: | ||
| schema["items"] = build_json_schema_field(field.items, depth=depth + 1, max_depth=max_depth) | ||
|
|
||
| return schema | ||
|
|
||
|
|
||
| def build_json_schema_properties( | ||
| fields: dict[str, OutputField], *, depth: int = 0, max_depth: int = 10 | ||
| ) -> dict[str, Any]: | ||
| """Build a JSON-Schema ``properties`` mapping from named ``OutputField`` definitions. | ||
|
|
||
| Args: | ||
| fields: Mapping from field name to output field definition. | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
|
|
||
| Returns: | ||
| A JSON-Schema ``properties`` object. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| _check_depth(depth, max_depth) | ||
|
|
||
| return { | ||
| name: build_json_schema_field(field, depth=depth, max_depth=max_depth) | ||
| for name, field in fields.items() | ||
| } | ||
|
|
||
|
|
||
| def build_prompt_schema_field( | ||
| field: OutputField, | ||
| *, | ||
| field_name: str | None, | ||
| depth: int = 0, | ||
| max_depth: int = 10, | ||
| description_fallback: bool = False, | ||
| ) -> dict[str, Any]: | ||
| """Build a prompt-facing schema fragment for a single ``OutputField``. | ||
|
|
||
| When ``description_fallback`` is true and the field has no explicit | ||
| description and ``field_name`` is provided, the description is filled | ||
| with ``"The {field_name} field"``. Array items are built with | ||
| ``field_name=None`` so they do not gain a fallback description. | ||
|
|
||
| Args: | ||
| field: The output field definition. | ||
| field_name: The field name used for the fallback description, or | ||
| ``None`` to suppress the fallback. | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
| description_fallback: Whether to synthesize a description when one | ||
| is not explicitly set. | ||
|
|
||
| Returns: | ||
| A prompt-facing schema fragment dictionary. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| _check_depth(depth, max_depth) | ||
|
|
||
| description = field.description | ||
| if description_fallback and description is None and field_name is not None: | ||
| description = f"The {field_name} field" | ||
|
|
||
| schema: dict[str, Any] = {"type": field.type} | ||
| if description: | ||
| schema["description"] = description | ||
|
|
||
| if field.type == "object" and field.properties: | ||
| schema["properties"] = build_prompt_schema_properties( | ||
| field.properties, | ||
| depth=depth + 1, | ||
| max_depth=max_depth, | ||
| description_fallback=description_fallback, | ||
| ) | ||
| schema["required"] = list(field.properties.keys()) | ||
|
|
||
| if field.type == "array" and field.items: | ||
| schema["items"] = build_prompt_schema_field( | ||
| field.items, | ||
| field_name=None, | ||
| depth=depth + 1, | ||
| max_depth=max_depth, | ||
| description_fallback=description_fallback, | ||
| ) | ||
|
|
||
| return schema | ||
|
|
||
|
|
||
| def build_prompt_schema_properties( | ||
| fields: dict[str, OutputField], | ||
| *, | ||
| depth: int = 0, | ||
| max_depth: int = 10, | ||
| description_fallback: bool = False, | ||
| ) -> dict[str, Any]: | ||
| """Build a prompt-facing schema mapping from named ``OutputField`` definitions. | ||
|
|
||
| Args: | ||
| fields: Mapping from field name to output field definition. | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
| description_fallback: Whether to synthesize a description when one | ||
| is not explicitly set. | ||
|
|
||
| Returns: | ||
| A prompt-facing schema mapping. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| _check_depth(depth, max_depth) | ||
|
|
||
| return { | ||
| name: build_prompt_schema_field( | ||
| field, | ||
| field_name=name, | ||
| depth=depth, | ||
| max_depth=max_depth, | ||
| description_fallback=description_fallback, | ||
| ) | ||
| for name, field in fields.items() | ||
| } | ||
|
|
||
|
|
||
| def build_hermes_legacy_prompt_schema( | ||
| fields: dict[str, OutputField], *, depth: int = 0, max_depth: int = 10 | ||
| ) -> dict[str, Any]: | ||
| """Build the Hermes legacy prompt-facing schema mapping. | ||
|
|
||
| This matches the legacy Hermes provider behavior: descriptions fall back | ||
| to ``"The {field_name} field"`` at the top level, but array items do not | ||
| receive a fallback description. Unlike the generic prompt builder, object | ||
| items inside arrays are emitted with ``properties`` but no ``required`` | ||
| key, and array-of-array items collapse to ``{"type": "array"}`` without | ||
| further recursion (an explicit item description is still kept). | ||
|
|
||
| Args: | ||
| fields: Mapping from field name to output field definition. | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
|
|
||
| Returns: | ||
| The Hermes legacy prompt-facing schema mapping. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| _check_depth(depth, max_depth) | ||
|
|
||
| result: dict[str, Any] = {} | ||
| for field_name, field_def in fields.items(): | ||
| field_schema: dict[str, Any] = {"type": field_def.type} | ||
|
|
||
| if field_def.description: | ||
| field_schema["description"] = field_def.description | ||
| else: | ||
| field_schema["description"] = f"The {field_name} field" | ||
|
|
||
| if field_def.type == "object" and field_def.properties: | ||
| field_schema["properties"] = build_hermes_legacy_prompt_schema( | ||
| field_def.properties, depth=depth + 1, max_depth=max_depth | ||
| ) | ||
| field_schema["required"] = list(field_def.properties.keys()) | ||
|
|
||
| if field_def.type == "array" and field_def.items: | ||
| field_schema["items"] = _build_hermes_legacy_item_schema( | ||
| field_def.items, depth=depth + 1, max_depth=max_depth | ||
| ) | ||
|
|
||
| result[field_name] = field_schema | ||
|
|
||
| return result | ||
|
|
||
|
|
||
| def _build_hermes_legacy_item_schema( | ||
| field: OutputField, *, depth: int, max_depth: int | ||
| ) -> dict[str, Any]: | ||
| """Build the Hermes legacy schema fragment for an array item. | ||
|
|
||
| Object items include ``properties`` but no ``required``. Array items of | ||
| any kind collapse to the bare ``{"type": "array"}`` shape with no | ||
| inner recursion (an explicit item description is still kept). | ||
|
|
||
| Args: | ||
| field: The array item output field definition. | ||
| depth: Current nesting depth. | ||
| max_depth: Maximum allowed nesting depth. | ||
|
|
||
| Returns: | ||
| The Hermes legacy item schema fragment. | ||
|
|
||
| Raises: | ||
| SchemaDepthError: When the depth limit is exceeded. | ||
| """ | ||
| item_schema: dict[str, Any] = {"type": field.type} | ||
|
|
||
| if field.description: | ||
| item_schema["description"] = field.description | ||
|
|
||
| if field.type == "object" and field.properties: | ||
| _check_depth(depth, max_depth) | ||
| # Pinned legacy counting: the array itself advanced depth by one when | ||
| # calling this helper, so the item's properties must recurse at the | ||
| # same depth (not depth + 1) to match pre-refactor Hermes behavior. | ||
| item_schema["properties"] = build_hermes_legacy_prompt_schema( | ||
| field.properties, depth=depth, max_depth=max_depth | ||
| ) | ||
|
|
||
| return item_schema | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This says array-of-array items collapse "without further recursion or description," but
_build_hermes_legacy_item_schemakeeps the description when the item has one (line 277). Only the recursion into nested items actually gets dropped. Worth tightening the wording so it matches what's actually pinned.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tightened in 7a6d1f5 — both docstrings now say the collapse drops recursion but keeps an explicit item description, matching what's pinned.