Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion python/packages/core/agent_framework/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ def _restore_compaction_annotation_in_additional_properties(
"image_generation_tool_result",
"mcp_server_tool_call",
"mcp_server_tool_result",
"search_tool_call",
"search_tool_result",
"shell_tool_call",
"shell_tool_result",
"shell_command_output",
Expand Down Expand Up @@ -864,6 +866,56 @@ def from_function_result(
raw_representation=raw_representation,
)

@classmethod
def from_search_tool_call(
cls: type[ContentT],
call_id: str,
*,
tool_name: str,
arguments: str | Mapping[str, Any] | None = None,
status: str | None = None,
annotations: Sequence[Annotation] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
raw_representation: Any = None,
) -> ContentT:
"""Create search tool call content."""
return cls(
"search_tool_call",
call_id=call_id,
tool_name=tool_name,
arguments=arguments,
status=status,
annotations=annotations,
additional_properties=additional_properties,
raw_representation=raw_representation,
)

@classmethod
def from_search_tool_result(
cls: type[ContentT],
call_id: str,
*,
tool_name: str,
result: Any = None,
items: Sequence[Content] | None = None,
status: str | None = None,
annotations: Sequence[Annotation] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
raw_representation: Any = None,
) -> ContentT:
"""Create search tool result content."""
return cls(
"search_tool_result",
call_id=call_id,
tool_name=tool_name,
result=result,
items=list(items) if items is not None else None,
status=status,
annotations=annotations,
additional_properties=additional_properties,
raw_representation=raw_representation,
)

@classmethod
def from_usage(
cls: type[ContentT],
Expand Down Expand Up @@ -1478,7 +1530,7 @@ def has_top_level_media_type(self, top_level_media_type: Literal["application",
return span.lower() == top_level_media_type.lower()

def parse_arguments(self) -> dict[str, Any | None] | None:
"""Parse arguments from function_call or mcp_server_tool_call content.
"""Parse arguments from function_call, mcp_server_tool_call, or search_tool_call content.

If arguments cannot be parsed as JSON or the result is not a dict,
they are returned as a dictionary with a single key "raw".
Expand Down
66 changes: 66 additions & 0 deletions python/packages/openai/agent_framework_openai/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,13 +549,15 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]:
chunk,
options=validated_options,
function_call_ids=function_call_ids,
seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids,
)
else:
async for chunk in await client.responses.create(stream=True, **run_options):
yield self._parse_chunk_from_openai(
chunk,
options=validated_options,
function_call_ids=function_call_ids,
seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids,
)
except Exception as ex:
self._handle_request_error(ex)
Expand Down Expand Up @@ -1587,6 +1589,54 @@ def _join_shell_commands(commands: Sequence[str]) -> str:
"""Join shell commands into a single executable command string."""
return "\n".join(command for command in commands if command).strip()

@staticmethod
def _serialize_provider_payload(value: Any) -> Any:
"""Convert OpenAI SDK objects into JSON-serializable Python values."""
if isinstance(value, BaseModel):
return value.model_dump(mode="json", exclude_none=True)
if isinstance(value, Mapping):
return {str(key): RawOpenAIChatClient._serialize_provider_payload(item) for key, item in value.items()} # type: ignore[reportUnknownVariableType]
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
return [RawOpenAIChatClient._serialize_provider_payload(item) for item in value] # type: ignore[reportUnknownVariableType]
return value

@staticmethod
def _get_search_tool_name(item_type: str) -> str:
"""Map OpenAI search output item types to unified content tool names."""
return "web_search" if item_type == "web_search_call" else "file_search"

def _parse_search_tool_call_content(self, item: Any) -> Content:
"""Create unified search tool call content from an OpenAI search output item."""
item_type = getattr(item, "type", "")
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
if item_type == "web_search_call":
arguments = self._serialize_provider_payload(getattr(item, "action", None))
else:
arguments = {"queries": list(getattr(item, "queries", []) or [])}
return Content.from_search_tool_call(
call_id=call_id,
tool_name=self._get_search_tool_name(item_type),
arguments=arguments,
status=getattr(item, "status", None),
raw_representation=item,
)

def _parse_search_tool_result_content(self, item: Any) -> Content:
"""Create unified search tool result content from an OpenAI search output item."""
item_type = getattr(item, "type", "")
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
if item_type == "web_search_call":
result = {"action": self._serialize_provider_payload(getattr(item, "action", None))}
else:
result = {"results": self._serialize_provider_payload(getattr(item, "results", None))}
return Content.from_search_tool_result(
call_id=call_id,
tool_name=self._get_search_tool_name(item_type),
result=result,
status=getattr(item, "status", None),
raw_representation=item,
)

# region Parse methods
def _parse_response_from_openai(
self,
Expand Down Expand Up @@ -1788,6 +1838,9 @@ def _parse_response_from_openai(
raw_representation=item,
)
)
case "web_search_call" | "file_search_call":
contents.append(self._parse_search_tool_call_content(item))
contents.append(self._parse_search_tool_result_content(item))
case "mcp_approval_request": # ResponseOutputMcpApprovalRequest
contents.append(
Content.from_function_approval_request(
Expand Down Expand Up @@ -2377,8 +2430,19 @@ def _parse_chunk_from_openai(
additional_properties=additional_properties_empty or None,
)
)
case "web_search_call" | "file_search_call":
contents.append(self._parse_search_tool_call_content(event_item))
case _:
logger.debug("Unparsed event of type: %s: %s", event.type, event)
case (
"response.web_search_call.in_progress"
| "response.web_search_call.searching"
| "response.web_search_call.completed"
| "response.file_search_call.in_progress"
| "response.file_search_call.searching"
| "response.file_search_call.completed"
):
pass
case "response.function_call_arguments.delta":
call_id, name = function_call_ids.get(event.output_index, (None, None))
if call_id and name:
Expand Down Expand Up @@ -2514,6 +2578,8 @@ def _get_ann_value(key: str) -> Any:
raw_representation=done_item,
)
)
elif getattr(done_item, "type", None) in ("web_search_call", "file_search_call"):
contents.append(self._parse_search_tool_result_content(done_item))
case _:
logger.debug("Unparsed event of type: %s: %s", event.type, event)

Expand Down
Loading
Loading