Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0ee01f1
Add output_json_schema property to Agents
g-eoj Nov 17, 2025
80dd6e3
Add test outline
g-eoj Nov 17, 2025
7127de6
Fix typecheck errors
g-eoj Nov 17, 2025
291fa1f
Merge branch 'main' into 3225
g-eoj Nov 17, 2025
d1b2399
Capture title and desc for tool output
g-eoj Nov 17, 2025
4454583
Add tests
g-eoj Nov 17, 2025
fdc8820
Fix typecheck errors
g-eoj Nov 17, 2025
93de392
Merge branch 'main' into 3225
g-eoj Nov 17, 2025
79253f1
Change to method
g-eoj Nov 19, 2025
ba9433f
Merge branch 'main' into 3225
g-eoj Nov 19, 2025
09184e2
More test stuff
g-eoj Nov 19, 2025
0d58762
Improve test coverage
g-eoj Nov 19, 2025
0043115
Address feedback
g-eoj Nov 21, 2025
123eabc
Merge branch 'main' into 3225
g-eoj Nov 21, 2025
66f26b5
Fix native output
g-eoj Nov 22, 2025
74ac9b6
Split test file and add more tests
g-eoj Nov 22, 2025
c0bc7fd
Refactor
g-eoj Nov 22, 2025
cff87ad
Skip deferred requests test
g-eoj Nov 23, 2025
c44c9c7
Small fixes and more tests
g-eoj Nov 24, 2025
1530488
Merge branch 'main' into 3225
g-eoj Nov 24, 2025
00c6ddd
Update deferred requests snapshot
g-eoj Nov 24, 2025
30c7b54
Maybe fix test coverage
g-eoj Nov 24, 2025
37f9bed
Simplify code
g-eoj Nov 24, 2025
4e8509f
Make copy of processor json schema
g-eoj Nov 24, 2025
74ec324
Use tool_defs instead of instead of processors
g-eoj Nov 24, 2025
b0b796f
Merge branch 'main' into 3225
DouweM Nov 26, 2025
3be8f95
Revert "Use tool_defs instead of instead of processors"
g-eoj Nov 26, 2025
b92da23
Address feedback
g-eoj Nov 26, 2025
5044fe4
Clean up tests
g-eoj Nov 26, 2025
355573d
Refactor
g-eoj Nov 26, 2025
d0e1e9c
Do not discriminate
g-eoj Nov 28, 2025
f561373
Only include the data and media_type keys for BinaryImage
g-eoj Nov 28, 2025
dad1e17
Revert unneeded changes
g-eoj Nov 28, 2025
597d299
Add test
g-eoj Nov 28, 2025
0b55d53
Merge branch 'main' into 3225
g-eoj Nov 28, 2025
a0e8b24
No duplicate schemas
g-eoj Nov 28, 2025
64b61d1
Use Agent.output_types to construct JSON schema
g-eoj Dec 1, 2025
cedeb8e
Small fixes
g-eoj Dec 2, 2025
4e305d7
Don't modify _output.py
g-eoj Dec 2, 2025
85e929f
Merge branch 'main' into 3225
g-eoj Dec 2, 2025
d78106b
Handle TextOutput
g-eoj Dec 2, 2025
1fd144b
Fix function output
g-eoj Dec 2, 2025
5283df9
Maybe fix coverage
g-eoj Dec 2, 2025
e6bc181
Small refactor to address comments
g-eoj Dec 2, 2025
e2415af
Refactor for clarity
g-eoj Dec 2, 2025
0110d47
Fix BinaryImage
g-eoj Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion pydantic_ai_slim/pydantic_ai/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import TYPE_CHECKING, Any, ClassVar, overload

from opentelemetry.trace import NoOpTracer, use_span
from pydantic import TypeAdapter
from pydantic.json_schema import GenerateJsonSchema
from typing_extensions import Self, TypeVar, deprecated

