diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2b440ec01..9c79272951 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,6 +60,6 @@ repos: rev: v2.3.0 hooks: - id: codespell - args: ['--skip', 'tests/models/cassettes/*,docs/a2a/fasta2a.md'] + args: ['--skip', 'tests/models/cassettes/*,docs/a2a/fasta2a.md,tests/models/test_groq.py'] additional_dependencies: - tomli diff --git a/pydantic_ai_slim/pydantic_ai/_agent_graph.py b/pydantic_ai_slim/pydantic_ai/_agent_graph.py index 1c83a28520..bd62b0d81c 100644 --- a/pydantic_ai_slim/pydantic_ai/_agent_graph.py +++ b/pydantic_ai_slim/pydantic_ai/_agent_graph.py @@ -12,6 +12,7 @@ from opentelemetry.trace import Tracer from typing_extensions import TypeGuard, TypeVar, assert_never +from pydantic_ai.builtin_tools import AbstractBuiltinTool from pydantic_graph import BaseNode, Graph, GraphRunContext from pydantic_graph.nodes import End, NodeRunEndT @@ -94,6 +95,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]): output_validators: list[_output.OutputValidator[DepsT, OutputDataT]] function_tools: dict[str, Tool[DepsT]] = dataclasses.field(repr=False) + builtin_tools: list[AbstractBuiltinTool] = dataclasses.field(repr=False) mcp_servers: Sequence[MCPServer] = dataclasses.field(repr=False) default_retries: int @@ -262,6 +264,7 @@ async def add_mcp_server_tools(server: MCPServer) -> None: output_schema = ctx.deps.output_schema return models.ModelRequestParameters( function_tools=function_tool_defs, + builtin_tools=ctx.deps.builtin_tools, allow_text_output=_output.allow_text_output(output_schema), output_tools=output_schema.tool_defs() if output_schema is not None else [], ) @@ -414,7 +417,7 @@ async def stream( async for _event in stream: pass - async def _run_stream( + async def _run_stream( # noqa C901 self, ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]] ) -> AsyncIterator[_messages.HandleResponseEvent]: if self._events_iterator is None: @@ -430,6 +433,10 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: texts.append(part.content) elif isinstance(part, _messages.ToolCallPart): tool_calls.append(part) + elif isinstance(part, _messages.ServerToolCallPart): + yield _messages.ServerToolCallEvent(part) + elif isinstance(part, _messages.ServerToolReturnPart): + yield _messages.ServerToolResultEvent(part) else: assert_never(part) diff --git a/pydantic_ai_slim/pydantic_ai/_parts_manager.py b/pydantic_ai_slim/pydantic_ai/_parts_manager.py index aabd9f03ae..7ff9762fc1 100644 --- a/pydantic_ai_slim/pydantic_ai/_parts_manager.py +++ b/pydantic_ai_slim/pydantic_ai/_parts_manager.py @@ -23,6 +23,8 @@ ModelResponseStreamEvent, PartDeltaEvent, PartStartEvent, + ServerToolCallPart, + ServerToolCallPartDelta, TextPart, TextPartDelta, ToolCallPart, @@ -36,11 +38,11 @@ Type alias for a vendor identifier, which can be any hashable type (e.g., a string, UUID, etc.) """ -ManagedPart = Union[ModelResponsePart, ToolCallPartDelta] +ManagedPart = Union[ModelResponsePart, ToolCallPartDelta, ServerToolCallPartDelta] """ A union of types that are managed by the ModelResponsePartsManager. Because many vendors have streaming APIs that may produce not-fully-formed tool calls, -this includes ToolCallPartDelta's in addition to the more fully-formed ModelResponsePart's. +this includes ToolCallPartDelta's and ServerToolCallPartDelta's in addition to the more fully-formed ModelResponsePart's. """ @@ -57,12 +59,12 @@ class ModelResponsePartsManager: """Maps a vendor's "part" ID (if provided) to the index in `_parts` where that part resides.""" def get_parts(self) -> list[ModelResponsePart]: - """Return only model response parts that are complete (i.e., not ToolCallPartDelta's). + """Return only model response parts that are complete (i.e., not ToolCallPartDelta's or ServerToolCallPartDelta's). Returns: - A list of ModelResponsePart objects. ToolCallPartDelta objects are excluded. + A list of ModelResponsePart objects. ToolCallPartDelta and ServerToolCallPartDelta objects are excluded. """ - return [p for p in self._parts if not isinstance(p, ToolCallPartDelta)] + return [p for p in self._parts if not isinstance(p, (ToolCallPartDelta, ServerToolCallPartDelta))] def handle_text_delta( self, @@ -245,3 +247,89 @@ def handle_tool_call_part( self._parts.append(new_part) self._vendor_id_to_part_index[vendor_part_id] = new_part_index return PartStartEvent(index=new_part_index, part=new_part) + + def handle_server_tool_call_delta( + self, + *, + vendor_part_id: Hashable | None, + tool_name: str | None, + args: str | dict[str, Any] | None, + tool_call_id: str | None, + model_name: str | None, + ) -> ModelResponseStreamEvent | None: + """Handle or update a server tool call, creating or updating a `ServerToolCallPart` or `ServerToolCallPartDelta`. + + Managed items remain as `ServerToolCallPartDelta`s until they have at least a tool_name, at which + point they are upgraded to `ServerToolCallPart`s. + + If `vendor_part_id` is None, updates the latest matching ServerToolCallPart (or ServerToolCallPartDelta) + if any. Otherwise, a new part (or delta) may be created. + + Args: + vendor_part_id: The ID the vendor uses for this server tool call. + If None, the latest matching server tool call may be updated. + tool_name: The name of the server tool. If None, the manager does not enforce + a name match when `vendor_part_id` is None. + args: Arguments for the server tool call, either as a string, a dictionary of key-value pairs, or None. + tool_call_id: An optional string representing an identifier for this server tool call. + model_name: An optional string representing the model name that generated this server tool call. + + Returns: + - A `PartStartEvent` if a new ServerToolCallPart is created. + - A `PartDeltaEvent` if an existing part is updated. + - `None` if no new event is emitted (e.g., the part is still incomplete). + + Raises: + UnexpectedModelBehavior: If attempting to apply a server tool call delta to a part that is not + a ServerToolCallPart or ServerToolCallPartDelta. + """ + existing_matching_part_and_index: tuple[ServerToolCallPartDelta | ServerToolCallPart, int] | None = None + + if vendor_part_id is None: + # vendor_part_id is None, so check if the latest part is a matching server tool call or delta to update + # When the vendor_part_id is None, if the tool_name is _not_ None, assume this should be a new part rather + # than a delta on an existing one. We can change this behavior in the future if necessary for some model. + if tool_name is None and self._parts: + part_index = len(self._parts) - 1 + latest_part = self._parts[part_index] + if isinstance(latest_part, (ServerToolCallPart, ServerToolCallPartDelta)): # pragma: no branch + existing_matching_part_and_index = latest_part, part_index + else: + # vendor_part_id is provided, so look up the corresponding part or delta + part_index = self._vendor_id_to_part_index.get(vendor_part_id) + if part_index is not None: + existing_part = self._parts[part_index] + if not isinstance(existing_part, (ServerToolCallPartDelta, ServerToolCallPart)): + raise UnexpectedModelBehavior(f'Cannot apply a server tool call delta to {existing_part=}') + existing_matching_part_and_index = existing_part, part_index + + if existing_matching_part_and_index is None: + # No matching part/delta was found, so create a new ServerToolCallPartDelta (or ServerToolCallPart if fully formed) + delta = ServerToolCallPartDelta( + tool_name_delta=tool_name, args_delta=args, tool_call_id=tool_call_id, model_name=model_name + ) + part = delta.as_part() or delta + if vendor_part_id is not None: + self._vendor_id_to_part_index[vendor_part_id] = len(self._parts) + new_part_index = len(self._parts) + self._parts.append(part) + # Only emit a PartStartEvent if we have enough information to produce a full ServerToolCallPart + if isinstance(part, ServerToolCallPart): + return PartStartEvent(index=new_part_index, part=part) + else: + # Update the existing part or delta with the new information + existing_part, part_index = existing_matching_part_and_index + delta = ServerToolCallPartDelta( + tool_name_delta=tool_name, args_delta=args, tool_call_id=tool_call_id, model_name=model_name + ) + updated_part = delta.apply(existing_part) + self._parts[part_index] = updated_part + if isinstance(updated_part, ServerToolCallPart): + if isinstance(existing_part, ServerToolCallPartDelta): + # We just upgraded a delta to a full part, so emit a PartStartEvent + return PartStartEvent(index=part_index, part=updated_part) + else: + # We updated an existing part, so emit a PartDeltaEvent + if updated_part.tool_call_id and not delta.tool_call_id: + delta = replace(delta, tool_call_id=updated_part.tool_call_id) + return PartDeltaEvent(index=part_index, delta=delta) diff --git a/pydantic_ai_slim/pydantic_ai/_utils.py b/pydantic_ai_slim/pydantic_ai/_utils.py index 77c34fbacf..aa80f0b976 100644 --- a/pydantic_ai_slim/pydantic_ai/_utils.py +++ b/pydantic_ai_slim/pydantic_ai/_utils.py @@ -205,7 +205,13 @@ def now_utc() -> datetime: return datetime.now(tz=timezone.utc) -def guard_tool_call_id(t: _messages.ToolCallPart | _messages.ToolReturnPart | _messages.RetryPromptPart) -> str: +def guard_tool_call_id( + t: _messages.ToolCallPart + | _messages.ToolReturnPart + | _messages.RetryPromptPart + | _messages.ServerToolCallPart + | _messages.ServerToolReturnPart, +) -> str: """Type guard that either returns the tool call id or generates a new one if it's None.""" return t.tool_call_id or generate_tool_call_id() diff --git a/pydantic_ai_slim/pydantic_ai/agent.py b/pydantic_ai_slim/pydantic_ai/agent.py index e8a636d321..91be80b8c7 100644 --- a/pydantic_ai_slim/pydantic_ai/agent.py +++ b/pydantic_ai_slim/pydantic_ai/agent.py @@ -14,6 +14,7 @@ from pydantic.json_schema import GenerateJsonSchema from typing_extensions import Literal, Never, Self, TypeIs, TypeVar, deprecated +from pydantic_ai.builtin_tools import AbstractBuiltinTool, WebSearchTool from pydantic_graph import End, Graph, GraphRun, GraphRunContext from pydantic_graph._utils import get_event_loop @@ -174,6 +175,7 @@ def __init__( retries: int = 1, output_retries: int | None = None, tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (), + builtin_tools: Sequence[Literal['web-search'] | AbstractBuiltinTool] = (), prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None, mcp_servers: Sequence[MCPServer] = (), defer_model_check: bool = False, @@ -203,6 +205,7 @@ def __init__( result_tool_description: str | None = None, result_retries: int | None = None, tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (), + builtin_tools: Sequence[Literal['web-search'] | AbstractBuiltinTool] = (), prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None, mcp_servers: Sequence[MCPServer] = (), defer_model_check: bool = False, @@ -227,6 +230,7 @@ def __init__( retries: int = 1, output_retries: int | None = None, tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (), + builtin_tools: Sequence[Literal['web-search'] | AbstractBuiltinTool] = (), prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None, mcp_servers: Sequence[MCPServer] = (), defer_model_check: bool = False, @@ -256,6 +260,8 @@ def __init__( output_retries: The maximum number of retries to allow for result validation, defaults to `retries`. tools: Tools to register with the agent, you can also register tools via the decorators [`@agent.tool`][pydantic_ai.Agent.tool] and [`@agent.tool_plain`][pydantic_ai.Agent.tool_plain]. + builtin_tools: The builtin tools that the agent will use. This depends on the model, as some models may not + support certain tools. On models that don't support certain tools, the tool will be ignored. prepare_tools: custom method to prepare the tool definition of all tools for each step. This is useful if you want to customize the definition of multiple tools or you want to register a subset of tools for a given step. See [`ToolsPrepareFunc`][pydantic_ai.tools.ToolsPrepareFunc] @@ -342,6 +348,14 @@ def __init__( self._default_retries = retries self._max_result_retries = output_retries if output_retries is not None else retries self._mcp_servers = mcp_servers + self._builtin_tools: list[AbstractBuiltinTool] = [] + + for tool in builtin_tools: + if tool == 'web-search': + self._builtin_tools.append(WebSearchTool()) + else: + self._builtin_tools.append(tool) + self._prepare_tools = prepare_tools for tool in tools: if isinstance(tool, Tool): @@ -689,7 +703,8 @@ async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None: end_strategy=self.end_strategy, output_schema=output_schema, output_validators=output_validators, - function_tools=run_function_tools, + function_tools=self._function_tools, + builtin_tools=self._builtin_tools, mcp_servers=self._mcp_servers, default_retries=self._default_retries, tracer=tracer, diff --git a/pydantic_ai_slim/pydantic_ai/builtin_tools.py b/pydantic_ai_slim/pydantic_ai/builtin_tools.py new file mode 100644 index 0000000000..46cc7b6286 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/builtin_tools.py @@ -0,0 +1,95 @@ +from __future__ import annotations as _annotations + +from abc import ABC +from dataclasses import dataclass +from typing import Any, Literal + +from typing_extensions import TypedDict + +__all__ = ('AbstractBuiltinTool', 'WebSearchTool', 'UserLocation') + + +@dataclass +class AbstractBuiltinTool(ABC): + """A builtin tool that can be used by an agent. + + This class is abstract and cannot be instantiated directly. + + The builtin tools are passed to the model as part of the `ModelRequestParameters`. + """ + + def handle_custom_tool_definition(self, model: str) -> Any: ... + + +@dataclass +class WebSearchTool(AbstractBuiltinTool): + """A builtin tool that allows your agent to search the web for information. + + The parameters that PydanticAI passes depend on the model, as some parameters may not be supported by certain models. + """ + + search_context_size: Literal['low', 'medium', 'high'] = 'medium' + """The `search_context_size` parameter controls how much context is retrieved from the web to help the tool formulate a response. + + Supported by: + * OpenAI + """ + + user_location: UserLocation | None = None + """The `user_location` parameter allows you to localize search results based on a user's location. + + Supported by: + * Anthropic + * OpenAI + """ + + blocked_domains: list[str] | None = None + """If provided, these domains will never appear in results. + + With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both. + + Supported by: + * Anthropic (https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#domain-filtering) + * Groq (https://console.groq.com/docs/agentic-tooling#search-settings) + * MistralAI + """ + + allowed_domains: list[str] | None = None + """If provided, only these domains will be included in results. + + With Anthropic, you can only use one of `blocked_domains` or `allowed_domains`, not both. + + Supported by: + * Anthropic (https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool#domain-filtering) + * Groq (https://console.groq.com/docs/agentic-tooling#search-settings) + """ + + max_uses: int | None = None + """If provided, the tool will stop searching the web after the given number of uses. + + Supported by: + * Anthropic + """ + + +class UserLocation(TypedDict, total=False): + """Allows you to localize search results based on a user's location. + + Supported by: + * Anthropic + * OpenAI + """ + + city: str + country: str + region: str + timezone: str + + +class CodeExecutionTool(AbstractBuiltinTool): + """A builtin tool that allows your agent to execute code. + + Supported by: + * Anthropic + * OpenAI + """ diff --git a/pydantic_ai_slim/pydantic_ai/messages.py b/pydantic_ai_slim/pydantic_ai/messages.py index f19afea8df..b2c31cb28a 100644 --- a/pydantic_ai_slim/pydantic_ai/messages.py +++ b/pydantic_ai_slim/pydantic_ai/messages.py @@ -349,8 +349,8 @@ def otel_event(self, settings: InstrumentationSettings) -> Event: @dataclass(repr=False) -class ToolReturnPart: - """A tool return message, this encodes the result of running a tool.""" +class BaseToolReturnPart: + """Base class for tool return parts.""" tool_name: str """The name of the "tool" was called.""" @@ -364,9 +364,6 @@ class ToolReturnPart: timestamp: datetime = field(default_factory=_now_utc) """The timestamp, when the tool returned.""" - part_kind: Literal['tool-return'] = 'tool-return' - """Part type identifier, this is available on all parts as a discriminator.""" - def model_response_str(self) -> str: """Return a string representation of the content for the model.""" if isinstance(self.content, str): @@ -388,9 +385,29 @@ def otel_event(self, _settings: InstrumentationSettings) -> Event: body={'content': self.content, 'role': 'tool', 'id': self.tool_call_id, 'name': self.tool_name}, ) + def has_content(self) -> bool: + """Return `True` if the tool return has content.""" + return self.content is not None + __repr__ = _utils.dataclasses_no_defaults_repr +@dataclass(repr=False) +class ToolReturnPart(BaseToolReturnPart): + """A tool return message, this encodes the result of running a tool.""" + + part_kind: Literal['tool-return'] = 'tool-return' + """Part type identifier, this is available on all parts as a discriminator.""" + + +@dataclass(repr=False) +class ServerToolReturnPart(BaseToolReturnPart): + """A tool return message from a server tool.""" + + part_kind: Literal['server-tool-return'] = 'server-tool-return' + """Part type identifier, this is available on all parts as a discriminator.""" + + error_details_ta = pydantic.TypeAdapter(list[pydantic_core.ErrorDetails], config=pydantic.ConfigDict(defer_build=True)) @@ -503,7 +520,7 @@ def has_content(self) -> bool: @dataclass(repr=False) -class ToolCallPart: +class BaseToolCallPart: """A tool call from a model.""" tool_name: str @@ -521,9 +538,6 @@ class ToolCallPart: In case the tool call id is not provided by the model, PydanticAI will generate a random one. """ - part_kind: Literal['tool-call'] = 'tool-call' - """Part type identifier, this is available on all parts as a discriminator.""" - def args_as_dict(self) -> dict[str, Any]: """Return the arguments as a Python dictionary. @@ -560,7 +574,28 @@ def has_content(self) -> bool: __repr__ = _utils.dataclasses_no_defaults_repr -ModelResponsePart = Annotated[Union[TextPart, ToolCallPart], pydantic.Discriminator('part_kind')] +@dataclass(repr=False) +class ToolCallPart(BaseToolCallPart): + """A tool call from a model.""" + + part_kind: Literal['tool-call'] = 'tool-call' + """Part type identifier, this is available on all parts as a discriminator.""" + + +@dataclass(repr=False) +class ServerToolCallPart(BaseToolCallPart): + """A tool call from a server tool.""" + + model_name: str | None = None + """The name of the model that generated the response.""" + + part_kind: Literal['server-tool-call'] = 'server-tool-call' + """Part type identifier, this is available on all parts as a discriminator.""" + + +ModelResponsePart = Annotated[ + Union[TextPart, ToolCallPart, ServerToolCallPart, ServerToolReturnPart], pydantic.Discriminator('part_kind') +] """A message part returned by a model.""" @@ -789,7 +824,149 @@ def _apply_to_part(self, part: ToolCallPart) -> ToolCallPart: __repr__ = _utils.dataclasses_no_defaults_repr -ModelResponsePartDelta = Annotated[Union[TextPartDelta, ToolCallPartDelta], pydantic.Discriminator('part_delta_kind')] +@dataclass(repr=False) +class ServerToolCallPartDelta: + """A partial update (delta) for a `ServerToolCallPart` to modify tool name, arguments, tool call ID, or model name.""" + + tool_name_delta: str | None = None + """Incremental text to add to the existing tool name, if any.""" + + args_delta: str | dict[str, Any] | None = None + """Incremental data to add to the tool arguments. + + If this is a string, it will be appended to existing JSON arguments. + If this is a dict, it will be merged with existing dict arguments. + """ + + tool_call_id: str | None = None + """Optional tool call identifier, this is used by some models including OpenAI. + + Note this is never treated as a delta — it can replace None, but otherwise if a + non-matching value is provided an error will be raised.""" + + model_name: str | None = None + """Optional model name that generated the response. + + Note this is never treated as a delta — it can replace None, but otherwise if a + non-matching value is provided an error will be raised.""" + + part_delta_kind: Literal['server_tool_call'] = 'server_tool_call' + """Part delta type identifier, used as a discriminator.""" + + def as_part(self) -> ServerToolCallPart | None: + """Convert this delta to a fully formed `ServerToolCallPart` if possible, otherwise return `None`. + + Returns: + A `ServerToolCallPart` if `tool_name_delta` is set, otherwise `None`. + """ + if self.tool_name_delta is None: + return None + + return ServerToolCallPart( + self.tool_name_delta, + self.args_delta, + self.tool_call_id or _generate_tool_call_id(), + self.model_name + ) + + @overload + def apply(self, part: ModelResponsePart) -> ServerToolCallPart: ... + + @overload + def apply(self, part: ModelResponsePart | ServerToolCallPartDelta) -> ServerToolCallPart | ServerToolCallPartDelta: ... + + def apply(self, part: ModelResponsePart | ServerToolCallPartDelta) -> ServerToolCallPart | ServerToolCallPartDelta: + """Apply this delta to a part or delta, returning a new part or delta with the changes applied. + + Args: + part: The existing model response part or delta to update. + + Returns: + Either a new `ServerToolCallPart` or an updated `ServerToolCallPartDelta`. + + Raises: + ValueError: If `part` is neither a `ServerToolCallPart` nor a `ServerToolCallPartDelta`. + UnexpectedModelBehavior: If applying JSON deltas to dict arguments or vice versa. + """ + if isinstance(part, ServerToolCallPart): + return self._apply_to_part(part) + + if isinstance(part, ServerToolCallPartDelta): + return self._apply_to_delta(part) + + raise ValueError( # pragma: no cover + f'Can only apply ServerToolCallPartDeltas to ServerToolCallParts or ServerToolCallPartDeltas, not {part}' + ) + + def _apply_to_delta(self, delta: ServerToolCallPartDelta) -> ServerToolCallPart | ServerToolCallPartDelta: + """Internal helper to apply this delta to another delta.""" + if self.tool_name_delta: + # Append incremental text to the existing tool_name_delta + updated_tool_name_delta = (delta.tool_name_delta or '') + self.tool_name_delta + delta = replace(delta, tool_name_delta=updated_tool_name_delta) + + if isinstance(self.args_delta, str): + if isinstance(delta.args_delta, dict): + raise UnexpectedModelBehavior( + f'Cannot apply JSON deltas to non-JSON tool arguments ({delta=}, {self=})' + ) + updated_args_delta = (delta.args_delta or '') + self.args_delta + delta = replace(delta, args_delta=updated_args_delta) + elif isinstance(self.args_delta, dict): + if isinstance(delta.args_delta, str): + raise UnexpectedModelBehavior( + f'Cannot apply dict deltas to non-dict tool arguments ({delta=}, {self=})' + ) + updated_args_delta = {**(delta.args_delta or {}), **self.args_delta} + delta = replace(delta, args_delta=updated_args_delta) + + if self.tool_call_id: + delta = replace(delta, tool_call_id=self.tool_call_id) + + if self.model_name: + delta = replace(delta, model_name=self.model_name) + + # If we now have enough data to create a full ServerToolCallPart, do so + if delta.tool_name_delta is not None: + return ServerToolCallPart( + delta.tool_name_delta, + delta.args_delta, + delta.tool_call_id or _generate_tool_call_id(), + delta.model_name + ) + + return delta + + def _apply_to_part(self, part: ServerToolCallPart) -> ServerToolCallPart: + """Internal helper to apply this delta directly to a `ServerToolCallPart`.""" + if self.tool_name_delta: + # Append incremental text to the existing tool_name + tool_name = part.tool_name + self.tool_name_delta + part = replace(part, tool_name=tool_name) + + if isinstance(self.args_delta, str): + if isinstance(part.args, dict): + raise UnexpectedModelBehavior(f'Cannot apply JSON deltas to non-JSON tool arguments ({part=}, {self=})') + updated_json = (part.args or '') + self.args_delta + part = replace(part, args=updated_json) + elif isinstance(self.args_delta, dict): + if isinstance(part.args, str): + raise UnexpectedModelBehavior(f'Cannot apply dict deltas to non-dict tool arguments ({part=}, {self=})') + updated_dict = {**(part.args or {}), **self.args_delta} + part = replace(part, args=updated_dict) + + if self.tool_call_id: + part = replace(part, tool_call_id=self.tool_call_id) + + if self.model_name: + part = replace(part, model_name=self.model_name) + + return part + + __repr__ = _utils.dataclasses_no_defaults_repr + + +ModelResponsePartDelta = Annotated[Union[TextPartDelta, ToolCallPartDelta, ServerToolCallPartDelta], pydantic.Discriminator('part_delta_kind')] """A partial update (delta) for any model response part.""" @@ -883,6 +1060,29 @@ class FunctionToolResultEvent: __repr__ = _utils.dataclasses_no_defaults_repr +@dataclass(repr=False) +class ServerToolCallEvent: + """An event indicating the start to a call to a server tool.""" + + part: ServerToolCallPart + """The server tool call to make.""" + + event_kind: Literal['server_tool_call'] = 'server_tool_call' + """Event type identifier, used as a discriminator.""" + + +@dataclass(repr=False) +class ServerToolResultEvent: + """An event indicating the result of a server tool call.""" + + result: ServerToolReturnPart + """The result of the call to the server tool.""" + + event_kind: Literal['server_tool_result'] = 'server_tool_result' + """Event type identifier, used as a discriminator.""" + + HandleResponseEvent = Annotated[ - Union[FunctionToolCallEvent, FunctionToolResultEvent], pydantic.Discriminator('event_kind') + Union[FunctionToolCallEvent, FunctionToolResultEvent, ServerToolCallEvent, ServerToolResultEvent], + pydantic.Discriminator('event_kind'), ] diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index 2ebbcf75ee..0308c4eb10 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -16,6 +16,7 @@ import httpx from typing_extensions import Literal, TypeAliasType +from pydantic_ai.builtin_tools import AbstractBuiltinTool from pydantic_ai.profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec from .._parts_manager import ModelResponsePartsManager @@ -297,6 +298,7 @@ class ModelRequestParameters: """Configuration for an agent's request to a model, specifically related to tools and output handling.""" function_tools: list[ToolDefinition] = field(default_factory=list) + builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list) allow_text_output: bool = True output_tools: list[ToolDefinition] = field(default_factory=list) diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index 69808aa5f0..f3b20185a7 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -7,8 +7,16 @@ from datetime import datetime, timezone from typing import Any, Literal, Union, cast, overload +from anthropic.types.beta import ( + BetaCodeExecutionToolResultBlock, + BetaCodeExecutionToolResultBlockParam, + BetaServerToolUseBlockParam, + BetaWebSearchToolResultBlockParam, +) from typing_extensions import assert_never +from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool + from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage from .._utils import guard_tool_call_id as _guard_tool_call_id from ..messages import ( @@ -21,6 +29,8 @@ ModelResponsePart, ModelResponseStreamEvent, RetryPromptPart, + ServerToolCallPart, + ServerToolReturnPart, SystemPromptPart, TextPart, ToolCallPart, @@ -45,6 +55,7 @@ from anthropic.types.beta import ( BetaBase64PDFBlockParam, BetaBase64PDFSourceParam, + BetaCodeExecutionTool20250522Param, BetaContentBlock, BetaContentBlockParam, BetaImageBlockParam, @@ -59,15 +70,20 @@ BetaRawMessageStartEvent, BetaRawMessageStopEvent, BetaRawMessageStreamEvent, + BetaServerToolUseBlock, BetaTextBlock, BetaTextBlockParam, BetaTextDelta, BetaToolChoiceParam, BetaToolParam, BetaToolResultBlockParam, + BetaToolUnionParam, BetaToolUseBlock, BetaToolUseBlockParam, + BetaWebSearchTool20250305Param, + BetaWebSearchToolResultBlock, ) + from anthropic.types.beta.beta_web_search_tool_20250305_param import UserLocation from anthropic.types.model_param import ModelParam except ImportError as _import_error: @@ -208,6 +224,7 @@ async def _messages_create( ) -> BetaMessage | AsyncStream[BetaRawMessageStreamEvent]: # standalone function to make it easier to override tools = self._get_tools(model_request_parameters) + tools += self._get_builtin_tools(model_request_parameters) tool_choice: BetaToolChoiceParam | None if not tools: @@ -226,6 +243,7 @@ async def _messages_create( try: extra_headers = model_settings.get('extra_headers', {}) extra_headers.setdefault('User-Agent', get_user_agent()) + extra_headers.setdefault('anthropic-beta', 'code-execution-2025-05-22') return await self.client.beta.messages.create( max_tokens=model_settings.get('max_tokens', 1024), system=system_prompt or NOT_GIVEN, @@ -253,6 +271,31 @@ def _process_response(self, response: BetaMessage) -> ModelResponse: for item in response.content: if isinstance(item, BetaTextBlock): items.append(TextPart(content=item.text)) + elif isinstance(item, BetaWebSearchToolResultBlock): + items.append( + ServerToolReturnPart( + tool_name=item.type, + content=item.content, + tool_call_id=item.tool_use_id, + ) + ) + elif isinstance(item, BetaServerToolUseBlock): + items.append( + ServerToolCallPart( + model_name='anthropic', + tool_name=item.name, + args=cast(dict[str, Any], item.input), + tool_call_id=item.id, + ) + ) + elif isinstance(item, BetaCodeExecutionToolResultBlock): + items.append( + ServerToolReturnPart( + tool_name=item.type, + content=item.content, + tool_call_id=item.tool_use_id, + ) + ) else: assert isinstance(item, BetaToolUseBlock), f'unexpected item type {type(item)}' items.append( @@ -283,7 +326,25 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[B tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools] return tools - async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: + def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[BetaToolUnionParam]: + tools: list[BetaToolUnionParam] = [] + for tool in model_request_parameters.builtin_tools: + if isinstance(tool, WebSearchTool): + user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None + tools.append( + BetaWebSearchTool20250305Param( + name='web_search', + type='web_search_20250305', + allowed_domains=tool.allowed_domains, + blocked_domains=tool.blocked_domains, + user_location=user_location, + ) + ) + elif isinstance(tool, CodeExecutionTool): + tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522')) + return tools + + async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: # noqa: C901 """Just maps a `pydantic_ai.Message` to a `anthropic.types.MessageParam`.""" system_prompt_parts: list[str] = [] anthropic_messages: list[BetaMessageParam] = [] @@ -319,11 +380,17 @@ async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[Be if len(user_content_params) > 0: anthropic_messages.append(BetaMessageParam(role='user', content=user_content_params)) elif isinstance(m, ModelResponse): - assistant_content_params: list[BetaTextBlockParam | BetaToolUseBlockParam] = [] + assistant_content_params: list[ + BetaTextBlockParam + | BetaToolUseBlockParam + | BetaServerToolUseBlockParam + | BetaWebSearchToolResultBlockParam + | BetaCodeExecutionToolResultBlockParam + ] = [] for response_part in m.parts: if isinstance(response_part, TextPart): assistant_content_params.append(BetaTextBlockParam(text=response_part.content, type='text')) - else: + elif isinstance(response_part, ToolCallPart): tool_use_block_param = BetaToolUseBlockParam( id=_guard_tool_call_id(t=response_part), type='tool_use', @@ -331,6 +398,29 @@ async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[Be input=response_part.args_as_dict(), ) assistant_content_params.append(tool_use_block_param) + elif isinstance(response_part, ServerToolCallPart): + server_tool_use_block_param = BetaServerToolUseBlockParam( + id=_guard_tool_call_id(t=response_part), + type='server_tool_use', + name=cast(Literal['web_search', 'code_execution'], response_part.tool_name), + input=response_part.args_as_dict(), + ) + assistant_content_params.append(server_tool_use_block_param) + elif isinstance(response_part, ServerToolReturnPart): + tool_use_id = _guard_tool_call_id(t=response_part) + if response_part.tool_name == 'web_search_tool_result': + server_tool_result_block_param = BetaWebSearchToolResultBlockParam( + tool_use_id=tool_use_id, type=response_part.tool_name, content=response_part.content + ) + elif response_part.tool_name == 'code_execution_tool_result': + server_tool_result_block_param = BetaCodeExecutionToolResultBlockParam( + tool_use_id=tool_use_id, type=response_part.tool_name, content=response_part.content + ) + else: + raise ValueError(f'Unsupported tool name: {response_part.tool_name}') + assistant_content_params.append(server_tool_result_block_param) + else: + assert_never(response_part) anthropic_messages.append(BetaMessageParam(role='assistant', content=assistant_content_params)) else: assert_never(m) diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index 618c44dafd..10bdc5defa 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -10,6 +10,8 @@ from typing_extensions import assert_never +from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool +from pydantic_ai.exceptions import UserError from pydantic_ai.providers import Provider from .. import UnexpectedModelBehavior, _utils, usage @@ -24,6 +26,8 @@ ModelResponsePart, ModelResponseStreamEvent, RetryPromptPart, + ServerToolCallPart, + ServerToolReturnPart, SystemPromptPart, TextPart, ToolCallPart, @@ -54,10 +58,12 @@ FunctionDeclarationDict, GenerateContentConfigDict, GenerateContentResponse, + GoogleSearchDict, Part, PartDict, SafetySettingDict, ThinkingConfigDict, + ToolCodeExecutionDict, ToolConfigDict, ToolDict, ToolListUnionDict, @@ -208,6 +214,13 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[T ToolDict(function_declarations=[_function_declaration_from_tool(t)]) for t in model_request_parameters.output_tools ] + for tool in model_request_parameters.builtin_tools: + if isinstance(tool, WebSearchTool): + tools.append(ToolDict(google_search=GoogleSearchDict())) + elif isinstance(tool, CodeExecutionTool): + tools.append(ToolDict(code_execution=ToolCodeExecutionDict())) + else: + raise UserError(f'Unsupported builtin tool: {tool}') return tools or None def _get_tool_config( @@ -450,7 +463,18 @@ def _process_response_from_parts( ) -> ModelResponse: items: list[ModelResponsePart] = [] for part in parts: - if part.text is not None: + if part.executable_code is not None: + items.append(ServerToolCallPart(args=part.executable_code.model_dump(), tool_name='code_execution')) + elif part.code_execution_result is not None: + # TODO(Marcelo): Is the idea to generate the tool_call_id on the `executable_code`, and then pass it here? + items.append( + ServerToolReturnPart( + tool_name='code_execution', + content=part.code_execution_result.output, + tool_call_id="It doesn't have.", + ) + ) + elif part.text is not None: items.append(TextPart(content=part.text)) elif part.function_call: assert part.function_call.name is not None diff --git a/pydantic_ai_slim/pydantic_ai/models/groq.py b/pydantic_ai_slim/pydantic_ai/models/groq.py index 1b23d2f5ff..7a5265f08b 100644 --- a/pydantic_ai_slim/pydantic_ai/models/groq.py +++ b/pydantic_ai_slim/pydantic_ai/models/groq.py @@ -10,7 +10,7 @@ from typing_extensions import assert_never from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage -from .._utils import guard_tool_call_id as _guard_tool_call_id, number_to_datetime +from .._utils import guard_tool_call_id as _guard_tool_call_id, number_to_datetime, generate_tool_call_id from ..messages import ( BinaryContent, DocumentUrl, @@ -21,6 +21,8 @@ ModelResponsePart, ModelResponseStreamEvent, RetryPromptPart, + ServerToolCallPart, + ServerToolReturnPart, SystemPromptPart, TextPart, ToolCallPart, @@ -220,7 +222,7 @@ async def _completions_create( extra_headers = model_settings.get('extra_headers', {}) extra_headers.setdefault('User-Agent', get_user_agent()) return await self.client.chat.completions.create( - model=str(self._model_name), + model=self._model_name, messages=groq_messages, n=1, parallel_tool_calls=model_settings.get('parallel_tool_calls', NOT_GIVEN), @@ -249,6 +251,15 @@ def _process_response(self, response: chat.ChatCompletion) -> ModelResponse: timestamp = number_to_datetime(response.created) choice = response.choices[0] items: list[ModelResponsePart] = [] + if choice.message.executed_tools: + for tool in choice.message.executed_tools: + tool_call_id = generate_tool_call_id() + items.append( + ServerToolCallPart( + tool_name=tool.type, args=tool.arguments, model_name='groq', tool_call_id=tool_call_id + ) + ) + items.append(ServerToolReturnPart(tool_name=tool.type, content=tool.output, tool_call_id=tool_call_id)) if choice.message.content is not None: items.append(TextPart(content=choice.message.content)) if choice.message.tool_calls is not None: diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index bb8aadcf92..63fa572453 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -8,8 +8,11 @@ from datetime import datetime from typing import Any, Literal, Union, cast, overload +from openai.types.responses.response_code_interpreter_tool_call import ResultLogs +from openai.types.responses.tool_param import CodeInterpreter from typing_extensions import assert_never +from pydantic_ai.builtin_tools import WebSearchTool from pydantic_ai.profiles.openai import OpenAIModelProfile from pydantic_ai.providers import Provider, infer_provider @@ -26,6 +29,9 @@ ModelResponsePart, ModelResponseStreamEvent, RetryPromptPart, + ServerToolCallPart, + ServerToolCallPartDelta, + ServerToolReturnPart, SystemPromptPart, TextPart, ToolCallPart, @@ -58,6 +64,11 @@ from openai.types.chat.chat_completion_content_part_image_param import ImageURL from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio from openai.types.chat.chat_completion_content_part_param import File, FileFile + from openai.types.chat.completion_create_params import ( + WebSearchOptions, + WebSearchOptionsUserLocation, + WebSearchOptionsUserLocationApproximate, + ) from openai.types.responses import ComputerToolParam, FileSearchToolParam, WebSearchToolParam from openai.types.responses.response_input_param import FunctionCallOutput, Message from openai.types.shared import ReasoningEffort @@ -123,7 +134,7 @@ class OpenAIResponsesModelSettings(OpenAIModelSettings, total=False): ALL FIELDS MUST BE `openai_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS. """ - openai_builtin_tools: Sequence[FileSearchToolParam | WebSearchToolParam | ComputerToolParam] + openai_builtin_tools: Sequence[FileSearchToolParam | WebSearchToolParam | ComputerToolParam | CodeInterpreter] """The provided OpenAI built-in tools to use. See [OpenAI's built-in tools](https://platform.openai.com/docs/guides/tools?api-mode=responses) for more details. @@ -263,6 +274,7 @@ async def _completions_create( model_request_parameters: ModelRequestParameters, ) -> chat.ChatCompletion | AsyncStream[ChatCompletionChunk]: tools = self._get_tools(model_request_parameters) + web_search_options = self._get_web_search_options(model_request_parameters) # standalone function to make it easier to override if not tools: @@ -298,6 +310,7 @@ async def _completions_create( logprobs=model_settings.get('openai_logprobs', NOT_GIVEN), top_logprobs=model_settings.get('openai_top_logprobs', NOT_GIVEN), user=model_settings.get('openai_user', NOT_GIVEN), + web_search_options=web_search_options or NOT_GIVEN, extra_headers=extra_headers, extra_body=model_settings.get('extra_body'), ) @@ -367,6 +380,19 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[c tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools] return tools + def _get_web_search_options(self, model_request_parameters: ModelRequestParameters) -> WebSearchOptions | None: + for tool in model_request_parameters.builtin_tools: + if isinstance(tool, WebSearchTool): + if tool.user_location: + return WebSearchOptions( + search_context_size=tool.search_context_size, + user_location=WebSearchOptionsUserLocation( + type='approximate', + approximate=WebSearchOptionsUserLocationApproximate(**tool.user_location), + ), + ) + return WebSearchOptions(search_context_size=tool.search_context_size) + async def _map_messages(self, messages: list[ModelMessage]) -> list[chat.ChatCompletionMessageParam]: """Just maps a `pydantic_ai.Message` to a `openai.types.ChatCompletionMessageParam`.""" openai_messages: list[chat.ChatCompletionMessageParam] = [] @@ -382,6 +408,9 @@ async def _map_messages(self, messages: list[ModelMessage]) -> list[chat.ChatCom texts.append(item.content) elif isinstance(item, ToolCallPart): tool_calls.append(self._map_tool_call(item)) + # OpenAI doesn't return server tools calls. + elif isinstance(item, (ServerToolCallPart, ServerToolReturnPart)): + continue else: assert_never(item) message_param = chat.ChatCompletionAssistantMessageParam(role='assistant') @@ -599,6 +628,8 @@ def _process_response(self, response: responses.Response) -> ModelResponse: for item in response.output: if item.type == 'function_call': items.append(ToolCallPart(item.name, item.arguments, tool_call_id=item.call_id)) + if item.type == 'code_interpreter_call': + items.append(ServerToolCallPart(tool_name=item.type, model_name="openai", args={"code": item.code, "container_id": item.container_id, "out": item.results}, tool_call_id=item.id)) return ModelResponse(items, usage=_map_usage(response), model_name=response.model, timestamp=timestamp) async def _process_streamed_response( @@ -644,6 +675,7 @@ async def _responses_create( ) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]: tools = self._get_tools(model_request_parameters) tools = list(model_settings.get('openai_builtin_tools', [])) + tools + tools = self._get_builtin_tools(model_request_parameters) + tools # standalone function to make it easier to override if not tools: @@ -655,12 +687,12 @@ async def _responses_create( instructions, openai_messages = await self._map_messages(messages) reasoning = self._get_reasoning(model_settings) - try: extra_headers = model_settings.get('extra_headers', {}) extra_headers.setdefault('User-Agent', get_user_agent()) return await self.client.responses.create( input=openai_messages, + include=["code_interpreter_call.outputs"], model=self._model_name, instructions=instructions, parallel_tool_calls=model_settings.get('parallel_tool_calls', NOT_GIVEN), @@ -696,6 +728,20 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[r tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools] return tools + def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.ToolParam]: + tools: list[responses.ToolParam] = [] + for tool in model_request_parameters.builtin_tools: + if isinstance(tool, WebSearchTool): + web_search_tool = responses.WebSearchToolParam( + type='web_search_preview', search_context_size=tool.search_context_size + ) + if tool.user_location: + web_search_tool['user_location'] = responses.web_search_tool_param.UserLocation( + type='approximate', **tool.user_location + ) + tools.append(web_search_tool) + return tools + def _map_tool_definition(self, f: ToolDefinition) -> responses.FunctionToolParam: return { 'name': f.name, @@ -749,11 +795,15 @@ async def _map_messages( openai_messages.append(responses.EasyInputMessageParam(role='assistant', content=item.content)) elif isinstance(item, ToolCallPart): openai_messages.append(self._map_tool_call(item)) + # OpenAI doesn't return server tools calls. + elif isinstance(item, (ServerToolCallPart)): + openai_messages.append(self._map_code_interpreter_tool_call(item)) else: assert_never(item) else: assert_never(message) instructions = self._get_instructions(messages) or NOT_GIVEN + print(openai_messages) return instructions, openai_messages @staticmethod @@ -764,6 +814,18 @@ def _map_tool_call(t: ToolCallPart) -> responses.ResponseFunctionToolCallParam: name=t.tool_name, type='function_call', ) + + @staticmethod + def _map_code_interpreter_tool_call(t: ServerToolCallPart) -> responses.ResponseCodeInterpreterToolCallParam: + args = t.args_as_dict() if t.args else {} + return responses.ResponseCodeInterpreterToolCallParam( # type: ignore the results parameter is wrong it uses output + id=t.tool_call_id, + code=args.get("code", ""), + container_id=args.get("container_id", ""), + status="completed", + type="code_interpreter_call", + outputs=[ResultLogs(logs="hello", type="logs").model_dump()] # Convert to dict for JSON serialization + ) @staticmethod async def _map_user_prompt(part: UserPromptPart) -> responses.EasyInputMessageParam: @@ -923,6 +985,7 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: self._usage += _map_usage(chunk.response) elif isinstance(chunk, responses.ResponseOutputItemAddedEvent): + print(chunk) if isinstance(chunk.item, responses.ResponseFunctionToolCall): yield self._parts_manager.handle_tool_call_part( vendor_part_id=chunk.item.id, @@ -936,11 +999,86 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: pass elif isinstance(chunk, responses.ResponseTextDeltaEvent): - yield self._parts_manager.handle_text_delta(vendor_part_id=chunk.content_index, content=chunk.delta) - + yield self._parts_manager.handle_text_delta(vendor_part_id=chunk.item_id, content=chunk.delta) elif isinstance(chunk, responses.ResponseTextDoneEvent): pass # there's nothing we need to do here + elif isinstance(chunk, responses.ResponseCodeInterpreterCallCodeDeltaEvent): + # Handle incremental code updates for code interpreter + maybe_event = self._parts_manager.handle_server_tool_call_delta( + vendor_part_id=chunk.item_id, # type: ignore + tool_name="code_interpreter_call", + args={"code": chunk.delta, "status": "in_progress"}, + tool_call_id=str(chunk.item_id), # type: ignore + model_name=self._model_name, + ) + if maybe_event is not None: + yield maybe_event + + elif isinstance(chunk, responses.ResponseCodeInterpreterCallCodeDoneEvent): + # Handle code completion for code interpreter + args = {"status": "interpreting"} + if chunk.code: + args["code"] = chunk.code + + maybe_event = self._parts_manager.handle_server_tool_call_delta( + vendor_part_id=chunk.item_id, # type: ignore + tool_name=None, # Don't update tool name + args=args, + tool_call_id=chunk.item_id, # type: ignore + model_name=self._model_name, + ) + if maybe_event is not None: + yield maybe_event + + elif isinstance(chunk, responses.ResponseCodeInterpreterCallInProgressEvent): + # Handle when code interpretation starts + + tool_call = chunk.code_interpreter_call + args = {} + if tool_call: + args = { + "status": tool_call.status, + } + args["code"] = tool_call.code + if tool_call and tool_call.container_id: + args["container_id"] = tool_call.container_id + + elif isinstance(chunk, responses.ResponseCodeInterpreterCallInterpretingEvent): + # Handle when code is being interpreted + tool_call = chunk.code_interpreter_call + args = {} + if tool_call: + args = { + "status": tool_call.status, + } + if tool_call and tool_call.code: + args["code"] = tool_call.code + if tool_call and tool_call.container_id: + args["container_id"] = tool_call.container_id + + maybe_event = self._parts_manager.handle_server_tool_call_delta( + vendor_part_id=chunk.item_id, # type: ignore + tool_name=None, # Don't update tool name + args=args, + tool_call_id=None, + model_name=self._model_name, + ) + if maybe_event is not None: + yield maybe_event + + elif isinstance(chunk, responses.ResponseCodeInterpreterCallCompletedEvent): + # Handle completed code interpreter call with results + maybe_event = self._parts_manager.handle_server_tool_call_delta( + vendor_part_id=chunk.item_id, # type: ignore + tool_name=None, # Don't update tool name + args={"status": "completed"}, + tool_call_id=None, + model_name=self._model_name, + ) + if maybe_event is not None: + yield maybe_event + else: # pragma: no cover warnings.warn( f'Handling of this event type is not yet implemented. Please report on our GitHub: {chunk}', diff --git a/pydantic_ai_slim/pyproject.toml b/pydantic_ai_slim/pyproject.toml index 631cc196d0..19a3374607 100644 --- a/pydantic_ai_slim/pyproject.toml +++ b/pydantic_ai_slim/pyproject.toml @@ -61,12 +61,12 @@ dependencies = [ # WARNING if you add optional groups, please update docs/install.md logfire = ["logfire>=3.11.0"] # Models -openai = ["openai>=1.75.0"] +openai = ["openai>=1.84.0"] cohere = ["cohere>=5.13.11; platform_system != 'Emscripten'"] vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"] google = ["google-genai>=1.15.0"] anthropic = ["anthropic>=0.52.0"] -groq = ["groq>=0.15.0"] +groq = ["groq>=0.25.0"] mistral = ["mistralai>=1.2.5"] bedrock = ["boto3>=1.35.74"] # Tools diff --git a/tests/models/cassettes/test_anthropic/test_anthropic_code_execution_tool.yaml b/tests/models/cassettes/test_anthropic/test_anthropic_code_execution_tool.yaml new file mode 100644 index 0000000000..76f19dc008 --- /dev/null +++ b/tests/models/cassettes/test_anthropic/test_anthropic_code_execution_tool.yaml @@ -0,0 +1,86 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '250' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 1024 + messages: + - content: + - text: How much is 3 * 12390? + type: text + role: user + model: claude-sonnet-4-0 + stream: false + tool_choice: + type: auto + tools: + - name: code_execution + type: code_execution_20250522 + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '927' + content-type: + - application/json + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + container: + expires_at: '2025-05-26T13:30:45.703429+00:00' + id: container_011CPW9LpfbF8dmXMvVNCiQJ + content: + - text: I'll calculate 3 * 12390 for you. + type: text + - id: srvtoolu_01CPfaeVC7ju4VsdzxjSLDrY + input: + code: |- + result = 3 * 12390 + print(f"3 * 12390 = {result}") + name: code_execution + type: server_tool_use + - content: + content: [] + return_code: 0 + stderr: '' + stdout: | + 3 * 12390 = 37170 + type: code_execution_result + tool_use_id: srvtoolu_01CPfaeVC7ju4VsdzxjSLDrY + type: code_execution_tool_result + - text: The answer is **37,170**. + type: text + id: msg_015H6Emn2T8vZhE52mU2jF1U + model: claude-sonnet-4-20250514 + role: assistant + stop_reason: end_turn + stop_sequence: null + type: message + usage: + cache_creation_input_tokens: 0 + cache_read_input_tokens: 0 + input_tokens: 1630 + output_tokens: 105 + server_tool_use: + web_search_requests: 0 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_anthropic/test_anthropic_server_tool_pass_history_to_another_provider.yaml b/tests/models/cassettes/test_anthropic/test_anthropic_server_tool_pass_history_to_another_provider.yaml new file mode 100644 index 0000000000..a73ea3aa45 --- /dev/null +++ b/tests/models/cassettes/test_anthropic/test_anthropic_server_tool_pass_history_to_another_provider.yaml @@ -0,0 +1,280 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '312' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 1024 + messages: + - content: + - text: What day is today? + type: text + role: user + model: claude-3-5-sonnet-latest + stream: false + tool_choice: + type: auto + tools: + - allowed_domains: null + blocked_domains: null + name: web_search + type: web_search_20250305 + user_location: null + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '15304' + content-type: + - application/json + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - text: Let me search for today's date. + type: text + - id: srvtoolu_01BJh8n7va96puUF3hhNnYnY + input: + query: current date today May 26 2025 + name: web_search + type: server_tool_use + - content: + - encrypted_content: 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 + page_age: null + title: May 2025 Calendar + type: web_search_result + url: https://www.calendar-365.com/calendar/2025/May.html + - encrypted_content: EuwCCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDNOs+T2nScTnsxVJZRoM3g9Mm5RlKN0sgiPBIjC1PJPZRfoUgRpmpfVZcF8sBTMUr6Lu9BK9xOxTQ8nMMBGjC7kFOrwJZ3ENgXLhRzcq7wF51cgLQ+Pb0MiXBGozKXa1r7DbogMoIacrDz92bvzbwXbo4mTQMipnow47//PYVelJXipxVAA6ISswEiYPGXyCjqSy6QEIde1TOc8nvMpaM0Y4u3DdOXez5EsgP+ZxIYly/I7JS35h/azV0nbpzEKsptZwNWwuBtNIYbkklPaluda13QbkE0KK1Cl6PnkNMVbO0Uo9XURcwLn5aDLJol0GJGVpuiApXVJoMk7e7L/Ib+p1BE77VW619KzDmQvuAjQXZR28Fb5KEbRxXkSaq7xAZnYZyQFtOlqg/07Tv+V4g0WYZhI3tn0phBe0w+95NhgD + page_age: null + title: May 26, 2025 Calendar with Holidays & Count Down - USA + type: web_search_result + url: https://www.wincalendar.com/Calendar/Date/May-26-2025 + - encrypted_content: Es0ICioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDE+yP8aOxk+46r0AERoMFOlAyJD/AGdtDxeVIjDDhlbQbqqMmuMK9bN7ZNTwHWQwahkAaZd/LltU3eIPifLnfk4Apnck+mUOM61zRiQq0Ac12DCFUzhrlxvkXqpK3gWjgosGbUyzt8WbmwSaEmCbXf9F4bNc7+LKvQUtEceYmCesYcQa0lOT+88IhhRTxYBMyXo0Ws3Pw1yrLJi9P1JqfpcnWebg8McOGY/pD/MFeJORQvH+0UvGjBoMD0uaPPY1U6cFC7EWL+IebMjjmpj80RqAi/xjOOnU2XO8N98dFCzW4GiI9IWut8OBv8u3kLpmyYOV6cVxr3WTEntDv3onveQvTIyJFiDG8bmOKK72ONWrevnrKGQ7JY2gPYp5CakS+SYahIKiJOOY3cWQfUEheIfEZeuGadI907uvREyb1bf4h/0KsFjnOBWrHXdl7LIDRSfz8OIwfJufTERGCfG0vzMiHzFJbFcezciz3K9m25QjxL676NcrYbgZGFhIXar9jNvPAjGcUecpaOLAEztHx7pd0nHMsW5KKUTS0yU5sMFNY1+RMSKk8kVoCcBtOuOsiegf+BJV/J997IvbQG5A7h/72VsVyUbiBDT1z77VjKTPcerZr4EReCobAoyGdMKt1mihwvYvsyvZepAkvrxOoKnb+2pkzhZQtxGt0ub23yhSZyLgi0frVSS04KeYncVWW7cQJJygzfDS37pdSPnU0wZmAgZLzQ3X1NVmR9AQVSQ+IcIoVSCuru/WiBcS7aJPC4uh6cn5sB9C6yThrD8i7T7DJGbR/uutIADP9IgGUzdoppStgQugx4TwnQI9SC6e36zjfsK6NeUkxLi2b4T1uuRDvjgUDc0OXGZdoGQgc8nHoe7rexoLJqeWat19KNwy4DqnM7oywivVJWDJ2ThV4863f3+hYRlXeuxnojBsmENJsSxZtF46ye1gfPSvBYlEpO/Uu9FzR3yy4bB3b/qOh2+JMTLkk3R5u4C6Ca/Nye0bpnsKG7xjW2cKguqfKhUuUFaSucCCJqg/hT21DlUiHXZQ6wq6Ywi9C9l+wJbzfJWWHczjkTJvWhFq8saXSAzfWDd8tTKH1zje7NN38jRQPUDxS0CWMM78/+BgWoqsB5+IjkEl5QHV55GSCg+05HQPLyHQWWjBdMLWr+Uu3T0kKI0B/QDJPHO1mSX5AsGot5HZzva5qEPzn3vGly1n1vs14JWE+RrtIJTBHi0ypFhaO6oCRGejMRDbZhotP4gWW1VW0gLNikgfW0Z5waOb8wmLcN4vQhsHRFfZiXsqUF0dTXsme8+kF8qcVZNnPtghyf1ACvFgZ8TAF4PaM1wzpa8kDeBFvyp14U39R8XDJ3T6lGWR2rpv0WjS8d5h0RCZ3XlFuqPUXWEQ89OpEH8vEiYXGAM= + page_age: null + title: Daily Calendar for Monday, May 26, 2025 | Almanac.com + type: web_search_result + url: https://www.almanac.com/calendar/date/2025-05-26 + - encrypted_content: EpoFCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDClGwxB0YzJdub7VGhoMASvr8D1KqeBCa4knIjAV1lHpDjDIw6nag+Y/ax/aq2LhfZ1/CXJoRyRFiUGLVQHpERodH6/Kg+l8aLy01gAqnQQR8cCm+MlfMM3pSSNah8dZ0m5wam3enQ+5A3VLu0P/CF/TlwL5yB21pfaiu0lad9Ee99YFvs7LInvXpyFcUgLuhankIzNNZZbUIeS4sZIK39RrN+pmifOl6KQqT1f2tpJE6x7erDlAscnMvbRIr9U8Pwf8Au709bI6zcHqCg4GNJk2CqDf6kAXRHRRhcWYFtm8b0wvg9HaWVYZFOunZMJ1gFD3D82dSm3m4awyDSZm54A/9hmzE/lcQyiSVBC73NOmJMBxbNec16qaeJdUTZp33JYk0Fdro1VnkUiJyq5Kb2W7VYvfeQ1a2dLXPTYIsXeQXLHesmVTe7m0X70w0BgCAC+yKZPp68bbsEbsbjwxaWOxQ50seu8kkXDiEgscUK5e4N5YnPXMNjyMNZmlbhmFtw3abOmOCVdpcDORXGOgJZBWBkEqQ4mE8b+aEzXMGRqLzYjDKZHx11wolF3MGu/V/+f4lWEyNwR2Kipp67teOx1ypvl4DiZiPVkZwx9lt08u+TJGEGbTYtdPMQRLVX1K2K4YOEcP5/abNrdT9KdYH7dNscRDqDY04gi+47JyyTSRObcCjQun9LUqh0q6/NfIIWQwjguZ6dbYL67ymiuRgHJDXsPnc/P58qP4kRsClz9lu/XPTFuAzCQwaXioK3XiBNW73nSKyRaxm6/OxbYetPGIG2JUTkQtrmGL5AYJTfFbI6XOB1AFDcsoSC7nGAM= + page_age: null + title: How long ago was May 26th 2025? | howlongagogo.com + type: web_search_result + url: https://howlongagogo.com/date/2025/may/26 + - encrypted_content: Eq0DCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDDT/5zRyvf5kzhhLgxoM5vWn/Zjmu43rvWgPIjCf4z9SIoZxgNKyiLOqk422DLmHbFXGX+mhy6FPo43NOogfLhu/a3F3EPkiMxfd0UMqsAKIhRJKuEhfoi2saPCA3LpNhJ3YPO2NZyYL75SJdCbkfACe2YJOcCXHmt4hkrbA98Mrvh9DmtP6HNA9iYdsotXkDi0BuTlmiIcspblXnkpXLIURVpNYqqZ2jbjBEmi/mVlTonm9NJwx1rZHzwciKhaIrz440Vk/uTIqpiMb22U00hWrQ563gNlMSXpvIpyayhJcCnNEjQybJjQUsKaMqb/3Xbp8NUoWtidUb4ARNeoNe4BlzMEEcIPc0QWi1CKoQJH3JazxAhMSqvc4SVWXRyspZZP6lSHhMcIYlf/yorVYVF3FmL1djZfxXbVnI/VbFbpoNejvnOh5QIdbDrHu6UN3u2iGZBUJLgs6i9OUwMWw8kpi2WUm+3JeRkdyThE0M9WAhK+4qYD3UHgFCcucX7ZeGAM= + page_age: null + title: How many days until 26th May 2025? + type: web_search_result + url: https://days.to/26-may/2025 + - encrypted_content: EpACCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDNXuvVNnWjvXgGql0RoMXwqBXk4fJbTzoM0pIjC1HFbYVJOlp39xpYzw/uJVUO6cC2L9ScOu9ceP2MDjyBWuE0X2LxBsyFjM3ZwzQtYqkwHukWybclFQu5CuM7gntDFNVYsSAVDbslXo5WwOlljEXnC7ohNjuB4lJfXS5HomdOmnUs0/thHHvR5mvCvHt5o+XrnTV8PSOXJvZonVctfjBeMe/3VWaQX2Pr8F71hiBVghYnc4lqnvA/nebBXcqdz2VTlG7V2/u+KCGVfR27l0+BX9s8vroR6dZ7flEBZBSDDzt3QYAw== + page_age: null + title: What day of the week will May 26th 2025 be on? | howlongagogo.com + type: web_search_result + url: https://howlongagogo.com/day-of-the-week/2025-05-26 + - encrypted_content: EtkGCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDNqnvrEdyAJMqZiouRoMl3zAD6YOqFa5Y+LGIjCFzjG2cb9Uv/fUGE2iFHKU3CP5i7yVrpvusD4UBOrARkME7qTDdfIczSbj1PvU7ygq3AUQ9yGVSgg9AB7qawws2Zx43sVi5sDW/thrC8owactbSzc2qE9IsmucxwknSZXemJFSMW3I2vWhlpJVcfipESwRyVSnVNoTivU7/aIbnI15/NOGATtd/TR0mkyO2MMMAFIHiEfq0iuvMUX/EmnEqlSzDngrlWdPvb0qDrZIkSVEYFE5FEPHXuXEFEWZ/yLs7rUK7M9kc161gTbQ/NMALrWDjKtFtgI20Wf2luvbfX8G8kivyPttdUk9aiBxStWetpJZqsBEGT1auj3hk7FaHU2FTNW0LGvIe++QZNL+w/nKa+YuNGpJCxAiLt3X+2hnZmz8Mskfyfl5I5EWt7eOQmoR0qStl7Fh9Myb3vZ0rpZ8gZeaLd1xn6pKKa+tBonm1Oq1zQvsoMkMwTcMRK3LA53dF3rtkgSFyBAWis/tenbdrRVUGlicecn/bSd9FVFoGM5RWYvXR9/3zr3SBf2qNTT2jmeLqcl07LYUk+WDGcas6vAVGDXxVoAh+uAC0WziUtKfv//V4IeleGEOLJNdX1jDC8uQMKyGuRCXNRJAoHRhgUr2F46Ax9xJOHWXKDheklfqTFff0wf93d5U/d4p52RaeM7jcBexjz0wMQRBL91ZOZVweRHbV2BVTBpPH79Y9W/UmBEycRnTmurJJspZre10EVH7MIX/4DRhxBipdZrQFgqTrCgbN6dRFLlQR9w1n+Y2pSEXnSeHdwOTPVHSKBKQO43XCKWmJPgNFELcRBZdVxGcE9xd2/dzs0C6Aa7vXZ/5ZPx9rgis1TY2UegdpiyWmKV0yw1K2LKpq8BOdH4t3OPDTPvJrymaZwldTb+w6rLGf0p1YAkZIBPpUX9tAstQ1HAz58zHnLhQfUzpm5ft4S3m2i+WkyjzIkh4F2Y1bIvbwQvqT2x3rGJRRWJGyg9PDEVCY4gjKObhjkQbanRG58uPuf7mBofFqKbUBJOngnAvSmGibWNn9uSftf8YAw== + page_age: null + title: Calendar 2025 + type: web_search_result + url: https://www.timeanddate.com/calendar/ + - encrypted_content: Eq0CCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDJI9LlFy1IIRylVnDxoMIXCLnspt86G8qxeIIjD0AiJH0r4ULSsSOXmrBj0ntQEjeADrBeCC4P53iCMga7fv0Uxic9EYRY73zIkr7aAqsAFql3nOsfaopCrkf/WC9qvKgDAeuBfKcTNjj+rfHn5HXgVM4BOpomSXxBc0CHUe3Heqw2mD9+Hy99flWI60pgPB+umunXeOjz5HC4W1q39ROTzvMkdpJzvXoUgG/dbgjJ9vEvZ0VC4z+ZwRAYSNgOgiRt3zApcZJ4iSsF/XgrQNs2L2ThcQyc84+lKKAaKPrSl6Lwm1bH+HTFYIm9KPyhvlbfEbp5buj/wQgn4Na/3qrhgD + page_age: null + title: May 26 + type: web_search_result + url: https://www.nationaldaycalendar.com/may/may-26 + - encrypted_content: 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 + page_age: null + title: Day Numbers for 2025 + type: web_search_result + url: https://www.epochconverter.com/days/2025 + - encrypted_content: EosGCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDDVXhko+GaoaARLiWxoMhhwixTlmn7YeimyXIjBqLMdGYisjngIKCxw2kKV4whivBWinF+rFSah3+1JUtoRUor9o+HmU0StGZxaPydsqjgUGl0OUiZ7Dlc0YqCN8bOGaoUeUv7MpoODHTH97tj67PKZmSbEkDp0J1bJzxHd4WeL1SdNnExDsDcSk4joJFA4lBVAAgxvBogir7w/ws8fRoANAp3F4T9IoZHSQVR2PhtRpymmT1JIaelypYDnda1cLVKrOhF26o8zSp3sDKsilZRW7TSGG1YmzEEKdFK0h/jrGO9yr35kL8vzIF1vdXeJbJa79pFo8e1OLxq9Z2e3y5thBAB3aMK+WMT6n3vRaaRpG4/sqL69lUB/J8Ju1X2VAqfUhYe1GbTKVwAqv+Fllq/NQfZywAaqkHF0YqzMFNYYNB0KTpzrPetfmvXMvog2I0qn8Xuuefw6ROAgsUJaIbyY2b2AbO9Ohxlqv08Lgt/CEmghIi2I/zvt4Y5AjUZzpRUqYAomrkagze50qj5P5Mj191pU+SLehkArpjDp131C5k3jhJ/6m7EaRpv7R4JWxWZCETvqonBRkooPRYqTZkHAD61WjNrFzDOn3fOHjrK9XjQMYE6C9rLOQf/Cy3XdlJgmSa6OKzak0NhRzIA8h7O16Mlf5kaMz5gW4vhIY/luKVQCgo7O39nxitIw94BlW0u0qT0XzCFgS05ADWdIC7SHJaU8NB4x2h0zQTTX2P5m0U2giLYPO86lUWicae08l14+JUB+uPlpDhNU7UQl29SM77+XnzdGiX/Kk99k8FcMrgp6+X5riQWuE0+rBhuDzOnVhGOgFCazxWCIpXIAvYqin429shbJtttU/upLPrbj9HsXpCJjOPI7+FLK7JXH5tH3rz1OCa/14jkNd4GTg5eGNnJSDgSJ2dWxC9oRQyFub6rPc58n9BCkHzcqrF502Q4TTEfNUuQsqBMJL8yEYAw== + page_age: null + title: Countdown to May 26, 2025 in Washington DC, District of Columbia + type: web_search_result + url: https://www.timeanddate.com/countdown/to?msg=Cressida_Glitter's+birthday&year=2025&month=5&day=26&hour=0&min=0&sec=0&fromtheme=birthday + tool_use_id: srvtoolu_01BJh8n7va96puUF3hhNnYnY + type: web_search_tool_result + - text: "\n\nBased on the search results, " + type: text + - citations: + - cited_text: Current moon phase · Moon calendar 2025 · Moon phases (Full Moon) 2025 & 2026 · World Clock · World + Clock · Calculate · Period between two dates · Mon... + encrypted_index: Eo8BCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDEgJNRfMEmnW19PSuBoMpvnoqkDvIRkx6yxdIjBNsAkQg3SrCCN1zCEKO4LtptywdqMNvgULDnwzA4e8JoIj6nDuov4EZIdg37AOh2QqEx0w/3OTuMMUW8z0FsIsEkBStrkYBA== + title: May 2025 Calendar + type: web_search_result_location + url: https://www.calendar-365.com/calendar/2025/May.html + text: today is Monday, May 26, 2025 (Week 22) + type: text + - text: '. This is notably ' + type: text + - citations: + - cited_text: The custom of honoring ancestors by cleaning cemeteries and decorating graves is an ancient and worldwide + tradition, but the specific origin of Memori... + encrypted_index: Eo8BCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDDh1HLd3wIXrMlbIYRoMJbIyO6jhVRytVw9tIjBp5E9G6P/4oCUT+AvhQia2q7og82j1ZKFJMEyZllOT5wNB8wxo/hua8sprObTuApUqEwNbgxwhB3Rzdzbu8jotXE3VzSYYBA== + title: Daily Calendar for Monday, May 26, 2025 | Almanac.com + type: web_search_result_location + url: https://www.almanac.com/calendar/date/2025-05-26 + text: Memorial Day, which was originally known as Decoration Day + type: text + - text: '. ' + type: text + - citations: + - cited_text: 'The year 2025 has 365 days. ' + encrypted_index: Eo8BCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDGSJfkRRZt9POYx8TRoM9ZykWxngb2XQ3vvZIjDwg7OYkVRW8t1wzFmTQ1TL5akbS2y3zcQRN/I2nU4v3NsNZbfOSHEBhWOql3U/nekqE4EtVZ7XYLel8iTWpvOfRFX8iUAYBA== + title: Day Numbers for 2025 + type: web_search_result_location + url: https://www.epochconverter.com/days/2025 + text: The year 2025 is a regular year with 365 days + type: text + - text: . + type: text + id: msg_014DEwKKUs2hUThC8aqhrc5d + model: claude-3-5-sonnet-20241022 + role: assistant + stop_reason: end_turn + stop_sequence: null + type: message + usage: + cache_creation_input_tokens: 0 + cache_read_input_tokens: 0 + input_tokens: 9582 + output_tokens: 178 + server_tool_use: + web_search_requests: 1 + service_tier: standard + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '732' + content-type: + - application/json + host: + - api.openai.com + method: POST + parsed_body: + input: + - content: What day is today? + role: user + - content: Let me search for today's date. + role: assistant + - content: "\n\nBased on the search results, " + role: assistant + - content: today is Monday, May 26, 2025 (Week 22) + role: assistant + - content: '. This is notably ' + role: assistant + - content: Memorial Day, which was originally known as Decoration Day + role: assistant + - content: '. ' + role: assistant + - content: The year 2025 is a regular year with 365 days + role: assistant + - content: . + role: assistant + - content: What day is tomorrow? + role: user + model: gpt-4.1 + stream: false + tool_choice: auto + tools: + - search_context_size: medium + type: web_search_preview + uri: https://api.openai.com/v1/responses + response: + headers: + alt-svc: + - h3=":443"; ma=86400 + connection: + - keep-alive + content-length: + - '1494' + content-type: + - application/json + openai-organization: + - pydantic-28gund + openai-processing-ms: + - '1271' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + background: false + created_at: 1748263711 + error: null + id: resp_6834631faf2481918638284f62855ddf040b4e5d7e74f261 + incomplete_details: null + instructions: null + max_output_tokens: null + metadata: {} + model: gpt-4.1-2025-04-14 + object: response + output: + - content: + - annotations: [] + text: Tomorrow will be **Tuesday, May 27, 2025**. + type: output_text + id: msg_68346320b7608191a49fcd12e06dd3b5040b4e5d7e74f261 + role: assistant + status: completed + type: message + parallel_tool_calls: true + previous_response_id: null + reasoning: + effort: null + summary: null + service_tier: default + status: completed + store: true + temperature: 1.0 + text: + format: + type: text + tool_choice: auto + tools: + - search_context_size: medium + type: web_search_preview + user_location: + city: null + country: US + region: null + timezone: null + type: approximate + top_p: 1.0 + truncation: disabled + usage: + input_tokens: 410 + input_tokens_details: + cached_tokens: 0 + output_tokens: 17 + output_tokens_details: + reasoning_tokens: 0 + total_tokens: 427 + user: null + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_anthropic/test_anthropic_web_search_tool.yaml b/tests/models/cassettes/test_anthropic/test_anthropic_web_search_tool.yaml new file mode 100644 index 0000000000..57f5e285cd --- /dev/null +++ b/tests/models/cassettes/test_anthropic/test_anthropic_web_search_tool.yaml @@ -0,0 +1,166 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '312' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 1024 + messages: + - content: + - text: What day is today? + type: text + role: user + model: claude-3-5-sonnet-latest + stream: false + tool_choice: + type: auto + tools: + - allowed_domains: null + blocked_domains: null + name: web_search + type: web_search_20250305 + user_location: null + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '42593' + content-type: + - application/json + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - text: Let me search for current events to help establish today's date. + type: text + - id: srvtoolu_01MqVvTi9LWTrMRuZ2KttD3M + input: + query: current events news today May 26 2025 + name: web_search + type: server_tool_use + - content: + - encrypted_content: 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 + page_age: null + title: 'News: U.S. and World News Headlines : NPR' + type: web_search_result + url: https://www.npr.org/sections/news/ + - encrypted_content: ErwHCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDK2W0Qu0wNgI6mf5EhoMNCqr1gVeM/Fj3PSYIjBrgyGKmTOrJLgDCXcOvRtrbigKDeccd5oypMBGnMVhm6h3Ade/9+vNOwI3ByflKmwqvwZqUUdfJ6+k9ZDrmb7VM6ktRqsZ4Z++yOdyubDNbsyM6RdwYuNi+bS5ZUON+rMd8+ZrQYlGYqq7NF43o5klxpac+Dsgx3OlKbu6Hq6eiKOQ3rdPGYlUYKdDouAx6RjypXjhYkqErPrjlFZhNv2lO6cohI0QU66p6b7G8UMVyweqYZ+2QYTFfbwU5VdIAOiQW8PBgNwPC5LRnidfbiT04VY+cEsNW04zOq9coXs4NgFRw2WDCZDBPGTEJex0xv7vD0/D0YpBhfiawNJ8FgBTI6q0gXQ2+YwqelVaZ+BDpu2JeRABLXiXQAMIiBBiayofacvfgJZ4omPY1JRiJwX5IpbLFqLcNz2fWr8veYedwrDZV/lOjyn755WTp2i89GD4Pv53htWrDOH8/YJBQ9u5KA2DFz7zAtRLyPqvPz3YaLMr3ATFvs8m0igrllgC5uaWPWfO/28RU7QNnxyBLGNonF3dtz3Uu2naeNvxjRhqCtUOON5odOahtPrRs5qkjv/UrL2YzlnfsRL4Qb/qsGJE6YWScvLhjBaum29Whk2p6RtYJqzzSqDbk0jxKe/hNatl3s2JF1bAW4L7p9FnsK1v/G7AYSaIYl4RDLGuL1bFOKGKVlUZtohNMws+gvTCYKdhQzfurimTsNIpBP4Ci6aJ+/yACa22AXGhZQqyiOS7yxI6zj3vZdQGFBle1TjDpzveY2Nz/kuuTCPbGsWt5kd9v7BkWvkNacqZ70KijyIk5dVt3H0q4eavyNLU0gF4hSCPDHW7eeWXTmNs1YniKiaHrwmOOqXjw2PCQrZv0i7UQRjDmRQqx9NtuqzMup9DRPbQuZM23b8JwzqA0Qjyxc5pTlWRL9aU+U7ZKOD1OdBszAU54c5N9jOca8S2Plt4TGJcAv2Wy73Bex74GPlkHcKWO8TJYhrV4ZF2nMjssncQEKCltJaZg6TJpazpLKoQ1XmYmgzebbVMRc8RTDXk335AYKkN62xRnfrDd5T5wBhGbPNQeF7PGigtAK/SpSpTna/vmGOBul/cONWOFFKNdY+FtCAGd4AOo3s/N8QUnKR66TEv9ocuVep1UZxV2fJcqIuJukutfT9eWPcou6VImLUzRQMYAw== + page_age: 4 days ago + title: The Biggest News Stories Of 2025 + type: web_search_result + url: https://92q.com/playlist/the-biggest-news-stories-of-2025/ + - encrypted_content: EuYCCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDCmLeaYgzUtJ4Mi+fBoMpktwxWvlSdpeNRRlIjDckkEHzeMKWP+vkSJ+7ci91OlLvn1nTU4wG+0am9miZ+68Q8XjsyCBGekIPeSsgpkq6QFTAvqrNhd55GMbj8VQtB/7oV66lwp8PzaymgQlLzCnxBdZ6IRYyEd6XwFOPrWCwyjtlKbwRiM2NIaNsGcrraBVrDfsjCz20qsDPGNsQf587z/TD3zWUSelhjhf+T5nDCEXUkYM2+4MaGP5Ty57Khh3WQr5q6Q46m85jBBF+akWf1uKZEgjgFug1ufj/8TXEEAaKCVY9YeXTXfYH8DocKveCXH4Bp9TNbgx55UrL8NdXiwdtpI/zqY+8hM/SiaVeXXI/Rbmjg3HzFTLfrH4wSrl5awdKWuGwQy8nqRISZlwNVnwFY0e8uc08xgD + page_age: null + title: ABC News – Breaking News, Latest News and Videos + type: web_search_result + url: https://abcnews.go.com/ + - encrypted_content: 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 + page_age: null + title: Breaking News, Latest News and Videos | CNN + type: web_search_result + url: https://www.cnn.com/ + - encrypted_content: ErQCCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDGPat3RtBffd6jd9uxoMkx9uAfM4hgJRbrR4IjCBVNWqux+TsqDP0poLm+ss84SLrVR4rAcjrQSDPna9ZfR0OFhPjv2ko1ZVuBzeRE4qtwGOPV6my0G/y4nPEH8gNVc3y/8uZzh7O8CBrduzchEMd5RRXLlsC+bU/SjZ+5LBYGzAVwRCfVXIdaJ0/d8RYdJWHo3bvKc5Lu/WFPV6Po9gVHLOU5WVDsyzwmrvqzCYC0UhkUMa0yf5j7WTFaT+kgHZcFcbvYPG53USqNh0seahaaCC5fJRjRBTAvuyj4md+ppTjIXGZEp3rTMG3MTkv8t60MgPzn4ObLGEmBQIQrfES9G2BT1k7lUYAw== + page_age: null + title: 'Philippines Top Stories: Politics, Environment, Education, Trending | Inquirer.net' + type: web_search_result + url: https://newsinfo.inquirer.net + - encrypted_content: Er8qCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDHxmRLcrViwuD4QB+xoM7WGSYO1wzb6z0HchIjATj+RTz1UTP1XUgzt/sKVxIcnJjndgdx3zaOKZ/CMx4ib/mUBO2GKxhojugf+2p5kqwik956URo2GiacJOXWHP0cyE0HmZMDSHK1Nqs0Y8aXMl1iWcTu2Q1ZmBq4+AQ8IQc423bw58O5dc3bS1sdbQJyrd/YL/9SG6Df73ou97ktQ1Ij/MdEHQuDMHhvVDoESB4+i7NDUU4aLqgBFiOGCEozcSTWUdK5ePwtOMSOEHUCOJ7lcxTzDpcTg0tKH1Qo+HANXFl63xNQGbJyxUUBGyGjiAe2vWb6kvW6owWSL5HHYnJ0pzpxska1ovW0yt06Nw9ZuotsX0Xq84sa/Ceg/fFCkMsLoREsCknC9di0zda3CgMrdX481wowpRS0dgj586+6SX/b9C9k7y9htoMLdsG38chq/yHAKeUrtjxHRUI7rLsS63jmVrFxj6Sbggo2fL1bEFliDjL4SFVz8Fu0XaFBlq+S9cU76uj/vVh76btLnNOKjBZyZvZ5LG8XqHBE6AN0nCx19o8zOpvXYej+hMftkU1fHljvT6HJSHw0YUjyflvx06S4JXH12HG5h2r/86E4qHw3Q76sY+dvzRR0IvyGvmtKVPlJame6h4N1epLclnYzk/wfukJGlJLHOhypvFl3oYYdeAr1UCEV+EiU8O9uLl5i1fwtFvK2+SPN+hQIdGGr8ur9TkwGWSCCiJFxurE5L7QlYfh7zZRTbACtwssOq1qcLHGxz7ZCLDvzTzZZjuKu9DghY652BHa7RVnx86ynDt96iaGwMgJxzSBE6xCjr1FH/FbP5neOOiqO1jslLd6qie6UtbOw39ECIYDxtz2qL8BnaBHjn9Y7O1/fM96qVMGU1cC/x52veH3rnSLcuPYudqMCIAINDQqwekl3bYKkeSM7IZjN1pFhER3yjchFZfsAmBTIPL5KzOdqefJw0ZDxjvpjEvoi3dX8WZtnZj8NrBCg8i+cj7gVgABa+PJd2YnGoIEF+UYBseM2g1elGWmAC/uFU+jSe4z8TrMmbbpk3TOwIWS7W2drCOs0/SMOZabw2OL1rnC3crODB/pAZ5AeKmi/jaBq9loSCqHQNga9dryz0tSsz7+csOndZ+AwRjPc8hEtR4b32kFObLK5907LEhBfWu0HFgWqYL85CaE52ZL6ShOQ1QVlge8B0F7EUiH+MaOK/9Wb+qMYCGm+umzs4MIqB1Sby8L6+Fp5NgH3rvIpLsM8s8h1QhQ3gWy+jF7h1PQ0HaFx+VJzK5vjv5Pqzo6ME/veoDxNxJmyCSRCvm2DsDEdlFwLe5ONBlkjvKg0KQqgg65Y+vbxXtrSvtqskT4aWWRmBN+gt6i28l5hI53jQFEnm0GU7aQ/v7Hjzjh54cIe9zvVc2LT4DsGZAK95KcF5B5/RH/VK5LJUwx5SCi+O3WS/Ht2v42gqr8UnvgOVXn7A3O8A1rnZm02qHEUf5APRMEhjAQzQE0lm68JTvEsNlmIsaNuO6c60XcSgzjIRZac/S/8ONaigrmxsfK6A+QlcxAniqsmXavu8gzhKIlAaLvff4B2uGLUyeDp9DXPyVdw7nmLPynPWnTe2xFlHQ28krDN9lPnSbK7DcGi5BVgVikjQwqJjUi+wYX+nCqVy0Djm/wNr4M2MixbbVxppvD7F0bWK70f9UZ8pblH0xK3fcnYzLTXLvcfSGjHsU8M6gohZTUoUroRdDEAmSfApBORQbtst6KNWuxCddDRBLnP+S66HwdvViZstOVrlC5l6eLsysk7KjYx4RxlWTZ5FuzBafbmZRR5RfNzTSzPXXNMSyAKJe97zrQR9Nh6YAEdyTO7bNY4OccTM7UYzFlC/vY3Rkza5oNd5heMU1QphqdygD2YIZ/dMeYUam1M+qdjLPBC6WN6HjqjMNV5QUaCDUO+HOg58jR7OWmG0Hho2cEkaUKuQ0oRlDSK69Gazimj5y3h2+QLcf87hbQJF9ovmFIZpKWRGUOo+QMB8aSKlKrHYRCIJsDTaQhbI7SksT1haHFwE4YxzXlU9HBdbFQmfRhw524LphCN7S8BmUo1FgpYUSNIoX2XgAge/Yor2HwnfMdvEJQVDyrbUO6WxaACpYTCgvPa60pVDTO08kfLyYWDoSFeG9PwSdWxDkZ0DS2/618eKASVJ4sJsrJdFwkTCs51FFxehEbYEqoM6ujFVvqLb/MMBOoqdQUURh+3mwx2e8gygYFQkSkraRU1fYiiL1oufMs9DVyMm9rVKuPB/FbDDj6ZAUMfXbsnlAnsJbwZuyYOkp2SawPPOKfhNqjOkbwb5xpj9uM+DJA37Z/OE+S6q4Vhi3jILsiQzeOnIwPCJEO07dMW77xz38i0LiNphTDqn7MZuKHDTRyIwgynKLyI1icusB4zgz1RJVBaTeehH+YlDkn+tA4zUs2HjAu/PWHzN1sk765Fu8gbJCTDBLT93W58kj7V3YWPsD/FYiodVNXKLzXV1Mt+xln0Od+Uu0bQp3wKS6q7A+KEplb2onOFrtr3IVg3QLsEsBM18yC+91hGfrr7fZjo/I9QnhG9hNQpDzuMAOGElMeCMYHC02qALzfYH9sY3havDhPHemeoGbQag8tRLrFVpRI/qcSf6t7T7XqTjX+Kp7MayiNNnuSWC+ULbX1MuGEhMMvaiOvbzUIRsIwPJvk4TpJh17Hof7bdVf3t5HwlYeqlJNpWK195qatt/sOyK86GXAXnXVeFzShKAuntbvXcp7Y5DxbzEizHFSq9I8O6ANgNLCMuvGtxIC3MwzsPtEkMTDBHG78ZHlBnHdzCmkIxRy9NIxvkNZg0drPt3F7WpjMnW1I94zadixQij1IR+Ms2D50uUQwGRc2wRd49Gg6GSyg2E7jiDOwIuoXVWdmA2nxZHtIyjPjTrpkm5MbTFMJ4OvJwSAMTtN9MMx+Obg09AnDyE8E2OB4MYirozaLBff8uCO2Cfs+Ow5IgNIotmSfgOg3VtqlFOXY/zRuWBLS+IMc2gHYXVYEiiXrlnDt5VbUcXAMW3Pn7LAj33lMctiqUWsKBrWsLpXWZ9p/ueiwFtortqHtkjcEbFhM4r2q2VXXHoApMk0yt9lFQbk9lqurgFeX6PQgVkXvdGHXDWkk/K7QbKW8LvBPz/8uS40gKUPPWfekpTu521x5zAayCjhNAtcBZA6JqoE1DWOucJ+EIWajSLMTuQheamq2DtkV9OBR4DpbH60FYA//kdFPiK4dDTY4ylN7vuO0G28yTFZuTnDSLRqrnEhVTdIrDEcxcQmy6DbpzzX4zDOBwnVTUuuXxfL8f9UFrjYgp6Nvc1Kvw1Kj272qON4LZfP3qhsqCcb8NchDFnKsyBOt8LWkMI8x3OhCjGj2neAjHQni6TVjqOLu3XjpeSDaITP7ss7EAZMmlnXOHzN02kJTshp0LvhDoT5Qiio8CtQOMtMoFZWT/XHUyUbP0/VYJHTnB19zUkYL3O7o9T34Phq0ShzdcZucO1+d6NJAjQ+aaI0D1CGhkAa0AvBN5/sp3bVTFYN4tG8XV0oJ6rdu0vwKxOfMQpRceCGVKP+/xqyKIVOY6RLrf8kXqD5IWvQyaCItSoxESRN8fQH2H6C0H1j+h1Rl/i1EoZkon/zsleSoPFJBYtDuw86AM4KiVoG1MEXmtOSuFGMQwMjYb2V371s6bD+uJy/DE+rihJk8ZnIpDjNKX/kqy2fsHF98Su7p67/VyZ9vg95vSVsrlbz6paciTaCarmVYK7rqyfZOolTjJ6PjbfdZ5eAITw2lxn7uM8bKrC3+MwsoWI8+HoJRfApA+uxqFvVH+cknXwT0ZHVADwGafrEEmsdR1BqWh66L5k0gNY/xn31a/aAqw7yfayim6WyWtawb5UFBzCMkn1skhvhqv0ij65I2+HyW+wJB/krTx13EE5QKnSVJb3pSTTqzW9o6BYcirKLZr+Y1iV0z2L+MFfKKzFNmycQFUflmsn1RACM+xG6qpOqX/b1Orpyez5Uu85It8dy2lV89mYJggZeksti+x7QP7R7uIAbyZwFgpNvmg3I9kIcOahD77kJbeHNHTFGdvlA7OpZoq9kffHCcZsjLLtNoxNlI68tXF72/EDTXez8f3xZE7rMRcEqSOGNqIcaThy/yJ4cICHEkSUKtmgW9sKPoQXl+CHmLn1KF1SFoXfQCCnpFH59TBZvCuTwMroSI/ZGogJt/adOpsKybOWy0tsHXgbnjJrfyKxYdJEiX3JQPLCjO0Cma2wWpPQiDtwa1yXvXqq6yGU770tcwXdYxoF5PvTCYgFXBLl4SWn0H6ckNo1C55osayn8ZewZlPNsMntYCxygziAgOHbfdX5KuBCIP5aSfuJ6hyfqj6QLY/h0d5ghG+2ZWn4hoDwuc2/sEWnguIjFM4Y6HNibyq0DOH0UFNIkCJFMYJa8NB6sPqHzPhbiNvzrDXcJuFIs4we73LGulLpyYkfpzHaMkx52P029saGw0XdthWCF+7bLbB/2D2A1AJJBrYI/ooEFxAIOBk8qEGfUNOSLCJTnTiCo99iCGf7sUAVYNGO3NPpq0hotwbGbZfBIyyEo33CNoUbInHrnEsw9yj5mbxA5nE9Kqk+UyyxyzNHV0oEcVsUaEy8QYOqi5YTAC9/cAUj3VWtq13COYyEIZ0bX7XVASC4opBwVIfw9ZO9Fn66U3kgYtKZ975m/R7HkoS2YfKzI+0uuP/sgOIr6rCEBYkVpJi9ckHdm8EzAH1Miy64mL7M6nb5MAiMqXOoygVPSp7HL5ISke1WkWjCc2IQcdDjbeLkQS1INMduZCyXj8HNfDnTJVVlA/fkZGarYgngc18oBvuJ7yeDMRn2dLZUSOL4k2Q6EKiOyaQO0aIwG+yuHUaZFBS6mUDSn2InWiv9Owi8xHurykjJcBZEPXLDdkUfw0qoEvTYIL/sz0A8gb9nVpP9BQc+h1VA5eAdwJGmjA5hYHsvjiyvs8psFXGwrrKNqEMLqIaYZA9TCZM+16Xi0Z0it2koo0wLwl7OnxWL8pOUEElhUshtNqaYiI0/wdJjbtvgH7ry23SNxXov3cNOFqsn/suyBZSuKFqh3RfqnL3GTCb2fQzB5iXYRU4V7hDrRtYTJ6rYUn4nw5+VNWhPr+S4ok4TjiWnfIjLi7WDg++YDvwyubwA8sbH8gK10jTFV3WJyKkOXt7/CAPC24Tq/DwlRyYsP+WsjAQI3SKFgy5tROUpEsCr97aVSF/aPSO0LkAs5c0s1Lixg/ICLB0gCbuHAiuVAFj8Sb2yTghjiO+iVuZHwEf6yjCBtrpLBWrJQOpcsQ+OBEv6Sr5lA9LJSsC6sJ2ubVeOeeau0JEatKDZkFFUX2JLgtvgzNw1TrAbSEM5pY8zEvl4NiQvislYXgVVmJsHhOK1eeteSDDzbHiL763BctMCpUQvrOiNLZWCwn3R6nqliY5udpDwEgz3PjEW+r0Rc8NZXm1FKKrelwdluzHSH/cN14ShwFeNDVirTpRoWo3cDxmzi7DmuZMGc4oYAtUOsts1jO4prqVKxGldUUS0n9dOHzXD+cPhuG6yRt8SJzVUrfRBK0W8cWaFrIBC/tKtxFvGnPhNRJZel04NEyDwb2zwEx2LIx8aZ4YH7Kt0KWGJRaffQuePpxomiZ0OdXxcSYvOybZhdD5d4EJmIgWKqB5hF2QhBMxhEBn1UoBUqI5zHPOUR80j/t8eMl7O7Z3dpDxaDs30mhY5QS2ZvKqPhAieKPd2b/o/47feqtNm7kDbDVuiaeKkt3Rg/tS1PJguq//6byk6DCVAua3VMS0zZ/ie6WmfkzXfCi7vtfzDzs7nvzqwg/b5BoIg6wsOrhQ2OPvQQ55KrBDj54KgzZPBLLXz+I6mkss/JFR4hRpIyD0KENtIG+3+ITAINuA1YT2Dhs6l/XIWRx6uKeM3+OIDJqUWXnQmNGdN+Alzh/wrqtheE3ciqTL4ZrEYXNrwIYJ0ZU2Iadzv4MwISWeQvr98epm+LeJ2IVEoa5QdX708xshvKi1F1qIRayoGDJ/gz4PiQoDM+Yi1teuowyVRjZ7+XWSl4urfkRKkHPgDnpPTKI93zS5E1v5XZSZrxaJrXAM7dPwLUJ1+OxV8vkEtv+3m5pA0mJ4p8qB+VeeQeGYoOIDSHFYYaoGPq+OiYP511ucORAlqRY1LFeZCvVJgWDCh33ylDHPrw1z8atXWvAEu6Ejk0Nv88MOMZj2q5WM7uLgzazn/GWHSighyMhjU5LJY8ixSTFPisVIZryH8sEQxjotkSYIGYpidJSYYltriZ89KB6A41WxBCrrOifdzhjNNLl70AcGuXkt8IsNpGYbLAP6LIAtQFQhbktjcfMcwlxvtYJt7yC232ga9POlQyzcDAis+EVutIo0SkKN7cu6KV6jJkeoPGl/feOM3Q91iJG7RkejVCvTgKBM5URjRr68np/3hwSxsnutl2BZnlUnDll+mZT/m2MIxId1p628G37kupY0gtH6eWdPsKif4xAY7RV7UtxpjEiUWeCXDEX6gChcWNgHT7LR/9egRCpLUtEoCQe9fMo6+HkIQcIbaRMqCdgffa4k4GRLRxFPdZ3f+hCAhRM4DhnwNnUrCGgD0izNsjOekzzUAMDpKswhxXfbxBXJZSZ4ZBBBSIUN3K4aCBKO9xYra62oNWU/6fgkWUZr1DosPpFypR1Iwi91GafCfKFb90EcmJwpOLbHaBkX5PU1HxZVYyH0qaXIfPStL+OFuUMbhBXrdOlPprVF2q5lg0a4nsUD+b5yUcgjn116AxXsocVL8E18LlY1mxBTzP2BRB8h2Z78jfn0EFTR4Sb1SW5onrLbYZC+Zfx6MrQRPnrgeO7Yt4O36hUhsL4bRFq78dx7A+78GNlTlWtRn4dxmuH82+5kMmW/G0y7pozSHVv9y0i0uyYBMe3a8TzhfjZ62tApbxduXL1hDhhzpoHSjyeic74QndYU3ixkrI2sjCpnODlNWfNcEDJ5eVfSepoBdvtwxVX9Go9N1NWk4tKSQS+VnP70Ua2yCZWmI3It/0Q0NGL8eJ5wfpq3WOCa8TmQiV5Zx8e2LjnyLlYj7RsODQZSSet0V4zOr8SOgQ56Q6kwyW9rnjVZItW0lm1h2CqQvlnvF/Acmrzbr/UTEIrEqTGQpaqdxdLOk5ybihhfTgWaTPJ9oRKomxGAM= + page_age: null + title: Portal:Current events/May 2025 - Wikipedia + type: web_search_result + url: https://en.wikipedia.org/wiki/Portal:Current_events/May_2025 + - encrypted_content: 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 + page_age: null + title: Portal:Current events - Wikipedia + type: web_search_result + url: https://en.wikipedia.org/wiki/Portal:Current_events + - encrypted_content: EsoJCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDAYjLeq1Kci5o2pi8BoMqJacE/46sh+pR01VIjDPMDUx0d3wj2zbUnowpxFwHvCCnCwLoPMxQsQm/dnvm0Fzfga5o6zXqDwWpXvUzhEqzQg/X9w2opL/m5o7bUw/TubgkYaR6l4t2n0oQlGgetbSj1gOEor/WWJ8bWXL3BZnS2xkIzwGrbLdlPDn/NoXICDQZ+P3IlA6B8CVwiijOnNq+x5nTpX0m794VFJIfO5SdupAiWqhWmtqt8XhcWs8W2gnhDDvNsBG6oH2ZsRenxt3n7a7eWo7yRk/KSHdBM9c+L5r3wu3Ul81DW9CuE6KqUdFjjePJfWKL8OvzfkvJjIqcRaqc/3RIRZbSPbimBiRMXtCBZeCYE1yeBs3xLQ7TJXgRM/9ScKromcFWckYpGXBYSGL8SiXXoBUD7pLsuP5FnRnZUkQLCHTLoId0/w4jVbuXmDh3oipIlGUQCSbp3FkogFB/CZFpKz4tY5E9WQ4pBkApGYgAeGgOOStiUW3pE9oCy5TRpCfilrg66RtJozGI+LWM/XYuuOwSK+f+/c6AaUJ7av+LCUSPFI6G1XErfHK/KeBSJp7ZVoRXn/f7yJXlZvybKQXdN6UtxqxRJbil9RnmmXsBc6cesWW/cHbz01V8tkaqcYdrtJdVM/LesICK77C/JYiA6PQsneeg5xdZDCUp7yUO9P/CHMBqhPB8geS9y4dG7UIdJrFbv43cGOiqoSBsBGCLCc7crptYYGydT6YBgKb+ktUJm14MfbF8lzKt6SVYpn8KWL2dyhsDbfi88h51fvZqDV6loTDpyHbMHeJoA4pIxLhkBIriQOLNnEIEwqTGy2XFy326bahzINKJVTY1mMq2v3O0Snl0DNcAZ1X/iHt393xPgdcSy6c2+sDRexvpU4grX1GGFD4E8kg1QP0fErasq17XzRVpnU7Kedk/ntU/X6zeI3aTEeyRNG7IPH67w6GyIF8XmgCh25H6bCBGN87N8hnPSVAy8/qIMcfZYaF1c8W/QB9n7HBWhQgdyZv3relj0Ur0xdRi2osqo+k2c0a9mmIVupbzpLAxfY7LiwU8Edsr+1WY62x1omk+b4XNiGnhHnrF4B2o+f89icgAVSqRo2ydqIUDnZUYewu8jjUg/j+WUI8yKqZHCgCRdkm4fDSOcK8faTeaITl1iI6XFbUicEWZzG87tFykNSv5fz+ueDbMj936cm97rPUhp/qMnS2uloAxmiWLcS2/oV605i97ccR9IlwB0tt259e9iCvltjxzcC6P95vbhLS94+xVNOG2fmQtzE8oyaREZBkwSjVHuJ3lDAxvHDRYY8F+lkuLE4AvLiye3CDAMXNyCrG+/xiQIBNUGs+1aV9edHMmwpCVs99Q+nHO1RBVPljY607Q6u06Wt4VHnY+45+IxzpHHWXxg3Jn8Lh1AuzFKEaRWaI7JDSCgJmYxjIwkUO7988PWjOmFLquOd6mQsQ6iVG/89zSwr019RlAQRDIbMimefHIYhLm4S/Y8TzPhLFXJ6FaxrPFAkkkp2LnLQUoNKlo2h64JaAerGAku2FEwn/vo3hsCXwILg6R4QYAw== + page_age: null + title: Current Affairs Today – Current Affairs – 2025-26 - GKToday + type: web_search_result + url: https://www.gktoday.in/current-affairs/ + - encrypted_content: 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 + page_age: 3 weeks ago + title: 'May 6, 2025: Top news events to look out for today - People Daily' + type: web_search_result + url: https://peopledaily.digital/news/may-6-2025-top-news-events-to-look-out-for-today + - encrypted_content: EvcfCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDLM35Bum8iGp3KQslRoM14ZW+oAvbXsy7nUsIjCLxAF5JOrnB/xWs20058EEqp98kwMCubS8ohl/TFUHHJN7eeUDJz7IuZOFycr4+2Eq+h4AUNPhuzguwpPktjSAdmE8fd4sXi46MXN+AqpE3NlTX7NqhmtEhPnwn3HdMGnBiQMG///1824z5wmFCjV2v/aqK/HIy1wvC7M0C/oWcQiBhVR1zNhPbGz153Vt5tw/XgOusVQZz+8tKl3yXac7lWmksW03m2JK39XQFFcs5CZJIaYTqqReD28AyyAaNFW29WF9GrW13HCaOn8YeSHP+zVLqBWR2+WmEnIDStBBEpnl5QVyjhFMdiUG/0TzPTfhXOvHJo5WL/pef7qEKG1ECVjkF4BbYGXh/4E+3CFu2xaFO7Kfcds3pqb2hPgU5gaBnXFAv8QRXxPqfAWOX54vAW0H1ahBM9sUQQcfK8XTVyoVvEF/ImqoC8m4I7ciw8cGW9g7UF4ML8w8NGefqMDeWBz57Q3fPDkAZdr3OLdaUQkY2Vub+LFeI9hAqbLBixWmG6l9iTytGF/XuBuqcM81HOo9BaD0Dgh11IUcz3F2iCo53yoUAqjC40nL/oHYbeJHSGKbhZSYjZc16WQ1RKw8AAbaKxQofOKVH7L+eYxoUnUzbl5WnqiwKdy9k7/lOH0o+/Xu3CUlyi9kTuFRv3MfhuZCmB3t/sflVtPBqSNin7wXEcUduJlODsWQ2zPzqbqjLJr8Uc0Bxjpb3MzwOeSAVrkG09Dxn/mdtdRoJ11WsLDqna7SJ9LBGqD0liHqFPF0b3Zi5Xm00dIjhpe5mGHZPiTEfkC7Rtt86Ifl9pvuaEPiCIAMF2TRfGAHsA59C3yBdsSnbdV2OOuK6JOqdLyt1qEP9tGDkYX2fdU4fgyK/gva7KR4sX00DpC95D16vt0AhUhr4uE4CZAxyzp898Q990qgmjkGccTiM5VbDk+1cFE0q8Kksb0Byd2JCUelWl/sFlMHYJHzswVshTeGRgwaUiiwICrgBB6Oc/21+qISLLka8+dyIvnDmSNG0KUp4a1bLA8TR4WlTB5THJoM9kWEqhPIPkx+Q1DmqPzSvPuCNfXOiNBUrAsLFOVij3l+B9zNJDJEi67UeQ81cGclwstJyI+F04QjOxynhRmjsMY3Vj9n7tZ1MjoYfclrcFaV7H98USfV7Z1Jid+e5qS3t8ZP7w0v3CSMfKZpo1WB0R/cDIE+fS+APoydzO/k6EL155uYA4UoFyKAAoEcgnNkBK9E8AhZPpvila+XCtdCrq1Rrrp3J4O4e4DWcWefL/dhWuslr4UhlAhjbfvyz4yCphHKbAakZjh0SD2J+1laXJaiZenpo954DfggYKIYlvriyjGikWvcebJey0b5qw3+Mol38WBRXt9ikYKNNUONeLDsiBXgoOo84kAGigQ2O1c1aV1oAX7xcPVfIhUWnFQ2gY5wtfPeqWLCEYaenNlN8G7kqIPcWVLKdbeMk0PCmyMeZQi4HlxO/cwkLnf3fI4++7/AL7zlEFYYei17YP2NjvYTKD5PQld3bcEKYozrF5LVReRbMpwhaLTLVmowuU4jwLawC7vEv34ALQPblM0MJijk+JuafV9uQ7y9/w90OxaRZ0Gnvb/ZuRcY35g8OjB3TLONRU7vlAYKoUH513Lkjk9lGNcjep3AeiuboLuFD2AbVv8CmZ4lsAs+NeN6R5c8arThgqBIiNfprN3uStBoqHp2hnEU2NfAxPHblGRVSfmEUvJJL3yfb28eVG05Fp4W9qp+Ju49V6242x0/DXOhV2Dz6uUYsJJotNX4Ei+2HcjNSRGQDvmBmvrzxHynybVls3SY86LUAogQ7cl381a2n1gIhUFootBUpRSQSBTm5EEsLCpBWaC+itiRj6c+dV+qcQvinExRLuRLRyIDWmvZnfHNypERhLnfuAqLG4z9cplHajHnlBLA7lJIeFwhTZCHmhDw6sTwmQhpx5gCbdFhPHkBK4KycXhbdhV8ksE6efOXqI6ZArPEbAs0EklZkukS9j2i5W3xLaeO7TT9RXgthdcIpDdpTpby5E+dX8Z/e5TSbUQSQZhoMfZPyJY4Y4+LGq5t4FgRJJq5oLiRCEFokq+7JHuhnHI/yvHgERjR1pq8hiffv3h38bm8aIoe9dnmQBL3HeRgIPba4L1E5R95G+WzhToeHmn9E53oWSjXe8PpQHack54hR8qSHJmHsjjsADUjo0mrOBZa7hMwkX1Z7ysPL0p5W58Fx7Pi5w0DDBRY+KKfjMm/tZw60uMR7UfK0xfweOl43GRe/nwXH+7t2Rp2jpuAWGSH2uhKyvnQpZl3zTLwG8BLLAQOhXFblOK+Ozo9M51hJESZCDfxUG+QDGh41AVrX//t/4ZiY3h6EbwPI8j+/YIyxDsQewGnCrJ8zKqt0b7Evq57FM70q8Xc5uIoxPB0ZtfSGLY8kZLY+aYDGTy1IIpTa11q4CGC7RGlOV0qkcZcuyHhAF9h2zsjLrbeBQEdgHoXT57CZhMua4iQTqh4oHwq0k3bekt+gYp97Mx50R2UDCw24dfCeBrEeZuE5Sin1HxXt9/OaiP+cjNP7hGRZf0wYe0Y23XgDVfRwOmpCASSscBgjeimT9XviurY/RaI3ilfMJMsb/f/reoXEzglV4o+i/F6aBt970M47H+KoKptQIwKSDYcXxDbv1YzaTafmgKHObn5nXzB1BAMQIoNtUb3/2ZH6HfWjaXVuPoqYUS2GXpcRnxBDqvaFx44BOwP02q7uuUXkLI49j7TMpT3tnWk2nc5HMtZOetakbjklR2CcVEGKAxttR7wMUrNWBh7lUYuIeicuQBsl1rgGP9BP5pjkFh7ttxzQw3ShTDp2AfzZlogK7y8TKACZU2pHEe8HQ4rXuuYUR08+zxqrXBrzBKNsbLK5X37Z6nrcMntLjr6L3x7nx4bfoHKDp3lLWDfjH7AFfPJXBtSOk3+cuntDt50rhMFgxGx6iwAQJuT8T3ABoaiyDTIsKLL8wRT5STRRZXjBGqsRX6JyXkBmUFlqM5f1Gc91ArKRrjJDNS9+3+8t7z3z6jMVMMjaW1bFJlwe71TrQIGFzVltwflr1+1HwYp7KMzsdeIQlUDSeoy19xl8fPDKaulUHe5RjOsKwCp3rqIW/l5yrZ2cPfdugFs0NJeGj6P1s/myBxd9J2BNw/SSUEVqFvIYHPbwJNe56TmDAkpIXM6/p41h4H58Ezw99jCNzJf9akBunZCxh3gMFigG8EMTTXNdUMkICeYG3PZs3zjax68X62e/sFA3MWjlb5P+ULvuev0kmXyh83Ot4C2b+a1XR5lRp91KE60i8OyGbDRycctX9EhQENSgvG3gblDD0OSkVbyRGqC/BqACu9Q9N2cWBPCJib8AtW/MDCtIbbe/TQg8rPCRLVkKOZpqfJDKNcXCbfd5d0hjXuut9el43TzwlbfrOKzY8Piubx3u6TtA9iXwit/vPuAZb7pYivaswBJrdIg3q3UbTUZrCWKpenAQuI2i1PWbFPrNXmT3WP8ucGiOw4BZL/us2SmoHI/QgKzZ7kYrB9rFaR3Eyoxm0khw20ZrGbep1VUuKlQHLG+OQzBrarYRG6d2Or5WlUgtV7jmMTWaZThFJ00YDGDpwRx11t79Ul2rX7iDCTr1IacM2S1zdPm9A790O7UEroB63OFc6YyG6UT2m7H2mo1KnD92GLjSra19NBE9WaY3L+SPLpxlOL+jqovWZqN1aRHlUIaO0pW/c0mootGjajXdW95RHjCwuvOJ59JJfRGtawht5AhFzjfejqBReAiBgP/rypuFQE9Czz+2C6rPm56lbi7GDTqIFDqjsfP5wUYhPwvMDFYgpIvRx4/MFjCPhG99FgrnbEi5WhTiwlFBm3+KVsGtEC035GmM2OKCTzLhgc5SZdbiw7y1FTDmz6es4RRnuOfcUKOg9nOs9/bqJkaAZJ13cZjJ4OI3LBZCifHJ8HX740yytpJu0mO/5qkCUGMz4CIb3so1HUY4yN6JyzBsVDa442n6CfcF/0EIlwS67WW3sq/r2GmvNAFgBQvtRckwmoA0qc2A3/OMzu7vcEDiMnD/Mj20+cM89PYWl6eCp7MA3CVfFvdcxdRqpcEWCZCz5nZSABdlcKuvdwaHANzvWUtIj5tjGyloHsOtErPa5PYcWDa78e/zQ5jJzWcI7/V+7RIjXWtr8hdWSju4SSxeJITGEnr82AuXrtcQR4N8FTd2c+oudOhZI/+vP6o24mgpYvM4vh3RxCiit/fc8A0TyL6uTXXCDMT6Zd3VdyO1L/szRNfxrzGW2KifJ7j6vlQ6y/70VYek01PqNYIHWhbcU3vxT9L4RKvl1xfWDtnwVBey8nVynS+GqBixUaHeITUwFmmgqLgsusOhybqm47PQDu6cK6wdqLgv0OKu5CleyvApsHWL/bWUY7qgXOEVZSO9fjeaE4TBd+ZCWiZBCW8GTxWTBxQNJ7Rt6qYEW2Qu9vY1sl8Lad2AABeDxTeY74CGyGGrhHO5LaA5gLdWmgfBi3nMZVODuIwpjFjtcnOwEXLevSIzcljrM80fMiCBkviECr45Mu7zAAIWMuEEy5mSkMsY0ifxmhFLGp63xCUc1iaouY/geO1Pu53MH0zh/Vm6Jka3Iks+5l9lSwJ8PlLKTViyfVynQseOLGPYCD7070r2OKvV1eZEZochpJFHcB3eC9WBIOTBWAyR/1QNnOXx0nl6/Co2ROFV8I6FvmXl7vdLsfogynpeH5hTGvbMxGUIhlOBPRrdvytXYB5I1EGMCYd1Hwl7iGX5FtktQx0epzBuLeYpBaoMEl0KgkCUPorpQqkE2FmREB9aVpM8QYayC5tqJZhhV1+6Ec+SEE+Ol8+ZG+0K+Dogbx6ra/ktD1X2X4QPeieLGvCLGFgVlzVxmuryoZa+m8E9JFnt3DvyqOnZ/GjutTdI1/JC/JJ2q4IvNo/oFQyqZitB/NX3IGXIm7Qe+AGVXYukItPSh9wNp1dmlCHQwMdN6fu9HOh5NswBrXqAR/TbK+7JjIY6HeWlykdOUeE//3e0SACTbjq7EbH0mbnWLTGPLCAhb49c5RJbXNJrPKWxLj5y9eDAxTrpqUQ3IfjjGiU9JBUTAUjwlKrE2/skjZtJbVegv1QhBFuwaUloEXHh89oOBh+4B5KbxqlS/YXtrHfKbFewdGiRSV4KUYc1FV4emyUZmn27joV1qc93UgWkqyAgXg9X75I7GtygxzN0SYqMp1R1LSOofRiqHMLOMs68He0BOPCRHw21/veVKiC1gN5R5g64DvLH4uhL+BBf15TivY/XnJKPJKtmG8pEWd6uXX/fYSo670WD2A7tWV1ZhszWai3tgH/1wR7kpOzik6wkhgD + page_age: 7 hours ago + title: 26 May 2025 UPSC Current Affairs - Daily News Headlines + type: web_search_result + url: https://testbook.com/ias-preparation/upsc-current-affairs-for-26-may-2025 + tool_use_id: srvtoolu_01MqVvTi9LWTrMRuZ2KttD3M + type: web_search_tool_result + - text: "\n\nBased on the search results, today is Monday, May 26, 2025. This is confirmed by several sources:\n\n1. " + type: text + - citations: + - cited_text: "Katja Ridderbusch hide caption toggle caption Katja Ridderbusch · May 26, 2025 \x95 Efforts to improve + officers' mental health have grown over the p..." + encrypted_index: Eo8BCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDDbwqv5slJiinAczsRoMhSEnYjGPJgqqehvhIjD+BvlqTC7HRjtE/aw2RZWQrImtkFRHsMc+fLuePmKCTV6CchthAUcDrpTbM2sVfeoqE2azhBPbtwuu/6GGPopddo8c5owYBA== + title: 'News: U.S. and World News Headlines : NPR' + type: web_search_result_location + url: https://www.npr.org/sections/news/ + - cited_text: "May 26, 2025 \x95 It's Memorial Day. " + encrypted_index: EpABCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDP+cEzOyt7FZcwJ+GBoMyXx/ouZvvFOJk680IjAWGSs9Nrqov5SEum6N3Gk0FGVUoIgOxF4aOdlcLDVixGpcVil7pLWnwcKf6aH1DhIqFFQUWnEOplUMZH6nQ1RQprK0YB59GAQ= + title: 'News: U.S. and World News Headlines : NPR' + type: web_search_result_location + url: https://www.npr.org/sections/news/ + text: It's Memorial Day today, May 26, 2025 + type: text + - text: "\n\n2. " + type: text + - citations: + - cited_text: 'May 2025 is the fifth month of the current common year. The month, which began on a Thursday, will + end on a Saturday after 31 days. ' + encrypted_index: EpMBCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDKg1IA50Gwr+acWT5BoMGYg/QqdGIWZ1xBQKIjAkgAmLPkrAfHcJ5l/9cBkuIDZFR5HqWIMts7szVwtYu+KUtw636GWrp1YgAWw+PHkqF0xqh9pIZ2wPGUKGN4beDYqhKyS3yKNSGAQ= + title: Portal:Current events/May 2025 - Wikipedia + type: web_search_result_location + url: https://en.wikipedia.org/wiki/Portal:Current_events/May_2025 + text: May 2025 is the fifth month of the current common year. The month began on a Thursday and will end on a Saturday + after 31 days + type: text + - text: "\n\n3. " + type: text + - citations: + - cited_text: On May 26, 2025, India and the world witnessed significant developments across various sectors. In recent + developments shaping global and national aff... + encrypted_index: EpIBCioIAxgCIiQ0NGFlNjc2Yy05NThmLTRkNjgtOTEwOC1lYWU5ZGU3YjM2NmISDFkt41LpatakOxyiFhoMv3NkIlXjrN17gwJmIjBYF1Ds1OSA6s5ysr6ObQrej6qB8TY2C7CLY3A9izWkBKF92leDuAKzxrbr0q2HuTYqFvMJ033dlXOze2WorB4B4VxlNRvBw7EYBA== + title: 26 May 2025 UPSC Current Affairs - Daily News Headlines + type: web_search_result_location + url: https://testbook.com/ias-preparation/upsc-current-affairs-for-26-may-2025 + text: On May 26, 2025, there are significant developments happening, including India's launch of the Bharat Forecasting + System to boost weather prediction and disaster preparedness + type: text + id: msg_01WWvGdHT1E2kYaV5ZLWMB2N + model: claude-3-5-sonnet-20241022 + role: assistant + stop_reason: end_turn + stop_sequence: null + type: message + usage: + cache_creation_input_tokens: 0 + cache_read_input_tokens: 0 + input_tokens: 16312 + output_tokens: 258 + server_tool_use: + web_search_requests: 1 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_google/test_google_model_code_execution_tool.yaml b/tests/models/cassettes/test_google/test_google_model_code_execution_tool.yaml new file mode 100644 index 0000000000..7301ab260c --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_model_code_execution_tool.yaml @@ -0,0 +1,89 @@ +interactions: +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '234' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What day is today in Utrecht? + role: user + generationConfig: {} + systemInstruction: + parts: + - text: You are a helpful chatbot. + role: user + tools: + - codeExecution: {} + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '1462' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=7674 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - text: |+ + To determine the current day in Utrecht, I need to know the current date and time. I will use a tool to get this information. + + - executableCode: + code: | + import datetime + import pytz + + utrecht_timezone = pytz.timezone('Europe/Amsterdam') + now_utrecht = datetime.datetime.now(utrecht_timezone) + print(now_utrecht.strftime("%A, %Y-%m-%d")) + language: PYTHON + - codeExecutionResult: + outcome: OUTCOME_OK + output: | + Wednesday, 2025-05-28 + - text: | + Today is Wednesday, May 28, 2025 in Utrecht. + role: model + finishReason: STOP + modelVersion: gemini-2.0-flash + responseId: 8ww3aLDxJY24qsMP97vYeA + usageMetadata: + candidatesTokenCount: 119 + candidatesTokensDetails: + - modality: TEXT + tokenCount: 119 + promptTokenCount: 13 + promptTokensDetails: + - modality: TEXT + tokenCount: 13 + toolUsePromptTokenCount: 114 + toolUsePromptTokensDetails: + - modality: TEXT + tokenCount: 114 + totalTokenCount: 246 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml b/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml new file mode 100644 index 0000000000..e880750ace --- /dev/null +++ b/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml @@ -0,0 +1,199 @@ +interactions: +- request: + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '233' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + method: POST + parsed_body: + contents: + - parts: + - text: What day is today in Utrecht? + role: user + generationConfig: {} + systemInstruction: + parts: + - text: You are a helpful chatbot. + role: user + tools: + - googleSearch: {} + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent + response: + headers: + alt-svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + content-length: + - '5991' + content-type: + - application/json; charset=UTF-8 + server-timing: + - gfet4t7; dur=1340 + transfer-encoding: + - chunked + vary: + - Origin + - X-Origin + - Referer + parsed_body: + candidates: + - content: + parts: + - text: | + Today is Wednesday, May 28, 2025, in Utrecht. + role: model + finishReason: STOP + groundingMetadata: + retrievalMetadata: {} + searchEntryPoint: + renderedContent: | + +