mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 11:19:56 +00:00
### Motivation and Context The deep research tool was introduced for the AzureAIAgent; however, we didn't have support yet in SK. Adding support now for using the deep research tool for both streaming and non-streaming. Samples added as well. <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description - Add support for deep research tool. - Add samples with notes on prerequisites to configure the sample to run properly. - Added support for `AZURE_AI_AGENT_DEEP_RESEARCH_MODEL` env var within `AzureAIAgentSettings` class. - Bumped min allowed `azure-ai-agents` package ver to 1.2.0b3. - Closes #12978 <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1058 lines
36 KiB
Python
1058 lines
36 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
from azure.ai.agents.models import (
|
|
MessageDeltaImageFileContent,
|
|
MessageDeltaImageFileContentObject,
|
|
MessageDeltaTextContent,
|
|
MessageDeltaTextFileCitationAnnotation,
|
|
MessageDeltaTextFilePathAnnotation,
|
|
MessageDeltaTextUrlCitationAnnotation,
|
|
MessageImageFileContent,
|
|
MessageTextContent,
|
|
MessageTextFileCitationAnnotation,
|
|
MessageTextFilePathAnnotation,
|
|
MessageTextUrlCitationAnnotation,
|
|
RequiredFunctionToolCall,
|
|
RequiredMcpToolCall,
|
|
RunStep,
|
|
RunStepAzureAISearchToolCall,
|
|
RunStepBingCustomSearchToolCall,
|
|
RunStepBingGroundingToolCall,
|
|
RunStepDeepResearchToolCall,
|
|
RunStepDeltaCodeInterpreterImageOutput,
|
|
RunStepDeltaCodeInterpreterLogOutput,
|
|
RunStepDeltaCodeInterpreterToolCall,
|
|
RunStepDeltaFileSearchToolCall,
|
|
RunStepDeltaFunctionToolCall,
|
|
RunStepFileSearchToolCall,
|
|
RunStepFunctionToolCall,
|
|
RunStepMcpToolCall,
|
|
RunStepOpenAPIToolCall,
|
|
ThreadMessage,
|
|
ThreadRun,
|
|
)
|
|
|
|
from semantic_kernel.contents.annotation_content import AnnotationContent, CitationType
|
|
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
|
from semantic_kernel.contents.file_reference_content import FileReferenceContent
|
|
from semantic_kernel.contents.function_call_content import FunctionCallContent
|
|
from semantic_kernel.contents.function_result_content import FunctionResultContent
|
|
from semantic_kernel.contents.image_content import ImageContent
|
|
from semantic_kernel.contents.streaming_annotation_content import StreamingAnnotationContent
|
|
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
|
from semantic_kernel.contents.streaming_file_reference_content import StreamingFileReferenceContent
|
|
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
|
|
from semantic_kernel.contents.text_content import TextContent
|
|
from semantic_kernel.contents.utils.author_role import AuthorRole
|
|
from semantic_kernel.utils.feature_stage_decorator import experimental
|
|
|
|
if TYPE_CHECKING:
|
|
from azure.ai.agents.models import (
|
|
MessageDeltaChunk,
|
|
RunStepDeltaToolCallObject,
|
|
)
|
|
|
|
_URL_PATTERN = re.compile(r"https?://[^\s\]\)]+", re.IGNORECASE)
|
|
THREAD_MESSAGE_ID = "thread_message_id"
|
|
|
|
"""
|
|
The methods in this file are used with Azure AI Agent
|
|
related code. They are used to invoke, create chat messages,
|
|
or generate message content.
|
|
"""
|
|
|
|
|
|
@experimental
|
|
def get_message_contents(message: "ChatMessageContent") -> list[dict[str, Any]]:
|
|
"""Get the message contents.
|
|
|
|
Args:
|
|
message: The message.
|
|
"""
|
|
contents: list[dict[str, Any]] = []
|
|
for content in message.items:
|
|
match content:
|
|
case TextContent():
|
|
# Make sure text is a string
|
|
final_text = content.text
|
|
if not isinstance(final_text, str):
|
|
if isinstance(final_text, (list, tuple)):
|
|
final_text = " ".join(map(str, final_text))
|
|
else:
|
|
final_text = str(final_text)
|
|
|
|
contents.append({"type": "text", "text": final_text})
|
|
|
|
case ImageContent():
|
|
if content.uri:
|
|
contents.append(content.to_dict())
|
|
|
|
case FileReferenceContent():
|
|
contents.append({
|
|
"type": "image_file",
|
|
"image_file": {"file_id": content.file_id},
|
|
})
|
|
|
|
case FunctionResultContent():
|
|
final_result = content.result
|
|
match final_result:
|
|
case str():
|
|
contents.append({"type": "text", "text": final_result})
|
|
case list() | tuple():
|
|
contents.append({"type": "text", "text": " ".join(map(str, final_result))})
|
|
case _:
|
|
contents.append({"type": "text", "text": str(final_result)})
|
|
|
|
return contents
|
|
|
|
|
|
@experimental
|
|
def generate_message_content(
|
|
assistant_name: str, message: "ThreadMessage", completed_step: "RunStep | None" = None
|
|
) -> ChatMessageContent:
|
|
"""Generate message content."""
|
|
role = AuthorRole(message.role)
|
|
|
|
metadata = (
|
|
{
|
|
"created_at": completed_step.created_at,
|
|
"message_id": message.id, # message needs to be defined in context
|
|
"thread_message_id": message.id, # Add `thread_message_id` to avoid breaking the existing `message_id` key
|
|
"step_id": completed_step.id,
|
|
"run_id": completed_step.run_id,
|
|
"thread_id": completed_step.thread_id,
|
|
"agent_id": completed_step.agent_id,
|
|
"usage": completed_step.usage,
|
|
}
|
|
if completed_step is not None
|
|
else None
|
|
)
|
|
|
|
content: ChatMessageContent = ChatMessageContent(role=role, name=assistant_name, metadata=metadata) # type: ignore
|
|
|
|
messages: list[MessageImageFileContent | MessageTextContent] = cast(
|
|
list[MessageImageFileContent | MessageTextContent], message.content or []
|
|
)
|
|
for item_content in messages:
|
|
if item_content.type == "text":
|
|
content.items.append(
|
|
TextContent(
|
|
text=item_content.text.value,
|
|
)
|
|
)
|
|
for annotation in item_content.text.annotations:
|
|
content.items.append(generate_annotation_content(annotation)) # type: ignore
|
|
elif item_content.type == "image_file":
|
|
content.items.append(
|
|
FileReferenceContent(
|
|
file_id=item_content.image_file.file_id,
|
|
)
|
|
)
|
|
return content
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_message_content(
|
|
assistant_name: str,
|
|
message_delta_event: "MessageDeltaChunk",
|
|
thread_msg_id: str | None = None,
|
|
) -> StreamingChatMessageContent:
|
|
"""Generate streaming message content from a MessageDeltaEvent."""
|
|
delta = message_delta_event.delta
|
|
|
|
# Determine the role
|
|
role = AuthorRole(delta.role) if delta.role is not None else AuthorRole("assistant")
|
|
|
|
items: list[StreamingTextContent | StreamingAnnotationContent | StreamingFileReferenceContent] = []
|
|
|
|
delta_chunks: list[MessageDeltaImageFileContent | MessageDeltaTextContent] = cast(
|
|
list[MessageDeltaImageFileContent | MessageDeltaTextContent], delta.content or []
|
|
)
|
|
|
|
for delta_block in delta_chunks:
|
|
if delta_block.type == "text":
|
|
if delta_block.text and delta_block.text.value: # Ensure text is not None
|
|
text_value = delta_block.text.value
|
|
items.append(
|
|
StreamingTextContent(
|
|
text=text_value,
|
|
choice_index=delta_block.index,
|
|
)
|
|
)
|
|
# Process annotations if any
|
|
if delta_block.text.annotations:
|
|
for annotation in delta_block.text.annotations or []:
|
|
if isinstance(
|
|
annotation,
|
|
(
|
|
MessageDeltaTextFileCitationAnnotation,
|
|
MessageDeltaTextFilePathAnnotation,
|
|
MessageDeltaTextUrlCitationAnnotation,
|
|
),
|
|
):
|
|
items.append(generate_streaming_annotation_content(annotation))
|
|
elif delta_block.type == "image_file":
|
|
assert isinstance(delta_block, MessageDeltaImageFileContent) # nosec
|
|
if delta_block.image_file and isinstance(delta_block.image_file, MessageDeltaImageFileContentObject):
|
|
file_id = delta_block.image_file.file_id
|
|
items.append(
|
|
StreamingFileReferenceContent(
|
|
file_id=file_id,
|
|
)
|
|
)
|
|
|
|
metadata: dict[str, Any] | None = None
|
|
if thread_msg_id:
|
|
metadata = {THREAD_MESSAGE_ID: thread_msg_id}
|
|
|
|
return StreamingChatMessageContent(role=role, name=assistant_name, items=items, choice_index=0, metadata=metadata) # type: ignore
|
|
|
|
|
|
@experimental
|
|
def get_function_call_contents(
|
|
run: "ThreadRun", function_steps: dict[str, FunctionCallContent]
|
|
) -> list[FunctionCallContent]:
|
|
"""Extract function call contents from the run.
|
|
|
|
Args:
|
|
run: The run.
|
|
function_steps: The function steps
|
|
|
|
Returns:
|
|
The list of function call contents.
|
|
"""
|
|
function_call_contents: list[FunctionCallContent] = []
|
|
required_action = getattr(run, "required_action", None)
|
|
submit_tool_outputs = getattr(required_action, "submit_tool_outputs", None)
|
|
if not submit_tool_outputs or not hasattr(submit_tool_outputs, "tool_calls"):
|
|
return function_call_contents
|
|
tool_calls = getattr(submit_tool_outputs, "tool_calls", [])
|
|
if not isinstance(tool_calls, (list, tuple)):
|
|
return function_call_contents
|
|
for tool_call in tool_calls:
|
|
if not isinstance(tool_call, RequiredFunctionToolCall):
|
|
continue
|
|
fcc = FunctionCallContent(
|
|
id=tool_call.id,
|
|
index=getattr(tool_call, "index", None),
|
|
name=tool_call.function.name,
|
|
arguments=tool_call.function.arguments,
|
|
)
|
|
function_call_contents.append(fcc)
|
|
function_steps[tool_call.id] = fcc
|
|
return function_call_contents
|
|
|
|
|
|
@experimental
|
|
def generate_function_call_content(agent_name: str, fccs: list[FunctionCallContent]) -> ChatMessageContent:
|
|
"""Generate function call content.
|
|
|
|
Args:
|
|
agent_name: The agent name.
|
|
fccs: The function call contents.
|
|
|
|
Returns:
|
|
ChatMessageContent: The chat message content containing the function call content as the items.
|
|
"""
|
|
return ChatMessageContent(role=AuthorRole.ASSISTANT, name=agent_name, items=fccs) # type: ignore
|
|
|
|
|
|
@experimental
|
|
def generate_function_call_streaming_content(
|
|
agent_name: str,
|
|
fccs: list[FunctionCallContent],
|
|
) -> StreamingChatMessageContent:
|
|
"""Generate function call content.
|
|
|
|
Args:
|
|
agent_name: The agent name.
|
|
fccs: The function call contents.
|
|
|
|
Returns:
|
|
StreamingChatMessageContent: The chat message content containing the function call content as the items.
|
|
"""
|
|
return StreamingChatMessageContent(role=AuthorRole.ASSISTANT, choice_index=0, name=agent_name, items=fccs) # type: ignore
|
|
|
|
|
|
@experimental
|
|
def generate_function_result_content(
|
|
agent_name: str, function_step: FunctionCallContent, tool_call: "RunStepFunctionToolCall"
|
|
) -> ChatMessageContent:
|
|
"""Generate function result content."""
|
|
function_call_content: ChatMessageContent = ChatMessageContent(role=AuthorRole.TOOL, name=agent_name) # type: ignore
|
|
function_call_content.items.append(
|
|
FunctionResultContent(
|
|
function_name=function_step.function_name,
|
|
plugin_name=function_step.plugin_name,
|
|
id=function_step.id,
|
|
result=tool_call.function.get("output"), # type: ignore
|
|
)
|
|
)
|
|
return function_call_content
|
|
|
|
|
|
@experimental
|
|
def generate_bing_grounding_content(
|
|
agent_name: str, bing_tool_call: "RunStepBingGroundingToolCall | RunStepBingCustomSearchToolCall"
|
|
) -> ChatMessageContent:
|
|
"""Generate function result content related to a Bing Grounding Tool or Bing Custom Search Tool."""
|
|
message_content: ChatMessageContent = ChatMessageContent(role=AuthorRole.ASSISTANT, name=agent_name) # type: ignore
|
|
|
|
# Extract tool details based on the specific tool type
|
|
if isinstance(bing_tool_call, RunStepBingGroundingToolCall):
|
|
tool_details = bing_tool_call.bing_grounding
|
|
elif isinstance(bing_tool_call, RunStepBingCustomSearchToolCall):
|
|
tool_details = bing_tool_call.bing_custom_search
|
|
else:
|
|
# This should never happen with proper typing, but provides safety
|
|
raise TypeError(f"Unsupported Bing tool call type: {type(bing_tool_call)}")
|
|
|
|
message_content.items.append(
|
|
FunctionCallContent(
|
|
id=bing_tool_call.id,
|
|
name=bing_tool_call.type,
|
|
function_name=bing_tool_call.type,
|
|
arguments=tool_details,
|
|
)
|
|
)
|
|
return message_content
|
|
|
|
|
|
@experimental
|
|
def generate_azure_ai_search_content(
|
|
agent_name: str, azure_ai_search_tool_call: "RunStepAzureAISearchToolCall"
|
|
) -> ChatMessageContent | None:
|
|
"""Generate function result content related to an Azure AI Search Tool."""
|
|
items: list[FunctionCallContent | FunctionResultContent] = []
|
|
|
|
# Azure AI Search tool call contains both tool call input and output
|
|
arguments = azure_ai_search_tool_call.azure_ai_search.get("input")
|
|
if arguments:
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=azure_ai_search_tool_call.id,
|
|
name=azure_ai_search_tool_call.type,
|
|
function_name=azure_ai_search_tool_call.type,
|
|
arguments=arguments,
|
|
inner_content=azure_ai_search_tool_call,
|
|
)
|
|
)
|
|
result = azure_ai_search_tool_call.azure_ai_search.get("output")
|
|
if result:
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=azure_ai_search_tool_call.type,
|
|
id=azure_ai_search_tool_call.id,
|
|
result=result,
|
|
inner_content=azure_ai_search_tool_call,
|
|
)
|
|
)
|
|
return ChatMessageContent(role=AuthorRole.ASSISTANT, name=agent_name, items=items) if items else None # type: ignore
|
|
|
|
|
|
@experimental
|
|
def generate_file_search_content(
|
|
agent_name: str, file_search_tool_call: "RunStepFileSearchToolCall"
|
|
) -> ChatMessageContent:
|
|
"""Generate function result content related to an Azure AI Search Tool."""
|
|
message_content: ChatMessageContent = ChatMessageContent(role=AuthorRole.ASSISTANT, name=agent_name) # type: ignore
|
|
# Azure AI Search tool call contains both tool call input and output
|
|
message_content.items.append(
|
|
FunctionCallContent(
|
|
id=file_search_tool_call.id,
|
|
name=file_search_tool_call.type,
|
|
function_name=file_search_tool_call.type,
|
|
arguments=file_search_tool_call.file_search.get("ranking_options", None),
|
|
)
|
|
)
|
|
message_content.items.append(
|
|
FunctionResultContent(
|
|
function_name=file_search_tool_call.type,
|
|
id=file_search_tool_call.id,
|
|
result=file_search_tool_call.file_search.get("results", None),
|
|
)
|
|
)
|
|
return message_content
|
|
|
|
|
|
@experimental
|
|
def generate_deep_research_content(
|
|
agent_name: str, deep_research_tool_call: "RunStepDeepResearchToolCall"
|
|
) -> ChatMessageContent:
|
|
"""Generate content for a Deep Research tool call.
|
|
|
|
Emits both the tool call (input) and the tool result (output). If URLs are present
|
|
in the output text, a simple "Citations" section with unique URLs is appended as text.
|
|
|
|
Args:
|
|
agent_name: The agent name.
|
|
deep_research_tool_call: The deep research tool call details.
|
|
|
|
Returns:
|
|
ChatMessageContent summarizing the deep research call and result.
|
|
"""
|
|
items: list[FunctionCallContent | FunctionResultContent | TextContent] = []
|
|
|
|
details = deep_research_tool_call.deep_research
|
|
# Function call (input)
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=deep_research_tool_call.id,
|
|
name=deep_research_tool_call.type,
|
|
function_name=deep_research_tool_call.type,
|
|
arguments={"input": getattr(details, "input", None)},
|
|
inner_content=deep_research_tool_call,
|
|
)
|
|
)
|
|
|
|
# Function result (output)
|
|
output_text = getattr(details, "output", None)
|
|
if output_text:
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=deep_research_tool_call.type,
|
|
id=deep_research_tool_call.id,
|
|
result=output_text,
|
|
inner_content=deep_research_tool_call,
|
|
)
|
|
)
|
|
|
|
# Optional: Append a simple citations section from any URLs in the output
|
|
urls = _extract_unique_urls(str(output_text))
|
|
if urls:
|
|
citations_lines = ["## Citations"] + [f"{i + 1}. [{u}]({u})" for i, u in enumerate(urls)]
|
|
items.append(TextContent(text="\n\n" + "\n".join(citations_lines)))
|
|
|
|
return ChatMessageContent(role=AuthorRole.ASSISTANT, name=agent_name, items=items) # type: ignore
|
|
|
|
|
|
def _extract_unique_urls(text: str) -> list[str]:
|
|
"""Extract unique HTTP/HTTPS URLs from text in order of appearance."""
|
|
seen: set[str] = set()
|
|
ordered: list[str] = []
|
|
for match in _URL_PATTERN.finditer(text or ""):
|
|
url = match.group(0)
|
|
if url not in seen:
|
|
seen.add(url)
|
|
ordered.append(url)
|
|
return ordered
|
|
|
|
|
|
@experimental
|
|
def generate_openapi_content(agent_name: str, openapi_tool_call: RunStepOpenAPIToolCall) -> ChatMessageContent:
|
|
"""Generate ChatMessageContent for a non-streaming OpenAPI tool call."""
|
|
tool_id = openapi_tool_call.get("id")
|
|
tool_type = openapi_tool_call.get("type", "openapi")
|
|
function: dict[str, Any] = openapi_tool_call.get("function", {})
|
|
|
|
items: list[FunctionCallContent | FunctionResultContent] = []
|
|
|
|
arguments = function.get("arguments")
|
|
if arguments:
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=tool_id,
|
|
name=tool_type,
|
|
function_name=function.get("name"),
|
|
arguments=arguments,
|
|
)
|
|
)
|
|
|
|
output = function.get("output")
|
|
if output:
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=function.get("name"),
|
|
id=tool_id,
|
|
name=tool_type,
|
|
result=output,
|
|
)
|
|
)
|
|
|
|
return ChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
items=items, # type: ignore
|
|
name=agent_name,
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_code_interpreter_content(agent_name: str, code: str) -> "ChatMessageContent":
|
|
"""Generate code interpreter content.
|
|
|
|
Args:
|
|
agent_name: The agent name.
|
|
code: The code.
|
|
|
|
Returns:
|
|
ChatMessageContent: The chat message content.
|
|
"""
|
|
return ChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
content=code,
|
|
name=agent_name,
|
|
metadata={"code": True},
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_function_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> "StreamingChatMessageContent | None":
|
|
"""Generate streaming function content.
|
|
|
|
Args:
|
|
agent_name: The agent name.
|
|
step_details: The function step.
|
|
|
|
Returns:
|
|
StreamingChatMessageContent: The chat message content.
|
|
"""
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
items: list[FunctionCallContent] = []
|
|
|
|
tool_calls: list[
|
|
RunStepDeltaCodeInterpreterToolCall | RunStepDeltaFileSearchToolCall | RunStepDeltaFunctionToolCall
|
|
] = cast(
|
|
list[RunStepDeltaCodeInterpreterToolCall | RunStepDeltaFileSearchToolCall | RunStepDeltaFunctionToolCall],
|
|
step_details.tool_calls or [],
|
|
)
|
|
|
|
for tool in tool_calls:
|
|
if tool.type == "function" and tool.function:
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=tool.id,
|
|
index=getattr(tool, "index", None),
|
|
name=tool.function.name,
|
|
arguments=tool.function.arguments,
|
|
)
|
|
)
|
|
|
|
return (
|
|
StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
items=items, # type: ignore
|
|
choice_index=0,
|
|
)
|
|
if len(items) > 0
|
|
else None
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_bing_grounding_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> StreamingChatMessageContent | None:
|
|
"""Generate StreamingChatMessageContent for Bing Grounding and Bing Custom Search tool calls."""
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
items: list[FunctionCallContent] = []
|
|
for index, tool in enumerate(step_details.tool_calls):
|
|
if tool.type not in ("bing_grounding", "bing_custom_search"):
|
|
continue
|
|
|
|
# Extract tool details based on the specific tool type
|
|
if tool.type == "bing_grounding":
|
|
tool_details = tool.get("bing_grounding", {})
|
|
elif tool.type == "bing_custom_search":
|
|
tool_details = tool.get("bing_custom_search", {})
|
|
else:
|
|
continue
|
|
|
|
request_url = tool_details.get("requesturl", None)
|
|
response_metadata = tool_details.get("response_metadata", None)
|
|
|
|
if not request_url and not response_metadata:
|
|
continue
|
|
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=tool.id,
|
|
index=index,
|
|
name=tool.type,
|
|
function_name=tool.type,
|
|
arguments=tool_details,
|
|
)
|
|
)
|
|
|
|
if not items:
|
|
return None
|
|
|
|
return StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
choice_index=0,
|
|
items=items, # type: ignore
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_azure_ai_search_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> StreamingChatMessageContent | None:
|
|
"""Generate function result content related to a Bing Grounding Tool."""
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
items: list[FunctionCallContent | FunctionResultContent] = []
|
|
|
|
for index, tool in enumerate(step_details.tool_calls):
|
|
if tool.type == "azure_ai_search":
|
|
azure_ai_search_tool = cast(RunStepAzureAISearchToolCall, tool)
|
|
azure_ai_search_dict: dict = azure_ai_search_tool.get("azure_ai_search", None)
|
|
arguments = azure_ai_search_dict.get("input", {}) if azure_ai_search_dict else None
|
|
if arguments:
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=azure_ai_search_tool.id,
|
|
index=index,
|
|
name=azure_ai_search_tool.type,
|
|
function_name=azure_ai_search_tool.type,
|
|
arguments=arguments,
|
|
inner_content=azure_ai_search_tool,
|
|
)
|
|
)
|
|
result = azure_ai_search_dict.get("output", {}) if azure_ai_search_dict else None
|
|
if result:
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=azure_ai_search_tool.type,
|
|
id=azure_ai_search_tool.id,
|
|
result=result,
|
|
inner_content=azure_ai_search_tool,
|
|
)
|
|
)
|
|
|
|
return (
|
|
StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
choice_index=0,
|
|
items=items, # type: ignore
|
|
)
|
|
if items
|
|
else None
|
|
) # type: ignore
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_deep_research_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> StreamingChatMessageContent | None:
|
|
"""Generate streaming content related to a Deep Research Tool.
|
|
|
|
Emits FunctionCallContent for the input and FunctionResultContent for the output
|
|
as they appear in streamed tool call deltas.
|
|
"""
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
items: list[FunctionCallContent | FunctionResultContent] = []
|
|
|
|
for index, tool in enumerate(step_details.tool_calls):
|
|
if tool.type == "deep_research":
|
|
deep_research_dict: dict = tool.get("deep_research", {})
|
|
arguments = {"input": deep_research_dict.get("input")}
|
|
if any(v is not None for v in arguments.values()):
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=tool.get("id"),
|
|
index=index,
|
|
name=tool.type,
|
|
function_name=tool.type,
|
|
arguments=arguments,
|
|
)
|
|
)
|
|
result = deep_research_dict.get("output")
|
|
if result is not None:
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=tool.type,
|
|
id=tool.get("id"),
|
|
result=result,
|
|
)
|
|
)
|
|
|
|
return (
|
|
StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
choice_index=0,
|
|
items=items, # type: ignore
|
|
)
|
|
if items
|
|
else None
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_file_search_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> StreamingChatMessageContent | None:
|
|
"""Generate function result content related to a File Search Tool."""
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
items: list[FunctionCallContent | FunctionResultContent] = []
|
|
|
|
for index, tool in enumerate(step_details.tool_calls):
|
|
if tool.type == "file_search":
|
|
file_search_tool = cast(RunStepFileSearchToolCall, tool)
|
|
arguments = getattr(file_search_tool, "file_search", None)
|
|
results: list[Any] = []
|
|
if arguments is not None:
|
|
results = arguments.pop("results", None)
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=file_search_tool.id,
|
|
index=index,
|
|
name=file_search_tool.type,
|
|
function_name=file_search_tool.type,
|
|
arguments=arguments,
|
|
)
|
|
)
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=file_search_tool.type,
|
|
id=file_search_tool.id,
|
|
name=file_search_tool.type,
|
|
result=results,
|
|
)
|
|
)
|
|
|
|
return StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
choice_index=0,
|
|
items=items, # type: ignore
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_openapi_content(
|
|
agent_name: str,
|
|
step_details: "RunStepDeltaToolCallObject",
|
|
) -> "StreamingChatMessageContent | None":
|
|
"""Generate OpenAPI content for streaming function/tool call messages."""
|
|
if not getattr(step_details, "tool_calls", None):
|
|
return None
|
|
|
|
items: list[FunctionCallContent | FunctionResultContent] = [] # type: ignore
|
|
|
|
for index, tool in enumerate(step_details.tool_calls or []):
|
|
if tool.get("type") != "openapi":
|
|
continue
|
|
|
|
func: dict[str, Any] = tool.get("function")
|
|
tool_id = tool.get("id")
|
|
arguments = func.get("arguments") if func else None
|
|
if arguments:
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=tool_id,
|
|
index=index,
|
|
name="openapi",
|
|
function_name=func.get("name") if func else None,
|
|
arguments=arguments,
|
|
)
|
|
)
|
|
|
|
output = func.get("output") if func else None
|
|
if output:
|
|
items.append(
|
|
FunctionResultContent(
|
|
function_name=func.get("name") if func else None,
|
|
id=tool_id,
|
|
name="openapi",
|
|
result=output,
|
|
)
|
|
)
|
|
|
|
if not items:
|
|
return None
|
|
|
|
return StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
choice_index=0,
|
|
items=items, # type: ignore
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_code_interpreter_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> "StreamingChatMessageContent | None":
|
|
"""Generate code interpreter content.
|
|
|
|
Args:
|
|
agent_name: The agent name.
|
|
step_details: The current step details.
|
|
|
|
Returns:
|
|
StreamingChatMessageContent: The chat message content.
|
|
"""
|
|
items: list[StreamingTextContent | StreamingFileReferenceContent] = []
|
|
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
metadata: dict[str, bool] = {}
|
|
for index, tool in enumerate(step_details.tool_calls):
|
|
if isinstance(tool, RunStepDeltaCodeInterpreterToolCall):
|
|
code_interpreter_tool_call = tool.code_interpreter
|
|
if code_interpreter_tool_call is None:
|
|
continue
|
|
if code_interpreter_tool_call.input:
|
|
items.append(
|
|
StreamingTextContent(
|
|
choice_index=index,
|
|
text=code_interpreter_tool_call.input,
|
|
)
|
|
)
|
|
metadata["code"] = True
|
|
if code_interpreter_tool_call.outputs:
|
|
for output in code_interpreter_tool_call.outputs:
|
|
if (
|
|
isinstance(output, RunStepDeltaCodeInterpreterImageOutput)
|
|
and output.image is not None
|
|
and output.image.file_id
|
|
):
|
|
items.append(
|
|
StreamingFileReferenceContent(
|
|
file_id=output.image.file_id,
|
|
)
|
|
)
|
|
if isinstance(output, RunStepDeltaCodeInterpreterLogOutput) and output.logs:
|
|
items.append(
|
|
StreamingTextContent(
|
|
choice_index=index,
|
|
text=output.logs,
|
|
)
|
|
)
|
|
|
|
return (
|
|
StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
items=items, # type: ignore
|
|
choice_index=0,
|
|
metadata=metadata if metadata else None,
|
|
)
|
|
if len(items) > 0
|
|
else None
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_annotation_content(
|
|
annotation: MessageTextFilePathAnnotation | MessageTextFileCitationAnnotation | MessageTextUrlCitationAnnotation,
|
|
) -> AnnotationContent:
|
|
"""Generate annotation content with safe attribute access."""
|
|
file_id = None
|
|
url = None
|
|
title = None
|
|
citation_type = None
|
|
if isinstance(annotation, MessageTextFilePathAnnotation) and annotation.file_path:
|
|
file_id = annotation.file_path.file_id
|
|
citation_type = CitationType.FILE_PATH
|
|
elif isinstance(annotation, MessageTextFileCitationAnnotation) and annotation.file_citation:
|
|
file_id = annotation.file_citation.file_id
|
|
citation_type = CitationType.FILE_CITATION
|
|
elif isinstance(annotation, MessageTextUrlCitationAnnotation) and annotation.url_citation:
|
|
url = annotation.url_citation.url
|
|
title = annotation.url_citation.title
|
|
citation_type = CitationType.URL_CITATION
|
|
|
|
return AnnotationContent(
|
|
file_id=file_id,
|
|
quote=getattr(annotation, "text", None),
|
|
start_index=getattr(annotation, "start_index", None),
|
|
end_index=getattr(annotation, "end_index", None),
|
|
url=url,
|
|
title=title,
|
|
citation_type=citation_type,
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_annotation_content(
|
|
annotation: MessageDeltaTextFilePathAnnotation
|
|
| MessageDeltaTextFileCitationAnnotation
|
|
| MessageDeltaTextUrlCitationAnnotation,
|
|
) -> StreamingAnnotationContent:
|
|
"""Generate streaming annotation content with defensive checks."""
|
|
file_id = None
|
|
url = None
|
|
quote = None
|
|
title = None
|
|
citation_type = None
|
|
if isinstance(annotation, MessageDeltaTextFilePathAnnotation) and annotation.file_path:
|
|
file_id = annotation.file_path.file_id
|
|
quote = getattr(annotation, "text", None)
|
|
citation_type = CitationType.FILE_PATH
|
|
elif isinstance(annotation, MessageDeltaTextFileCitationAnnotation) and annotation.file_citation:
|
|
file_id = annotation.file_citation.file_id
|
|
quote = getattr(annotation, "text", None)
|
|
citation_type = CitationType.FILE_CITATION
|
|
elif isinstance(annotation, MessageDeltaTextUrlCitationAnnotation) and annotation.url_citation:
|
|
url = annotation.url_citation.url
|
|
title = annotation.url_citation.title
|
|
quote = annotation.get("text", None)
|
|
citation_type = CitationType.URL_CITATION
|
|
|
|
return StreamingAnnotationContent(
|
|
file_id=file_id,
|
|
quote=quote,
|
|
start_index=getattr(annotation, "start_index", None),
|
|
end_index=getattr(annotation, "end_index", None),
|
|
url=url,
|
|
title=title,
|
|
citation_type=citation_type,
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_mcp_content(agent_name: str, mcp_tool_call: RunStepMcpToolCall) -> ChatMessageContent:
|
|
"""Generate MCP tool content.
|
|
|
|
Args:
|
|
agent_name: The name of the agent.
|
|
mcp_tool_call: The MCP tool call.
|
|
|
|
Returns:
|
|
The generated content.
|
|
"""
|
|
mcp_result = FunctionResultContent(
|
|
function_name=mcp_tool_call.name,
|
|
id=mcp_tool_call.id,
|
|
result=mcp_tool_call.output,
|
|
)
|
|
|
|
return ChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
items=[mcp_result],
|
|
inner_content=mcp_tool_call, # type: ignore
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_mcp_call_content(agent_name: str, mcp_tool_calls: list[RequiredMcpToolCall]) -> ChatMessageContent:
|
|
"""Generate MCP tool call content.
|
|
|
|
Args:
|
|
agent_name: The name of the agent.
|
|
mcp_tool_calls: The MCP tool calls.
|
|
|
|
Returns:
|
|
The generated content.
|
|
"""
|
|
content_items: list[FunctionCallContent] = []
|
|
for mcp_call in mcp_tool_calls:
|
|
content_items.append(
|
|
FunctionCallContent(
|
|
id=mcp_call.id,
|
|
name=mcp_call.name,
|
|
function_name=mcp_call.name,
|
|
arguments=mcp_call.arguments,
|
|
server_label=mcp_call.server_label,
|
|
)
|
|
)
|
|
|
|
return ChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
items=content_items, # type: ignore
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_mcp_call_content(
|
|
agent_name: str, mcp_tool_calls: list["RequiredMcpToolCall"]
|
|
) -> "StreamingChatMessageContent | None":
|
|
"""Generate streaming MCP content.
|
|
|
|
Args:
|
|
agent_name: The name of the agent.
|
|
mcp_tool_calls: The mcp tool call details.
|
|
|
|
Returns:
|
|
The generated streaming content.
|
|
"""
|
|
items: list[FunctionCallContent] = []
|
|
for index, tool in enumerate(mcp_tool_calls or []):
|
|
if isinstance(tool, RequiredMcpToolCall):
|
|
items.append(
|
|
FunctionCallContent(
|
|
id=tool.id,
|
|
index=index,
|
|
name=tool.name,
|
|
function_name=tool.name,
|
|
arguments=tool.arguments,
|
|
server_label=tool.server_label,
|
|
)
|
|
)
|
|
|
|
return (
|
|
StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
items=items, # type: ignore
|
|
choice_index=0,
|
|
)
|
|
if items
|
|
else None
|
|
)
|
|
|
|
|
|
@experimental
|
|
def generate_streaming_mcp_content(
|
|
agent_name: str, step_details: "RunStepDeltaToolCallObject"
|
|
) -> StreamingChatMessageContent | None:
|
|
"""Generate MCP tool content.
|
|
|
|
Args:
|
|
agent_name: The name of the agent.
|
|
step_details: The steps details with mcp tool call.
|
|
|
|
Returns:
|
|
The generated content.
|
|
"""
|
|
if not step_details.tool_calls:
|
|
return None
|
|
|
|
items: list[FunctionResultContent] = []
|
|
|
|
for _, tool in enumerate(step_details.tool_calls):
|
|
if tool.type == "mcp":
|
|
mcp_tool_call = cast(RunStepMcpToolCall, tool)
|
|
if not mcp_tool_call.get("output"):
|
|
continue
|
|
mcp_result = FunctionResultContent(
|
|
function_name=mcp_tool_call.get("name"),
|
|
id=mcp_tool_call.get("id"),
|
|
result=mcp_tool_call.get("output"),
|
|
)
|
|
items.append(mcp_result)
|
|
|
|
return (
|
|
StreamingChatMessageContent(
|
|
role=AuthorRole.ASSISTANT,
|
|
name=agent_name,
|
|
items=items, # type: ignore
|
|
inner_content=mcp_tool_call, # type: ignore
|
|
choice_index=0,
|
|
)
|
|
if items
|
|
else None
|
|
) # type: ignore
|