Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion docs/api/models/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ async def model_function(
print(info)
"""
AgentInfo(
function_tools=[], allow_text_output=True, output_tools=[], model_settings=None
function_tools=[],
allow_text_output=True,
output_tools=[],
model_settings=None,
model_request_parameters=ModelRequestParameters(
function_tools=[], builtin_tools=[], output_tools=[]
),
instructions=None,
)
"""
return ModelResponse(parts=[TextPart('hello world')])
Expand Down
6 changes: 3 additions & 3 deletions docs/builtin-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ making it ideal for queries that require up-to-date data.
|----------|-----------|-------|
| OpenAI Responses || Full feature support. To include search results on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] that's available via [`ModelResponse.builtin_tool_calls`][pydantic_ai.messages.ModelResponse.builtin_tool_calls], enable the [`OpenAIResponsesModelSettings.openai_include_web_search_sources`][pydantic_ai.models.openai.OpenAIResponsesModelSettings.openai_include_web_search_sources] [model setting](agents.md#model-run-settings). |
| Anthropic || Full feature support |
| Google || No parameter support. No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is generated when streaming. Using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| Google || No parameter support. No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is generated when streaming. Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| Groq || Limited parameter support. To use web search capabilities with Groq, you need to use the [compound models](https://console.groq.com/docs/compound). |
| OpenAI Chat Completions || Not supported |
| Bedrock || Not supported |
Expand Down Expand Up @@ -123,7 +123,7 @@ in a secure environment, making it perfect for computational tasks, data analysi
| Provider | Supported | Notes |
|----------|-----------|-------|
| OpenAI || To include code execution output on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] that's available via [`ModelResponse.builtin_tool_calls`][pydantic_ai.messages.ModelResponse.builtin_tool_calls], enable the [`OpenAIResponsesModelSettings.openai_include_code_execution_outputs`][pydantic_ai.models.openai.OpenAIResponsesModelSettings.openai_include_code_execution_outputs] [model setting](agents.md#model-run-settings). If the code execution generated images, like charts, they will be available on [`ModelResponse.images`][pydantic_ai.messages.ModelResponse.images] as [`BinaryImage`][pydantic_ai.messages.BinaryImage] objects. The generated image can also be used as [image output](output.md#image-output) for the agent run. |
| Google || Using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| Google || Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| Anthropic || |
| Groq || |
| Bedrock || |
Expand Down Expand Up @@ -315,7 +315,7 @@ allowing it to pull up-to-date information from the web.

| Provider | Supported | Notes |
|----------|-----------|-------|
| Google || No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is currently generated; please submit an issue if you need this. Using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| Google || No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is currently generated; please submit an issue if you need this. Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
| OpenAI || |
| Anthropic || |
| Groq || |
Expand Down
29 changes: 17 additions & 12 deletions pydantic_ai_slim/pydantic_ai/_agent_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,10 @@ async def _prepare_request_parameters(
) -> models.ModelRequestParameters:
"""Build tools and create an agent model."""
output_schema = ctx.deps.output_schema
output_object = None
if isinstance(output_schema, _output.NativeOutputSchema):
output_object = output_schema.object_def

prompted_output_template = (
output_schema.template if isinstance(output_schema, _output.PromptedOutputSchema) else None
)

function_tools: list[ToolDefinition] = []
output_tools: list[ToolDefinition] = []
Expand All @@ -391,7 +392,8 @@ async def _prepare_request_parameters(
builtin_tools=ctx.deps.builtin_tools,
output_mode=output_schema.mode,
output_tools=output_tools,
output_object=output_object,
output_object=output_schema.object_def,
prompted_output_template=prompted_output_template,
allow_text_output=output_schema.allows_text,
allow_image_output=output_schema.allows_image,
)
Expand Down Expand Up @@ -489,7 +491,6 @@ async def _prepare_request(
message_history = _clean_message_history(message_history)

model_request_parameters = await _prepare_request_parameters(ctx)
model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)

