8000 Toolsets by DouweM · Pull Request #2024 · pydantic/pydantic-ai · GitHub
[go: up one dir, main page]

Skip to content

Toolsets #2024

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
merged 162 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
162 commits
Select commit Hold shift + click to select a range
e290951
WIP: Output modes
DouweM Jun 3, 2025
2056539
WIP: More output modes
DouweM Jun 3, 2025
bceba19
Merge remote-tracking branch 'origin/main' into output-modes
DouweM Jun 3, 2025
0cb25c4
Fix tests
DouweM Jun 3, 2025
933b74e
Remove syntax invalid before Python 3.12
DouweM Jun 3, 2025
7974df0
Fix tests
DouweM Jun 3, 2025
9cc19e2
Add TextOutput marker
DouweM Jun 9, 2025
bc6bb65
Merge remote-tracking branch 'origin/main' into output-modes
DouweM Jun 9, 2025
0e356a3
Add VCR recording of new test
DouweM Jun 9, 2025
81312dc
Implement additional output modes in GeminiModel and GoogleModel
DouweM Jun 10, 2025
52ef4d5
Fix prompted_json on OpenAIResponses
DouweM Jun 10, 2025
fe05956
Test output modes on Gemini and Anthropic
DouweM Jun 10, 2025
94421f3
Add VCR recordings of Gemini output mode tests
DouweM Jun 10, 2025
1902d00
Remove some old TODO comments
DouweM Jun 10, 2025
1f53c9b
Add missing VCR recording of Gemini output mode test
DouweM Jun 10, 2025
a4c2877
Add more missing VCR recordings
DouweM Jun 10, 2025
56e58f9
Fix OpenAI tools
DouweM Jun 10, 2025
a5234e1
Improve test coverage
DouweM Jun 10, 2025
40def08
Update unsupported output mode error message
DouweM Jun 10, 2025
837d305
Improve test coverage
DouweM Jun 10, 2025
3598bef
Merge branch 'main' into output-modes
DouweM Jun 10, 2025
5f71ba8
Test streaming with structured text output
DouweM Jun 10, 2025
cfc2749
Make TextOutputFunction Python 3.9 compatible
DouweM Jun 10, 2025
a137641
Properly merge JSON schemas accounting for defs
DouweM Jun 11, 2025
f495d46
Refactor output schemas and modes: more 'isinstance(output_schema, ..…
DouweM Jun 12, 2025
449ed0d
Merge branch 'main' into output-modes
DouweM Jun 12, 2025
e70d249
Clean up some variable names
DouweM Jun 12, 2025
4592b0b
Improve test coverage
DouweM Jun 12, 2025
db1c628
Merge branch 'main' into output-modes
DouweM Jun 13, 2025
f57d078
Combine JsonSchemaOutput and PromptedJsonOutput into StructuredTextOu…
DouweM Jun 13, 2025
5112455
Add missing cassettes
DouweM Jun 13, 2025
416cc7d
Can't use dataclass kw_only on 3.9
DouweM Jun 13, 2025
4b0e5cf
Improve test coverage
DouweM Jun 13, 2025
094920f
Improve test coverage
DouweM Jun 13, 2025
9f61706
Improve test coverage
DouweM Jun 13, 2025
9f51387
Remove unnecessary coverage ignores
DouweM Jun 13, 2025
9a1e628
Remove unnecessary coverage ignore
DouweM Jun 13, 2025
2b5fa81
Add docs
DouweM Jun 13, 2025
6c4662b
Fix docs refs
DouweM Jun 13, 2025
3ed3431
Fix nested list in docs
DouweM Jun 13, 2025
3d77818
Merge branch 'main' into output-modes
DouweM Jun 17, 2025
a86d7d4
Split StructuredTextOutput into ModelStructuredOutput and PromptedStr…
DouweM Jun 17, 2025
ce985a0
Merge branch 'main' into output-modes
DouweM Jun 17, 2025
71d1655
Fix WrapperModel.profile
DouweM Jun 17, 2025
8c04144
Update output modes docs
DouweM Jun 17, 2025
d78b5f7
Add examples to output mode marker docstrings
DouweM Jun 17, 2025
70d1197
Fix mypy type inference
DouweM Jun 17, 2025
2eb7fd1
Improve test coverage
DouweM Jun 17, 2025
25ccb54
Merge branch 'main' into output-modes
DouweM Jun 17, 2025
9e00c32
Import cast and RunContext in _function_schema
DouweM Jun 17, 2025
7de3c0d
Move RunContext and AgentDepsT into their own module to solve circula…
DouweM Jun 17, 2025
4029fac
Make _run_context module private, RunContext can be accessed through …
DouweM Jun 17, 2025
98bccf2
Merge branch 'main' into output-modes
DouweM Jun 19, 2025
8041cf3
Fix thinking part related tests
DouweM Jun 19, 2025
9bfed04
Implement Toolset
DouweM Jun 20, 2025
0f8da74
Make MCPServer a Toolset
DouweM Jun 20, 2025
8a29836
--no-edit
DouweM Jun 21, 2025
3d2012c
Add MappedToolset
DouweM Jun 21, 2025
901267d
Import Never from typing_extensions instead of typing
DouweM Jun 21, 2025
b9258d7
from __future__ import annotations
DouweM Jun 21, 2025
27ccbd1
Update client.md
DouweM Jun 21, 2025
3031e55
Pass only RunToolset to agent graph
DouweM Jun 21, 2025
ebd0b57
Make WrapperToolset abstract
DouweM Jun 21, 2025
867bf68
Introduce ToolDefinition.kind == 'pending'
DouweM Jun 21, 2025
c1115ae
Rename pending tools to deferred tools
DouweM Jun 21, 2025
6abd603
Merge branch 'main' into toolsets
DouweM Jun 24, 2025
a2f69df
Fix retries
DouweM Jun 24, 2025
0e0bf35
Remove duplicate cassettes
DouweM Jun 24, 2025
735df29
Merge branch 'main' into toolsets
DouweM Jun 26, 2025
8745a7a
Pass just one toolset into the run
DouweM Jun 26, 2025
05aa972
WIP
DouweM Jun 26, 2025
ad6e826
Fix streaming tool calls
DouweM Jun 27, 2025
84cd954
Stop double counting retries and reset on success
DouweM Jun 27, 2025
74a56ae
Fix retry error wrapping
DouweM Jun 27, 2025
0360e77
Make DeferredToolCalls work with streaming
DouweM Jun 30, 2025
6607b00
Merge branch 'main' into toolsets
DouweM Jun 30, 2025
8a3febb
Let toolsets be overridden in run/iter/run_stream/run_sync
DouweM Jun 30, 2025
2e200ac
Add DeferredToolset
DouweM Jun 30, 2025
1cb7f32
Add LangChainToolset
DouweM Jun 30, 2025
a6eba43
Add Agent.prepare_output_tools
DouweM Jun 30, 2025
0c96126
Require WrapperToolset subclasses to implement their own prepare_for_run
DouweM Jul 1, 2025
2348f45
Require DeferredToolCalls to be used with other output type
DouweM Jul 1, 2025
9dc684e
Merge branch 'main' into toolsets
DouweM Jul 1, 2025
f3124c0
Lots of cleanup
DouweM Jul 1, 2025
f660cc1
Some more tweaks
DouweM Jul 2, 2025
64dacbb
Merge branch 'main' into toolsets
DouweM Jul 2, 2025
5ca305e
Fix docs example
DouweM Jul 2, 2025
c5ef5f6
Address some feedback
DouweM Jul 2, 2025
badbe23
Merge branch 'main' into toolsets
DouweM Jul 2, 2025
acddb8d
Add sampling_model to Agent __init__, iter, run (etc), and override, …
DouweM Jul 2, 2025
89fc266
Turn RunContext.retries from a defaultdict into a dict again as the 0…
DouweM Jul 2, 2025
7e3331b
Remove unnecessary if TYPE_CHECKING
DouweM Jul 2, 2025
ebf6f40
Remove Agent sampling_model field (and method argument) in favor of A…
DouweM Jul 3, 2025
f7db040
Allow OutputSpec to be nested
DouweM Jul 3, 2025
fe07149
Document Agent.__aenter__
DouweM Jul 3, 2025
a0f4678
Import Self from typing_extensions instead of typing
DouweM Jul 3, 2025
db82d00
Actually use Agent.prepare_output_tools
DouweM Jul 4, 2025
dea8050
Update test to account for fact that text output with early end_strat…
DouweM Jul 4, 2025
131a325
Improve test coverage
DouweM Jul 4, 2025
8203732
Merge branch 'main' into toolsets
DouweM Jul 4, 2025
778962c
Make Agent MCP-related tests only run when mcp can be imported
DouweM Jul 4, 2025
e6575a9
Add tests
DouweM Jul 4, 2025
9f9ee55
AbstractToolset.call_tool now takes a ToolCallPart
DouweM Jul 4, 2025
a3c9a59
Fix MCP process_tool_call example
DouweM Jul 4, 2025
6eae653
Fix test coverage
DouweM Jul 4, 2025
2b3a9e5
Merge branch 'main' into toolsets
DouweM Jul 4, 2025
b2aa894
Improve coverage
DouweM Jul 4, 2025
ecf6f75
Merge branch 'main' into toolsets
DouweM Jul 8, 2025
1c2d221
Address feedback
DouweM Jul 8, 2025
ca4915b
Make test_docs_examples an async test so Python 3.9 lets us instantia…
DouweM Jul 8, 2025
972e4a7
Merge branch 'main' into toolsets
DouweM Jul 8, 2025
93bb682
Fix test snapshots
DouweM Jul 8, 2025
8a986be
Revert "Make test_docs_examples an async test so Python 3.9 lets us i…
DouweM Jul 8, 2025
9c399c7
Make asyncio.Lock work in Python 3.9 when there is no event loop yet
DouweM Jul 8, 2025
a4f8c48
Address feedback, fix docs test
DouweM Jul 8, 2025
3e1847f
Give the A2A task some more time to complete
DouweM Jul 8, 2025
4daa152
Branch is OK to not be covered
DouweM Jul 8, 2025
c5c6f00
agent.iter(toolsets=...) is now additional, while new agent.override(…
DouweM Jul 9, 2025
f9ba559
Respect overridden toolsets in Agent.__aenter__ and Agent.set_mcp_sam…
DouweM Jul 9, 2025
b165503
Fix tool conflict error message
DouweM Jul 9, 2025
4baa710
Rename FunctionToolset.register_{tool,function} to add_{tool,function}
DouweM Jul 9, 2025
39e0353
Branch is OK to not be covered
DouweM Jul 9, 2025
18fcdf7
Add test to ensure tools can be added during a run
DouweM Jul 9, 2025
af6ce7d
Make CallableToolset public as we're going to want to let people defi…
DouweM Jul 9, 2025
93e6691
Make it easier to override tool call behavior by subclassing WrapperT…
DouweM Jul 10, 2025
3a4c4c8
Start writing docs
DouweM Jul 10, 2025
87aaa6c
Make WrapperToolset easier to subclass with new _rewrap_for_run method
DouweM Jul 10, 2025
8b81e65
Add classes I forget to add and push
DouweM Jul 10, 2025
e72548e
Make all public toolsets importable from pydantic_ai.toolsets
DouweM Jul 10, 2025
b8c93f1
Add ACIToolset
DouweM Jul 10, 2025
f87319c
Document LangChainToolset and ACIToolset
DouweM Jul 10, 2025
8136441
Merge branch 'main' into toolsets
DouweM Jul 10, 2025
239fc3d
Toolset._call_tool is always async
DouweM Jul 10, 2025
febbd08
A WrapperToolset subclass with no additional fields does not need to …
DouweM Jul 10, 2025
cfa9ccc
Add some more docs
DouweM Jul 10, 2025
50a72a0
Merge branch 'main' into toolsets
DouweM Jul 10, 2025
0151e20
Mostly finish docs
DouweM Jul 11, 2025
d27b4ec
Fix FunctionToolset.max_retries
DouweM Jul 11, 2025
692898e
Fix docs example output
DouweM Jul 11, 2025
06838e2
Make AbstractToolset overridable methods public
DouweM Jul 15, 2025
215eaae
Merge branch 'main' into toolsets
DouweM Jul 15, 2025
8018600
Merge remote-tracking branch 'origin/toolsets' into toolsets
DouweM Jul 15, 2025 8000
b2637f8
WIP
DouweM Jul 15, 2025
a25df7f
WIP
DouweM Jul 15, 2025
13d9c03
Remove AbstractToolset.for_run_step
DouweM Jul 15, 2025
ac5f77d
Rename AbstractToolset.accept to apply
DouweM Jul 15, 2025
0c8b25a
Fix toolsets docs
DouweM Jul 15, 2025
6046a1c
Fix example for 3.9
DouweM Jul 15, 2025
70d24da
Update docs
DouweM Jul 15, 2025
a4dedb3
Improve coverage
DouweM Jul 15, 2025
f48bd73
Improve docstrings
DouweM Jul 15, 2025
a4e0c04
Improve docstrings
DouweM Jul 16, 2025
fef897d
Fix docs link
DouweM Jul 16, 2025
9d3d240
Fix docs links
DouweM Jul 16, 2025
f3d1ae0
Add filtered, prefixed, prepared, renamed and wrap convenience method…
DouweM Jul 16, 2025
f756132
Merge branch 'main' into toolsets
DouweM Jul 16, 2025
57b0720
Move tool call tracing to ToolManager
DouweM Jul 16, 2025
e2e0f58
Merge branch 'main' into toolsets
DouweM Jul 16, 2025
7830c73
Fix huggingface_hub.AsyncInferenceClient link in docs
DouweM Jul 16, 2025
c9c8873
Add huggingface doc to nav
DouweM Jul 16, 2025
7e4629e
Fix coverage
DouweM Jul 16, 2025
28c753d
Fix coverage
DouweM Jul 16, 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
Prev Previous commit
Next Next commit
Combine JsonSchemaOutput and PromptedJsonOutput into StructuredTextOu…
…tput
  • Loading branch information
DouweM committed Jun 13, 2025
commit f57d078160de4c9fe8bcd1a2cc1850cc4a2fde8f
5 changes: 2 additions & 3 deletions pydantic_ai_slim/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
)
from .format_prompt import format_as_xml
from .messages import AudioUrl, BinaryContent, DocumentUrl, ImageUrl, VideoUrl
from .result import JsonSchemaOutput, PromptedJsonOutput, ToolOutput
from .result import StructuredTextOutput, ToolOutput
from .tools import RunContext, Tool