Expand All @@ -34,7 +35,8 @@
UserPromptNode,
capture_run_messages,
)
from .._output import OutputToolset
from .._json_schema import JsonSchema
from .._output import OutputToolset, _flatten_output_spec # pyright: ignore[reportPrivateUsage]
from .._tool_manager import ToolManager
from ..builtin_tools import AbstractBuiltinTool
from ..models.instrumented import InstrumentationSettings, InstrumentedModel, instrument_model
Expand Down Expand Up @@ -955,6 +957,55 @@ def decorator(
self._system_prompt_functions.append(_system_prompt.SystemPromptRunner[AgentDepsT](func, dynamic=dynamic))
return func

def output_json_schema(self, output_type: OutputSpec[OutputDataT | RunOutputDataT] | None = None) -> JsonSchema:
Copy link
Collaborator

Choose a reason for hiding this comment

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

@g-eoj This is so much cleaner that I'm a little embarrassed I didn't come up with it before.

"""The output return JSON schema."""
if output_type is None:
output_type = self.output_type

# forces output_type to an iterable
output_type = _flatten_output_spec(output_type)

flat_output_type: list[OutputSpec[OutputDataT | RunOutputDataT] | type[str]] = []
for output_spec in output_type:
if isinstance(output_spec, _output.NativeOutput):
flat_output_type += _flatten_output_spec(output_spec.outputs)
elif isinstance(output_spec, _output.PromptedOutput):
flat_output_type += _flatten_output_spec(output_spec.outputs)
elif isinstance(output_spec, _output.TextOutput):
flat_output_type.append(str)
elif isinstance(output_spec, _output.ToolOutput):
flat_output_type += _flatten_output_spec(output_spec.output)
elif inspect.isfunction(output_spec) or inspect.ismethod(output_spec):
return_annotation = inspect.signature(output_spec).return_annotation
flat_output_type += _flatten_output_spec(return_annotation)
else:
flat_output_type.append(output_spec)

json_schemas: list[JsonSchema] = []
for output_spec in flat_output_type:
json_schema = TypeAdapter(output_spec).json_schema(mode='serialization')

if output_spec == _messages.BinaryImage:
properties = json_schema.get('properties', {})
kept_properties = {}
for key, value in properties.items():
if key in ['data', 'media_type']:
kept_properties[key] = value
json_schema['properties'] = kept_properties
json_schema.pop('required', None)

if json_schema not in json_schemas:
json_schemas.append(json_schema)

if len(json_schemas) == 1:
return json_schemas[0]
else:
json_schemas, all_defs = _utils.merge_json_schema_defs(json_schemas)
json_schema: JsonSchema = {'anyOf': json_schemas}
if all_defs:
json_schema['$defs'] = all_defs
return json_schema

@overload
def output_validator(
self, func: Callable[[RunContext[AgentDepsT], OutputDataT], OutputDataT], /
Expand Down
6 changes: 6 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
result,
usage as _usage,
)
from .._json_schema import JsonSchema
from .._tool_manager import ToolManager
from ..builtin_tools import AbstractBuiltinTool
from ..output import OutputDataT, OutputSpec
Expand Down Expand Up @@ -122,6 +123,11 @@ def toolsets(self) -> Sequence[AbstractToolset[AgentDepsT]]:
"""
raise NotImplementedError

@abstractmethod
def output_json_schema(self, output_type: OutputSpec[OutputDataT | RunOutputDataT] | None = None) -> JsonSchema:
"""The output JSON schema."""
raise NotImplementedError

@overload
async def run(
self,
Expand Down
4 changes: 4 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
models,
usage as _usage,
)
from .._json_schema import JsonSchema
from ..builtin_tools import AbstractBuiltinTool
from ..output import OutputDataT, OutputSpec
from ..run import AgentRun
Expand Down Expand Up @@ -67,6 +68,9 @@ async def __aenter__(self) -> AbstractAgent[AgentDepsT, OutputDataT]:
async def __aexit__(self, *args: Any) -> bool | None:
return await self.wrapped.__aexit__(*args)

def output_json_schema(self, output_type: OutputSpec[OutputDataT | RunOutputDataT] | None = None) -> JsonSchema:
return self.wrapped.output_json_schema(output_type=output_type)

@overload
def iter(
self,
Expand Down
5 changes: 4 additions & 1 deletion pydantic_ai_slim/pydantic_ai/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,14 +611,17 @@ def format(self) -> str:
__repr__ = _utils.dataclasses_no_defaults_repr


@dataclass(init=False, repr=False)
class BinaryImage(BinaryContent):
"""Binary content that's guaranteed to be an image."""

media_type: ImageMediaType | str

def __init__(
self,
data: bytes,
*,
media_type: str,
media_type: ImageMediaType | str,
identifier: str | None = None,
vendor_metadata: dict[str, Any] | None = None,
# Required for inline-snapshot which expects all dataclass `__init__` methods to take all field names as kwargs.
Expand Down
9 changes: 9 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5248,6 +5248,15 @@ def foo() -> str:
assert wrapper_agent.name == 'wrapped'
assert wrapper_agent.output_type == agent.output_type
assert wrapper_agent.event_stream_handler == agent.event_stream_handler
assert wrapper_agent.output_json_schema() == snapshot(
{
'type': 'object',
'properties': {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'string'}},
'title': 'Foo',
'required': ['a', 'b'],
}
)
assert wrapper_agent.output_json_schema(output_type=str) == snapshot({'type': 'string'})

bar_toolset = FunctionToolset()

Expand Down
Loading