model_settings = ctx.deps.model_settings
usage = ctx.state.usage
Expand Down Expand Up @@ -570,7 +571,7 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa
# we got an empty response.
# this sometimes happens with anthropic (and perhaps other models)
# when the model has already returned text along side tool calls
if text_processor := output_schema.text_processor:
if text_processor := output_schema.text_processor: # pragma: no branch
# in this scenario, if text responses are allowed, we return text from the most recent model
# response, if any
for message in reversed(ctx.state.message_history):
Expand All @@ -584,8 +585,12 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa
# not part of the final result output, so we reset the accumulated text
text = '' # pragma: no cover
if text:
self._next_node = await self._handle_text_response(ctx, text, text_processor)
return
try:
self._next_node = await self._handle_text_response(ctx, text, text_processor)
return
except ToolRetryError:
# If the text from the preview response was invalid, ignore it.
pass

# Go back to the model request node with an empty request, which means we'll essentially
# resubmit the most recent request that resulted in an empty response,
Expand Down Expand Up @@ -622,11 +627,11 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa
else:
assert_never(part)

# At the moment, we prioritize at least executing tool calls if they are present.
# In the future, we'd consider making this configurable at the agent or run level.
# This accounts for cases like anthropic returns that might contain a text response
# and a tool call response, where the text response just indicates the tool call will happen.
try:
# At the moment, we prioritize at least executing tool calls if they are present.
# In the future, we'd consider making this configurable at the agent or run level.
# This accounts for cases like anthropic returns that might contain a text response
# and a tool call response, where the text response just indicates the tool call will happen.
alternatives: list[str] = []
if tool_calls:
async for event in self._handle_tool_calls(ctx, tool_calls):
Expand Down
125 changes: 20 additions & 105 deletions pydantic_ai_slim/pydantic_ai/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from pydantic import Json, TypeAdapter, ValidationError
from pydantic_core import SchemaValidator, to_json
from typing_extensions import Self, TypedDict, TypeVar, assert_never
from typing_extensions import Self, TypedDict, TypeVar

from pydantic_ai._instrumentation import InstrumentationNames

Expand All @@ -26,7 +26,6 @@
OutputSpec,
OutputTypeOrFunction,
PromptedOutput,
StructuredOutputMode,
TextOutput,
TextOutputFunc,
ToolOutput,
Expand All @@ -36,7 +35,7 @@
from .toolsets.abstract import AbstractToolset, ToolsetTool

if TYPE_CHECKING:
from .profiles import ModelProfile
pass

T = TypeVar('T')
"""An invariant TypeVar."""
Expand Down Expand Up @@ -212,59 +211,30 @@ async def validate(


@dataclass(kw_only=True)
class BaseOutputSchema(ABC, Generic[OutputDataT]):
class OutputSchema(ABC, Generic[OutputDataT]):
text_processor: BaseOutputProcessor[OutputDataT] | None = None
toolset: OutputToolset[Any] | None = None
object_def: OutputObjectDefinition | None = None
allows_deferred_tools: bool = False
allows_image: bool = False

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

@property
def allows_text(self) -> bool:
return self.text_processor is not None


@dataclass(init=False)
class OutputSchema(BaseOutputSchema[OutputDataT], ABC):
"""Model the final output from an agent run."""

@classmethod
@overload
def build(
cls,
output_spec: OutputSpec[OutputDataT],
*,
default_mode: StructuredOutputMode,
name: str | None = None,
description: str | None = None,
strict: bool | None = None,
) -> OutputSchema[OutputDataT]: ...

@classmethod
@overload
def build(
cls,
output_spec: OutputSpec[OutputDataT],
*,
default_mode: None = None,
name: str | None = None,
description: str | None = None,
strict: bool | None = None,
) -> BaseOutputSchema[OutputDataT]: ...

@classmethod
def build( # noqa: C901
cls,
output_spec: OutputSpec[OutputDataT],
*,
default_mode: StructuredOutputMode | None = None,
name: str | None = None,
description: str | None = None,
strict: bool | None = None,
) -> BaseOutputSchema[OutputDataT]:
) -> OutputSchema[OutputDataT]:
"""Build an OutputSchema dataclass from an output type."""
outputs = _flatten_output_spec(output_spec)

Expand Down Expand Up @@ -382,15 +352,12 @@ def build( # noqa: C901
)