__all__ = (
Expand Down Expand Up @@ -43,8 +43,7 @@
'RunContext',
# result
'ToolOutput',
'JsonSchemaOutput',
'PromptedJsonOutput',
'StructuredTextOutput',
# format_prompt
'format_as_xml',
)
Expand Down
19 changes: 16 additions & 3 deletions pydantic_ai_slim/pydantic_ai/_agent_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,25 @@ async def add_mcp_server_tools(server: MCPServer) -> None:
function_tool_defs = await ctx.deps.prepare_tools(run_context, function_tool_defs) or []

output_schema = ctx.deps.output_schema
model_profile = ctx.deps.model.profile

output_tools = []
output_object = None
if isinstance(output_schema, _output.ToolOutputSchema):
output_tools = output_schema.tool_defs()
elif isinstance(output_schema, _output.StructuredTextOutputSchema):
if not output_schema.use_instructions(model_profile):
output_object = output_schema.object_def

# Both ToolOrTextOutputSchema and StructuredTextOutputSchema inherit from TextOutputSchema
allow_text_output = isinstance(output_schema, _output.TextOutputSchema)

return models.ModelRequestParameters(
function_tools=function_tool_defs,
output_mode=output_schema.mode,
output_object=output_schema.object_def if isinstance(output_schema, _output.JsonTextOutputSchema) else None,
output_tools=output_schema.tool_defs() if isinstance(output_schema, _output.ToolOutputSchema) else [],
allow_text_output=isinstance(output_schema, _output.TextOutputSchema),
output_tools=output_tools,
output_object=output_object,
allow_text_output=allow_text_output,
)


