Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
53 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
b125e1b
Better solution for BinaryImage custom JSON schema
g-eoj Dec 8, 2025
6ede66f
Merge branch 'main' into 3225
g-eoj Dec 8, 2025
604c681
Fix coverage
g-eoj Dec 8, 2025
199afe7
Revert changes to BinaryImage
g-eoj Dec 10, 2025
14a198f
Refactor based on comments
g-eoj Dec 10, 2025
3f3b695
Merge branch 'main' into 3225
g-eoj Dec 10, 2025
47c5ea6
Revert changes to agent init
g-eoj Dec 10, 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
80 changes: 79 additions & 1 deletion pydantic_ai_slim/pydantic_ai/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload

from pydantic import Json, TypeAdapter, ValidationError
Expand All @@ -15,6 +16,7 @@
from pydantic_ai._instrumentation import InstrumentationNames

from . import _function_schema, _utils, messages as _messages
from ._json_schema import JsonSchema
from ._run_context import AgentDepsT, RunContext
from .exceptions import ModelRetry, ToolRetryError, UserError
from .output import (
Expand Down Expand Up @@ -226,6 +228,10 @@ def mode(self) -> OutputMode:
def allows_text(self) -> bool:
return self.text_processor is not None

@cached_property
def json_schema(self) -> JsonSchema:
raise NotImplementedError()

@classmethod
def build( # noqa: C901
cls,
Expand Down Expand Up @@ -353,7 +359,9 @@ def build( # noqa: C901

if len(other_outputs) > 0:
return AutoOutputSchema(
processor=cls._build_processor(other_outputs, name=name, description=description, strict=strict),
processor=cls._build_processor(
outputs=other_outputs, name=name, description=description, strict=strict
),
toolset=toolset,
allows_deferred_tools=allows_deferred_tools,
allows_image=allows_image,
Expand All @@ -377,6 +385,56 @@ def _build_processor(

return UnionOutputProcessor(outputs=outputs, strict=strict, name=name, description=description)

def build_json_schema(self) -> JsonSchema: # noqa: C901
# allow any output with {'type': 'string'} if no constraints
if not any([self.allows_deferred_tools, self.allows_image, self.object_def, self.toolset]):
return TypeAdapter(str).json_schema()

json_schemas: list[ObjectJsonSchema] = []

processor = getattr(self, 'processor', None)
if isinstance(processor, ObjectOutputProcessor):
json_schema = processor.object_def.json_schema
if k := processor.outer_typed_dict_key:
json_schema = json_schema['properties'][k]
json_schemas.append(json_schema)

elif self.toolset:
if self.allows_text:
json_schema = TypeAdapter(str).json_schema()
json_schemas.append(json_schema)
for tool_processor in self.toolset.processors.values():
json_schema = tool_processor.object_def.json_schema
if k := tool_processor.outer_typed_dict_key:
json_schema = json_schema['properties'][k]
if json_schema not in json_schemas:
json_schemas.append(json_schema)

elif self.allows_text:
json_schema = TypeAdapter(str).json_schema()
json_schemas.append(json_schema)

if self.allows_deferred_tools:
json_schema = TypeAdapter(DeferredToolRequests).json_schema(mode='serialization')
if json_schema not in json_schemas:
json_schemas.append(json_schema)

if self.allows_image:
json_schema = TypeAdapter(_messages.BinaryImage).json_schema()
json_schema = {k: v for k, v in json_schema['properties'].items() if k in ['data', 'media_type']}
if json_schema not in json_schemas:
json_schemas.append(json_schema)

if len(json_schemas) == 1:
return json_schemas[0]

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


@dataclass(init=False)
class AutoOutputSchema(OutputSchema[OutputDataT]):
Expand Down Expand Up @@ -405,6 +463,10 @@ def __init__(
def mode(self) -> OutputMode:
return 'auto'

@cached_property
def json_schema(self) -> JsonSchema:
return self.build_json_schema()


@dataclass(init=False)
class TextOutputSchema(OutputSchema[OutputDataT]):
Expand All @@ -425,6 +487,10 @@ def __init__(
def mode(self) -> OutputMode:
return 'text'

@cached_property
def json_schema(self) -> JsonSchema:
return self.build_json_schema()


class ImageOutputSchema(OutputSchema[OutputDataT]):
def __init__(self, *, allows_deferred_tools: bool):
Expand All @@ -434,6 +500,10 @@ def __init__(self, *, allows_deferred_tools: bool):
def mode(self) -> OutputMode:
return 'image'

@cached_property
def json_schema(self) -> JsonSchema:
return self.build_json_schema()


@dataclass(init=False)
class StructuredTextOutputSchema(OutputSchema[OutputDataT], ABC):
Expand All @@ -450,6 +520,10 @@ def __init__(
)
self.processor = processor

@cached_property
def json_schema(self) -> JsonSchema:
return self.build_json_schema()


class NativeOutputSchema(StructuredTextOutputSchema[OutputDataT]):
@property
Expand Down Expand Up @@ -516,6 +590,10 @@ def __init__(
def mode(self) -> OutputMode:
return 'tool'

@cached_property
def json_schema(self) -> JsonSchema:
return self.build_json_schema()


class BaseOutputProcessor(ABC, Generic[OutputDataT]):
@abstractmethod
Expand Down
6 changes: 6 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
UserPromptNode,
capture_run_messages,
)
from .._json_schema import JsonSchema
from .._output import OutputToolset
from .._tool_manager import ToolManager
from ..builtin_tools import AbstractBuiltinTool
Expand Down Expand Up @@ -955,6 +956,11 @@ def decorator(
self._system_prompt_functions.append(_system_prompt.SystemPromptRunner[AgentDepsT](func, dynamic=dynamic))
return func

def output_json_schema(self, output_type: OutputSpec[RunOutputDataT] | None = None) -> JsonSchema:
"""The output JSON schema."""
output_schema = self._prepare_output_schema(output_type)
return output_schema.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[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[RunOutputDataT] | None = None) -> JsonSchema:
return self.wrapped.output_json_schema(output_type=output_type)

@overload
def iter(
self,
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': {'type': 'integer'}, '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
Loading