if len(other_outputs) > 0:
schema = OutputSchemaWithoutMode(
return AutoOutputSchema(
processor=cls._build_processor(other_outputs, name=name, description=description, strict=strict),
toolset=toolset,
allows_deferred_tools=allows_deferred_tools,
allows_image=allows_image,
)
if default_mode:
schema = schema.with_default_mode(default_mode)
return schema

if allows_image:
return ImageOutputSchema(allows_deferred_tools=allows_deferred_tools)
Expand All @@ -410,22 +377,9 @@ def _build_processor(

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

@property
@abstractmethod
def mode(self) -> OutputMode:
raise NotImplementedError()

def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by this model."""
if self.allows_image and not profile.supports_image_output:
raise UserError('Image output is not supported by this model.')

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


@dataclass(init=False)
class OutputSchemaWithoutMode(BaseOutputSchema[OutputDataT]):
class AutoOutputSchema(OutputSchema[OutputDataT]):
processor: BaseObjectOutputProcessor[OutputDataT]

def __init__(
Expand All @@ -439,32 +393,17 @@ def __init__(
# At that point we may not know yet what output mode we're going to use if no model was provided or it was deferred until agent.run time,
# but we cover ourselves just in case we end up using the tool output mode.
super().__init__(
allows_deferred_tools=allows_deferred_tools,
toolset=toolset,
object_def=processor.object_def,
text_processor=processor,
allows_deferred_tools=allows_deferred_tools,
allows_image=allows_image,
)
self.processor = processor

def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
if mode == 'native':
return NativeOutputSchema(
processor=self.processor,
allows_deferred_tools=self.allows_deferred_tools,
allows_image=self.allows_image,
)
elif mode == 'prompted':
return PromptedOutputSchema(
processor=self.processor,
allows_deferred_tools=self.allows_deferred_tools,
allows_image=self.allows_image,
)
elif mode == 'tool':
return ToolOutputSchema(
toolset=self.toolset, allows_deferred_tools=self.allows_deferred_tools, allows_image=self.allows_image
)
else:
assert_never(mode)
@property
def mode(self) -> OutputMode:
return 'auto'


@dataclass(init=False)
Expand All @@ -486,10 +425,6 @@ def __init__(
def mode(self) -> OutputMode:
return 'text'

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


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

def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by this model."""
# This already raises if image output is not supported by this model.
super().raise_if_unsupported(profile)


@dataclass(init=False)
class StructuredTextOutputSchema(OutputSchema[OutputDataT], ABC):
Expand All @@ -513,25 +443,19 @@ def __init__(
self, *, processor: BaseObjectOutputProcessor[OutputDataT], allows_deferred_tools: bool, allows_image: bool
):
super().__init__(
text_processor=processor, allows_deferred_tools=allows_deferred_tools, allows_image=allows_image
text_processor=processor,
object_def=processor.object_def,
allows_deferred_tools=allows_deferred_tools,
allows_image=allows_image,
)
self.processor = processor

@property
def object_def(self) -> OutputObjectDefinition:
return self.processor.object_def


class NativeOutputSchema(StructuredTextOutputSchema[OutputDataT]):
@property
def mode(self) -> OutputMode:
return 'native'

def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by this model."""
if not profile.supports_json_schema_output:
raise UserError('Native structured output is not supported by this model.')


@dataclass(init=False)
class PromptedOutputSchema(StructuredTextOutputSchema[OutputDataT]):
Expand Down Expand Up @@ -570,14 +494,11 @@ def build_instructions(cls, template: str, object_def: OutputObjectDefinition) -

return template.format(schema=json.dumps(schema))

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

def instructions(self, default_template: str) -> str:
def instructions(self, default_template: str) -> str: # pragma: no cover
"""Get instructions to tell model to output JSON matching the schema."""
template = self.template or default_template
object_def = self.object_def
assert object_def is not None
return self.build_instructions(template, object_def)


Expand All @@ -602,12 +523,6 @@ def __init__(
def mode(self) -> OutputMode:
return 'tool'

def raise_if_unsupported(self, profile: ModelProfile) -> None:
"""Raise an error if the mode is not supported by this model."""
super().raise_if_unsupported(profile)
if not profile.supports_tools:
raise UserError('Tool output is not supported by this model.')


class BaseOutputProcessor(ABC, Generic[OutputDataT]):
@abstractmethod
Expand Down
Loading