Expand Down
167 changes: 74 additions & 93 deletions pydantic_ai_slim/pydantic_ai/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Iterable, Iterator, Sequence
from dataclasses import dataclass, field
from typing import Any, Callable, Generic, Literal, Union, cast, overload
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Union, cast, overload

from pydantic import TypeAdapter, ValidationError
from pydantic_core import SchemaValidator
Expand All @@ -17,6 +17,9 @@
from .exceptions import ModelRetry, UserError
from .tools import AgentDepsT, GenerateToolJsonSchema, ObjectJsonSchema, RunContext, ToolDefinition

if TYPE_CHECKING:
from .profiles import ModelProfile

T = TypeVar('T')
"""An invariant TypeVar."""
OutputDataT_inv = TypeVar('OutputDataT_inv', default=str)
Expand Down Expand Up @@ -145,10 +148,18 @@ class TextOutput(Generic[OutputDataT]):


@dataclass(init=False)
class JsonSchemaOutput(Generic[OutputDataT]):
"""Marker class to use JSON schema output for outputs."""
class StructuredTextOutput(Generic[OutputDataT]):
"""Marker class to use structured text output for outputs."""

outputs: Sequence[OutputTypeOrFunction[OutputDataT]]
instructions: bool | str | None
"""Whether to use the model's built-in functionality for structured output matching a JSON schema, or to pass the JSON schema to the model as instructions.

If `None`, we'll use the model's built-in functionality if it's supported, and otherwise pass the JSON schema to the model as instructions.
If `True`, we'll pass the JSON schema to the model using the instructions template specified on the model's profile.
If `False`, we'll use the model's built-in functionality and raise an error if it's not supported.
If `str`, we'll pass the JSON schema to the model using the specified instructions template.
"""
name: str | None
description: str | None
strict: bool | None
Expand All @@ -160,30 +171,13 @@ def __init__(
name: str | None = None,
description: str | None = None,
strict: bool | None = True,
instructions: bool | str | None = None,
):
self.outputs = flatten_output_spec(type_)
self.name = name
self.description = description
self.strict = strict


class PromptedJsonOutput(Generic[OutputDataT]):
"""Marker class to use prompted JSON mode for outputs."""

outputs: Sequence[OutputTypeOrFunction[OutputDataT]]
name: str | None
description: str | None

def __init__(
self,
type_: OutputTypeOrFunction[OutputDataT] | Sequence[OutputTypeOrFunction[OutputDataT]],
*,
name: str | None = None,
description: str | None = None,
):
self.outputs = flatten_output_spec(type_)
self.name = name
self.description = description
self.instructions = instructions


T_co = TypeVar('T_co', covariant=True)
Expand All @@ -197,9 +191,8 @@ def __init__(
OutputTypeOrFunction[T_co],
ToolOutput[T_co],
TextOutput[T_co],
StructuredTextOutput[T_co],
Sequence[Union[OutputTypeOrFunction[T_co], ToolOutput[T_co], TextOutput[T_co]]],
JsonSchemaOutput[T_co],
PromptedJsonOutput[T_co],
],
type_params=(T_co,),
)
Expand All @@ -213,30 +206,17 @@ def __init__(
type_params=(T_co,),
)


OutputMode = Literal['text', 'tool', 'json_schema', 'prompted_json', 'tool_or_text']
OutputMode = Literal['text', 'tool', 'structured_text', 'tool_or_text']
"""All output modes."""
SupportableOutputMode = Literal['tool', 'json_schema']
"""Output modes that require specific support by a model (class). Used by ModelProfile.output_modes"""
StructuredOutputMode = Literal['tool', 'json_schema', 'prompted_json']
"""Output modes that can be used for any structured output. Used by ModelProfile.default_output_mode"""
StructuredOutputMode = Literal['tool', 'structured_text']
"""Output modes that can be used for structured output. Used by ModelProfile.default_structured_output_mode"""


class BaseOutputSchema(ABC, Generic[OutputDataT]):
@property
@abstractmethod
def mode(self) -> OutputMode | None:
raise NotImplementedError()

@abstractmethod
def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
raise NotImplementedError()

@abstractmethod
def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
raise NotImplementedError()

@property
def tools(self) -> dict[str, OutputTool[OutputDataT]]:
"""Get the tools for this output schema."""
Expand Down Expand Up @@ -269,7 +249,7 @@ def build(
name: str | None = None,
description: str | None = None,
strict: bool | None = None,
) -> OutputSchemaWithoutMode[OutputDataT]: ...
) -> BaseOutputSchema[OutputDataT]: ...

@classmethod
def build(
Expand All @@ -285,19 +265,15 @@ def build(
if output_spec is str:
return PlainTextOutputSchema()

if isinstance(output_spec, JsonSchemaOutput):
return JsonSchemaOutputSchema(
if isinstance(output_spec, StructuredTextOutput):
return StructuredTextOutputSchema(
cls._build_processor(
output_spec.outputs,
name=output_spec.name,
description=output_spec.description,
strict=output_spec.strict,
),
)

if isinstance(output_spec, PromptedJsonOutput):
return PromptedJsonOutputSchema(
cls._build_processor(output_spec.outputs, name=output_spec.name, description=output_spec.description),
instructions=output_spec.instructions,
)

text_outputs: Sequence[type[str] | TextOutput[OutputDataT]] = []
Expand Down Expand Up @@ -407,6 +383,11 @@ def _build_processor(
def mode(self) -> OutputMode:
raise NotImplementedError()

@abstractmethod
def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by the model."""
raise NotImplementedError()

def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
return self

Expand All @@ -424,28 +405,14 @@ def __init__(
self.processor = processor
self._tools = tools

@property
def mode(self) -> None:
return None # pragma: no cover

def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
if mode == 'json_schema':
return JsonSchemaOutputSchema(
self.processor,
)
elif mode == 'prompted_json':
return PromptedJsonOutputSchema(
self.processor,
)
if mode == 'structured_text':
return StructuredTextOutputSchema(self.processor)
elif mode == 'tool':
return ToolOutputSchema(tools=self.tools)
return ToolOutputSchema(self.tools)
else:
assert_never(mode)

def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
return False # pragma: no cover

@property
def tools(self) -> dict[str, OutputTool[OutputDataT]]:
"""Get the tools for this output schema."""
Expand Down Expand Up @@ -474,9 +441,9 @@ class PlainTextOutputSchema(TextOutputSchema[OutputDataT]):
def mode(self) -> OutputMode:
return 'text'

def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
return True
def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by the model."""
pass

async def process(
self,
Expand Down Expand Up @@ -504,9 +471,18 @@ async def process(
)


@dataclass
class JsonTextOutputSchema(TextOutputSchema[OutputDataT], ABC):
@dataclass(init=False)
class StructuredTextOutputSchema(TextOutputSchema[OutputDataT]):
processor: ObjectOutputProcessor[OutputDataT] | UnionOutputProcessor[OutputDataT]
_instructions: bool | str | None = None

def __init__(
self,
processor: ObjectOutputProcessor[OutputDataT] | UnionOutputProcessor[OutputDataT],
instructions: bool | str | None = None,
):
self.processor = processor
self._instructions = instructions

@property
def object_def(self) -> OutputObjectDefinition:
Expand Down Expand Up @@ -536,28 +512,31 @@ async def process(
text, run_context, allow_partial=allow_partial, wrap_validation_errors=wrap_validation_errors
)


class JsonSchemaOutputSchema(JsonTextOutputSchema[OutputDataT]):
@property
def mode(self) -> OutputMode:
return 'json_schema'

def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
return 'json_schema' in supported_modes

return 'structured_text'

def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by the model."""
if self._instructions is False and not profile.supports_json_schema_response_format:
raise UserError('Structured output without using instructions is not supported by the model.')

def use_instructions(self, profile: ModelProfile) -> bool:
if isinstance(self._instructions, bool):
return self._instructions
elif isinstance(self._instructions, str):
return True
else:
return not profile.supports_json_schema_response_format

class PromptedJsonOutputSchema(JsonTextOutputSchema[OutputDataT]):
@property
def mode(self) -> OutputMode:
return 'prompted_json'
def instructions(self, template: str) -> str:
"""Get instructions to tell model to output JSON matching the schema."""
if isinstance(self._instructions, str):
template = self._instructions

def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
return True
if '{schema}' not in template:
raise UserError("Structured output instructions template must contain a '{schema}' placeholder.")

def instructions(self, template: str) -> str:
"""Get instructions for model to output manual JSON matching the schema."""
object_def = self.object_def
schema = object_def.json_schema.copy()
if object_def.name:
Expand All @@ -579,9 +558,10 @@ def __init__(self, tools: dict[str, OutputTool[OutputDataT]]):
def mode(self) -> OutputMode:
return 'tool'

def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
return 'tool' in supported_modes
def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by the model."""
if not profile.supports_tools:
raise UserError('Output tools are not supported by the model.')

@property
def tools(self) -> dict[str, OutputTool[OutputDataT]]:
Expand Down Expand Up @@ -630,9 +610,10 @@ def __init__(
def mode(self) -> OutputMode:
return 'tool_or_text'

def is_supported(self, supported_modes: set[SupportableOutputMode]) -> bool:
"""Whether the mode is supported by the model."""
return 'tool' in supported_modes
def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by the model."""
if not profile.supports_tools:
raise UserError('Output tools are not supported by the model.')


@dataclass
Expand Down
20 changes: 11 additions & 9 deletions pydantic_ai_slim/pydantic_ai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ def __init__(
warnings.warn('`result_retries` is deprecated, use `max_result_retries` instead', DeprecationWarning)
output_retries = result_retries

default_output_mode = self.model.profile.default_output_mode if isinstance(self.model, models.Model) else None
default_output_mode = (
self.model.profile.default_structured_output_mode if isinstance(self.model, models.Model) else None
)
self._output_schema = _output.OutputSchema[OutputDataT].build(
output_type,
default_mode=default_output_mode,
Expand Down Expand Up @@ -676,9 +678,11 @@ async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None:
*[await func.run(run_context) for func in self._instructions_functions],
]

if isinstance(output_schema, _output.PromptedJsonOutputSchema):
template = model_used.profile.prompted_json_output_instructions
instructions = output_schema.instructions(template)
model_profile = model_used.profile
if isinstance(output_schema, _output.StructuredTextOutputSchema) and output_schema.use_instructions(
model_profile
):
instructions = output_schema.instructions(model_profile.structured_output_instructions_template)
parts.append(instructions)

parts = [p for p in parts if p]
Expand Down Expand Up @@ -1651,14 +1655,12 @@ def _prepare_output_schema(
output_type,
name=self._deprecated_result_tool_name,
description=self._deprecated_result_tool_description,
default_mode=model_profile.default_output_mode,
default_mode=model_profile.default_structured_output_mode,
)
else:
schema = self._output_schema.with_default_mode(model_profile.default_output_mode)
schema = self._output_schema.with_default_mode(model_profile.default_structured_output_mode)

if not schema.is_supported(model_profile.output_modes):
modes = ', '.join(f"'{m}'" for m in model_profile.output_modes)
raise exceptions.UserError(f"Output mode '{schema.mode}' is not among supported modes: {modes}")
schema.raise_if_unsupported(model_profile)

return schema # pyright: ignore[reportReturnType]

Expand Down
Loading
Loading
0