mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [Breaking] Simplified Content types to a single class with classmethod constructors. (#3252)
* ported Content to a new model * fixed linting * fixes * fixed data format handling * fix for 3.10 mypy * fix * fix int test
This commit is contained in:
@@ -31,11 +31,8 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Contents,
|
||||
DataContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
normalize_messages,
|
||||
prepend_agent_framework_to_user_agent,
|
||||
)
|
||||
@@ -333,7 +330,7 @@ class A2AAgent(BaseAgent):
|
||||
A2APart(
|
||||
root=FilePart(
|
||||
file=FileWithBytes(
|
||||
bytes=_get_uri_data(content.uri),
|
||||
bytes=_get_uri_data(content.uri), # type: ignore[arg-type]
|
||||
mime_type=content.media_type,
|
||||
),
|
||||
metadata=content.additional_properties,
|
||||
@@ -362,19 +359,19 @@ class A2AAgent(BaseAgent):
|
||||
metadata=cast(dict[str, Any], message.additional_properties),
|
||||
)
|
||||
|
||||
def _parse_contents_from_a2a(self, parts: Sequence[A2APart]) -> list[Contents]:
|
||||
"""Parse A2A Parts into Agent Framework Contents.
|
||||
def _parse_contents_from_a2a(self, parts: Sequence[A2APart]) -> list[Content]:
|
||||
"""Parse A2A Parts into Agent Framework Content.
|
||||
|
||||
Transforms A2A protocol Parts into framework-native Content objects,
|
||||
handling text, file (URI/bytes), and data parts with metadata preservation.
|
||||
"""
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
for part in parts:
|
||||
inner_part = part.root
|
||||
match inner_part.kind:
|
||||
case "text":
|
||||
contents.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=inner_part.text,
|
||||
additional_properties=inner_part.metadata,
|
||||
raw_representation=inner_part,
|
||||
@@ -383,7 +380,7 @@ class A2AAgent(BaseAgent):
|
||||
case "file":
|
||||
if isinstance(inner_part.file, FileWithUri):
|
||||
contents.append(
|
||||
UriContent(
|
||||
Content.from_uri(
|
||||
uri=inner_part.file.uri,
|
||||
media_type=inner_part.file.mime_type or "",
|
||||
additional_properties=inner_part.metadata,
|
||||
@@ -392,7 +389,7 @@ class A2AAgent(BaseAgent):
|
||||
)
|
||||
elif isinstance(inner_part.file, FileWithBytes):
|
||||
contents.append(
|
||||
DataContent(
|
||||
Content.from_data(
|
||||
data=base64.b64decode(inner_part.file.bytes),
|
||||
media_type=inner_part.file.mime_type or "",
|
||||
additional_properties=inner_part.metadata,
|
||||
@@ -401,7 +398,7 @@ class A2AAgent(BaseAgent):
|
||||
)
|
||||
case "data":
|
||||
contents.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=json.dumps(inner_part.data),
|
||||
additional_properties=inner_part.metadata,
|
||||
raw_representation=inner_part,
|
||||
|
||||
@@ -24,12 +24,8 @@ from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
DataContent,
|
||||
ErrorContent,
|
||||
HostedFileContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from agent_framework.a2a import A2AAgent
|
||||
from pytest import fixture, raises
|
||||
@@ -289,8 +285,8 @@ def test_parse_contents_from_a2a_conversion(a2a_agent: A2AAgent) -> None:
|
||||
|
||||
# Verify conversion
|
||||
assert len(contents) == 2
|
||||
assert isinstance(contents[0], TextContent)
|
||||
assert isinstance(contents[1], TextContent)
|
||||
assert contents[0].type == "text"
|
||||
assert contents[1].type == "text"
|
||||
assert contents[0].text == "First part"
|
||||
assert contents[1].text == "Second part"
|
||||
|
||||
@@ -299,7 +295,7 @@ def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None
|
||||
"""Test _prepare_message_for_a2a with ErrorContent."""
|
||||
|
||||
# Create ChatMessage with ErrorContent
|
||||
error_content = ErrorContent(message="Test error message")
|
||||
error_content = Content.from_error(message="Test error message")
|
||||
message = ChatMessage(role=Role.USER, contents=[error_content])
|
||||
|
||||
# Convert to A2A message
|
||||
@@ -314,7 +310,7 @@ def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None:
|
||||
"""Test _prepare_message_for_a2a with UriContent."""
|
||||
|
||||
# Create ChatMessage with UriContent
|
||||
uri_content = UriContent(uri="http://example.com/file.pdf", media_type="application/pdf")
|
||||
uri_content = Content.from_uri(uri="http://example.com/file.pdf", media_type="application/pdf")
|
||||
message = ChatMessage(role=Role.USER, contents=[uri_content])
|
||||
|
||||
# Convert to A2A message
|
||||
@@ -330,7 +326,7 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None:
|
||||
"""Test _prepare_message_for_a2a with DataContent."""
|
||||
|
||||
# Create ChatMessage with DataContent (base64 data URI)
|
||||
data_content = DataContent(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
|
||||
data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
|
||||
message = ChatMessage(role=Role.USER, contents=[data_content])
|
||||
|
||||
# Convert to A2A message
|
||||
@@ -368,7 +364,7 @@ async def test_run_stream_with_message_response(a2a_agent: A2AAgent, mock_a2a_cl
|
||||
assert len(updates[0].contents) == 1
|
||||
|
||||
content = updates[0].contents[0]
|
||||
assert isinstance(content, TextContent)
|
||||
assert content.type == "text"
|
||||
assert content.text == "Streaming response from agent!"
|
||||
|
||||
assert updates[0].response_id == "msg-stream-123"
|
||||
@@ -414,10 +410,10 @@ def test_prepare_message_for_a2a_with_multiple_contents() -> None:
|
||||
message = ChatMessage(
|
||||
role=Role.USER,
|
||||
contents=[
|
||||
TextContent(text="Here's the analysis:"),
|
||||
DataContent(data=b"binary data", media_type="application/octet-stream"),
|
||||
UriContent(uri="https://example.com/image.png", media_type="image/png"),
|
||||
TextContent(text='{"structured": "data"}'),
|
||||
Content.from_text(text="Here's the analysis:"),
|
||||
Content.from_data(data=b"binary data", media_type="application/octet-stream"),
|
||||
Content.from_uri(uri="https://example.com/image.png", media_type="image/png"),
|
||||
Content.from_text(text='{"structured": "data"}'),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -445,7 +441,7 @@ def test_parse_contents_from_a2a_with_data_part() -> None:
|
||||
|
||||
assert len(contents) == 1
|
||||
|
||||
assert isinstance(contents[0], TextContent)
|
||||
assert contents[0].type == "text"
|
||||
assert contents[0].text == '{"key": "value", "number": 42}'
|
||||
assert contents[0].additional_properties == {"source": "test"}
|
||||
|
||||
@@ -470,7 +466,7 @@ def test_prepare_message_for_a2a_with_hosted_file() -> None:
|
||||
# Create message with hosted file content
|
||||
message = ChatMessage(
|
||||
role=Role.USER,
|
||||
contents=[HostedFileContent(file_id="hosted://storage/document.pdf")],
|
||||
contents=[Content.from_hosted_file(file_id="hosted://storage/document.pdf")],
|
||||
)
|
||||
|
||||
result = agent._prepare_message_for_a2a(message) # noqa: SLF001
|
||||
@@ -507,7 +503,7 @@ def test_parse_contents_from_a2a_with_hosted_file_uri() -> None:
|
||||
|
||||
assert len(contents) == 1
|
||||
|
||||
assert isinstance(contents[0], UriContent)
|
||||
assert contents[0].type == "uri"
|
||||
assert contents[0].uri == "hosted://storage/document.pdf"
|
||||
assert contents[0].media_type == "" # Converted None to empty string
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
DataContent,
|
||||
FunctionCallContent,
|
||||
Content,
|
||||
use_chat_middleware,
|
||||
use_function_invocation,
|
||||
)
|
||||
from agent_framework._middleware import use_chat_middleware
|
||||
from agent_framework._tools import use_function_invocation
|
||||
from agent_framework._types import BaseContent, Contents
|
||||
from agent_framework.observability import use_instrumentation
|
||||
|
||||
from ._event_converters import AGUIEventConverter
|
||||
@@ -53,26 +51,11 @@ else:
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerFunctionCallContent(BaseContent):
|
||||
"""Wrapper for server function calls to prevent client re-execution.
|
||||
|
||||
All function calls from the remote server are server-side executions.
|
||||
This wrapper prevents @use_function_invocation from trying to execute them again.
|
||||
"""
|
||||
|
||||
function_call_content: FunctionCallContent
|
||||
|
||||
def __init__(self, function_call_content: FunctionCallContent) -> None:
|
||||
"""Initialize with the function call content."""
|
||||
super().__init__(type="server_function_call")
|
||||
self.function_call_content = function_call_content
|
||||
|
||||
|
||||
def _unwrap_server_function_call_contents(contents: MutableSequence[Contents | dict[str, Any]]) -> None:
|
||||
"""Replace ServerFunctionCallContent instances with their underlying call content."""
|
||||
def _unwrap_server_function_call_contents(contents: MutableSequence[Content | dict[str, Any]]) -> None:
|
||||
"""Replace server_function_call instances with their underlying call content."""
|
||||
for idx, content in enumerate(contents):
|
||||
if isinstance(content, ServerFunctionCallContent):
|
||||
contents[idx] = content.function_call_content # type: ignore[assignment]
|
||||
if content.type == "server_function_call": # type: ignore[union-attr]
|
||||
contents[idx] = content.function_call # type: ignore[assignment, union-attr]
|
||||
|
||||
|
||||
TBaseChatClient = TypeVar("TBaseChatClient", bound=type[BaseChatClient[Any]])
|
||||
@@ -93,7 +76,7 @@ def _apply_server_function_call_unwrap(chat_client: TBaseChatClient) -> TBaseCha
|
||||
@wraps(original_get_streaming_response)
|
||||
async def streaming_wrapper(self, *args: Any, **kwargs: Any) -> AsyncIterable[ChatResponseUpdate]:
|
||||
async for update in original_get_streaming_response(self, *args, **kwargs):
|
||||
_unwrap_server_function_call_contents(cast(MutableSequence[Contents | dict[str, Any]], update.contents))
|
||||
_unwrap_server_function_call_contents(cast(MutableSequence[Content | dict[str, Any]], update.contents))
|
||||
yield update
|
||||
|
||||
chat_client.get_streaming_response = streaming_wrapper # type: ignore[assignment]
|
||||
@@ -105,9 +88,7 @@ def _apply_server_function_call_unwrap(chat_client: TBaseChatClient) -> TBaseCha
|
||||
response = await original_get_response(self, *args, **kwargs)
|
||||
if response.messages:
|
||||
for message in response.messages:
|
||||
_unwrap_server_function_call_contents(
|
||||
cast(MutableSequence[Contents | dict[str, Any]], message.contents)
|
||||
)
|
||||
_unwrap_server_function_call_contents(cast(MutableSequence[Content | dict[str, Any]], message.contents))
|
||||
return response
|
||||
|
||||
chat_client.get_response = response_wrapper # type: ignore[assignment]
|
||||
@@ -289,13 +270,13 @@ class AGUIChatClient(BaseChatClient[TAGUIChatOptions], Generic[TAGUIChatOptions]
|
||||
last_message = messages[-1]
|
||||
|
||||
for content in last_message.contents:
|
||||
if isinstance(content, DataContent) and content.media_type == "application/json":
|
||||
if isinstance(content, Content) and content.type == "data" and content.media_type == "application/json":
|
||||
try:
|
||||
uri = content.uri
|
||||
if uri.startswith("data:application/json;base64,"):
|
||||
if uri.startswith("data:application/json;base64,"): # type: ignore[union-attr]
|
||||
import base64
|
||||
|
||||
encoded_data = uri.split(",", 1)[1]
|
||||
encoded_data = uri.split(",", 1)[1] # type: ignore[union-attr]
|
||||
decoded_bytes = base64.b64decode(encoded_data)
|
||||
state = json.loads(decoded_bytes.decode("utf-8"))
|
||||
|
||||
@@ -433,19 +414,19 @@ class AGUIChatClient(BaseChatClient[TAGUIChatOptions], Generic[TAGUIChatOptions]
|
||||
)
|
||||
# Distinguish client vs server tools
|
||||
for i, content in enumerate(update.contents):
|
||||
if isinstance(content, FunctionCallContent):
|
||||
if content.type == "function_call": # type: ignore[attr-defined]
|
||||
logger.debug(
|
||||
f"[AGUIChatClient] Function call: {content.name}, in client_tool_set: {content.name in client_tool_set}"
|
||||
f"[AGUIChatClient] Function call: {content.name}, in client_tool_set: {content.name in client_tool_set}" # type: ignore[attr-defined]
|
||||
)
|
||||
if content.name in client_tool_set:
|
||||
if content.name in client_tool_set: # type: ignore[attr-defined]
|
||||
# Client tool - let @use_function_invocation execute it
|
||||
if not content.additional_properties:
|
||||
content.additional_properties = {}
|
||||
content.additional_properties["agui_thread_id"] = thread_id
|
||||
if not content.additional_properties: # type: ignore[attr-defined]
|
||||
content.additional_properties = {} # type: ignore[attr-defined]
|
||||
content.additional_properties["agui_thread_id"] = thread_id # type: ignore[attr-defined]
|
||||
else:
|
||||
# Server tool - wrap so @use_function_invocation ignores it
|
||||
logger.debug(f"[AGUIChatClient] Wrapping server tool: {content.name}")
|
||||
self._register_server_tool_placeholder(content.name)
|
||||
update.contents[i] = ServerFunctionCallContent(content) # type: ignore
|
||||
logger.debug(f"[AGUIChatClient] Wrapping server tool: {content.name}") # type: ignore[union-attr]
|
||||
self._register_server_tool_placeholder(content.name) # type: ignore[arg-type]
|
||||
update.contents[i] = Content(type="server_function_call", function_call=content) # type: ignore
|
||||
|
||||
yield update
|
||||
|
||||
@@ -6,12 +6,9 @@ from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
ErrorContent,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
|
||||
|
||||
@@ -117,7 +114,7 @@ class AGUIEventConverter:
|
||||
return ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
message_id=self.current_message_id,
|
||||
contents=[TextContent(text=delta)],
|
||||
contents=[Content.from_text(text=delta)],
|
||||
)
|
||||
|
||||
def _handle_text_message_end(self, event: dict[str, Any]) -> ChatResponseUpdate | None:
|
||||
@@ -133,7 +130,7 @@ class AGUIEventConverter:
|
||||
return ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=self.current_tool_call_id or "",
|
||||
name=self.current_tool_name or "",
|
||||
arguments="",
|
||||
@@ -149,7 +146,7 @@ class AGUIEventConverter:
|
||||
return ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=self.current_tool_call_id or "",
|
||||
name=self.current_tool_name or "",
|
||||
arguments=delta,
|
||||
@@ -170,7 +167,7 @@ class AGUIEventConverter:
|
||||
return ChatResponseUpdate(
|
||||
role=Role.TOOL,
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=tool_call_id,
|
||||
result=result,
|
||||
)
|
||||
@@ -197,7 +194,7 @@ class AGUIEventConverter:
|
||||
role=Role.ASSISTANT,
|
||||
finish_reason=FinishReason.CONTENT_FILTER,
|
||||
contents=[
|
||||
ErrorContent(
|
||||
Content.from_error(
|
||||
message=error_message,
|
||||
error_code="RUN_ERROR",
|
||||
)
|
||||
|
||||
@@ -25,10 +25,7 @@ from ag_ui.core import (
|
||||
)
|
||||
from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
TextContent,
|
||||
Content,
|
||||
prepare_function_call_results,
|
||||
)
|
||||
|
||||
@@ -96,20 +93,22 @@ class AgentFrameworkEventBridge:
|
||||
logger.info(f"Processing AgentRunUpdate with {len(update.contents)} content items")
|
||||
for idx, content in enumerate(update.contents):
|
||||
logger.info(f" Content {idx}: type={type(content).__name__}")
|
||||
if isinstance(content, TextContent):
|
||||
events.extend(self._handle_text_content(content))
|
||||
elif isinstance(content, FunctionCallContent):
|
||||
events.extend(self._handle_function_call_content(content))
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
events.extend(self._handle_function_result_content(content))
|
||||
elif isinstance(content, FunctionApprovalRequestContent):
|
||||
events.extend(self._handle_function_approval_request_content(content))
|
||||
|
||||
match content.type:
|
||||
case "text":
|
||||
events.extend(self._handle_text_content(content))
|
||||
case "function_call":
|
||||
events.extend(self._handle_function_call_content(content))
|
||||
case "function_result":
|
||||
events.extend(self._handle_function_result_content(content))
|
||||
case "function_approval_request":
|
||||
events.extend(self._handle_function_approval_request_content(content))
|
||||
case _:
|
||||
logger.warning(f" Unsupported content type: {content.type}, skipping.")
|
||||
return events
|
||||
|
||||
def _handle_text_content(self, content: TextContent) -> list[BaseEvent]:
|
||||
def _handle_text_content(self, content: Content) -> list[BaseEvent]:
|
||||
events: list[BaseEvent] = []
|
||||
logger.info(f" TextContent found: length={len(content.text)}")
|
||||
logger.info(f" TextContent found: length={len(content.text)}") # type: ignore[arg-type]
|
||||
logger.info(
|
||||
" Flags: skip_text_content=%s, should_stop_after_confirm=%s",
|
||||
self.skip_text_content,
|
||||
@@ -122,7 +121,7 @@ class AgentFrameworkEventBridge:
|
||||
|
||||
if self.should_stop_after_confirm:
|
||||
logger.info(" SKIPPING TextContent: waiting for confirm_changes response")
|
||||
self.suppressed_summary += content.text
|
||||
self.suppressed_summary += content.text # type: ignore[operator]
|
||||
logger.info(f" Suppressed summary length={len(self.suppressed_summary)}")
|
||||
return events
|
||||
|
||||
@@ -150,14 +149,14 @@ class AgentFrameworkEventBridge:
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
def _handle_function_call_content(self, content: FunctionCallContent) -> list[BaseEvent]:
|
||||
def _handle_function_call_content(self, content: Content) -> list[BaseEvent]:
|
||||
events: list[BaseEvent] = []
|
||||
if content.name:
|
||||
logger.debug(f"Tool call: {content.name} (call_id: {content.call_id})")
|
||||
|
||||
if not content.name and not content.call_id and not self.current_tool_call_name:
|
||||
args_length = len(str(content.arguments)) if content.arguments else 0
|
||||
logger.warning(f"FunctionCallContent missing name and call_id. args_length={args_length}")
|
||||
logger.warning(f"Content missing name and call_id. args_length={args_length}")
|
||||
|
||||
tool_call_id = self._coalesce_tool_call_id(content)
|
||||
# Only emit ToolCallStartEvent once per tool call (when it's a new tool call)
|
||||
@@ -190,7 +189,7 @@ class AgentFrameworkEventBridge:
|
||||
|
||||
return events
|
||||
|
||||
def _coalesce_tool_call_id(self, content: FunctionCallContent) -> str:
|
||||
def _coalesce_tool_call_id(self, content: Content) -> str:
|
||||
if content.call_id:
|
||||
return content.call_id
|
||||
if self.current_tool_call_id:
|
||||
@@ -286,7 +285,7 @@ class AgentFrameworkEventBridge:
|
||||
self.pending_state_updates[state_key] = state_value
|
||||
return events
|
||||
|
||||
def _handle_function_result_content(self, content: FunctionResultContent) -> list[BaseEvent]:
|
||||
def _handle_function_result_content(self, content: Content) -> list[BaseEvent]:
|
||||
events: list[BaseEvent] = []
|
||||
if content.call_id:
|
||||
end_event = ToolCallEndEvent(
|
||||
@@ -310,7 +309,7 @@ class AgentFrameworkEventBridge:
|
||||
|
||||
result_event = ToolCallResultEvent(
|
||||
message_id=result_message_id,
|
||||
tool_call_id=content.call_id,
|
||||
tool_call_id=content.call_id, # type: ignore[arg-type]
|
||||
content=result_content,
|
||||
role="tool",
|
||||
)
|
||||
@@ -367,7 +366,7 @@ class AgentFrameworkEventBridge:
|
||||
self.current_tool_call_name = None
|
||||
return events
|
||||
|
||||
def _emit_confirm_changes_tool_call(self, function_call: FunctionCallContent | None = None) -> list[BaseEvent]:
|
||||
def _emit_confirm_changes_tool_call(self, function_call: Content | None = None) -> list[BaseEvent]:
|
||||
"""Emit a confirm_changes tool call for Dojo UI compatibility.
|
||||
|
||||
Args:
|
||||
@@ -419,7 +418,7 @@ class AgentFrameworkEventBridge:
|
||||
logger.info("Set flag to stop run after confirm_changes")
|
||||
return events
|
||||
|
||||
def _emit_function_approval_tool_call(self, function_call: FunctionCallContent) -> list[BaseEvent]:
|
||||
def _emit_function_approval_tool_call(self, function_call: Content) -> list[BaseEvent]:
|
||||
"""Emit a tool call that can drive UI approval for function requests."""
|
||||
tool_call_name = "confirm_changes"
|
||||
if self.approval_tool_name and self.approval_tool_name != function_call.name:
|
||||
@@ -462,13 +461,13 @@ class AgentFrameworkEventBridge:
|
||||
logger.info("Set flag to stop run after confirm_changes")
|
||||
return events
|
||||
|
||||
def _handle_function_approval_request_content(self, content: FunctionApprovalRequestContent) -> list[BaseEvent]:
|
||||
def _handle_function_approval_request_content(self, content: Content) -> list[BaseEvent]:
|
||||
events: list[BaseEvent] = []
|
||||
logger.info("=== FUNCTION APPROVAL REQUEST ===")
|
||||
logger.info(f" Function: {content.function_call.name}")
|
||||
logger.info(f" Call ID: {content.function_call.call_id}")
|
||||
logger.info(f" Function: {content.function_call.name}") # type: ignore[union-attr]
|
||||
logger.info(f" Call ID: {content.function_call.call_id}") # type: ignore[union-attr]
|
||||
|
||||
parsed_args = content.function_call.parse_arguments()
|
||||
parsed_args = content.function_call.parse_arguments() # type: ignore[union-attr]
|
||||
parsed_arg_keys = list(parsed_args.keys()) if parsed_args else "None"
|
||||
logger.info(f" Parsed args keys: {parsed_arg_keys}")
|
||||
|
||||
@@ -478,12 +477,12 @@ class AgentFrameworkEventBridge:
|
||||
list(self.predict_state_config.keys()) if self.predict_state_config else "None",
|
||||
)
|
||||
for state_key, config in self.predict_state_config.items():
|
||||
if config["tool"] != content.function_call.name:
|
||||
if config["tool"] != content.function_call.name: # type: ignore[union-attr]
|
||||
continue
|
||||
tool_arg_name = config["tool_argument"]
|
||||
logger.info(
|
||||
" MATCHED tool '%s' for state key '%s', arg='%s'",
|
||||
content.function_call.name,
|
||||
content.function_call.name, # type: ignore[union-attr]
|
||||
state_key,
|
||||
tool_arg_name,
|
||||
)
|
||||
@@ -500,11 +499,11 @@ class AgentFrameworkEventBridge:
|
||||
)
|
||||
events.append(state_snapshot)
|
||||
|
||||
if content.function_call.call_id:
|
||||
if content.function_call.call_id: # type: ignore[union-attr]
|
||||
end_event = ToolCallEndEvent(
|
||||
tool_call_id=content.function_call.call_id,
|
||||
tool_call_id=content.function_call.call_id, # type: ignore[union-attr]
|
||||
)
|
||||
logger.info(f"Emitting ToolCallEndEvent for approval-required tool '{content.function_call.call_id}'")
|
||||
logger.info(f"Emitting ToolCallEndEvent for approval-required tool '{content.function_call.call_id}'") # type: ignore[union-attr]
|
||||
events.append(end_event)
|
||||
|
||||
# Emit the function_approval_request custom event for UI implementations that support it
|
||||
@@ -513,18 +512,18 @@ class AgentFrameworkEventBridge:
|
||||
value={
|
||||
"id": content.id,
|
||||
"function_call": {
|
||||
"call_id": content.function_call.call_id,
|
||||
"name": content.function_call.name,
|
||||
"arguments": content.function_call.parse_arguments(),
|
||||
"call_id": content.function_call.call_id, # type: ignore[union-attr]
|
||||
"name": content.function_call.name, # type: ignore[union-attr]
|
||||
"arguments": content.function_call.parse_arguments(), # type: ignore[union-attr]
|
||||
},
|
||||
},
|
||||
)
|
||||
logger.info(f"Emitting function_approval_request custom event for '{content.function_call.name}'")
|
||||
logger.info(f"Emitting function_approval_request custom event for '{content.function_call.name}'") # type: ignore[union-attr]
|
||||
events.append(approval_event)
|
||||
|
||||
# Emit a UI-friendly approval tool call for function approvals.
|
||||
if self.require_confirmation:
|
||||
events.extend(self._emit_function_approval_tool_call(content.function_call))
|
||||
events.extend(self._emit_function_approval_tool_call(content.function_call)) # type: ignore[arg-type]
|
||||
|
||||
# Signal orchestrator to stop the run and wait for user approval response
|
||||
self.should_stop_after_confirm = True
|
||||
|
||||
@@ -8,11 +8,8 @@ from typing import Any, cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
prepare_function_call_results,
|
||||
)
|
||||
|
||||
@@ -40,11 +37,11 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
tool_ids = {
|
||||
str(content.call_id)
|
||||
for content in msg.contents or []
|
||||
if isinstance(content, FunctionCallContent) and content.call_id
|
||||
if content.type == "function_call" and content.call_id
|
||||
}
|
||||
confirm_changes_call = None
|
||||
for content in msg.contents or []:
|
||||
if isinstance(content, FunctionCallContent) and content.name == "confirm_changes":
|
||||
if content.type == "function_call" and content.name == "confirm_changes":
|
||||
confirm_changes_call = content
|
||||
break
|
||||
|
||||
@@ -59,7 +56,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
approval_call_ids: set[str] = set()
|
||||
approval_accepted: bool | None = None
|
||||
for content in msg.contents or []:
|
||||
if type(content) is FunctionApprovalResponseContent:
|
||||
if content.type == "function_approval_response":
|
||||
if content.function_call and content.function_call.call_id:
|
||||
approval_call_ids.add(str(content.function_call.call_id))
|
||||
if approval_accepted is None:
|
||||
@@ -79,7 +76,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
synthetic_result = ChatMessage(
|
||||
role="tool",
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=pending_confirm_changes_id,
|
||||
result="Confirmed" if approval_accepted else "Rejected",
|
||||
)
|
||||
@@ -93,12 +90,12 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
if pending_confirm_changes_id:
|
||||
user_text = ""
|
||||
for content in msg.contents or []:
|
||||
if isinstance(content, TextContent):
|
||||
user_text = content.text
|
||||
if content.type == "text":
|
||||
user_text = content.text # type: ignore[assignment]
|
||||
break
|
||||
|
||||
try:
|
||||
parsed = json.loads(user_text)
|
||||
parsed = json.loads(user_text) # type: ignore[arg-type]
|
||||
if "accepted" in parsed:
|
||||
logger.info(
|
||||
f"Injecting synthetic tool result for confirm_changes call_id={pending_confirm_changes_id}"
|
||||
@@ -106,7 +103,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
synthetic_result = ChatMessage(
|
||||
role="tool",
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=pending_confirm_changes_id,
|
||||
result="Confirmed" if parsed.get("accepted") else "Rejected",
|
||||
)
|
||||
@@ -130,7 +127,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
synthetic_result = ChatMessage(
|
||||
role="tool",
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=pending_call_id,
|
||||
result="Tool execution skipped - user provided follow-up message",
|
||||
)
|
||||
@@ -149,7 +146,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
continue
|
||||
keep = False
|
||||
for content in msg.contents or []:
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if content.type == "function_result" and content.call_id:
|
||||
call_id = str(content.call_id)
|
||||
if call_id in pending_tool_call_ids:
|
||||
keep = True
|
||||
@@ -175,7 +172,7 @@ def _deduplicate_messages(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
for idx, msg in enumerate(messages):
|
||||
role_value = get_role_value(msg)
|
||||
|
||||
if role_value == "tool" and msg.contents and isinstance(msg.contents[0], FunctionResultContent):
|
||||
if role_value == "tool" and msg.contents and msg.contents[0].type == "function_result":
|
||||
call_id = str(msg.contents[0].call_id)
|
||||
key: Any = (role_value, call_id)
|
||||
|
||||
@@ -184,7 +181,7 @@ def _deduplicate_messages(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
existing_msg = unique_messages[existing_idx]
|
||||
|
||||
existing_result = None
|
||||
if existing_msg.contents and isinstance(existing_msg.contents[0], FunctionResultContent):
|
||||
if existing_msg.contents and existing_msg.contents[0].type == "function_result":
|
||||
existing_result = existing_msg.contents[0].result
|
||||
new_result = msg.contents[0].result
|
||||
|
||||
@@ -198,11 +195,9 @@ def _deduplicate_messages(messages: list[ChatMessage]) -> list[ChatMessage]:
|
||||
seen_keys[key] = len(unique_messages)
|
||||
unique_messages.append(msg)
|
||||
|
||||
elif (
|
||||
role_value == "assistant" and msg.contents and any(isinstance(c, FunctionCallContent) for c in msg.contents)
|
||||
):
|
||||
elif role_value == "assistant" and msg.contents and any(c.type == "function_call" for c in msg.contents):
|
||||
tool_call_ids = tuple(
|
||||
sorted(str(c.call_id) for c in msg.contents if isinstance(c, FunctionCallContent) and c.call_id)
|
||||
sorted(str(c.call_id) for c in msg.contents if c.type == "function_call" and c.call_id)
|
||||
)
|
||||
key = (role_value, tool_call_ids)
|
||||
|
||||
@@ -275,15 +270,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
function_payload_dict["arguments"] = modified_args
|
||||
return
|
||||
|
||||
def _find_matching_func_call(call_id: str) -> FunctionCallContent | None:
|
||||
def _find_matching_func_call(call_id: str) -> Content | None:
|
||||
for prev_msg in result:
|
||||
role_val = prev_msg.role.value if hasattr(prev_msg.role, "value") else str(prev_msg.role)
|
||||
if role_val != "assistant":
|
||||
continue
|
||||
for content in prev_msg.contents or []:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
if content.call_id == call_id and content.name != "confirm_changes":
|
||||
return content
|
||||
if content.type == "function_call" and content.call_id == call_id and content.name != "confirm_changes":
|
||||
return content
|
||||
return None
|
||||
|
||||
def _parse_arguments(arguments: Any) -> dict[str, Any] | None:
|
||||
@@ -301,9 +295,9 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
continue
|
||||
direct_call = None
|
||||
confirm_call = None
|
||||
sibling_calls: list[FunctionCallContent] = []
|
||||
sibling_calls: list[Content] = []
|
||||
for content in prev_msg.contents or []:
|
||||
if not isinstance(content, FunctionCallContent):
|
||||
if content.type != "function_call":
|
||||
continue
|
||||
if content.call_id == tool_call_id:
|
||||
direct_call = content
|
||||
@@ -407,7 +401,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
if not (
|
||||
(m.role.value if hasattr(m.role, "value") else str(m.role)) == "tool"
|
||||
and any(
|
||||
isinstance(c, FunctionResultContent) and c.call_id == approval_call_id
|
||||
c.type == "function_result" and c.call_id == approval_call_id
|
||||
for c in (m.contents or [])
|
||||
)
|
||||
)
|
||||
@@ -465,9 +459,9 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
matching_func_call.arguments = updated_args
|
||||
_update_tool_call_arguments(messages, str(approval_call_id), merged_args)
|
||||
# Create a new FunctionCallContent with the modified arguments
|
||||
func_call_for_approval = FunctionCallContent(
|
||||
call_id=matching_func_call.call_id,
|
||||
name=matching_func_call.name,
|
||||
func_call_for_approval = Content.from_function_call(
|
||||
call_id=matching_func_call.call_id, # type: ignore[arg-type]
|
||||
name=matching_func_call.name, # type: ignore[arg-type]
|
||||
arguments=json.dumps(filtered_args),
|
||||
)
|
||||
logger.info(f"Using modified arguments from approval: {filtered_args}")
|
||||
@@ -476,7 +470,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
func_call_for_approval = matching_func_call
|
||||
|
||||
# Create FunctionApprovalResponseContent for the agent framework
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
approval_response = Content.from_function_approval_response(
|
||||
approved=accepted,
|
||||
id=str(approval_call_id),
|
||||
function_call=func_call_for_approval,
|
||||
@@ -491,7 +485,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
# Keep the old behavior for backwards compatibility
|
||||
chat_msg = ChatMessage(
|
||||
role=Role.USER,
|
||||
contents=[TextContent(text=approval_payload_text)],
|
||||
contents=[Content.from_text(text=approval_payload_text)],
|
||||
additional_properties={"is_tool_result": True, "tool_call_id": str(tool_call_id or "")},
|
||||
)
|
||||
if "id" in msg:
|
||||
@@ -511,7 +505,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
func_result = str(result_content)
|
||||
chat_msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id=str(tool_call_id), result=func_result)],
|
||||
contents=[Content.from_function_result(call_id=str(tool_call_id), result=func_result)],
|
||||
)
|
||||
if "id" in msg:
|
||||
chat_msg.message_id = msg["id"]
|
||||
@@ -527,21 +521,21 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
|
||||
chat_msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id=str(tool_call_id), result=result_content)],
|
||||
contents=[Content.from_function_result(call_id=str(tool_call_id), result=result_content)],
|
||||
)
|
||||
if "id" in msg:
|
||||
chat_msg.message_id = msg["id"]
|
||||
result.append(chat_msg)
|
||||
continue
|
||||
|
||||
# If assistant message includes tool calls, convert to FunctionCallContent(s)
|
||||
# If assistant message includes tool calls, convert to Content.from_function_call(s)
|
||||
tool_calls = msg.get("tool_calls") or msg.get("toolCalls")
|
||||
if tool_calls:
|
||||
contents: list[Any] = []
|
||||
# Include any assistant text content if present
|
||||
content_text = msg.get("content")
|
||||
if isinstance(content_text, str) and content_text:
|
||||
contents.append(TextContent(text=content_text))
|
||||
contents.append(Content.from_text(text=content_text))
|
||||
# Convert each tool call entry
|
||||
for tc in tool_calls:
|
||||
if not isinstance(tc, dict):
|
||||
@@ -558,7 +552,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
arguments = func_dict.get("arguments")
|
||||
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name=name,
|
||||
arguments=arguments,
|
||||
@@ -580,14 +574,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
approval_contents: list[Any] = []
|
||||
for approval in msg["function_approvals"]:
|
||||
# Create FunctionCallContent with the modified arguments
|
||||
func_call = FunctionCallContent(
|
||||
func_call = Content.from_function_call(
|
||||
call_id=approval.get("call_id", ""),
|
||||
name=approval.get("name", ""),
|
||||
arguments=approval.get("arguments", {}),
|
||||
)
|
||||
|
||||
# Create the approval response
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
approval_response = Content.from_function_approval_response(
|
||||
approved=approval.get("approved", True),
|
||||
id=approval.get("id", ""),
|
||||
function_call=func_call,
|
||||
@@ -599,9 +593,9 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
|
||||
# Regular text message
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, str):
|
||||
chat_msg = ChatMessage(role=role, contents=[TextContent(text=content)])
|
||||
chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=content)])
|
||||
else:
|
||||
chat_msg = ChatMessage(role=role, contents=[TextContent(text=str(content))])
|
||||
chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=str(content))])
|
||||
|
||||
if "id" in msg:
|
||||
chat_msg.message_id = msg["id"]
|
||||
@@ -652,9 +646,9 @@ def agent_framework_messages_to_agui(messages: list[ChatMessage] | list[dict[str
|
||||
tool_result_call_id: str | None = None
|
||||
|
||||
for content in msg.contents:
|
||||
if isinstance(content, TextContent):
|
||||
content_text += content.text
|
||||
elif isinstance(content, FunctionCallContent):
|
||||
if content.type == "text":
|
||||
content_text += content.text # type: ignore[operator]
|
||||
elif content.type == "function_call":
|
||||
tool_calls.append(
|
||||
{
|
||||
"id": content.call_id,
|
||||
@@ -665,7 +659,7 @@ def agent_framework_messages_to_agui(messages: list[ChatMessage] | list[dict[str
|
||||
},
|
||||
}
|
||||
)
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
# Tool result content - extract call_id and result
|
||||
tool_result_call_id = content.call_id
|
||||
# Serialize result to string using core utility
|
||||
@@ -702,8 +696,13 @@ def extract_text_from_contents(contents: list[Any]) -> str:
|
||||
"""
|
||||
text_parts: list[str] = []
|
||||
for content in contents:
|
||||
if isinstance(content, TextContent):
|
||||
text_parts.append(content.text)
|
||||
if type_ := getattr(content, "type", None):
|
||||
if type_ == "text_reasoning":
|
||||
continue
|
||||
if text := getattr(content, "text", None):
|
||||
text_parts.append(text)
|
||||
continue
|
||||
# TODO (moonbox3): should this handle both text and text_reasoning?
|
||||
elif hasattr(content, "text"):
|
||||
text_parts.append(content.text)
|
||||
return "".join(text_parts)
|
||||
|
||||
@@ -9,10 +9,7 @@ from typing import TYPE_CHECKING, Any
|
||||
from ag_ui.core import StateSnapshotEvent
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
TextContent,
|
||||
Content,
|
||||
)
|
||||
|
||||
from .._utils import get_role_value, safe_json_parse
|
||||
@@ -37,9 +34,9 @@ def pending_tool_call_ids(messages: list[ChatMessage]) -> set[str]:
|
||||
resolved_ids: set[str] = set()
|
||||
for msg in messages:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionCallContent) and content.call_id:
|
||||
if content.type == "function_call" and content.call_id:
|
||||
pending_ids.add(str(content.call_id))
|
||||
elif isinstance(content, FunctionResultContent) and content.call_id:
|
||||
elif content.type == "function_result" and content.call_id:
|
||||
resolved_ids.add(str(content.call_id))
|
||||
return pending_ids - resolved_ids
|
||||
|
||||
@@ -56,7 +53,7 @@ def is_state_context_message(message: ChatMessage) -> bool:
|
||||
if get_role_value(message) != "system":
|
||||
return False
|
||||
for content in message.contents:
|
||||
if isinstance(content, TextContent) and content.text.startswith("Current state of the application:"):
|
||||
if content.type == "text" and content.text.startswith("Current state of the application:"): # type: ignore[union-attr]
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -139,7 +136,7 @@ def tool_calls_match_state(
|
||||
if get_role_value(msg) != "assistant":
|
||||
continue
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionCallContent) and content.name == tool_name:
|
||||
if content.type == "function_call" and content.name == tool_name:
|
||||
tool_args = safe_json_parse(content.arguments)
|
||||
break
|
||||
if tool_args is not None:
|
||||
@@ -287,7 +284,7 @@ def collect_approved_state_snapshots(
|
||||
if get_role_value(msg) != "user":
|
||||
continue
|
||||
for content in msg.contents:
|
||||
if type(content) is FunctionApprovalResponseContent:
|
||||
if content.type == "function_approval_response":
|
||||
if not content.function_call or not content.approved:
|
||||
continue
|
||||
parsed_args = content.function_call.parse_arguments()
|
||||
@@ -319,7 +316,7 @@ def collect_approved_state_snapshots(
|
||||
return events
|
||||
|
||||
|
||||
def latest_approval_response(messages: list[ChatMessage]) -> FunctionApprovalResponseContent | None:
|
||||
def latest_approval_response(messages: list[ChatMessage]) -> Content | None:
|
||||
"""Get the latest approval response from messages.
|
||||
|
||||
Args:
|
||||
@@ -332,12 +329,12 @@ def latest_approval_response(messages: list[ChatMessage]) -> FunctionApprovalRes
|
||||
return None
|
||||
last_message = messages[-1]
|
||||
for content in last_message.contents:
|
||||
if type(content) is FunctionApprovalResponseContent:
|
||||
if content.type == "function_approval_response":
|
||||
return content
|
||||
return None
|
||||
|
||||
|
||||
def approval_steps(approval: FunctionApprovalResponseContent) -> list[Any]:
|
||||
def approval_steps(approval: Content) -> list[Any]:
|
||||
"""Extract steps from an approval response.
|
||||
|
||||
Args:
|
||||
@@ -346,9 +343,7 @@ def approval_steps(approval: FunctionApprovalResponseContent) -> list[Any]:
|
||||
Returns:
|
||||
List of steps, or empty list if none
|
||||
"""
|
||||
state_args: Any | None = None
|
||||
if approval.additional_properties:
|
||||
state_args = approval.additional_properties.get("ag_ui_state_args")
|
||||
state_args = approval.additional_properties.get("ag_ui_state_args", None)
|
||||
if isinstance(state_args, dict):
|
||||
steps = state_args.get("steps")
|
||||
if isinstance(steps, list):
|
||||
@@ -365,7 +360,7 @@ def approval_steps(approval: FunctionApprovalResponseContent) -> list[Any]:
|
||||
|
||||
|
||||
def is_step_based_approval(
|
||||
approval: FunctionApprovalResponseContent,
|
||||
approval: Content,
|
||||
predict_state_config: dict[str, dict[str, str]] | None,
|
||||
) -> bool:
|
||||
"""Check if an approval is step-based.
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from ag_ui.core import CustomEvent, EventType
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage, Content
|
||||
|
||||
|
||||
class StateManager:
|
||||
@@ -71,7 +71,7 @@ class StateManager:
|
||||
return ChatMessage(
|
||||
role="system",
|
||||
contents=[
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=(
|
||||
"Current state of the application:\n"
|
||||
f"{state_json}\n\n"
|
||||
|
||||
@@ -25,13 +25,11 @@ from agent_framework import (
|
||||
AgentProtocol,
|
||||
AgentThread,
|
||||
ChatAgent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
TextContent,
|
||||
Content,
|
||||
FunctionInvocationConfiguration,
|
||||
)
|
||||
from agent_framework._middleware import extract_and_merge_function_middleware
|
||||
from agent_framework._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
_collect_approval_responses, # type: ignore
|
||||
_replace_approval_contents_with_results, # type: ignore
|
||||
_try_execute_function_calls, # type: ignore
|
||||
@@ -285,12 +283,12 @@ class HumanInTheLoopOrchestrator(Orchestrator):
|
||||
last_message = context.last_message
|
||||
if last_message:
|
||||
for content in last_message.contents:
|
||||
if isinstance(content, TextContent):
|
||||
if content.type == "text":
|
||||
tool_content_text = content.text
|
||||
break
|
||||
|
||||
try:
|
||||
tool_result = json.loads(tool_content_text)
|
||||
tool_result = json.loads(tool_content_text) # type: ignore[arg-type]
|
||||
accepted = tool_result.get("accepted", False)
|
||||
steps = tool_result.get("steps", [])
|
||||
|
||||
@@ -328,7 +326,7 @@ class HumanInTheLoopOrchestrator(Orchestrator):
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Failed to parse tool result: {tool_content_text}")
|
||||
yield RunErrorEvent(message=f"Invalid tool result format: {tool_content_text[:100]}")
|
||||
yield RunErrorEvent(message=f"Invalid tool result format: {tool_content_text[:100]}") # type: ignore[index]
|
||||
yield event_bridge.create_run_finished_event()
|
||||
|
||||
|
||||
@@ -441,25 +439,24 @@ class DefaultOrchestrator(Orchestrator):
|
||||
logger.info(f" Message {i}: role={role}, id={msg_id}")
|
||||
if hasattr(msg, "contents") and msg.contents:
|
||||
for j, content in enumerate(msg.contents):
|
||||
content_type = type(content).__name__
|
||||
if isinstance(content, TextContent):
|
||||
logger.debug(" Content %s: %s - text_length=%s", j, content_type, len(content.text))
|
||||
elif isinstance(content, FunctionCallContent):
|
||||
if content.type == "text":
|
||||
logger.debug(" Content %s: %s - text_length=%s", j, content.type, len(content.text)) # type: ignore[arg-type]
|
||||
elif content.type == "function_call":
|
||||
arg_length = len(str(content.arguments)) if content.arguments else 0
|
||||
logger.debug(
|
||||
" Content %s: %s - %s args_length=%s", j, content_type, content.name, arg_length
|
||||
" Content %s: %s - %s args_length=%s", j, content.type, content.name, arg_length
|
||||
)
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
result_preview = type(content.result).__name__ if content.result is not None else "None"
|
||||
logger.debug(
|
||||
" Content %s: %s - call_id=%s, result_type=%s",
|
||||
j,
|
||||
content_type,
|
||||
content.type,
|
||||
content.call_id,
|
||||
result_preview,
|
||||
)
|
||||
else:
|
||||
logger.debug(f" Content {j}: {content_type}")
|
||||
logger.debug(f" Content {j}: {content.type}")
|
||||
|
||||
pending_tool_calls: list[dict[str, Any]] = []
|
||||
tool_calls_by_id: dict[str, dict[str, Any]] = {}
|
||||
@@ -536,16 +533,14 @@ class DefaultOrchestrator(Orchestrator):
|
||||
logger.error("Failed to execute approved tool calls; injecting error results.")
|
||||
approved_function_results = []
|
||||
|
||||
normalized_results: list[FunctionResultContent] = []
|
||||
normalized_results: list[Content] = []
|
||||
for idx, approval in enumerate(approved_responses):
|
||||
if idx < len(approved_function_results) and isinstance(
|
||||
approved_function_results[idx], FunctionResultContent
|
||||
):
|
||||
if idx < len(approved_function_results) and approved_function_results[idx].type == "function_result":
|
||||
normalized_results.append(approved_function_results[idx])
|
||||
continue
|
||||
call_id = approval.function_call.call_id or approval.id
|
||||
call_id = approval.function_call.call_id or approval.id # type: ignore[union-attr]
|
||||
normalized_results.append(
|
||||
FunctionResultContent(call_id=call_id, result="Error: Tool call invocation failed.")
|
||||
Content.from_function_result(call_id=call_id, result="Error: Tool call invocation failed.") # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
_replace_approval_contents_with_results(messages, fcc_todo, normalized_results) # type: ignore
|
||||
@@ -661,8 +656,8 @@ class DefaultOrchestrator(Orchestrator):
|
||||
if all_updates is not None:
|
||||
all_updates.append(update)
|
||||
if event_bridge.current_message_id is None and update.contents:
|
||||
has_tool_call = any(isinstance(content, FunctionCallContent) for content in update.contents)
|
||||
has_text = any(isinstance(content, TextContent) for content in update.contents)
|
||||
has_tool_call = any(content.type == "function_call" for content in update.contents)
|
||||
has_text = any(content.type == "text" for content in update.contents)
|
||||
if has_tool_call and not has_text:
|
||||
tool_message_id = generate_event_id()
|
||||
event_bridge.current_message_id = tool_message_id
|
||||
|
||||
@@ -6,6 +6,7 @@ import sys
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from agent_framework import ChatOptions
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar
|
||||
@@ -19,8 +20,6 @@ __all__ = [
|
||||
"RunMetadata",
|
||||
]
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PredictStateConfig(TypedDict):
|
||||
"""Configuration for predictive state updates."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from ag_ui.core import (
|
||||
TextMessageStartEvent,
|
||||
ToolCallStartEvent,
|
||||
)
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, Content, ai_function
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -221,7 +221,6 @@ class TaskStepsAgentWithExecution:
|
||||
chat_client = chat_agent.chat_client # type: ignore
|
||||
|
||||
# Build messages for summary call
|
||||
from agent_framework._types import ChatMessage, TextContent
|
||||
|
||||
original_messages = input_data.get("messages", [])
|
||||
|
||||
@@ -234,7 +233,7 @@ class TaskStepsAgentWithExecution:
|
||||
messages.append(
|
||||
ChatMessage(
|
||||
role=msg.get("role", "user"),
|
||||
contents=[TextContent(text=content_str)],
|
||||
contents=[Content.from_text(text=content_str)],
|
||||
)
|
||||
)
|
||||
elif isinstance(msg, ChatMessage):
|
||||
@@ -245,7 +244,7 @@ class TaskStepsAgentWithExecution:
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text="The steps have been successfully executed. Provide a brief one-sentence summary."
|
||||
)
|
||||
],
|
||||
|
||||
@@ -50,11 +50,9 @@ async def main():
|
||||
print("\nAssistant: ", end="", flush=True)
|
||||
|
||||
# Display text content as it streams
|
||||
from agent_framework import TextContent
|
||||
|
||||
for content in update.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
print(f"\033[96m{content.text}\033[0m", end="", flush=True)
|
||||
if hasattr(content, "text") and content.text: # type: ignore[attr-defined]
|
||||
print(f"\033[96m{content.text}\033[0m", end="", flush=True) # type: ignore[attr-defined]
|
||||
|
||||
# Display finish reason if present
|
||||
if update.finish_reason:
|
||||
|
||||
@@ -73,11 +73,9 @@ async def streaming_example(client: AGUIChatClient, thread_id: str | None = None
|
||||
if not thread_id and update.additional_properties:
|
||||
thread_id = update.additional_properties.get("thread_id")
|
||||
|
||||
from agent_framework import TextContent
|
||||
|
||||
for content in update.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
print(content.text, end="", flush=True)
|
||||
if content.type == "text" and content.text: # type: ignore[attr-defined]
|
||||
print(content.text, end="", flush=True) # type: ignore[attr-defined]
|
||||
|
||||
print("\n")
|
||||
return thread_id
|
||||
@@ -138,13 +136,11 @@ async def tool_example(client: AGUIChatClient, thread_id: str | None = None):
|
||||
print(f"Assistant: {response.text}")
|
||||
|
||||
# Show tool calls if any
|
||||
from agent_framework import FunctionCallContent
|
||||
|
||||
tool_called = False
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
print(f"\n[Tool Called: {content.name}]")
|
||||
if content.type == "function_call": # type: ignore[attr-defined]
|
||||
print(f"\n[Tool Called: {content.name}]") # type: ignore[attr-defined]
|
||||
tool_called = True
|
||||
|
||||
if not tool_called:
|
||||
@@ -176,7 +172,7 @@ async def conversation_example(client: AGUIChatClient):
|
||||
|
||||
# Second turn - using same thread
|
||||
print("\nUser: What's my name?\n")
|
||||
response2 = await client.get_response("What's my name?", metadata={"thread_id": thread_id})
|
||||
response2 = await client.get_response("What's my name?", options={"metadata": {"thread_id": thread_id}})
|
||||
print(f"Assistant: {response2.text}")
|
||||
|
||||
# Check if context was maintained
|
||||
@@ -186,7 +182,7 @@ async def conversation_example(client: AGUIChatClient):
|
||||
# Third turn
|
||||
print("\nUser: Can you also tell me what 10 * 5 is?\n")
|
||||
response3 = await client.get_response(
|
||||
"Can you also tell me what 10 * 5 is?", metadata={"thread_id": thread_id}, tools=[calculate]
|
||||
"Can you also tell me what 10 * 5 is?", options={"metadata": {"thread_id": thread_id}}, tools=[calculate]
|
||||
)
|
||||
print(f"Assistant: {response3.text}")
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent, FunctionCallContent, FunctionResultContent, TextContent, ai_function
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
from agent_framework.ag_ui import AGUIChatClient
|
||||
|
||||
# Enable debug logging
|
||||
@@ -141,8 +141,9 @@ async def main():
|
||||
# Build from contents when no direct text
|
||||
parts: list[str] = []
|
||||
for c in getattr(m, "contents", []) or []:
|
||||
if isinstance(c, FunctionCallContent):
|
||||
args = c.arguments
|
||||
content_type = getattr(c, "type", None)
|
||||
if content_type == "function_call":
|
||||
args = getattr(c, "arguments", None)
|
||||
if isinstance(args, dict):
|
||||
try:
|
||||
import json as _json
|
||||
@@ -152,12 +153,15 @@ async def main():
|
||||
args_str = str(args)
|
||||
else:
|
||||
args_str = str(args or "{}")
|
||||
parts.append(f"tool_call {c.name} {args_str}")
|
||||
elif isinstance(c, FunctionResultContent):
|
||||
parts.append(f"tool_result[{c.call_id}]: {str(c.result)[:40]}")
|
||||
elif isinstance(c, TextContent):
|
||||
if c.text:
|
||||
parts.append(c.text)
|
||||
parts.append(f"tool_call {getattr(c, 'name', '?')} {args_str}")
|
||||
elif content_type == "function_result":
|
||||
call_id = getattr(c, "call_id", "?")
|
||||
result = getattr(c, "result", None)
|
||||
parts.append(f"tool_result[{call_id}]: {str(result)[:40]}")
|
||||
elif content_type == "text":
|
||||
text = getattr(c, "text", None)
|
||||
if text:
|
||||
parts.append(text)
|
||||
else:
|
||||
typename = getattr(c, "type", c.__class__.__name__)
|
||||
parts.append(f"<{typename}>")
|
||||
|
||||
@@ -11,14 +11,13 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from agent_framework_ag_ui._client import AGUIChatClient, ServerFunctionCallContent
|
||||
from agent_framework_ag_ui._client import AGUIChatClient
|
||||
from agent_framework_ag_ui._http_service import AGUIHttpService
|
||||
|
||||
|
||||
@@ -96,13 +95,11 @@ class TestAGUIChatClient:
|
||||
state_json = json.dumps(state_data)
|
||||
state_b64 = base64.b64encode(state_json.encode("utf-8")).decode("utf-8")
|
||||
|
||||
from agent_framework import DataContent
|
||||
|
||||
messages = [
|
||||
ChatMessage(role="user", text="Hello"),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[DataContent(uri=f"data:application/json;base64,{state_b64}")],
|
||||
contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -121,12 +118,10 @@ class TestAGUIChatClient:
|
||||
invalid_json = "not valid json"
|
||||
state_b64 = base64.b64encode(invalid_json.encode("utf-8")).decode("utf-8")
|
||||
|
||||
from agent_framework import DataContent
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[DataContent(uri=f"data:application/json;base64,{state_b64}")],
|
||||
contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -200,8 +195,8 @@ class TestAGUIChatClient:
|
||||
|
||||
first_content = updates[1].contents[0]
|
||||
second_content = updates[2].contents[0]
|
||||
assert isinstance(first_content, TextContent)
|
||||
assert isinstance(second_content, TextContent)
|
||||
assert first_content.type == "text"
|
||||
assert second_content.type == "text"
|
||||
assert first_content.text == "Hello"
|
||||
assert second_content.text == " world"
|
||||
|
||||
@@ -294,13 +289,12 @@ class TestAGUIChatClient:
|
||||
updates.append(update)
|
||||
|
||||
function_calls = [
|
||||
content for update in updates for content in update.contents if isinstance(content, FunctionCallContent)
|
||||
content for update in updates for content in update.contents if content.type == "function_call"
|
||||
]
|
||||
assert function_calls
|
||||
assert function_calls[0].name == "get_time_zone"
|
||||
assert not any(
|
||||
isinstance(content, ServerFunctionCallContent) for update in updates for content in update.contents
|
||||
)
|
||||
|
||||
assert not any(content.type == "server_function_call" for update in updates for content in update.contents)
|
||||
|
||||
async def test_server_tool_calls_not_executed_locally(self, monkeypatch: MonkeyPatch) -> None:
|
||||
"""Server tools should not trigger local function invocation even when client tools exist."""
|
||||
@@ -343,13 +337,11 @@ class TestAGUIChatClient:
|
||||
state_json = json.dumps(state_data)
|
||||
state_b64 = base64.b64encode(state_json.encode("utf-8")).decode("utf-8")
|
||||
|
||||
from agent_framework import DataContent
|
||||
|
||||
messages = [
|
||||
ChatMessage(role="user", text="Hello"),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[DataContent(uri=f"data:application/json;base64,{state_b64}")],
|
||||
contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, TextContent
|
||||
from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, Content
|
||||
from pydantic import BaseModel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
@@ -23,7 +23,7 @@ async def test_agent_initialization_basic():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent[ChatOptions](
|
||||
chat_client=StreamingChatClientStub(stream_fn),
|
||||
@@ -45,7 +45,7 @@ async def test_agent_initialization_with_state_schema():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
state_schema: dict[str, dict[str, Any]] = {"document": {"type": "string"}}
|
||||
@@ -61,7 +61,7 @@ async def test_agent_initialization_with_predict_state_config():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
predict_config = {"document": {"tool": "write_doc", "tool_argument": "content"}}
|
||||
@@ -77,7 +77,7 @@ async def test_agent_initialization_with_pydantic_state_schema():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
class MyState(BaseModel):
|
||||
document: str
|
||||
@@ -100,7 +100,7 @@ async def test_run_started_event_emission():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -124,7 +124,7 @@ async def test_predict_state_custom_event_emission():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
predict_config = {
|
||||
@@ -156,7 +156,7 @@ async def test_initial_state_snapshot_with_schema():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
state_schema = {"document": {"type": "string"}}
|
||||
@@ -186,7 +186,7 @@ async def test_state_initialization_object_type():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
state_schema: dict[str, dict[str, Any]] = {"recipe": {"type": "object", "properties": {}}}
|
||||
@@ -213,7 +213,7 @@ async def test_state_initialization_array_type():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
state_schema: dict[str, dict[str, Any]] = {"steps": {"type": "array", "items": {}}}
|
||||
@@ -240,7 +240,7 @@ async def test_run_finished_event_emission():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -262,7 +262,7 @@ async def test_tool_result_confirm_changes_accepted():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Document updated")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Document updated")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(
|
||||
@@ -309,7 +309,7 @@ async def test_tool_result_confirm_changes_rejected():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="OK")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="OK")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -343,7 +343,7 @@ async def test_tool_result_function_approval_accepted():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="OK")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="OK")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -389,7 +389,7 @@ async def test_tool_result_function_approval_rejected():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="OK")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="OK")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -431,7 +431,7 @@ async def test_thread_metadata_tracking():
|
||||
metadata = options.get("metadata")
|
||||
if metadata:
|
||||
thread_metadata.update(metadata)
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -462,7 +462,7 @@ async def test_state_context_injection():
|
||||
metadata = options.get("metadata")
|
||||
if metadata:
|
||||
thread_metadata.update(metadata)
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(
|
||||
@@ -492,7 +492,7 @@ async def test_no_messages_provided():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -516,7 +516,7 @@ async def test_message_end_event_emission():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Hello world")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Hello world")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(agent=agent)
|
||||
@@ -602,7 +602,7 @@ async def test_suppressed_summary_with_document_state():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Response")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Response")])
|
||||
|
||||
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
wrapper = AgentFrameworkAgent(
|
||||
@@ -650,7 +650,7 @@ async def test_agent_with_use_service_thread_is_false():
|
||||
thread = kwargs.get("thread")
|
||||
request_service_thread_id = thread.service_thread_id if thread else None
|
||||
yield ChatResponseUpdate(
|
||||
contents=[TextContent(text="Response")], response_id="resp_67890", conversation_id="conv_12345"
|
||||
contents=[Content.from_text(text="Response")], response_id="resp_67890", conversation_id="conv_12345"
|
||||
)
|
||||
|
||||
agent = ChatAgent(chat_client=StreamingChatClientStub(stream_fn))
|
||||
@@ -677,7 +677,7 @@ async def test_agent_with_use_service_thread_is_true():
|
||||
thread = kwargs.get("thread")
|
||||
request_service_thread_id = thread.service_thread_id if thread else None
|
||||
yield ChatResponseUpdate(
|
||||
contents=[TextContent(text="Response")], response_id="resp_67890", conversation_id="conv_12345"
|
||||
contents=[Content.from_text(text="Response")], response_id="resp_67890", conversation_id="conv_12345"
|
||||
)
|
||||
|
||||
agent = ChatAgent(chat_client=StreamingChatClientStub(stream_fn))
|
||||
@@ -693,7 +693,7 @@ async def test_agent_with_use_service_thread_is_true():
|
||||
|
||||
async def test_function_approval_mode_executes_tool():
|
||||
"""Test that function approval with approval_mode='always_require' sends the correct messages."""
|
||||
from agent_framework import FunctionResultContent, ai_function
|
||||
from agent_framework import ai_function
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
messages_received: list[Any] = []
|
||||
@@ -712,7 +712,7 @@ async def test_function_approval_mode_executes_tool():
|
||||
# Capture the messages received by the chat client
|
||||
messages_received.clear()
|
||||
messages_received.extend(messages)
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Processing completed")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Processing completed")])
|
||||
|
||||
agent = ChatAgent(
|
||||
chat_client=StreamingChatClientStub(stream_fn),
|
||||
@@ -770,7 +770,7 @@ async def test_function_approval_mode_executes_tool():
|
||||
tool_result_found = False
|
||||
for msg in messages_received:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if content.type == "function_result":
|
||||
tool_result_found = True
|
||||
assert content.call_id == "call_get_datetime_123"
|
||||
assert content.result == "2025/12/01 12:00:00"
|
||||
@@ -784,7 +784,7 @@ async def test_function_approval_mode_executes_tool():
|
||||
|
||||
async def test_function_approval_mode_rejection():
|
||||
"""Test that function approval rejection creates a rejection response."""
|
||||
from agent_framework import FunctionResultContent, ai_function
|
||||
from agent_framework import ai_function
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
messages_received: list[Any] = []
|
||||
@@ -803,7 +803,7 @@ async def test_function_approval_mode_rejection():
|
||||
# Capture the messages received by the chat client
|
||||
messages_received.clear()
|
||||
messages_received.extend(messages)
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="Operation cancelled")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Operation cancelled")])
|
||||
|
||||
agent = ChatAgent(
|
||||
name="test_agent",
|
||||
@@ -855,7 +855,7 @@ async def test_function_approval_mode_rejection():
|
||||
rejection_found = False
|
||||
for msg in messages_received:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if content.type == "function_result":
|
||||
rejection_found = True
|
||||
assert content.call_id == "call_delete_123"
|
||||
assert content.result == "Error: Tool call invocation was rejected by user."
|
||||
|
||||
@@ -12,7 +12,7 @@ from ag_ui.core import (
|
||||
ToolCallResultEvent,
|
||||
ToolCallStartEvent,
|
||||
)
|
||||
from agent_framework import AgentResponseUpdate, FunctionCallContent, FunctionResultContent, TextContent
|
||||
from agent_framework import AgentResponseUpdate, Content
|
||||
|
||||
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
|
||||
|
||||
@@ -22,7 +22,7 @@ async def test_tool_call_flow():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test-run", thread_id="test-thread")
|
||||
|
||||
# Step 1: Tool call starts
|
||||
tool_call = FunctionCallContent(
|
||||
tool_call = Content.from_function_call(
|
||||
call_id="weather-123",
|
||||
name="get_weather",
|
||||
arguments={"location": "Seattle"},
|
||||
@@ -44,7 +44,7 @@ async def test_tool_call_flow():
|
||||
assert "Seattle" in args_event.delta
|
||||
|
||||
# Step 2: Tool result comes back
|
||||
tool_result = FunctionResultContent(
|
||||
tool_result = Content.from_function_result(
|
||||
call_id="weather-123",
|
||||
result="Weather in Seattle: Rainy, 52°F",
|
||||
)
|
||||
@@ -71,8 +71,8 @@ async def test_text_with_tool_call():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test-run", thread_id="test-thread")
|
||||
|
||||
# Agent says something then calls a tool
|
||||
text_content = TextContent(text="Let me check the weather for you.")
|
||||
tool_call = FunctionCallContent(
|
||||
text_content = Content.from_text(text="Let me check the weather for you.")
|
||||
tool_call = Content.from_function_call(
|
||||
call_id="weather-456",
|
||||
name="get_forecast",
|
||||
arguments={"location": "San Francisco", "days": 3},
|
||||
@@ -102,9 +102,9 @@ async def test_multiple_tool_results():
|
||||
|
||||
# Multiple tool results
|
||||
results = [
|
||||
FunctionResultContent(call_id="tool-1", result="Result 1"),
|
||||
FunctionResultContent(call_id="tool-2", result="Result 2"),
|
||||
FunctionResultContent(call_id="tool-3", result="Result 3"),
|
||||
Content.from_function_result(call_id="tool-1", result="Result 1"),
|
||||
Content.from_function_result(call_id="tool-2", result="Result 2"),
|
||||
Content.from_function_result(call_id="tool-3", result="Result 3"),
|
||||
]
|
||||
|
||||
update = AgentResponseUpdate(contents=results)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"""Tests for document writer predictive state flow with confirm_changes."""
|
||||
|
||||
from ag_ui.core import EventType, StateDeltaEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallStartEvent
|
||||
from agent_framework import AgentResponseUpdate, FunctionCallContent, FunctionResultContent, TextContent
|
||||
from agent_framework import AgentResponseUpdate, Content
|
||||
|
||||
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
|
||||
|
||||
@@ -21,7 +21,7 @@ async def test_streaming_document_with_state_deltas():
|
||||
)
|
||||
|
||||
# Simulate streaming tool call - first chunk with name
|
||||
tool_call_start = FunctionCallContent(
|
||||
tool_call_start = Content.from_function_call(
|
||||
call_id="call_123",
|
||||
name="write_document_local",
|
||||
arguments='{"document":"Once',
|
||||
@@ -34,7 +34,9 @@ async def test_streaming_document_with_state_deltas():
|
||||
assert any(e.type == EventType.TOOL_CALL_ARGS for e in events1)
|
||||
|
||||
# Second chunk - incomplete JSON, should try partial extraction
|
||||
tool_call_chunk2 = FunctionCallContent(call_id="call_123", name="write_document_local", arguments=" upon a time")
|
||||
tool_call_chunk2 = Content.from_function_call(
|
||||
call_id="call_123", name="write_document_local", arguments=" upon a time"
|
||||
)
|
||||
update2 = AgentResponseUpdate(contents=[tool_call_chunk2])
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
|
||||
@@ -71,7 +73,7 @@ async def test_confirm_changes_emission():
|
||||
bridge.pending_state_updates = {"document": "A short story"}
|
||||
|
||||
# Tool result
|
||||
tool_result = FunctionResultContent(
|
||||
tool_result = Content.from_function_result(
|
||||
call_id="call_123",
|
||||
result="Document written.",
|
||||
)
|
||||
@@ -115,7 +117,7 @@ async def test_text_suppression_before_confirm():
|
||||
bridge.should_stop_after_confirm = True
|
||||
|
||||
# Text content that should be suppressed
|
||||
text = TextContent(text="I have written a story about pirates.")
|
||||
text = Content.from_text(text="I have written a story about pirates.")
|
||||
update = AgentResponseUpdate(contents=[text])
|
||||
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -146,7 +148,7 @@ async def test_no_confirm_for_non_predictive_tools():
|
||||
# Different tool (not in predict_state_config)
|
||||
bridge.current_tool_call_name = "get_weather"
|
||||
|
||||
tool_result = FunctionResultContent(
|
||||
tool_result = Content.from_function_result(
|
||||
call_id="call_456",
|
||||
result="Sunny, 72°F",
|
||||
)
|
||||
@@ -175,7 +177,7 @@ async def test_state_delta_deduplication():
|
||||
)
|
||||
|
||||
# First tool call with document
|
||||
tool_call1 = FunctionCallContent(
|
||||
tool_call1 = Content.from_function_call(
|
||||
call_id="call_1",
|
||||
name="write_document_local",
|
||||
arguments='{"document":"Same text"}',
|
||||
@@ -189,7 +191,7 @@ async def test_state_delta_deduplication():
|
||||
|
||||
# Second tool call with SAME document (shouldn't emit new delta)
|
||||
bridge.current_tool_call_name = "write_document_local"
|
||||
tool_call2 = FunctionCallContent(
|
||||
tool_call2 = Content.from_function_call(
|
||||
call_id="call_2",
|
||||
name="write_document_local",
|
||||
arguments='{"document":"Same text"}', # Identical content
|
||||
@@ -216,7 +218,7 @@ async def test_predict_state_config_multiple_fields():
|
||||
)
|
||||
|
||||
# Tool call with both fields
|
||||
tool_call = FunctionCallContent(
|
||||
tool_call = Content.from_function_call(
|
||||
call_id="call_999",
|
||||
name="create_post",
|
||||
arguments='{"title":"My Post","body":"Post content"}',
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import ChatAgent, ChatResponseUpdate, TextContent
|
||||
from agent_framework import ChatAgent, ChatResponseUpdate, Content
|
||||
from fastapi import FastAPI, Header, HTTPException
|
||||
from fastapi.params import Depends
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -20,7 +20,7 @@ from utils_test_ag_ui import StreamingChatClientStub, stream_from_updates
|
||||
|
||||
def build_chat_client(response_text: str = "Test response") -> StreamingChatClientStub:
|
||||
"""Create a typed chat client stub for endpoint tests."""
|
||||
updates = [ChatResponseUpdate(contents=[TextContent(text=response_text)])]
|
||||
updates = [ChatResponseUpdate(contents=[Content.from_text(text=response_text)])]
|
||||
return StreamingChatClientStub(stream_from_updates(updates))
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import json
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
TextContent,
|
||||
Content,
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +16,7 @@ async def test_basic_text_message_conversion():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(contents=[TextContent(text="Hello")])
|
||||
update = AgentResponseUpdate(contents=[Content.from_text(text="Hello")])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
assert len(events) == 2
|
||||
@@ -35,8 +32,8 @@ async def test_text_message_streaming():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update1 = AgentResponseUpdate(contents=[TextContent(text="Hello ")])
|
||||
update2 = AgentResponseUpdate(contents=[TextContent(text="world")])
|
||||
update1 = AgentResponseUpdate(contents=[Content.from_text(text="Hello ")])
|
||||
update2 = AgentResponseUpdate(contents=[Content.from_text(text="world")])
|
||||
|
||||
events1 = await bridge.from_agent_run_update(update1)
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
@@ -61,7 +58,7 @@ async def test_skip_text_content_for_structured_outputs():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread", skip_text_content=True)
|
||||
|
||||
update = AgentResponseUpdate(contents=[TextContent(text='{"result": "data"}')])
|
||||
update = AgentResponseUpdate(contents=[Content.from_text(text='{"result": "data"}')])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
# No events should be emitted
|
||||
@@ -74,9 +71,9 @@ async def test_skip_text_content_for_empty_text():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update1 = AgentResponseUpdate(contents=[TextContent(text="Hello ")])
|
||||
update2 = AgentResponseUpdate(contents=[TextContent(text="")]) # Empty chunk
|
||||
update3 = AgentResponseUpdate(contents=[TextContent(text="world")])
|
||||
update1 = AgentResponseUpdate(contents=[Content.from_text(text="Hello ")])
|
||||
update2 = AgentResponseUpdate(contents=[Content.from_text(text="")]) # Empty chunk
|
||||
update3 = AgentResponseUpdate(contents=[Content.from_text(text="world")])
|
||||
|
||||
events1 = await bridge.from_agent_run_update(update1)
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
@@ -105,7 +102,7 @@ async def test_tool_call_with_name():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(contents=[FunctionCallContent(name="search_web", call_id="call_123")])
|
||||
update = AgentResponseUpdate(contents=[Content.from_function_call(name="search_web", call_id="call_123")])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -121,15 +118,17 @@ async def test_tool_call_streaming_args():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
# First chunk: name only
|
||||
update1 = AgentResponseUpdate(contents=[FunctionCallContent(name="search_web", call_id="call_123")])
|
||||
update1 = AgentResponseUpdate(contents=[Content.from_function_call(name="search_web", call_id="call_123")])
|
||||
events1 = await bridge.from_agent_run_update(update1)
|
||||
|
||||
# Second chunk: arguments chunk 1 (name can be empty string for continuation)
|
||||
update2 = AgentResponseUpdate(contents=[FunctionCallContent(name="", call_id="call_123", arguments='{"query": "')])
|
||||
update2 = AgentResponseUpdate(
|
||||
contents=[Content.from_function_call(name="", call_id="call_123", arguments='{"query": "')]
|
||||
)
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
|
||||
# Third chunk: arguments chunk 2
|
||||
update3 = AgentResponseUpdate(contents=[FunctionCallContent(name="", call_id="call_123", arguments='AI"}')])
|
||||
update3 = AgentResponseUpdate(contents=[Content.from_function_call(name="", call_id="call_123", arguments='AI"}')])
|
||||
events3 = await bridge.from_agent_run_update(update3)
|
||||
|
||||
# First update: ToolCallStartEvent
|
||||
@@ -167,9 +166,11 @@ async def test_streaming_tool_call_no_duplicate_start_events():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
# Simulate streaming tool call: first chunk has name, subsequent chunks have name=""
|
||||
update1 = AgentResponseUpdate(contents=[FunctionCallContent(name="get_weather", call_id="call_789")])
|
||||
update2 = AgentResponseUpdate(contents=[FunctionCallContent(name="", call_id="call_789", arguments='{"loc":')])
|
||||
update3 = AgentResponseUpdate(contents=[FunctionCallContent(name="", call_id="call_789", arguments='"SF"}')])
|
||||
update1 = AgentResponseUpdate(contents=[Content.from_function_call(name="get_weather", call_id="call_789")])
|
||||
update2 = AgentResponseUpdate(
|
||||
contents=[Content.from_function_call(name="", call_id="call_789", arguments='{"loc":')]
|
||||
)
|
||||
update3 = AgentResponseUpdate(contents=[Content.from_function_call(name="", call_id="call_789", arguments='"SF"}')])
|
||||
|
||||
events1 = await bridge.from_agent_run_update(update1)
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
@@ -193,7 +194,7 @@ async def test_tool_result_with_dict():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
result_data = {"status": "success", "count": 42}
|
||||
update = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_123", result=result_data)])
|
||||
update = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_123", result=result_data)])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
# Should emit ToolCallEndEvent + ToolCallResultEvent
|
||||
@@ -214,7 +215,7 @@ async def test_tool_result_with_string():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_123", result="Search complete")])
|
||||
update = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_123", result="Search complete")])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
assert len(events) == 2
|
||||
@@ -229,7 +230,7 @@ async def test_tool_result_with_none():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_123", result=None)])
|
||||
update = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_123", result=None)])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
assert len(events) == 2
|
||||
@@ -247,8 +248,8 @@ async def test_multiple_tool_results_in_sequence():
|
||||
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionResultContent(call_id="call_1", result="Result 1"),
|
||||
FunctionResultContent(call_id="call_2", result="Result 2"),
|
||||
Content.from_function_result(call_id="call_1", result="Result 1"),
|
||||
Content.from_function_result(call_id="call_2", result="Result 2"),
|
||||
]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -272,12 +273,12 @@ async def test_function_approval_request_basic():
|
||||
require_confirmation=False,
|
||||
)
|
||||
|
||||
func_call = FunctionCallContent(
|
||||
func_call = Content.from_function_call(
|
||||
call_id="call_123",
|
||||
name="send_email",
|
||||
arguments={"to": "user@example.com", "subject": "Test"},
|
||||
)
|
||||
approval = FunctionApprovalRequestContent(
|
||||
approval = Content.from_function_approval_request(
|
||||
id="approval_001",
|
||||
function_call=func_call,
|
||||
)
|
||||
@@ -312,8 +313,8 @@ async def test_empty_predict_state_config():
|
||||
# Tool call with arguments
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="write_doc", call_id="call_1", arguments='{"content": "test"}'),
|
||||
FunctionResultContent(call_id="call_1", result="Done"),
|
||||
Content.from_function_call(name="write_doc", call_id="call_1", arguments='{"content": "test"}'),
|
||||
Content.from_function_result(call_id="call_1", result="Done"),
|
||||
]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -347,8 +348,8 @@ async def test_tool_not_in_predict_state_config():
|
||||
# Different tool name
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="search_web", call_id="call_1", arguments='{"query": "AI"}'),
|
||||
FunctionResultContent(call_id="call_1", result="Results"),
|
||||
Content.from_function_call(name="search_web", call_id="call_1", arguments='{"query": "AI"}'),
|
||||
Content.from_function_result(call_id="call_1", result="Results"),
|
||||
]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -376,8 +377,8 @@ async def test_state_management_tracking():
|
||||
# Streaming tool call
|
||||
update1 = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="write_doc", call_id="call_1"),
|
||||
FunctionCallContent(name="", call_id="call_1", arguments='{"content": "Hello"}'),
|
||||
Content.from_function_call(name="write_doc", call_id="call_1"),
|
||||
Content.from_function_call(name="", call_id="call_1", arguments='{"content": "Hello"}'),
|
||||
]
|
||||
)
|
||||
await bridge.from_agent_run_update(update1)
|
||||
@@ -387,7 +388,7 @@ async def test_state_management_tracking():
|
||||
assert bridge.pending_state_updates["document"] == "Hello"
|
||||
|
||||
# Tool result should update current_state
|
||||
update2 = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_1", result="Done")])
|
||||
update2 = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_1", result="Done")])
|
||||
await bridge.from_agent_run_update(update2)
|
||||
|
||||
# current_state should be updated
|
||||
@@ -413,12 +414,12 @@ async def test_wildcard_tool_argument():
|
||||
# Complete tool call with dict arguments
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
name="create_recipe",
|
||||
call_id="call_1",
|
||||
arguments={"title": "Pasta", "ingredients": ["pasta", "sauce"]},
|
||||
),
|
||||
FunctionResultContent(call_id="call_1", result="Created"),
|
||||
Content.from_function_result(call_id="call_1", result="Created"),
|
||||
]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -503,14 +504,14 @@ async def test_state_snapshot_after_tool_result():
|
||||
# Tool call with streaming args
|
||||
update1 = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="write_doc", call_id="call_1"),
|
||||
FunctionCallContent(name="", call_id="call_1", arguments='{"content": "Test"}'),
|
||||
Content.from_function_call(name="write_doc", call_id="call_1"),
|
||||
Content.from_function_call(name="", call_id="call_1", arguments='{"content": "Test"}'),
|
||||
]
|
||||
)
|
||||
await bridge.from_agent_run_update(update1)
|
||||
|
||||
# Tool result should trigger StateSnapshotEvent
|
||||
update2 = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_1", result="Done")])
|
||||
update2 = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_1", result="Done")])
|
||||
events = await bridge.from_agent_run_update(update2)
|
||||
|
||||
# Should have: ToolCallEnd, ToolCallResult, StateSnapshot, ToolCallStart (confirm_changes), ToolCallArgs, ToolCallEnd
|
||||
@@ -526,12 +527,12 @@ async def test_message_id_persistence_across_chunks():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
# First chunk
|
||||
update1 = AgentResponseUpdate(contents=[TextContent(text="Hello ")])
|
||||
update1 = AgentResponseUpdate(contents=[Content.from_text(text="Hello ")])
|
||||
events1 = await bridge.from_agent_run_update(update1)
|
||||
message_id = events1[0].message_id
|
||||
|
||||
# Second chunk
|
||||
update2 = AgentResponseUpdate(contents=[TextContent(text="world")])
|
||||
update2 = AgentResponseUpdate(contents=[Content.from_text(text="world")])
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
|
||||
# Should use same message_id
|
||||
@@ -546,14 +547,16 @@ async def test_tool_call_id_tracking():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
# First chunk with name
|
||||
update1 = AgentResponseUpdate(contents=[FunctionCallContent(name="search", call_id="call_1")])
|
||||
update1 = AgentResponseUpdate(contents=[Content.from_function_call(name="search", call_id="call_1")])
|
||||
await bridge.from_agent_run_update(update1)
|
||||
|
||||
assert bridge.current_tool_call_id == "call_1"
|
||||
assert bridge.current_tool_call_name == "search"
|
||||
|
||||
# Second chunk with args but no name
|
||||
update2 = AgentResponseUpdate(contents=[FunctionCallContent(name="", call_id="call_1", arguments='{"q":"AI"}')])
|
||||
update2 = AgentResponseUpdate(
|
||||
contents=[Content.from_function_call(name="", call_id="call_1", arguments='{"q":"AI"}')]
|
||||
)
|
||||
events2 = await bridge.from_agent_run_update(update2)
|
||||
|
||||
# Should still track same tool call
|
||||
@@ -576,8 +579,8 @@ async def test_tool_name_reset_after_result():
|
||||
# Tool call
|
||||
update1 = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="write_doc", call_id="call_1"),
|
||||
FunctionCallContent(name="", call_id="call_1", arguments='{"content": "Test"}'),
|
||||
Content.from_function_call(name="write_doc", call_id="call_1"),
|
||||
Content.from_function_call(name="", call_id="call_1", arguments='{"content": "Test"}'),
|
||||
]
|
||||
)
|
||||
await bridge.from_agent_run_update(update1)
|
||||
@@ -585,7 +588,7 @@ async def test_tool_name_reset_after_result():
|
||||
assert bridge.current_tool_call_name == "write_doc"
|
||||
|
||||
# Tool result with predictive state (should trigger confirm_changes and reset)
|
||||
update2 = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_1", result="Done")])
|
||||
update2 = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_1", result="Done")])
|
||||
await bridge.from_agent_run_update(update2)
|
||||
|
||||
# Tool name should be reset
|
||||
@@ -604,9 +607,9 @@ async def test_function_approval_with_wildcard_argument():
|
||||
},
|
||||
)
|
||||
|
||||
approval_content = FunctionApprovalRequestContent(
|
||||
approval_content = Content.from_function_approval_request(
|
||||
id="approval_1",
|
||||
function_call=FunctionCallContent(
|
||||
function_call=Content.from_function_call(
|
||||
name="submit", call_id="call_1", arguments='{"key1": "value1", "key2": "value2"}'
|
||||
),
|
||||
)
|
||||
@@ -632,9 +635,11 @@ async def test_function_approval_missing_argument():
|
||||
},
|
||||
)
|
||||
|
||||
approval_content = FunctionApprovalRequestContent(
|
||||
approval_content = Content.from_function_approval_request(
|
||||
id="approval_1",
|
||||
function_call=FunctionCallContent(name="process", call_id="call_1", arguments='{"other_field": "value"}'),
|
||||
function_call=Content.from_function_call(
|
||||
name="process", call_id="call_1", arguments='{"other_field": "value"}'
|
||||
),
|
||||
)
|
||||
|
||||
update = AgentResponseUpdate(contents=[approval_content])
|
||||
@@ -654,8 +659,8 @@ async def test_empty_predict_state_config_no_deltas():
|
||||
# Tool call with arguments
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="search", call_id="call_1"),
|
||||
FunctionCallContent(name="", call_id="call_1", arguments='{"query": "test"}'),
|
||||
Content.from_function_call(name="search", call_id="call_1"),
|
||||
Content.from_function_call(name="", call_id="call_1", arguments='{"query": "test"}'),
|
||||
]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -678,8 +683,8 @@ async def test_tool_with_no_matching_config():
|
||||
# Tool call for different tool
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="search_web", call_id="call_1"),
|
||||
FunctionCallContent(name="", call_id="call_1", arguments='{"query": "test"}'),
|
||||
Content.from_function_call(name="search_web", call_id="call_1"),
|
||||
Content.from_function_call(name="", call_id="call_1", arguments='{"query": "test"}'),
|
||||
]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
@@ -696,7 +701,7 @@ async def test_tool_call_without_name_or_id():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
# This should not crash but log an error
|
||||
update = AgentResponseUpdate(contents=[FunctionCallContent(name="", call_id="", arguments='{"arg": "val"}')])
|
||||
update = AgentResponseUpdate(contents=[Content.from_function_call(name="", call_id="", arguments='{"arg": "val"}')])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
# Should emit ToolCallArgsEvent with generated ID
|
||||
@@ -717,7 +722,7 @@ async def test_state_delta_count_logging():
|
||||
for i in range(15):
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(name="", call_id="call_1", arguments=f'{{"text": "Content variation {i}"}}'),
|
||||
Content.from_function_call(name="", call_id="call_1", arguments=f'{{"text": "Content variation {i}"}}'),
|
||||
]
|
||||
)
|
||||
# Set the tool name to match config
|
||||
@@ -737,7 +742,7 @@ async def test_tool_result_with_empty_list():
|
||||
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_123", result=[])])
|
||||
update = AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_123", result=[])])
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
assert len(events) == 2
|
||||
@@ -760,7 +765,7 @@ async def test_tool_result_with_single_text_content():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(
|
||||
contents=[FunctionResultContent(call_id="call_123", result=[MockTextContent("Hello from MCP tool!")])]
|
||||
contents=[Content.from_function_result(call_id="call_123", result=[MockTextContent("Hello from MCP tool!")])]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
@@ -785,7 +790,7 @@ async def test_tool_result_with_multiple_text_contents():
|
||||
|
||||
update = AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id="call_123",
|
||||
result=[MockTextContent("First result"), MockTextContent("Second result")],
|
||||
)
|
||||
@@ -812,7 +817,7 @@ async def test_tool_result_with_model_dump_objects():
|
||||
bridge = AgentFrameworkEventBridge(run_id="test_run", thread_id="test_thread")
|
||||
|
||||
update = AgentResponseUpdate(
|
||||
contents=[FunctionResultContent(call_id="call_123", result=[MockModel(value=1), MockModel(value=2)])]
|
||||
contents=[Content.from_function_result(call_id="call_123", result=[MockModel(value=1), MockModel(value=2)])]
|
||||
)
|
||||
events = await bridge.from_agent_run_update(update)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
"""Tests for human in the loop (function approval requests)."""
|
||||
|
||||
from agent_framework import AgentResponseUpdate, FunctionApprovalRequestContent, FunctionCallContent
|
||||
from agent_framework import AgentResponseUpdate, Content
|
||||
|
||||
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
|
||||
|
||||
@@ -17,12 +17,12 @@ async def test_function_approval_request_emission():
|
||||
)
|
||||
|
||||
# Create approval request
|
||||
func_call = FunctionCallContent(
|
||||
func_call = Content.from_function_call(
|
||||
call_id="call_123",
|
||||
name="send_email",
|
||||
arguments={"to": "user@example.com", "subject": "Test"},
|
||||
)
|
||||
approval_request = FunctionApprovalRequestContent(
|
||||
approval_request = Content.from_function_approval_request(
|
||||
id="approval_001",
|
||||
function_call=func_call,
|
||||
)
|
||||
@@ -56,12 +56,12 @@ async def test_function_approval_request_with_confirm_changes():
|
||||
require_confirmation=True,
|
||||
)
|
||||
|
||||
func_call = FunctionCallContent(
|
||||
func_call = Content.from_function_call(
|
||||
call_id="call_456",
|
||||
name="delete_file",
|
||||
arguments={"path": "/tmp/test.txt"},
|
||||
)
|
||||
approval_request = FunctionApprovalRequestContent(
|
||||
approval_request = Content.from_function_approval_request(
|
||||
id="approval_002",
|
||||
function_call=func_call,
|
||||
)
|
||||
@@ -109,22 +109,22 @@ async def test_multiple_approval_requests():
|
||||
require_confirmation=False,
|
||||
)
|
||||
|
||||
func_call_1 = FunctionCallContent(
|
||||
func_call_1 = Content.from_function_call(
|
||||
call_id="call_1",
|
||||
name="create_event",
|
||||
arguments={"title": "Meeting"},
|
||||
)
|
||||
approval_1 = FunctionApprovalRequestContent(
|
||||
approval_1 = Content.from_function_approval_request(
|
||||
id="approval_1",
|
||||
function_call=func_call_1,
|
||||
)
|
||||
|
||||
func_call_2 = FunctionCallContent(
|
||||
func_call_2 = Content.from_function_call(
|
||||
call_id="call_2",
|
||||
name="book_room",
|
||||
arguments={"room": "Conference A"},
|
||||
)
|
||||
approval_2 = FunctionApprovalRequestContent(
|
||||
approval_2 = Content.from_function_approval_request(
|
||||
id="approval_2",
|
||||
function_call=func_call_2,
|
||||
)
|
||||
@@ -164,12 +164,12 @@ async def test_function_approval_request_sets_stop_flag():
|
||||
|
||||
assert bridge.should_stop_after_confirm is False
|
||||
|
||||
func_call = FunctionCallContent(
|
||||
func_call = Content.from_function_call(
|
||||
call_id="call_stop_test",
|
||||
name="get_datetime",
|
||||
arguments={},
|
||||
)
|
||||
approval_request = FunctionApprovalRequestContent(
|
||||
approval_request = Content.from_function_approval_request(
|
||||
id="approval_stop_test",
|
||||
function_call=func_call,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent, Role, TextContent
|
||||
from agent_framework import ChatMessage, Content, Role
|
||||
|
||||
from agent_framework_ag_ui._message_adapters import (
|
||||
agent_framework_messages_to_agui,
|
||||
@@ -24,7 +24,7 @@ def sample_agui_message():
|
||||
@pytest.fixture
|
||||
def sample_agent_framework_message():
|
||||
"""Create a sample Agent Framework message."""
|
||||
return ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")], message_id="msg-123")
|
||||
return ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")], message_id="msg-123")
|
||||
|
||||
|
||||
def test_agui_to_agent_framework_basic(sample_agui_message):
|
||||
@@ -89,7 +89,7 @@ def test_agui_tool_result_to_agent_framework():
|
||||
assert message.role == Role.USER
|
||||
|
||||
assert len(message.contents) == 1
|
||||
assert isinstance(message.contents[0], TextContent)
|
||||
assert message.contents[0].type == "text"
|
||||
assert message.contents[0].text == '{"accepted": true, "steps": []}'
|
||||
|
||||
assert message.additional_properties is not None
|
||||
@@ -141,7 +141,7 @@ def test_agui_tool_approval_updates_tool_call_arguments():
|
||||
|
||||
assert len(messages) == 2
|
||||
assistant_msg = messages[0]
|
||||
func_call = next(content for content in assistant_msg.contents if isinstance(content, FunctionCallContent))
|
||||
func_call = next(content for content in assistant_msg.contents if content.type == "function_call")
|
||||
assert func_call.arguments == {
|
||||
"steps": [
|
||||
{"description": "Boil water", "status": "enabled"},
|
||||
@@ -157,11 +157,9 @@ def test_agui_tool_approval_updates_tool_call_arguments():
|
||||
]
|
||||
}
|
||||
|
||||
from agent_framework import FunctionApprovalResponseContent
|
||||
|
||||
approval_msg = messages[1]
|
||||
approval_content = next(
|
||||
content for content in approval_msg.contents if isinstance(content, FunctionApprovalResponseContent)
|
||||
content for content in approval_msg.contents if content.type == "function_approval_response"
|
||||
)
|
||||
assert approval_content.function_call.parse_arguments() == {
|
||||
"steps": [
|
||||
@@ -211,12 +209,9 @@ def test_agui_tool_approval_from_confirm_changes_maps_to_function_call():
|
||||
]
|
||||
|
||||
messages = agui_messages_to_agent_framework(messages_input)
|
||||
|
||||
from agent_framework import FunctionApprovalResponseContent
|
||||
|
||||
approval_msg = messages[1]
|
||||
approval_content = next(
|
||||
content for content in approval_msg.contents if isinstance(content, FunctionApprovalResponseContent)
|
||||
content for content in approval_msg.contents if content.type == "function_approval_response"
|
||||
)
|
||||
|
||||
assert approval_content.function_call.call_id == "call_tool"
|
||||
@@ -259,12 +254,9 @@ def test_agui_tool_approval_from_confirm_changes_falls_back_to_sibling_call():
|
||||
]
|
||||
|
||||
messages = agui_messages_to_agent_framework(messages_input)
|
||||
|
||||
from agent_framework import FunctionApprovalResponseContent
|
||||
|
||||
approval_msg = messages[1]
|
||||
approval_content = next(
|
||||
content for content in approval_msg.contents if isinstance(content, FunctionApprovalResponseContent)
|
||||
content for content in approval_msg.contents if content.type == "function_approval_response"
|
||||
)
|
||||
|
||||
assert approval_content.function_call.call_id == "call_tool"
|
||||
@@ -315,12 +307,9 @@ def test_agui_tool_approval_from_generate_task_steps_maps_to_function_call():
|
||||
]
|
||||
|
||||
messages = agui_messages_to_agent_framework(messages_input)
|
||||
|
||||
from agent_framework import FunctionApprovalResponseContent
|
||||
|
||||
approval_msg = messages[1]
|
||||
approval_content = next(
|
||||
content for content in approval_msg.contents if isinstance(content, FunctionApprovalResponseContent)
|
||||
content for content in approval_msg.contents if content.type == "function_approval_response"
|
||||
)
|
||||
|
||||
assert approval_content.function_call.call_id == "call_tool"
|
||||
@@ -380,15 +369,14 @@ def test_agui_function_approvals():
|
||||
assert msg.role == Role.USER
|
||||
assert len(msg.contents) == 2
|
||||
|
||||
from agent_framework import FunctionApprovalResponseContent
|
||||
|
||||
assert isinstance(msg.contents[0], FunctionApprovalResponseContent)
|
||||
assert msg.contents[0].type == "function_approval_response"
|
||||
assert msg.contents[0].approved is True
|
||||
assert msg.contents[0].id == "approval-1"
|
||||
assert msg.contents[0].function_call.name == "search"
|
||||
assert msg.contents[0].function_call.call_id == "call-1"
|
||||
|
||||
assert isinstance(msg.contents[1], FunctionApprovalResponseContent)
|
||||
assert msg.contents[1].type == "function_approval_response"
|
||||
assert msg.contents[1].id == "approval-2"
|
||||
assert msg.contents[1].approved is False
|
||||
|
||||
|
||||
@@ -406,7 +394,7 @@ def test_agui_non_string_content():
|
||||
|
||||
assert len(messages) == 1
|
||||
assert len(messages[0].contents) == 1
|
||||
assert isinstance(messages[0].contents[0], TextContent)
|
||||
assert messages[0].contents[0].type == "text"
|
||||
assert "nested" in messages[0].contents[0].text
|
||||
|
||||
|
||||
@@ -440,9 +428,9 @@ def test_agui_with_tool_calls_to_agent_framework():
|
||||
assert msg.role == Role.ASSISTANT
|
||||
assert msg.message_id == "msg-789"
|
||||
# First content is text, second is the function call
|
||||
assert isinstance(msg.contents[0], TextContent)
|
||||
assert msg.contents[0].type == "text"
|
||||
assert msg.contents[0].text == "Calling tool"
|
||||
assert isinstance(msg.contents[1], FunctionCallContent)
|
||||
assert msg.contents[1].type == "function_call"
|
||||
assert msg.contents[1].call_id == "call-123"
|
||||
assert msg.contents[1].name == "get_weather"
|
||||
assert msg.contents[1].arguments == {"location": "Seattle"}
|
||||
@@ -453,8 +441,8 @@ def test_agent_framework_to_agui_with_tool_calls():
|
||||
msg = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
TextContent(text="Calling tool"),
|
||||
FunctionCallContent(call_id="call-123", name="search", arguments={"query": "test"}),
|
||||
Content.from_text(text="Calling tool"),
|
||||
Content.from_function_call(call_id="call-123", name="search", arguments={"query": "test"}),
|
||||
],
|
||||
message_id="msg-456",
|
||||
)
|
||||
@@ -477,7 +465,7 @@ def test_agent_framework_to_agui_multiple_text_contents():
|
||||
"""Test concatenating multiple text contents."""
|
||||
msg = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[TextContent(text="Part 1 "), TextContent(text="Part 2")],
|
||||
contents=[Content.from_text(text="Part 1 "), Content.from_text(text="Part 2")],
|
||||
)
|
||||
|
||||
messages = agent_framework_messages_to_agui([msg])
|
||||
@@ -488,7 +476,7 @@ def test_agent_framework_to_agui_multiple_text_contents():
|
||||
|
||||
def test_agent_framework_to_agui_no_message_id():
|
||||
"""Test message without message_id - should auto-generate ID."""
|
||||
msg = ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])
|
||||
msg = ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])
|
||||
|
||||
messages = agent_framework_messages_to_agui([msg])
|
||||
|
||||
@@ -500,7 +488,7 @@ def test_agent_framework_to_agui_no_message_id():
|
||||
|
||||
def test_agent_framework_to_agui_system_role():
|
||||
"""Test system role conversion."""
|
||||
msg = ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="System")])
|
||||
msg = ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="System")])
|
||||
|
||||
messages = agent_framework_messages_to_agui([msg])
|
||||
|
||||
@@ -510,7 +498,7 @@ def test_agent_framework_to_agui_system_role():
|
||||
|
||||
def test_extract_text_from_contents():
|
||||
"""Test extracting text from contents list."""
|
||||
contents = [TextContent(text="Hello "), TextContent(text="World")]
|
||||
contents = [Content.from_text(text="Hello "), Content.from_text(text="World")]
|
||||
|
||||
result = extract_text_from_contents(contents)
|
||||
|
||||
@@ -533,7 +521,7 @@ class CustomTextContent:
|
||||
|
||||
def test_extract_text_from_custom_contents():
|
||||
"""Test extracting text from custom content objects."""
|
||||
contents = [CustomTextContent(text="Custom "), TextContent(text="Mixed")]
|
||||
contents = [CustomTextContent(text="Custom "), Content.from_text(text="Mixed")]
|
||||
|
||||
result = extract_text_from_contents(contents)
|
||||
|
||||
@@ -547,7 +535,7 @@ def test_agent_framework_to_agui_function_result_dict():
|
||||
"""Test converting FunctionResultContent with dict result to AG-UI."""
|
||||
msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-123", result={"key": "value", "count": 42})],
|
||||
contents=[Content.from_function_result(call_id="call-123", result={"key": "value", "count": 42})],
|
||||
message_id="msg-789",
|
||||
)
|
||||
|
||||
@@ -564,7 +552,7 @@ def test_agent_framework_to_agui_function_result_none():
|
||||
"""Test converting FunctionResultContent with None result to AG-UI."""
|
||||
msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-123", result=None)],
|
||||
contents=[Content.from_function_result(call_id="call-123", result=None)],
|
||||
message_id="msg-789",
|
||||
)
|
||||
|
||||
@@ -580,7 +568,7 @@ def test_agent_framework_to_agui_function_result_string():
|
||||
"""Test converting FunctionResultContent with string result to AG-UI."""
|
||||
msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-123", result="plain text result")],
|
||||
contents=[Content.from_function_result(call_id="call-123", result="plain text result")],
|
||||
message_id="msg-789",
|
||||
)
|
||||
|
||||
@@ -595,7 +583,7 @@ def test_agent_framework_to_agui_function_result_empty_list():
|
||||
"""Test converting FunctionResultContent with empty list result to AG-UI."""
|
||||
msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-123", result=[])],
|
||||
contents=[Content.from_function_result(call_id="call-123", result=[])],
|
||||
message_id="msg-789",
|
||||
)
|
||||
|
||||
@@ -617,7 +605,7 @@ def test_agent_framework_to_agui_function_result_single_text_content():
|
||||
|
||||
msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-123", result=[MockTextContent("Hello from MCP!")])],
|
||||
contents=[Content.from_function_result(call_id="call-123", result=[MockTextContent("Hello from MCP!")])],
|
||||
message_id="msg-789",
|
||||
)
|
||||
|
||||
@@ -640,7 +628,7 @@ def test_agent_framework_to_agui_function_result_multiple_text_contents():
|
||||
msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id="call-123",
|
||||
result=[MockTextContent("First result"), MockTextContent("Second result")],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent, TextContent
|
||||
from agent_framework import ChatMessage, Content
|
||||
|
||||
from agent_framework_ag_ui._message_adapters import _deduplicate_messages, _sanitize_tool_history
|
||||
|
||||
@@ -10,7 +10,7 @@ def test_sanitize_tool_history_injects_confirm_changes_result() -> None:
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
name="confirm_changes",
|
||||
call_id="call_confirm_123",
|
||||
arguments='{"changes": "test"}',
|
||||
@@ -19,7 +19,7 @@ def test_sanitize_tool_history_injects_confirm_changes_result() -> None:
|
||||
),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[TextContent(text='{"accepted": true}')],
|
||||
contents=[Content.from_text(text='{"accepted": true}')],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -37,11 +37,11 @@ def test_deduplicate_messages_prefers_non_empty_tool_results() -> None:
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call1", result="")],
|
||||
contents=[Content.from_function_result(call_id="call1", result="")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call1", result="result data")],
|
||||
contents=[Content.from_function_result(call_id="call1", result="result data")],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from agent_framework import (
|
||||
BaseChatClient,
|
||||
ChatAgent,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionInvocationConfiguration,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
|
||||
@@ -79,11 +79,11 @@ def _create_mock_chat_agent(
|
||||
if capture_messages is not None:
|
||||
capture_messages.extend(messages)
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text="ok")],
|
||||
contents=[Content.from_text(text="ok")],
|
||||
role="assistant",
|
||||
response_id=thread.metadata.get("ag_ui_run_id"), # type: ignore[attr-defined] (metadata always created in orchestrator)
|
||||
raw_representation=ChatResponseUpdate(
|
||||
contents=[TextContent(text="ok")],
|
||||
contents=[Content.from_text(text="ok")],
|
||||
conversation_id=thread.metadata.get("ag_ui_thread_id"), # type: ignore[attr-defined] (metadata always created in orchestrator)
|
||||
response_id=thread.metadata.get("ag_ui_run_id"), # type: ignore[attr-defined] (metadata always created in orchestrator)
|
||||
),
|
||||
@@ -253,7 +253,7 @@ async def test_state_context_injected_when_tool_call_state_mismatch() -> None:
|
||||
if role_value != "system":
|
||||
continue
|
||||
for content in msg.contents or []:
|
||||
if isinstance(content, TextContent) and content.text.startswith("Current state of the application:"):
|
||||
if content.type == "text" and content.text.startswith("Current state of the application:"):
|
||||
state_messages.append(content.text)
|
||||
assert state_messages
|
||||
assert "Vegetarian" in state_messages[0]
|
||||
@@ -302,6 +302,6 @@ async def test_state_context_not_injected_when_tool_call_matches_state() -> None
|
||||
if role_value != "system":
|
||||
continue
|
||||
for content in msg.contents or []:
|
||||
if isinstance(content, TextContent) and content.text.startswith("Current state of the application:"):
|
||||
if content.type == "text" and content.text.startswith("Current state of the application:"):
|
||||
state_messages.append(content.text)
|
||||
assert not state_messages
|
||||
|
||||
@@ -8,12 +8,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework import AgentResponseUpdate, ChatMessage, Content, ai_function
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework_ag_ui._agent import AgentConfig
|
||||
@@ -48,14 +43,14 @@ async def test_human_in_the_loop_json_decode_error() -> None:
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[TextContent(text="not valid json {")],
|
||||
contents=[Content.from_text(text="not valid json {")],
|
||||
additional_properties={"is_tool_result": True},
|
||||
)
|
||||
]
|
||||
|
||||
agent = StubAgent(
|
||||
default_options={"tools": [approval_tool], "response_format": None},
|
||||
updates=[AgentResponseUpdate(contents=[TextContent(text="response")], role="assistant")],
|
||||
updates=[AgentResponseUpdate(contents=[Content.from_text(text="response")], role="assistant")],
|
||||
)
|
||||
context = TestExecutionContext(
|
||||
input_data=input_data,
|
||||
@@ -78,14 +73,14 @@ async def test_human_in_the_loop_json_decode_error() -> None:
|
||||
|
||||
async def test_sanitize_tool_history_confirm_changes() -> None:
|
||||
"""Test sanitize_tool_history logic for confirm_changes synthetic result."""
|
||||
from agent_framework import ChatMessage, FunctionCallContent, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
# Create messages that will trigger confirm_changes synthetic result injection
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
name="confirm_changes",
|
||||
call_id="call_confirm_123",
|
||||
arguments='{"changes": "test"}',
|
||||
@@ -94,7 +89,7 @@ async def test_sanitize_tool_history_confirm_changes() -> None:
|
||||
),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[TextContent(text='{"accepted": true}')],
|
||||
contents=[Content.from_text(text='{"accepted": true}')],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -134,17 +129,17 @@ async def test_sanitize_tool_history_confirm_changes() -> None:
|
||||
|
||||
async def test_sanitize_tool_history_orphaned_tool_result() -> None:
|
||||
"""Test sanitize_tool_history removes orphaned tool results."""
|
||||
from agent_framework import ChatMessage, FunctionResultContent, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
# Tool result without preceding assistant tool call
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="orphan_123", result="orphaned data")],
|
||||
contents=[Content.from_function_result(call_id="orphan_123", result="orphaned data")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[TextContent(text="Hello")],
|
||||
contents=[Content.from_text(text="Hello")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -214,20 +209,20 @@ async def test_orphaned_tool_result_sanitization() -> None:
|
||||
|
||||
async def test_deduplicate_messages_empty_tool_results() -> None:
|
||||
"""Test deduplicate_messages prefers non-empty tool results."""
|
||||
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(name="test_tool", call_id="call_789", arguments="{}")],
|
||||
contents=[Content.from_function_call(name="test_tool", call_id="call_789", arguments="{}")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call_789", result="")],
|
||||
contents=[Content.from_function_result(call_id="call_789", result="")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call_789", result="real data")],
|
||||
contents=[Content.from_function_result(call_id="call_789", result="real data")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -259,20 +254,20 @@ async def test_deduplicate_messages_empty_tool_results() -> None:
|
||||
|
||||
async def test_deduplicate_messages_duplicate_assistant_tool_calls() -> None:
|
||||
"""Test deduplicate_messages removes duplicate assistant tool call messages."""
|
||||
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(name="test_tool", call_id="call_abc", arguments="{}")],
|
||||
contents=[Content.from_function_call(name="test_tool", call_id="call_abc", arguments="{}")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(name="test_tool", call_id="call_abc", arguments="{}")],
|
||||
contents=[Content.from_function_call(name="test_tool", call_id="call_abc", arguments="{}")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call_abc", result="result")],
|
||||
contents=[Content.from_function_result(call_id="call_abc", result="result")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -303,20 +298,20 @@ async def test_deduplicate_messages_duplicate_assistant_tool_calls() -> None:
|
||||
|
||||
async def test_deduplicate_messages_duplicate_system_messages() -> None:
|
||||
"""Test that deduplication logic is invoked for system messages."""
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="system",
|
||||
contents=[TextContent(text="You are a helpful assistant.")],
|
||||
contents=[Content.from_text(text="You are a helpful assistant.")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="system",
|
||||
contents=[TextContent(text="You are a helpful assistant.")],
|
||||
contents=[Content.from_text(text="You are a helpful assistant.")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[TextContent(text="Hello")],
|
||||
contents=[Content.from_text(text="Hello")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -387,20 +382,20 @@ async def test_state_context_injection() -> None:
|
||||
|
||||
async def test_state_context_injection_with_tool_calls_and_input_state() -> None:
|
||||
"""Test state context is injected when state is provided, even with tool calls."""
|
||||
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(name="get_weather", call_id="call_xyz", arguments="{}")],
|
||||
contents=[Content.from_function_call(name="get_weather", call_id="call_xyz", arguments="{}")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call_xyz", result="sunny")],
|
||||
contents=[Content.from_function_result(call_id="call_xyz", result="sunny")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[TextContent(text="Thanks")],
|
||||
contents=[Content.from_text(text="Thanks")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -452,7 +447,7 @@ async def test_structured_output_processing() -> None:
|
||||
default_options=DEFAULT_OPTIONS,
|
||||
updates=[
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text='{"ingredients": ["tomato"], "message": "Added tomato"}')],
|
||||
contents=[Content.from_text(text='{"ingredients": ["tomato"], "message": "Added tomato"}')],
|
||||
role="assistant",
|
||||
)
|
||||
],
|
||||
@@ -641,13 +636,13 @@ async def test_all_messages_filtered_handling() -> None:
|
||||
|
||||
async def test_confirm_changes_with_invalid_json_fallback() -> None:
|
||||
"""Test confirm_changes with invalid JSON falls back to normal processing."""
|
||||
from agent_framework import ChatMessage, FunctionCallContent, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
name="confirm_changes",
|
||||
call_id="call_confirm_invalid",
|
||||
arguments='{"changes": "test"}',
|
||||
@@ -656,7 +651,7 @@ async def test_confirm_changes_with_invalid_json_fallback() -> None:
|
||||
),
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[TextContent(text="invalid json {")],
|
||||
contents=[Content.from_text(text="invalid json {")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -688,19 +683,18 @@ async def test_confirm_changes_with_invalid_json_fallback() -> None:
|
||||
async def test_confirm_changes_closes_active_message_before_finish() -> None:
|
||||
"""Confirm-changes flow closes any active text message before run finishes."""
|
||||
from ag_ui.core import TextMessageEndEvent, TextMessageStartEvent
|
||||
from agent_framework import FunctionCallContent, FunctionResultContent
|
||||
|
||||
updates = [
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
name="write_document_local",
|
||||
call_id="call_1",
|
||||
arguments='{"document": "Draft"}',
|
||||
)
|
||||
]
|
||||
),
|
||||
AgentResponseUpdate(contents=[FunctionResultContent(call_id="call_1", result="Done")]),
|
||||
AgentResponseUpdate(contents=[Content.from_function_result(call_id="call_1", result="Done")]),
|
||||
]
|
||||
|
||||
orchestrator = DefaultOrchestrator()
|
||||
@@ -735,16 +729,16 @@ async def test_confirm_changes_closes_active_message_before_finish() -> None:
|
||||
|
||||
async def test_tool_result_kept_when_call_id_matches() -> None:
|
||||
"""Test tool result is kept when call_id matches pending tool calls."""
|
||||
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(name="get_data", call_id="call_match", arguments="{}")],
|
||||
contents=[Content.from_function_call(name="get_data", call_id="call_match", arguments="{}")],
|
||||
),
|
||||
ChatMessage(
|
||||
role="tool",
|
||||
contents=[FunctionResultContent(call_id="call_match", result="data")],
|
||||
contents=[Content.from_function_result(call_id="call_match", result="data")],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -794,11 +788,11 @@ async def test_agent_protocol_fallback_paths() -> None:
|
||||
**kwargs: Any,
|
||||
) -> AsyncGenerator[AgentResponseUpdate, None]:
|
||||
self.messages_received = messages
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="response")], role="assistant")
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="response")], role="assistant")
|
||||
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [ChatMessage(role="user", contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])]
|
||||
|
||||
orchestrator = DefaultOrchestrator()
|
||||
input_data: dict[str, Any] = {"messages": []}
|
||||
@@ -820,9 +814,9 @@ async def test_agent_protocol_fallback_paths() -> None:
|
||||
|
||||
async def test_initial_state_snapshot_with_array_schema() -> None:
|
||||
"""Test state initialization with array type schema."""
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [ChatMessage(role="user", contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])]
|
||||
|
||||
orchestrator = DefaultOrchestrator()
|
||||
input_data: dict[str, Any] = {"messages": [], "state": {}}
|
||||
@@ -851,9 +845,9 @@ async def test_response_format_skip_text_content() -> None:
|
||||
class OutputModel(BaseModel):
|
||||
result: str
|
||||
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
messages = [ChatMessage(role="user", contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])]
|
||||
|
||||
orchestrator = DefaultOrchestrator()
|
||||
input_data: dict[str, Any] = {"messages": []}
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ag_ui.core import RunFinishedEvent, RunStartedEvent
|
||||
from agent_framework import TextContent
|
||||
from agent_framework import Content
|
||||
from agent_framework._types import AgentResponseUpdate, ChatResponseUpdate
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
@@ -20,10 +20,10 @@ async def test_service_thread_id_when_there_are_updates():
|
||||
|
||||
updates: list[AgentResponseUpdate] = [
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="Hello, user!")],
|
||||
contents=[Content.from_text(text="Hello, user!")],
|
||||
response_id="resp_67890",
|
||||
raw_representation=ChatResponseUpdate(
|
||||
contents=[TextContent(text="Hello, user!")],
|
||||
contents=[Content.from_text(text="Hello, user!")],
|
||||
conversation_id="conv_12345",
|
||||
response_id="resp_67890",
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
from ag_ui.core import StateSnapshotEvent
|
||||
from agent_framework import ChatAgent, ChatResponseUpdate, TextContent
|
||||
from agent_framework import ChatAgent, ChatResponseUpdate, Content
|
||||
|
||||
from agent_framework_ag_ui._agent import AgentFrameworkAgent
|
||||
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
|
||||
@@ -20,7 +20,7 @@ from utils_test_ag_ui import StreamingChatClientStub, stream_from_updates
|
||||
@pytest.fixture
|
||||
def mock_agent() -> ChatAgent:
|
||||
"""Create a mock agent for testing."""
|
||||
updates = [ChatResponseUpdate(contents=[TextContent(text="Hello!")])]
|
||||
updates = [ChatResponseUpdate(contents=[Content.from_text(text="Hello!")])]
|
||||
chat_client = StreamingChatClientStub(stream_from_updates(updates))
|
||||
return ChatAgent(name="test_agent", instructions="Test agent", chat_client=chat_client)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from ag_ui.core import CustomEvent, EventType
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
|
||||
from agent_framework_ag_ui._orchestration._state_manager import StateManager
|
||||
@@ -47,5 +47,5 @@ def test_state_context_only_when_new_user_turn() -> None:
|
||||
|
||||
message = state_manager.state_context_message(is_new_user_turn=True, conversation_has_tool_calls=False)
|
||||
assert isinstance(message, ChatMessage)
|
||||
assert isinstance(message.contents[0], TextContent)
|
||||
assert message.contents[0].type == "text"
|
||||
assert "Current state of the application" in message.contents[0].text
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections.abc import AsyncIterator, MutableSequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, TextContent
|
||||
from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, Content
|
||||
from pydantic import BaseModel
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
@@ -43,7 +43,7 @@ async def test_structured_output_with_recipe():
|
||||
messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[TextContent(text='{"recipe": {"name": "Pasta"}, "message": "Here is your recipe"}')]
|
||||
contents=[Content.from_text(text='{"recipe": {"name": "Pasta"}, "message": "Here is your recipe"}')]
|
||||
)
|
||||
|
||||
agent = ChatAgent(name="test", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
@@ -86,7 +86,7 @@ async def test_structured_output_with_steps():
|
||||
{"id": "2", "description": "Step 2", "status": "pending"},
|
||||
]
|
||||
}
|
||||
yield ChatResponseUpdate(contents=[TextContent(text=json.dumps(steps_data))])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text=json.dumps(steps_data))])
|
||||
|
||||
agent = ChatAgent(name="test", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
agent.default_options = ChatOptions(response_format=StepsOutput)
|
||||
@@ -118,7 +118,7 @@ async def test_structured_output_with_no_schema_match():
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
updates = [
|
||||
ChatResponseUpdate(contents=[TextContent(text='{"data": {"key": "value"}}')]),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text='{"data": {"key": "value"}}')]),
|
||||
]
|
||||
|
||||
agent = ChatAgent(
|
||||
@@ -156,7 +156,7 @@ async def test_structured_output_without_schema():
|
||||
async def stream_fn(
|
||||
messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text='{"data": {"key": "value"}, "info": "processed"}')])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text='{"data": {"key": "value"}, "info": "processed"}')])
|
||||
|
||||
agent = ChatAgent(name="test", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
agent.default_options = ChatOptions(response_format=DataOutput)
|
||||
@@ -185,7 +185,7 @@ async def test_no_structured_output_when_no_response_format():
|
||||
"""Test that structured output path is skipped when no response_format."""
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
updates = [ChatResponseUpdate(contents=[TextContent(text="Regular text")])]
|
||||
updates = [ChatResponseUpdate(contents=[Content.from_text(text="Regular text")])]
|
||||
|
||||
agent = ChatAgent(
|
||||
name="test",
|
||||
@@ -216,7 +216,7 @@ async def test_structured_output_with_message_field():
|
||||
messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any
|
||||
) -> AsyncIterator[ChatResponseUpdate]:
|
||||
output_data = {"recipe": {"name": "Salad"}, "message": "Fresh salad recipe ready"}
|
||||
yield ChatResponseUpdate(contents=[TextContent(text=json.dumps(output_data))])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text=json.dumps(output_data))])
|
||||
|
||||
agent = ChatAgent(name="test", instructions="Test", chat_client=StreamingChatClientStub(stream_fn))
|
||||
agent.default_options = ChatOptions(response_format=RecipeOutput)
|
||||
|
||||
@@ -16,7 +16,7 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
TextContent,
|
||||
Content,
|
||||
)
|
||||
from agent_framework._clients import TOptions_co
|
||||
|
||||
@@ -91,7 +91,7 @@ class StubAgent(AgentProtocol):
|
||||
self.id = agent_id
|
||||
self.name = agent_name
|
||||
self.description = "stub agent"
|
||||
self.updates = updates or [AgentResponseUpdate(contents=[TextContent(text="response")], role="assistant")]
|
||||
self.updates = updates or [AgentResponseUpdate(contents=[Content.from_text(text="response")], role="assistant")]
|
||||
self.default_options: dict[str, Any] = (
|
||||
default_options if isinstance(default_options, dict) else {"tools": None, "response_format": None}
|
||||
)
|
||||
|
||||
@@ -7,31 +7,19 @@ from typing import Any, ClassVar, Final, Generic, Literal, TypedDict
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
Annotations,
|
||||
Annotation,
|
||||
BaseChatClient,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CitationAnnotation,
|
||||
CodeInterpreterToolCallContent,
|
||||
CodeInterpreterToolResultContent,
|
||||
Contents,
|
||||
ErrorContent,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
MCPServerToolCallContent,
|
||||
MCPServerToolResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
TextSpanRegion,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
get_logger,
|
||||
prepare_function_call_results,
|
||||
@@ -486,7 +474,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
a_content.append({
|
||||
"type": "image",
|
||||
"source": {
|
||||
"data": content.get_data_bytes_as_str(),
|
||||
"data": content.get_data_bytes_as_str(), # type: ignore[attr-defined]
|
||||
"media_type": content.media_type,
|
||||
"type": "base64",
|
||||
},
|
||||
@@ -653,9 +641,9 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
"""
|
||||
match event.type:
|
||||
case "message_start":
|
||||
usage_details: list[UsageContent] = []
|
||||
usage_details: list[Content] = []
|
||||
if event.message.usage and (details := self._parse_usage_from_anthropic(event.message.usage)):
|
||||
usage_details.append(UsageContent(details=details))
|
||||
usage_details.append(Content.from_usage(usage_details=details))
|
||||
|
||||
return ChatResponseUpdate(
|
||||
response_id=event.message.id,
|
||||
@@ -672,7 +660,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
case "message_delta":
|
||||
usage = self._parse_usage_from_anthropic(event.usage)
|
||||
return ChatResponseUpdate(
|
||||
contents=[UsageContent(details=usage, raw_representation=event.usage)] if usage else [],
|
||||
contents=[Content.from_usage(usage_details=usage, raw_representation=event.usage)] if usage else [],
|
||||
finish_reason=FINISH_REASON_MAP.get(event.delta.stop_reason) if event.delta.stop_reason else None,
|
||||
raw_representation=event,
|
||||
)
|
||||
@@ -702,24 +690,24 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
return None
|
||||
usage_details = UsageDetails(output_token_count=usage.output_tokens)
|
||||
if usage.input_tokens is not None:
|
||||
usage_details.input_token_count = usage.input_tokens
|
||||
usage_details["input_token_count"] = usage.input_tokens
|
||||
if usage.cache_creation_input_tokens is not None:
|
||||
usage_details.additional_counts["anthropic.cache_creation_input_tokens"] = usage.cache_creation_input_tokens
|
||||
usage_details["anthropic.cache_creation_input_tokens"] = usage.cache_creation_input_tokens # type: ignore[typeddict-unknown-key]
|
||||
if usage.cache_read_input_tokens is not None:
|
||||
usage_details.additional_counts["anthropic.cache_read_input_tokens"] = usage.cache_read_input_tokens
|
||||
usage_details["anthropic.cache_read_input_tokens"] = usage.cache_read_input_tokens # type: ignore[typeddict-unknown-key]
|
||||
return usage_details
|
||||
|
||||
def _parse_contents_from_anthropic(
|
||||
self,
|
||||
content: Sequence[BetaContentBlock | BetaRawContentBlockDelta | BetaTextBlock],
|
||||
) -> list[Contents]:
|
||||
) -> list[Content]:
|
||||
"""Parse contents from the Anthropic message."""
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
for content_block in content:
|
||||
match content_block.type:
|
||||
case "text" | "text_delta":
|
||||
contents.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=content_block.text,
|
||||
raw_representation=content_block,
|
||||
annotations=self._parse_citations_from_anthropic(content_block),
|
||||
@@ -729,7 +717,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
self._last_call_id_name = (content_block.id, content_block.name)
|
||||
if content_block.type == "mcp_tool_use":
|
||||
contents.append(
|
||||
MCPServerToolCallContent(
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id=content_block.id,
|
||||
tool_name=content_block.name,
|
||||
server_name=None,
|
||||
@@ -739,10 +727,10 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
)
|
||||
elif "code_execution" in (content_block.name or ""):
|
||||
contents.append(
|
||||
CodeInterpreterToolCallContent(
|
||||
Content.from_code_interpreter_tool_call(
|
||||
call_id=content_block.id,
|
||||
inputs=[
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=str(content_block.input),
|
||||
raw_representation=content_block,
|
||||
)
|
||||
@@ -752,7 +740,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
)
|
||||
else:
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=content_block.id,
|
||||
name=content_block.name,
|
||||
arguments=content_block.input,
|
||||
@@ -760,14 +748,14 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
)
|
||||
)
|
||||
case "mcp_tool_result":
|
||||
call_id, name = self._last_call_id_name or (None, None)
|
||||
parsed_output: list[Contents] | None = None
|
||||
call_id, _ = self._last_call_id_name or (None, None)
|
||||
parsed_output: list[Content] | None = None
|
||||
if content_block.content:
|
||||
if isinstance(content_block.content, list):
|
||||
parsed_output = self._parse_contents_from_anthropic(content_block.content)
|
||||
elif isinstance(content_block.content, (str, bytes)):
|
||||
parsed_output = [
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=str(content_block.content),
|
||||
raw_representation=content_block,
|
||||
)
|
||||
@@ -775,28 +763,27 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
else:
|
||||
parsed_output = self._parse_contents_from_anthropic([content_block.content])
|
||||
contents.append(
|
||||
MCPServerToolResultContent(
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id=content_block.tool_use_id,
|
||||
output=parsed_output,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
case "web_search_tool_result" | "web_fetch_tool_result":
|
||||
call_id, name = self._last_call_id_name or (None, None)
|
||||
call_id, _ = self._last_call_id_name or (None, None)
|
||||
contents.append(
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=content_block.tool_use_id,
|
||||
name=name if name and call_id == content_block.tool_use_id else "web_tool",
|
||||
result=content_block.content,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
case "code_execution_tool_result":
|
||||
code_outputs: list[Contents] = []
|
||||
code_outputs: list[Content] = []
|
||||
if content_block.content:
|
||||
if isinstance(content_block.content, BetaCodeExecutionToolResultError):
|
||||
code_outputs.append(
|
||||
ErrorContent(
|
||||
Content.from_error(
|
||||
message=content_block.content.error_code,
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
@@ -804,41 +791,41 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
else:
|
||||
if content_block.content.stdout:
|
||||
code_outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=content_block.content.stdout,
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
)
|
||||
if content_block.content.stderr:
|
||||
code_outputs.append(
|
||||
ErrorContent(
|
||||
Content.from_error(
|
||||
message=content_block.content.stderr,
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
)
|
||||
for code_file_content in content_block.content.content:
|
||||
code_outputs.append(
|
||||
HostedFileContent(
|
||||
Content.from_hosted_file(
|
||||
file_id=code_file_content.file_id,
|
||||
raw_representation=code_file_content,
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
CodeInterpreterToolResultContent(
|
||||
Content.from_code_interpreter_tool_result(
|
||||
call_id=content_block.tool_use_id,
|
||||
raw_representation=content_block,
|
||||
outputs=code_outputs,
|
||||
)
|
||||
)
|
||||
case "bash_code_execution_tool_result":
|
||||
bash_outputs: list[Contents] = []
|
||||
bash_outputs: list[Content] = []
|
||||
if content_block.content:
|
||||
if isinstance(
|
||||
content_block.content,
|
||||
BetaBashCodeExecutionToolResultError,
|
||||
):
|
||||
bash_outputs.append(
|
||||
ErrorContent(
|
||||
Content.from_error(
|
||||
message=content_block.content.error_code,
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
@@ -846,39 +833,38 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
else:
|
||||
if content_block.content.stdout:
|
||||
bash_outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=content_block.content.stdout,
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
)
|
||||
if content_block.content.stderr:
|
||||
bash_outputs.append(
|
||||
ErrorContent(
|
||||
Content.from_error(
|
||||
message=content_block.content.stderr,
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
)
|
||||
for bash_file_content in content_block.content.content:
|
||||
contents.append(
|
||||
HostedFileContent(
|
||||
Content.from_hosted_file(
|
||||
file_id=bash_file_content.file_id,
|
||||
raw_representation=bash_file_content,
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=content_block.tool_use_id,
|
||||
name=content_block.type,
|
||||
result=bash_outputs,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
case "text_editor_code_execution_tool_result":
|
||||
text_editor_outputs: list[Contents] = []
|
||||
text_editor_outputs: list[Content] = []
|
||||
match content_block.content.type:
|
||||
case "text_editor_code_execution_tool_result_error":
|
||||
text_editor_outputs.append(
|
||||
ErrorContent(
|
||||
Content.from_error(
|
||||
message=content_block.content.error_code
|
||||
and getattr(content_block.content, "error_message", ""),
|
||||
raw_representation=content_block.content,
|
||||
@@ -887,10 +873,12 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
case "text_editor_code_execution_view_result":
|
||||
annotations = (
|
||||
[
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
raw_representation=content_block.content,
|
||||
annotated_regions=[
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=content_block.content.start_line,
|
||||
end_index=content_block.content.start_line
|
||||
+ (content_block.content.num_lines or 0),
|
||||
@@ -903,7 +891,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
else None
|
||||
)
|
||||
text_editor_outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=content_block.content.content,
|
||||
annotations=annotations,
|
||||
raw_representation=content_block.content,
|
||||
@@ -911,10 +899,12 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
)
|
||||
case "text_editor_code_execution_str_replace_result":
|
||||
old_annotation = (
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
raw_representation=content_block.content,
|
||||
annotated_regions=[
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=content_block.content.old_start or 0,
|
||||
end_index=(
|
||||
(content_block.content.old_start or 0)
|
||||
@@ -928,13 +918,15 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
else None
|
||||
)
|
||||
new_annotation = (
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
raw_representation=content_block.content,
|
||||
snippet="\n".join(content_block.content.lines)
|
||||
snippet="\n".join(content_block.content.lines) # type: ignore[typeddict-item]
|
||||
if content_block.content.lines
|
||||
else None,
|
||||
annotated_regions=[
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=content_block.content.new_start or 0,
|
||||
end_index=(
|
||||
(content_block.content.new_start or 0)
|
||||
@@ -950,7 +942,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
annotations = [ann for ann in [old_annotation, new_annotation] if ann is not None]
|
||||
|
||||
text_editor_outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=(
|
||||
"\n".join(content_block.content.lines) if content_block.content.lines else ""
|
||||
),
|
||||
@@ -960,15 +952,14 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
)
|
||||
case "text_editor_code_execution_create_result":
|
||||
text_editor_outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=f"File update: {content_block.content.is_file_update}",
|
||||
raw_representation=content_block.content,
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=content_block.tool_use_id,
|
||||
name=content_block.type,
|
||||
result=text_editor_outputs,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
@@ -981,7 +972,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
# This matches OpenAI's behavior where streaming chunks have name="".
|
||||
call_id, _ = self._last_call_id_name if self._last_call_id_name else ("", "")
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name="",
|
||||
arguments=content_block.partial_json,
|
||||
@@ -990,7 +981,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
)
|
||||
case "thinking" | "thinking_delta":
|
||||
contents.append(
|
||||
TextReasoningContent(
|
||||
Content.from_text_reasoning(
|
||||
text=content_block.thinking,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
@@ -1001,65 +992,65 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
|
||||
def _parse_citations_from_anthropic(
|
||||
self, content_block: BetaContentBlock | BetaRawContentBlockDelta | BetaTextBlock
|
||||
) -> list[Annotations] | None:
|
||||
content_citations = getattr(content_block, "citations", None)
|
||||
if not content_citations:
|
||||
) -> list[Annotation] | None:
|
||||
content_blocks = getattr(content_block, "citations", None)
|
||||
if not content_blocks:
|
||||
return None
|
||||
annotations: list[Annotations] = []
|
||||
for citation in content_citations:
|
||||
cit = CitationAnnotation(raw_representation=citation)
|
||||
annotations: list[Annotation] = []
|
||||
for citation in content_blocks:
|
||||
cit = Annotation(type="citation", raw_representation=citation)
|
||||
match citation.type:
|
||||
case "char_location":
|
||||
cit.title = citation.title
|
||||
cit.snippet = citation.cited_text
|
||||
cit["title"] = citation.title
|
||||
cit["snippet"] = citation.cited_text
|
||||
if citation.file_id:
|
||||
cit.file_id = citation.file_id
|
||||
if not cit.annotated_regions:
|
||||
cit.annotated_regions = []
|
||||
cit.annotated_regions.append(
|
||||
cit["file_id"] = citation.file_id
|
||||
cit.setdefault("annotated_regions", [])
|
||||
cit["annotated_regions"].append( # type: ignore[attr-defined]
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=citation.start_char_index,
|
||||
end_index=citation.end_char_index,
|
||||
)
|
||||
)
|
||||
case "page_location":
|
||||
cit.title = citation.document_title
|
||||
cit.snippet = citation.cited_text
|
||||
cit["title"] = citation.document_title
|
||||
cit["snippet"] = citation.cited_text
|
||||
if citation.file_id:
|
||||
cit.file_id = citation.file_id
|
||||
if not cit.annotated_regions:
|
||||
cit.annotated_regions = []
|
||||
cit.annotated_regions.append(
|
||||
cit["file_id"] = citation.file_id
|
||||
cit.setdefault("annotated_regions", [])
|
||||
cit["annotated_regions"].append( # type: ignore[attr-defined]
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=citation.start_page_number,
|
||||
end_index=citation.end_page_number,
|
||||
)
|
||||
)
|
||||
case "content_block_location":
|
||||
cit.title = citation.document_title
|
||||
cit.snippet = citation.cited_text
|
||||
cit["title"] = citation.document_title
|
||||
cit["snippet"] = citation.cited_text
|
||||
if citation.file_id:
|
||||
cit.file_id = citation.file_id
|
||||
if not cit.annotated_regions:
|
||||
cit.annotated_regions = []
|
||||
cit.annotated_regions.append(
|
||||
cit["file_id"] = citation.file_id
|
||||
cit.setdefault("annotated_regions", [])
|
||||
cit["annotated_regions"].append( # type: ignore[attr-defined]
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=citation.start_block_index,
|
||||
end_index=citation.end_block_index,
|
||||
)
|
||||
)
|
||||
case "web_search_result_location":
|
||||
cit.title = citation.title
|
||||
cit.snippet = citation.cited_text
|
||||
cit.url = citation.url
|
||||
cit["title"] = citation.title
|
||||
cit["snippet"] = citation.cited_text
|
||||
cit["url"] = citation.url
|
||||
case "search_result_location":
|
||||
cit.title = citation.title
|
||||
cit.snippet = citation.cited_text
|
||||
cit.url = citation.source
|
||||
if not cit.annotated_regions:
|
||||
cit.annotated_regions = []
|
||||
cit.annotated_regions.append(
|
||||
cit["title"] = citation.title
|
||||
cit["snippet"] = citation.cited_text
|
||||
cit["url"] = citation.source
|
||||
cit.setdefault("annotated_regions", [])
|
||||
cit["annotated_regions"].append( # type: ignore[attr-defined]
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=citation.start_block_index,
|
||||
end_index=citation.end_block_index,
|
||||
)
|
||||
|
||||
@@ -10,16 +10,12 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponseUpdate,
|
||||
DataContent,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -170,7 +166,7 @@ def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: Magi
|
||||
message = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_123",
|
||||
name="get_weather",
|
||||
arguments={"location": "San Francisco"},
|
||||
@@ -194,9 +190,8 @@ def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: Ma
|
||||
message = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id="call_123",
|
||||
name="get_weather",
|
||||
result="Sunny, 72°F",
|
||||
)
|
||||
],
|
||||
@@ -219,7 +214,7 @@ def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: Mag
|
||||
chat_client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[TextReasoningContent(text="Let me think about this...")],
|
||||
contents=[Content.from_text_reasoning(text="Let me think about this...")],
|
||||
)
|
||||
|
||||
result = chat_client._prepare_message_for_anthropic(message)
|
||||
@@ -507,12 +502,12 @@ def test_process_message_basic(mock_anthropic_client: MagicMock) -> None:
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
assert len(response.messages[0].contents) == 1
|
||||
assert isinstance(response.messages[0].contents[0], TextContent)
|
||||
assert response.messages[0].contents[0].type == "text"
|
||||
assert response.messages[0].contents[0].text == "Hello there!"
|
||||
assert response.finish_reason == FinishReason.STOP
|
||||
assert response.usage_details is not None
|
||||
assert response.usage_details.input_token_count == 10
|
||||
assert response.usage_details.output_token_count == 5
|
||||
assert response.usage_details["input_token_count"] == 10
|
||||
assert response.usage_details["output_token_count"] == 5
|
||||
|
||||
|
||||
def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -536,7 +531,7 @@ def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None
|
||||
response = chat_client._process_message(mock_message)
|
||||
|
||||
assert len(response.messages[0].contents) == 1
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
assert response.messages[0].contents[0].type == "function_call"
|
||||
assert response.messages[0].contents[0].call_id == "call_123"
|
||||
assert response.messages[0].contents[0].name == "get_weather"
|
||||
assert response.finish_reason == FinishReason.TOOL_CALLS
|
||||
@@ -550,8 +545,8 @@ def test_parse_usage_from_anthropic_basic(mock_anthropic_client: MagicMock) -> N
|
||||
result = chat_client._parse_usage_from_anthropic(usage)
|
||||
|
||||
assert result is not None
|
||||
assert result.input_token_count == 10
|
||||
assert result.output_token_count == 5
|
||||
assert result["input_token_count"] == 10
|
||||
assert result["output_token_count"] == 5
|
||||
|
||||
|
||||
def test_parse_usage_from_anthropic_none(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -571,7 +566,7 @@ def test_parse_contents_from_anthropic_text(mock_anthropic_client: MagicMock) ->
|
||||
result = chat_client._parse_contents_from_anthropic(content)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].type == "text"
|
||||
assert result[0].text == "Hello!"
|
||||
|
||||
|
||||
@@ -590,7 +585,7 @@ def test_parse_contents_from_anthropic_tool_use(mock_anthropic_client: MagicMock
|
||||
result = chat_client._parse_contents_from_anthropic(content)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionCallContent)
|
||||
assert result[0].type == "function_call"
|
||||
assert result[0].call_id == "call_123"
|
||||
assert result[0].name == "get_weather"
|
||||
|
||||
@@ -613,7 +608,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a
|
||||
|
||||
result = chat_client._parse_contents_from_anthropic([tool_use_content])
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionCallContent)
|
||||
assert result[0].type == "function_call"
|
||||
assert result[0].call_id == "call_123"
|
||||
assert result[0].name == "get_weather" # Initial event has name
|
||||
|
||||
@@ -624,7 +619,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a
|
||||
|
||||
result = chat_client._parse_contents_from_anthropic([delta_content_1])
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionCallContent)
|
||||
assert result[0].type == "function_call"
|
||||
assert result[0].call_id == "call_123"
|
||||
assert result[0].name == "" # Delta events should have empty name
|
||||
assert result[0].arguments == '{"location":'
|
||||
@@ -636,7 +631,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a
|
||||
|
||||
result = chat_client._parse_contents_from_anthropic([delta_content_2])
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionCallContent)
|
||||
assert result[0].type == "function_call"
|
||||
assert result[0].call_id == "call_123"
|
||||
assert result[0].name == "" # Still empty name for subsequent deltas
|
||||
assert result[0].arguments == '"San Francisco"}'
|
||||
@@ -771,9 +766,7 @@ async def test_anthropic_client_integration_function_calling() -> None:
|
||||
|
||||
assert response is not None
|
||||
# Should contain function call
|
||||
has_function_call = any(
|
||||
isinstance(content, FunctionCallContent) for msg in response.messages for content in msg.contents
|
||||
)
|
||||
has_function_call = any(content.type == "function_call" for msg in response.messages for content in msg.contents)
|
||||
assert has_function_call
|
||||
|
||||
|
||||
@@ -872,8 +865,8 @@ async def test_anthropic_client_integration_images() -> None:
|
||||
ChatMessage(
|
||||
role=Role.USER,
|
||||
contents=[
|
||||
TextContent(text="Describe this image"),
|
||||
DataContent(media_type="image/jpeg", data=image_bytes),
|
||||
Content.from_text(text="Describe this image"),
|
||||
Content.from_data(media_type="image/jpeg", data=image_bytes),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
@@ -9,6 +10,8 @@ from typing import Any, ClassVar, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
Annotation,
|
||||
BaseChatClient,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
@@ -16,23 +19,16 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CitationAnnotation,
|
||||
Contents,
|
||||
Content,
|
||||
ContextProvider,
|
||||
DataContent,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedFileContent,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Middleware,
|
||||
Role,
|
||||
TextContent,
|
||||
TextSpanRegion,
|
||||
ToolProtocol,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
get_logger,
|
||||
prepare_function_call_results,
|
||||
@@ -50,9 +46,14 @@ from azure.ai.agents.models import (
|
||||
AgentStreamEvent,
|
||||
AsyncAgentEventHandler,
|
||||
AsyncAgentRunStream,
|
||||
BingCustomSearchTool,
|
||||
BingGroundingTool,
|
||||
CodeInterpreterToolDefinition,
|
||||
FileSearchTool,
|
||||
FunctionName,
|
||||
FunctionToolDefinition,
|
||||
ListSortOrder,
|
||||
McpTool,
|
||||
MessageDeltaChunk,
|
||||
MessageDeltaTextContent,
|
||||
MessageDeltaTextFileCitationAnnotation,
|
||||
@@ -422,7 +423,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
self,
|
||||
agent_id: str,
|
||||
run_options: dict[str, Any],
|
||||
required_action_results: list[FunctionResultContent | FunctionApprovalResponseContent] | None,
|
||||
required_action_results: list[Content] | None,
|
||||
) -> tuple[AsyncAgentRunStream[AsyncAgentEventHandler[Any]] | AsyncAgentEventHandler[Any], str]:
|
||||
"""Create the agent stream for processing.
|
||||
|
||||
@@ -506,9 +507,9 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
|
||||
def _extract_url_citations(
|
||||
self, message_delta_chunk: MessageDeltaChunk, azure_search_tool_calls: list[dict[str, Any]]
|
||||
) -> list[CitationAnnotation]:
|
||||
) -> list[Annotation]:
|
||||
"""Extract URL citations from MessageDeltaChunk."""
|
||||
url_citations: list[CitationAnnotation] = []
|
||||
url_citations: list[Annotation] = []
|
||||
|
||||
# Process each content item in the delta to find citations
|
||||
for content in message_delta_chunk.delta.content:
|
||||
@@ -520,6 +521,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
if annotation.start_index and annotation.end_index:
|
||||
annotated_regions = [
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=annotation.start_index,
|
||||
end_index=annotation.end_index,
|
||||
)
|
||||
@@ -530,11 +532,12 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
annotation.url_citation.url, azure_search_tool_calls
|
||||
)
|
||||
|
||||
# Create CitationAnnotation with real URL
|
||||
citation = CitationAnnotation(
|
||||
title=getattr(annotation.url_citation, "title", None),
|
||||
# Create Annotation with real URL
|
||||
citation = Annotation(
|
||||
type="citation",
|
||||
title=annotation.url_citation.title, # type: ignore[typeddict-item]
|
||||
url=real_url,
|
||||
snippet=None,
|
||||
snippet=None, # type: ignore[typeddict-item]
|
||||
annotated_regions=annotated_regions,
|
||||
raw_representation=annotation,
|
||||
)
|
||||
@@ -542,7 +545,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
|
||||
return url_citations
|
||||
|
||||
def _extract_file_path_contents(self, message_delta_chunk: MessageDeltaChunk) -> list[HostedFileContent]:
|
||||
def _extract_file_path_contents(self, message_delta_chunk: MessageDeltaChunk) -> list[Content]:
|
||||
"""Extract file references from MessageDeltaChunk annotations.
|
||||
|
||||
Code interpreter generates files that are referenced via file path or file citation
|
||||
@@ -559,7 +562,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
Returns:
|
||||
List of HostedFileContent objects for any files referenced in annotations
|
||||
"""
|
||||
file_contents: list[HostedFileContent] = []
|
||||
file_contents: list[Content] = []
|
||||
|
||||
for content in message_delta_chunk.delta.content:
|
||||
if isinstance(content, MessageDeltaTextContent) and content.text and content.text.annotations:
|
||||
@@ -570,14 +573,14 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
if file_path is not None:
|
||||
file_id = getattr(file_path, "file_id", None)
|
||||
if file_id:
|
||||
file_contents.append(HostedFileContent(file_id=file_id))
|
||||
file_contents.append(Content.from_hosted_file(file_id=file_id))
|
||||
elif isinstance(annotation, MessageDeltaTextFileCitationAnnotation):
|
||||
# Extract file_id from the file_citation annotation
|
||||
file_citation = getattr(annotation, "file_citation", None)
|
||||
if file_citation is not None:
|
||||
file_id = getattr(file_citation, "file_id", None)
|
||||
if file_id:
|
||||
file_contents.append(HostedFileContent(file_id=file_id))
|
||||
file_contents.append(Content.from_hosted_file(file_id=file_id))
|
||||
|
||||
return file_contents
|
||||
|
||||
@@ -644,9 +647,9 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
file_contents = self._extract_file_path_contents(event_data)
|
||||
|
||||
# Create contents with citations if any exist
|
||||
citation_content: list[Contents] = []
|
||||
citation_content: list[Content] = []
|
||||
if event_data.text or url_citations:
|
||||
text_content_obj = TextContent(text=event_data.text or "")
|
||||
text_content_obj = Content.from_text(text=event_data.text or "")
|
||||
if url_citations:
|
||||
text_content_obj.annotations = url_citations
|
||||
citation_content.append(text_content_obj)
|
||||
@@ -722,7 +725,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
self._capture_azure_search_tool_calls(event_data, azure_search_tool_calls)
|
||||
|
||||
if event_data.usage:
|
||||
usage_content = UsageContent(
|
||||
usage_content = Content.from_usage(
|
||||
UsageDetails(
|
||||
input_token_count=event_data.usage.prompt_tokens,
|
||||
output_token_count=event_data.usage.completion_tokens,
|
||||
@@ -757,19 +760,21 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
tool_call.code_interpreter,
|
||||
RunStepDeltaCodeInterpreterDetailItemObject,
|
||||
):
|
||||
code_contents: list[Contents] = []
|
||||
code_contents: list[Content] = []
|
||||
if tool_call.code_interpreter.input is not None:
|
||||
logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}")
|
||||
if tool_call.code_interpreter.outputs is not None:
|
||||
for output in tool_call.code_interpreter.outputs:
|
||||
if isinstance(output, RunStepDeltaCodeInterpreterLogOutput) and output.logs:
|
||||
code_contents.append(TextContent(text=output.logs))
|
||||
code_contents.append(Content.from_text(text=output.logs))
|
||||
if (
|
||||
isinstance(output, RunStepDeltaCodeInterpreterImageOutput)
|
||||
and output.image is not None
|
||||
and output.image.file_id is not None
|
||||
):
|
||||
code_contents.append(HostedFileContent(file_id=output.image.file_id))
|
||||
code_contents.append(
|
||||
Content.from_hosted_file(file_id=output.image.file_id)
|
||||
)
|
||||
yield ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=code_contents,
|
||||
@@ -822,12 +827,12 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
except Exception as ex:
|
||||
logger.debug(f"Failed to capture Azure AI Search tool call: {ex}")
|
||||
|
||||
def _parse_function_calls_from_azure_ai(self, event_data: ThreadRun, response_id: str | None) -> list[Contents]:
|
||||
def _parse_function_calls_from_azure_ai(self, event_data: ThreadRun, response_id: str | None) -> list[Content]:
|
||||
"""Parse function call contents from an Azure AI tool action event."""
|
||||
if isinstance(event_data, ThreadRun) and event_data.required_action is not None:
|
||||
if isinstance(event_data.required_action, SubmitToolOutputsAction):
|
||||
return [
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=f'["{response_id}", "{tool.id}"]',
|
||||
name=tool.function.name,
|
||||
arguments=tool.function.arguments,
|
||||
@@ -837,9 +842,9 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
]
|
||||
if isinstance(event_data.required_action, SubmitToolApprovalAction):
|
||||
return [
|
||||
FunctionApprovalRequestContent(
|
||||
Content.from_function_approval_request(
|
||||
id=f'["{response_id}", "{tool.id}"]',
|
||||
function_call=FunctionCallContent(
|
||||
function_call=Content.from_function_call(
|
||||
call_id=f'["{response_id}", "{tool.id}"]',
|
||||
name=tool.name,
|
||||
arguments=tool.arguments,
|
||||
@@ -875,7 +880,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
messages: MutableSequence[ChatMessage],
|
||||
options: Mapping[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> tuple[dict[str, Any], list[FunctionResultContent | FunctionApprovalResponseContent] | None]:
|
||||
) -> tuple[dict[str, Any], list[Content] | None]:
|
||||
agent_definition = await self._load_agent_definition_if_needed()
|
||||
|
||||
# Build run_options from options dict, excluding specific keys
|
||||
@@ -1052,7 +1057,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
) -> tuple[
|
||||
list[ThreadMessageOptions] | None,
|
||||
list[str],
|
||||
list[FunctionResultContent | FunctionApprovalResponseContent] | None,
|
||||
list[Content] | None,
|
||||
]:
|
||||
"""Prepare messages for Azure AI Agents API.
|
||||
|
||||
@@ -1064,28 +1069,34 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
Tuple of (additional_messages, instructions, required_action_results)
|
||||
"""
|
||||
instructions: list[str] = []
|
||||
required_action_results: list[FunctionResultContent | FunctionApprovalResponseContent] | None = None
|
||||
required_action_results: list[Content] | None = None
|
||||
additional_messages: list[ThreadMessageOptions] | None = None
|
||||
|
||||
for chat_message in messages:
|
||||
if chat_message.role.value in ["system", "developer"]:
|
||||
for text_content in [content for content in chat_message.contents if isinstance(content, TextContent)]:
|
||||
instructions.append(text_content.text)
|
||||
for text_content in [content for content in chat_message.contents if content.type == "text"]:
|
||||
instructions.append(text_content.text) # type: ignore[arg-type]
|
||||
continue
|
||||
|
||||
message_contents: list[MessageInputContentBlock] = []
|
||||
|
||||
for content in chat_message.contents:
|
||||
if isinstance(content, TextContent):
|
||||
message_contents.append(MessageInputTextBlock(text=content.text))
|
||||
elif isinstance(content, (DataContent, UriContent)) and content.has_top_level_media_type("image"):
|
||||
message_contents.append(MessageInputImageUrlBlock(image_url=MessageImageUrlParam(url=content.uri)))
|
||||
elif isinstance(content, (FunctionResultContent, FunctionApprovalResponseContent)):
|
||||
if required_action_results is None:
|
||||
required_action_results = []
|
||||
required_action_results.append(content)
|
||||
elif isinstance(content.raw_representation, MessageInputContentBlock):
|
||||
message_contents.append(content.raw_representation)
|
||||
match content.type:
|
||||
case "text":
|
||||
message_contents.append(MessageInputTextBlock(text=content.text)) # type: ignore[arg-type]
|
||||
case "data" | "uri":
|
||||
if content.has_top_level_media_type("image"):
|
||||
message_contents.append(
|
||||
MessageInputImageUrlBlock(image_url=MessageImageUrlParam(url=content.uri)) # type: ignore[arg-type]
|
||||
)
|
||||
# Only images are supported. Other media types are ignored.
|
||||
case "function_result" | "function_approval_response":
|
||||
if required_action_results is None:
|
||||
required_action_results = []
|
||||
required_action_results.append(content)
|
||||
case _:
|
||||
if isinstance(content.raw_representation, MessageInputContentBlock):
|
||||
message_contents.append(content.raw_representation)
|
||||
|
||||
if message_contents:
|
||||
if additional_messages is None:
|
||||
@@ -1099,9 +1110,85 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
|
||||
return additional_messages, instructions, required_action_results
|
||||
|
||||
async def _prepare_tools_for_azure_ai(
|
||||
self, tools: Sequence["ToolProtocol | MutableMapping[str, Any]"], run_options: dict[str, Any] | None = None
|
||||
) -> list[ToolDefinition | dict[str, Any]]:
|
||||
"""Prepare tool definitions for the Azure AI Agents API."""
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
match tool:
|
||||
case AIFunction():
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
case HostedWebSearchTool():
|
||||
additional_props = tool.additional_properties or {}
|
||||
config_args: dict[str, Any] = {}
|
||||
if count := additional_props.get("count"):
|
||||
config_args["count"] = count
|
||||
if freshness := additional_props.get("freshness"):
|
||||
config_args["freshness"] = freshness
|
||||
if market := additional_props.get("market"):
|
||||
config_args["market"] = market
|
||||
if set_lang := additional_props.get("set_lang"):
|
||||
config_args["set_lang"] = set_lang
|
||||
# Bing Grounding
|
||||
connection_id = additional_props.get("connection_id") or os.getenv("BING_CONNECTION_ID")
|
||||
# Custom Bing Search
|
||||
custom_connection_id = additional_props.get("custom_connection_id") or os.getenv(
|
||||
"BING_CUSTOM_CONNECTION_ID"
|
||||
)
|
||||
custom_instance_name = additional_props.get("custom_instance_name") or os.getenv(
|
||||
"BING_CUSTOM_INSTANCE_NAME"
|
||||
)
|
||||
bing_search: BingGroundingTool | BingCustomSearchTool | None = None
|
||||
if (connection_id) and not custom_connection_id and not custom_instance_name:
|
||||
if connection_id:
|
||||
conn_id = connection_id
|
||||
else:
|
||||
raise ServiceInitializationError("Parameter connection_id is not provided.")
|
||||
bing_search = BingGroundingTool(connection_id=conn_id, **config_args)
|
||||
if custom_connection_id and custom_instance_name:
|
||||
bing_search = BingCustomSearchTool(
|
||||
connection_id=custom_connection_id,
|
||||
instance_name=custom_instance_name,
|
||||
**config_args,
|
||||
)
|
||||
if not bing_search:
|
||||
raise ServiceInitializationError(
|
||||
"Bing search tool requires either 'connection_id' for Bing Grounding "
|
||||
"or both 'custom_connection_id' and 'custom_instance_name' for Custom Bing Search. "
|
||||
"These can be provided via additional_properties or environment variables: "
|
||||
"'BING_CONNECTION_ID', 'BING_CUSTOM_CONNECTION_ID', "
|
||||
"'BING_CUSTOM_INSTANCE_NAME'"
|
||||
)
|
||||
tool_definitions.extend(bing_search.definitions)
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_definitions.append(CodeInterpreterToolDefinition())
|
||||
case HostedMCPTool():
|
||||
mcp_tool = McpTool(
|
||||
server_label=tool.name.replace(" ", "_"),
|
||||
server_url=str(tool.url),
|
||||
allowed_tools=list(tool.allowed_tools) if tool.allowed_tools else [],
|
||||
)
|
||||
tool_definitions.extend(mcp_tool.definitions)
|
||||
case HostedFileSearchTool():
|
||||
vector_stores = [inp for inp in tool.inputs or [] if inp.type == "hosted_vector_store"]
|
||||
if vector_stores:
|
||||
file_search = FileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores]) # type: ignore[misc]
|
||||
tool_definitions.extend(file_search.definitions)
|
||||
# Set tool_resources for file search to work properly with Azure AI
|
||||
if run_options is not None and "tool_resources" not in run_options:
|
||||
run_options["tool_resources"] = file_search.resources
|
||||
case ToolDefinition():
|
||||
tool_definitions.append(tool)
|
||||
case dict():
|
||||
tool_definitions.append(tool)
|
||||
case _:
|
||||
raise ServiceInitializationError(f"Unsupported tool type: {type(tool)}")
|
||||
return tool_definitions
|
||||
|
||||
def _prepare_tool_outputs_for_azure_ai(
|
||||
self,
|
||||
required_action_results: list[FunctionResultContent | FunctionApprovalResponseContent] | None,
|
||||
required_action_results: list[Content] | None,
|
||||
) -> tuple[str | None, list[ToolOutput] | None, list[ToolApproval] | None]:
|
||||
"""Prepare function results and approvals for submission to the Azure AI API."""
|
||||
run_id: str | None = None
|
||||
@@ -1115,9 +1202,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
# We need to extract the run ID and ensure that the Output/Approval we send back to Azure
|
||||
# is only the call ID.
|
||||
run_and_call_ids: list[str] = (
|
||||
json.loads(content.call_id)
|
||||
if isinstance(content, FunctionResultContent)
|
||||
else json.loads(content.id)
|
||||
json.loads(content.call_id) if content.type == "function_result" else json.loads(content.id) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -1132,16 +1217,16 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
run_id = run_and_call_ids[0]
|
||||
call_id = run_and_call_ids[1]
|
||||
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if content.type == "function_result":
|
||||
if tool_outputs is None:
|
||||
tool_outputs = []
|
||||
tool_outputs.append(
|
||||
ToolOutput(tool_call_id=call_id, output=prepare_function_call_results(content.result))
|
||||
)
|
||||
elif isinstance(content, FunctionApprovalResponseContent):
|
||||
elif content.type == "function_approval_response":
|
||||
if tool_approvals is None:
|
||||
tool_approvals = []
|
||||
tool_approvals.append(ToolApproval(tool_call_id=call_id, approve=content.approved))
|
||||
tool_approvals.append(ToolApproval(tool_call_id=call_id, approve=content.approved)) # type: ignore[arg-type]
|
||||
|
||||
return run_id, tool_outputs, tool_approvals
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from agent_framework import (
|
||||
ContextProvider,
|
||||
HostedMCPTool,
|
||||
Middleware,
|
||||
TextContent,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
use_chat_middleware,
|
||||
@@ -477,8 +476,8 @@ class AzureAIClient(OpenAIBaseResponsesClient[TAzureAIClientOptions], Generic[TA
|
||||
# System/developer messages are turned into instructions, since there is no such message roles in Azure AI.
|
||||
for message in messages:
|
||||
if message.role.value in ["system", "developer"]:
|
||||
for text_content in [content for content in message.contents if isinstance(content, TextContent)]:
|
||||
instructions_list.append(text_content.text)
|
||||
for text_content in [content for content in message.contents if content.type == "text"]:
|
||||
instructions_list.append(text_content.text) # type: ignore[arg-type]
|
||||
else:
|
||||
result.append(message)
|
||||
|
||||
|
||||
@@ -6,12 +6,10 @@ from typing import Any, ClassVar, Literal, cast
|
||||
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
Contents,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
@@ -189,9 +187,9 @@ def to_azure_ai_agent_tools(
|
||||
)
|
||||
tool_definitions.extend(mcp_tool.definitions)
|
||||
case HostedFileSearchTool():
|
||||
vector_stores = [inp for inp in tool.inputs or [] if isinstance(inp, HostedVectorStoreContent)]
|
||||
vector_stores = [inp for inp in tool.inputs or [] if inp.type == "hosted_vector_store"]
|
||||
if vector_stores:
|
||||
file_search = AgentsFileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores])
|
||||
file_search = AgentsFileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores]) # type: ignore[misc]
|
||||
tool_definitions.extend(file_search.definitions)
|
||||
# Set tool_resources for file search to work properly with Azure AI
|
||||
if run_options is not None and "tool_resources" not in run_options:
|
||||
@@ -247,7 +245,7 @@ def _convert_dict_tool(tool: dict[str, Any]) -> ToolProtocol | dict[str, Any] |
|
||||
if tool_type == "file_search":
|
||||
file_search_config = tool.get("file_search", {})
|
||||
vector_store_ids = file_search_config.get("vector_store_ids", [])
|
||||
inputs = [HostedVectorStoreContent(vector_store_id=vs_id) for vs_id in vector_store_ids]
|
||||
inputs = [Content.from_hosted_vector_store(vector_store_id=vs_id) for vs_id in vector_store_ids]
|
||||
return HostedFileSearchTool(inputs=inputs if inputs else None) # type: ignore
|
||||
|
||||
if tool_type == "bing_grounding":
|
||||
@@ -287,7 +285,7 @@ def _convert_sdk_tool(tool: ToolDefinition) -> ToolProtocol | dict[str, Any] | N
|
||||
if tool_type == "file_search":
|
||||
file_search_config = getattr(tool, "file_search", None)
|
||||
vector_store_ids = getattr(file_search_config, "vector_store_ids", []) if file_search_config else []
|
||||
inputs = [HostedVectorStoreContent(vector_store_id=vs_id) for vs_id in vector_store_ids]
|
||||
inputs = [Content.from_hosted_vector_store(vector_store_id=vs_id) for vs_id in vector_store_ids]
|
||||
return HostedFileSearchTool(inputs=inputs if inputs else None) # type: ignore
|
||||
|
||||
if tool_type == "bing_grounding":
|
||||
@@ -372,18 +370,18 @@ def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[T
|
||||
elif tool_type == "code_interpreter":
|
||||
ci_tool = cast(CodeInterpreterTool, tool_dict)
|
||||
container = ci_tool.get("container", {})
|
||||
ci_inputs: list[Contents] = []
|
||||
ci_inputs: list[Content] = []
|
||||
if "file_ids" in container:
|
||||
for file_id in container["file_ids"]:
|
||||
ci_inputs.append(HostedFileContent(file_id=file_id))
|
||||
ci_inputs.append(Content.from_hosted_file(file_id=file_id))
|
||||
|
||||
agent_tools.append(HostedCodeInterpreterTool(inputs=ci_inputs if ci_inputs else None)) # type: ignore
|
||||
elif tool_type == "file_search":
|
||||
fs_tool = cast(ProjectsFileSearchTool, tool_dict)
|
||||
fs_inputs: list[Contents] = []
|
||||
fs_inputs: list[Content] = []
|
||||
if "vector_store_ids" in fs_tool:
|
||||
for vs_id in fs_tool["vector_store_ids"]:
|
||||
fs_inputs.append(HostedVectorStoreContent(vector_store_id=vs_id))
|
||||
fs_inputs.append(Content.from_hosted_vector_store(vector_store_id=vs_id))
|
||||
|
||||
agent_tools.append(
|
||||
HostedFileSearchTool(
|
||||
@@ -433,8 +431,8 @@ def to_azure_ai_tools(
|
||||
file_ids: list[str] = []
|
||||
if tool.inputs:
|
||||
for tool_input in tool.inputs:
|
||||
if isinstance(tool_input, HostedFileContent):
|
||||
file_ids.append(tool_input.file_id)
|
||||
if tool_input.type == "hosted_file":
|
||||
file_ids.append(tool_input.file_id) # type: ignore[misc, arg-type]
|
||||
container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None)
|
||||
ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container)
|
||||
azure_tools.append(ci_tool)
|
||||
@@ -453,11 +451,14 @@ def to_azure_ai_tools(
|
||||
if not tool.inputs:
|
||||
raise ValueError("HostedFileSearchTool requires inputs to be specified.")
|
||||
vector_store_ids: list[str] = [
|
||||
inp.vector_store_id for inp in tool.inputs if isinstance(inp, HostedVectorStoreContent)
|
||||
inp.vector_store_id # type: ignore[misc]
|
||||
for inp in tool.inputs
|
||||
if inp.type == "hosted_vector_store"
|
||||
]
|
||||
if not vector_store_ids:
|
||||
raise ValueError(
|
||||
"HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`."
|
||||
"HostedFileSearchTool requires inputs to be of type `Content` with "
|
||||
"type 'hosted_vector_store'."
|
||||
)
|
||||
fs_tool: ProjectsFileSearchTool = ProjectsFileSearchTool(vector_store_ids=vector_store_ids)
|
||||
if tool.max_results:
|
||||
|
||||
@@ -7,10 +7,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
ai_function,
|
||||
)
|
||||
@@ -509,7 +509,7 @@ def test_to_azure_ai_agent_tools_code_interpreter() -> None:
|
||||
|
||||
def test_to_azure_ai_agent_tools_file_search() -> None:
|
||||
"""Test converting HostedFileSearchTool with vector stores."""
|
||||
tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id="vs-123")])
|
||||
tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id="vs-123")])
|
||||
run_options: dict[str, Any] = {}
|
||||
|
||||
result = to_azure_ai_agent_tools([tool], run_options)
|
||||
|
||||
@@ -17,19 +17,12 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CitationAnnotation,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from agent_framework._serialization import SerializationMixin
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -368,7 +361,7 @@ async def test_azure_ai_chat_client_prepare_options_with_image_content(mock_agen
|
||||
# Mock get_agent
|
||||
mock_agents_client.get_agent = AsyncMock(return_value=None)
|
||||
|
||||
image_content = UriContent(uri="https://example.com/image.jpg", media_type="image/jpeg")
|
||||
image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg")
|
||||
messages = [ChatMessage(role=Role.USER, contents=[image_content])]
|
||||
|
||||
run_options, _ = await chat_client._prepare_options(messages, {}) # type: ignore
|
||||
@@ -551,7 +544,7 @@ def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_basic(mock_agen
|
||||
result = chat_client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionCallContent)
|
||||
assert result[0].type == "function_call"
|
||||
assert result[0].name == "get_weather"
|
||||
assert result[0].call_id == '["response_123", "call_123"]'
|
||||
|
||||
@@ -728,6 +721,121 @@ async def test_azure_ai_chat_client_prepare_options_mcp_with_headers(mock_agents
|
||||
assert mcp_resource["headers"] == headers
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_bing_grounding(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedWebSearchTool using Bing Grounding."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"connection_id": "test-connection-id",
|
||||
"count": 5,
|
||||
"freshness": "Day",
|
||||
"market": "en-US",
|
||||
"set_lang": "en",
|
||||
}
|
||||
)
|
||||
|
||||
# Mock BingGroundingTool
|
||||
with patch("agent_framework_azure_ai._chat_client.BingGroundingTool") as mock_bing_grounding:
|
||||
mock_bing_tool = MagicMock()
|
||||
mock_bing_tool.definitions = [{"type": "bing_grounding"}]
|
||||
mock_bing_grounding.return_value = mock_bing_tool
|
||||
|
||||
result = await chat_client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_grounding"}
|
||||
call_args = mock_bing_grounding.call_args[1]
|
||||
assert call_args["count"] == 5
|
||||
assert call_args["freshness"] == "Day"
|
||||
assert call_args["market"] == "en-US"
|
||||
assert call_args["set_lang"] == "en"
|
||||
assert "connection_id" in call_args
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_bing_grounding_with_connection_id(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_... with HostedWebSearchTool using Bing Grounding with connection_id (no HTTP call)."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"connection_id": "direct-connection-id",
|
||||
"count": 3,
|
||||
}
|
||||
)
|
||||
|
||||
# Mock BingGroundingTool
|
||||
with patch("agent_framework_azure_ai._chat_client.BingGroundingTool") as mock_bing_grounding:
|
||||
mock_bing_tool = MagicMock()
|
||||
mock_bing_tool.definitions = [{"type": "bing_grounding"}]
|
||||
mock_bing_grounding.return_value = mock_bing_tool
|
||||
|
||||
result = await chat_client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_grounding"}
|
||||
mock_bing_grounding.assert_called_once_with(connection_id="direct-connection-id", count=3)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_custom_bing(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedWebSearchTool using Custom Bing Search."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"custom_connection_id": "custom-connection-id",
|
||||
"custom_instance_name": "custom-instance",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
|
||||
# Mock BingCustomSearchTool
|
||||
with patch("agent_framework_azure_ai._chat_client.BingCustomSearchTool") as mock_custom_bing:
|
||||
mock_custom_tool = MagicMock()
|
||||
mock_custom_tool.definitions = [{"type": "bing_custom_search"}]
|
||||
mock_custom_bing.return_value = mock_custom_tool
|
||||
|
||||
result = await chat_client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_custom_search"}
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_file_search_with_vector_stores(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedFileSearchTool using vector stores."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
vector_store_input = Content.from_hosted_vector_store(vector_store_id="vs-123")
|
||||
file_search_tool = HostedFileSearchTool(inputs=[vector_store_input])
|
||||
|
||||
# Mock FileSearchTool
|
||||
with patch("agent_framework_azure_ai._chat_client.FileSearchTool") as mock_file_search:
|
||||
mock_file_tool = MagicMock()
|
||||
mock_file_tool.definitions = [{"type": "file_search"}]
|
||||
mock_file_tool.resources = {"vector_store_ids": ["vs-123"]}
|
||||
mock_file_search.return_value = mock_file_tool
|
||||
|
||||
run_options = {}
|
||||
result = await chat_client._prepare_tools_for_azure_ai([file_search_tool], run_options) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "file_search"}
|
||||
assert run_options["tool_resources"] == {"vector_store_ids": ["vs-123"]}
|
||||
mock_file_search.assert_called_once_with(vector_store_ids=["vs-123"])
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
@@ -741,9 +849,9 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals(
|
||||
chat_client._get_active_thread_run = AsyncMock(return_value=mock_thread_run) # type: ignore
|
||||
|
||||
# Mock required action results with approval response that matches run ID
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
approval_response = Content.from_function_approval_response(
|
||||
id='["test-run-id", "test-call-id"]',
|
||||
function_call=FunctionCallContent(
|
||||
function_call=Content.from_function_call(
|
||||
call_id='["test-run-id", "test-call-id"]', name="test_function", arguments="{}"
|
||||
),
|
||||
approved=True,
|
||||
@@ -839,7 +947,7 @@ async def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_ai_function_r
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Test with simple result
|
||||
function_result = FunctionResultContent(call_id='["run_123", "call_456"]', result="Simple result")
|
||||
function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result="Simple result")
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore
|
||||
|
||||
@@ -857,7 +965,7 @@ async def test_azure_ai_chat_client_convert_required_action_invalid_call_id(mock
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Invalid call_id format - should raise JSONDecodeError
|
||||
function_result = FunctionResultContent(call_id="invalid_json", result="result")
|
||||
function_result = Content.from_function_result(call_id="invalid_json", result="result")
|
||||
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore
|
||||
@@ -870,7 +978,7 @@ async def test_azure_ai_chat_client_convert_required_action_invalid_structure(
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Valid JSON but invalid structure (missing second element)
|
||||
function_result = FunctionResultContent(call_id='["run_123"]', result="result")
|
||||
function_result = Content.from_function_result(call_id='["run_123"]', result="result")
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore
|
||||
|
||||
@@ -894,7 +1002,7 @@ async def test_azure_ai_chat_client_convert_required_action_serde_model_results(
|
||||
|
||||
# Test with BaseModel result
|
||||
mock_result = MockResult(name="test", value=42)
|
||||
function_result = FunctionResultContent(call_id='["run_123", "call_456"]', result=mock_result)
|
||||
function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result=mock_result)
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore
|
||||
|
||||
@@ -922,7 +1030,7 @@ async def test_azure_ai_chat_client_convert_required_action_multiple_results(
|
||||
# Test with multiple results - mix of BaseModel and regular objects
|
||||
mock_basemodel = MockResult(data="model_data")
|
||||
results_list = [mock_basemodel, {"key": "value"}, "string_result"]
|
||||
function_result = FunctionResultContent(call_id='["run_123", "call_456"]', result=results_list)
|
||||
function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result=results_list)
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore
|
||||
|
||||
@@ -948,9 +1056,11 @@ async def test_azure_ai_chat_client_convert_required_action_approval_response(
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Test with approval response - need to provide required fields
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
approval_response = Content.from_function_approval_response(
|
||||
id='["run_123", "call_456"]',
|
||||
function_call=FunctionCallContent(call_id='["run_123", "call_456"]', name="test_function", arguments="{}"),
|
||||
function_call=Content.from_function_call(
|
||||
call_id='["run_123", "call_456"]', name="test_function", arguments="{}"
|
||||
),
|
||||
approved=True,
|
||||
)
|
||||
|
||||
@@ -985,7 +1095,7 @@ async def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_approval_
|
||||
result = chat_client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionApprovalRequestContent)
|
||||
assert result[0].type == "function_approval_request"
|
||||
assert result[0].id == '["response_123", "approval_call_123"]'
|
||||
assert result[0].function_call.name == "approve_action"
|
||||
assert result[0].function_call.call_id == '["response_123", "approval_call_123"]'
|
||||
@@ -1064,7 +1174,7 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_outputs(
|
||||
chat_client._get_active_thread_run = AsyncMock(return_value=mock_thread_run) # type: ignore
|
||||
|
||||
# Mock required action results with matching run ID
|
||||
function_result = FunctionResultContent(call_id='["test-run-id", "test-call-id"]', result="test result")
|
||||
function_result = Content.from_function_result(call_id='["test-run-id", "test-call-id"]', result="test result")
|
||||
|
||||
# Mock submit_tool_outputs_stream
|
||||
mock_handler = MagicMock()
|
||||
@@ -1115,14 +1225,13 @@ def test_azure_ai_chat_client_extract_url_citations_with_citations(mock_agents_c
|
||||
# Verify results
|
||||
assert len(citations) == 1
|
||||
citation = citations[0]
|
||||
assert isinstance(citation, CitationAnnotation)
|
||||
assert citation.url == "https://example.com/test"
|
||||
assert citation.title == "Test Title"
|
||||
assert citation.snippet is None
|
||||
assert citation.annotated_regions is not None
|
||||
assert len(citation.annotated_regions) == 1
|
||||
assert citation.annotated_regions[0].start_index == 10
|
||||
assert citation.annotated_regions[0].end_index == 20
|
||||
assert citation["url"] == "https://example.com/test"
|
||||
assert citation["title"] == "Test Title"
|
||||
assert citation["snippet"] is None
|
||||
assert citation["annotated_regions"] is not None
|
||||
assert len(citation["annotated_regions"]) == 1
|
||||
assert citation["annotated_regions"][0]["start_index"] == 10
|
||||
assert citation["annotated_regions"][0]["end_index"] == 20
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_extract_file_path_contents_with_file_path_annotation(
|
||||
@@ -1158,7 +1267,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_with_file_path_annotati
|
||||
|
||||
# Verify results
|
||||
assert len(file_contents) == 1
|
||||
assert isinstance(file_contents[0], HostedFileContent)
|
||||
assert file_contents[0].type == "hosted_file"
|
||||
assert file_contents[0].file_id == "assistant-test-file-123"
|
||||
|
||||
|
||||
@@ -1195,7 +1304,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_with_file_citation_anno
|
||||
|
||||
# Verify results
|
||||
assert len(file_contents) == 1
|
||||
assert isinstance(file_contents[0], HostedFileContent)
|
||||
assert file_contents[0].type == "hosted_file"
|
||||
assert file_contents[0].file_id == "cfile_test-citation-456"
|
||||
|
||||
|
||||
@@ -1305,7 +1414,7 @@ async def test_azure_ai_chat_client_streaming() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25"])
|
||||
@@ -1331,7 +1440,7 @@ async def test_azure_ai_chat_client_streaming_tools() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25"])
|
||||
@@ -1476,7 +1585,9 @@ async def test_azure_ai_chat_client_agent_file_search():
|
||||
)
|
||||
|
||||
# 2. Create file search tool with uploaded resources
|
||||
file_search_tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id=vector_store.id)])
|
||||
file_search_tool = HostedFileSearchTool(
|
||||
inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)]
|
||||
)
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=client,
|
||||
@@ -1795,7 +1906,7 @@ def test_azure_ai_chat_client_extract_url_citations_with_azure_search_enhanced_u
|
||||
# Verify real URL was used
|
||||
assert len(citations) == 1
|
||||
citation = citations[0]
|
||||
assert citation.url == "https://real-example.com/doc2" # doc_1 maps to index 1
|
||||
assert citation["url"] == "https://real-example.com/doc2" # doc_1 maps to index 1
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_init_with_auto_created_agents_client(
|
||||
|
||||
@@ -16,14 +16,12 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
@@ -298,9 +296,9 @@ async def test_prepare_messages_for_azure_ai_with_system_messages(
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are a helpful assistant.")]),
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="System response")]),
|
||||
ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="You are a helpful assistant.")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="System response")]),
|
||||
]
|
||||
|
||||
result_messages, instructions = client._prepare_messages_for_azure_ai(messages) # type: ignore
|
||||
@@ -318,8 +316,8 @@ async def test_prepare_messages_for_azure_ai_no_system_messages(
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Hi there!")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hi there!")]),
|
||||
]
|
||||
|
||||
result_messages, instructions = client._prepare_messages_for_azure_ai(messages) # type: ignore
|
||||
@@ -419,7 +417,7 @@ async def test_prepare_options_basic(mock_project_client: MagicMock) -> None:
|
||||
"""Test prepare_options basic functionality."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])]
|
||||
|
||||
with (
|
||||
patch.object(client.__class__.__bases__[0], "_prepare_options", return_value={"model": "test-model"}),
|
||||
@@ -453,7 +451,7 @@ async def test_prepare_options_with_application_endpoint(
|
||||
agent_version="1",
|
||||
)
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])]
|
||||
|
||||
with (
|
||||
patch.object(client.__class__.__bases__[0], "_prepare_options", return_value={"model": "test-model"}),
|
||||
@@ -492,7 +490,7 @@ async def test_prepare_options_with_application_project_client(
|
||||
agent_version="1",
|
||||
)
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])]
|
||||
|
||||
with (
|
||||
patch.object(client.__class__.__bases__[0], "_prepare_options", return_value={"model": "test-model"}),
|
||||
@@ -848,7 +846,7 @@ async def test_prepare_options_excludes_response_format(
|
||||
"""Test that prepare_options excludes response_format, text, and text_format from final run options."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hello")])]
|
||||
chat_options: ChatOptions = {}
|
||||
|
||||
with (
|
||||
@@ -992,7 +990,7 @@ def test_from_azure_ai_tools() -> None:
|
||||
|
||||
tool_input = parsed_tools[0].inputs[0]
|
||||
|
||||
assert tool_input and isinstance(tool_input, HostedFileContent) and tool_input.file_id == "file-1"
|
||||
assert tool_input and tool_input.type == "hosted_file" and tool_input.file_id == "file-1"
|
||||
|
||||
# Test File Search tool
|
||||
fs_tool = FileSearchTool(vector_store_ids=["vs-1"], max_num_results=5)
|
||||
@@ -1004,7 +1002,7 @@ def test_from_azure_ai_tools() -> None:
|
||||
|
||||
tool_input = parsed_tools[0].inputs[0]
|
||||
|
||||
assert tool_input and isinstance(tool_input, HostedVectorStoreContent) and tool_input.vector_store_id == "vs-1"
|
||||
assert tool_input and tool_input.type == "hosted_vector_store" and tool_input.vector_store_id == "vs-1"
|
||||
assert parsed_tools[0].max_results == 5
|
||||
|
||||
# Test Web Search tool
|
||||
|
||||
+61
-71
@@ -36,18 +36,8 @@ from typing import Any, cast
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
BaseContent,
|
||||
ChatMessage,
|
||||
DataContent,
|
||||
ErrorContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedFileContent,
|
||||
HostedVectorStoreContent,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
Content,
|
||||
UsageDetails,
|
||||
get_logger,
|
||||
)
|
||||
@@ -290,25 +280,25 @@ class DurableAgentStateContent:
|
||||
The corresponding DurableAgentStateContent subclass instance
|
||||
"""
|
||||
# Map AI content type to appropriate DurableAgentStateContent subclass
|
||||
if isinstance(content, DataContent):
|
||||
if isinstance(content, Content) and content.type == "data":
|
||||
return DurableAgentStateDataContent.from_data_content(content)
|
||||
if isinstance(content, ErrorContent):
|
||||
if isinstance(content, Content) and content.type == "error":
|
||||
return DurableAgentStateErrorContent.from_error_content(content)
|
||||
if isinstance(content, FunctionCallContent):
|
||||
if isinstance(content, Content) and content.type == "function_call":
|
||||
return DurableAgentStateFunctionCallContent.from_function_call_content(content)
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if isinstance(content, Content) and content.type == "function_result":
|
||||
return DurableAgentStateFunctionResultContent.from_function_result_content(content)
|
||||
if isinstance(content, HostedFileContent):
|
||||
if isinstance(content, Content) and content.type == "hosted_file":
|
||||
return DurableAgentStateHostedFileContent.from_hosted_file_content(content)
|
||||
if isinstance(content, HostedVectorStoreContent):
|
||||
if isinstance(content, Content) and content.type == "hosted_vector_store":
|
||||
return DurableAgentStateHostedVectorStoreContent.from_hosted_vector_store_content(content)
|
||||
if isinstance(content, TextContent):
|
||||
if isinstance(content, Content) and content.type == "text":
|
||||
return DurableAgentStateTextContent.from_text_content(content)
|
||||
if isinstance(content, TextReasoningContent):
|
||||
if isinstance(content, Content) and content.type == "text_reasoning":
|
||||
return DurableAgentStateTextReasoningContent.from_text_reasoning_content(content)
|
||||
if isinstance(content, UriContent):
|
||||
if isinstance(content, Content) and content.type == "uri":
|
||||
return DurableAgentStateUriContent.from_uri_content(content)
|
||||
if isinstance(content, UsageContent):
|
||||
if isinstance(content, Content) and content.type == "usage":
|
||||
return DurableAgentStateUsageContent.from_usage_content(content)
|
||||
return DurableAgentStateUnknownContent.from_unknown_content(content)
|
||||
|
||||
@@ -699,7 +689,7 @@ class DurableAgentStateResponse(DurableAgentStateEntry):
|
||||
correlation_id=correlation_id,
|
||||
created_at=_parse_created_at(response.created_at),
|
||||
messages=[DurableAgentStateMessage.from_chat_message(m) for m in response.messages],
|
||||
usage=DurableAgentStateUsage.from_usage(response.usage_details),
|
||||
usage=DurableAgentStateUsage.from_usage(response.usage_details), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@@ -868,11 +858,11 @@ class DurableAgentStateDataContent(DurableAgentStateContent):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_data_content(content: DataContent) -> DurableAgentStateDataContent:
|
||||
return DurableAgentStateDataContent(uri=content.uri, media_type=content.media_type)
|
||||
def from_data_content(content: Content) -> DurableAgentStateDataContent:
|
||||
return DurableAgentStateDataContent(uri=content.uri, media_type=content.media_type) # type: ignore[arg-type]
|
||||
|
||||
def to_ai_content(self) -> DataContent:
|
||||
return DataContent(uri=self.uri, media_type=self.media_type)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_uri(uri=self.uri, media_type=self.media_type)
|
||||
|
||||
|
||||
class DurableAgentStateErrorContent(DurableAgentStateContent):
|
||||
@@ -907,13 +897,13 @@ class DurableAgentStateErrorContent(DurableAgentStateContent):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_error_content(content: ErrorContent) -> DurableAgentStateErrorContent:
|
||||
def from_error_content(content: Content) -> DurableAgentStateErrorContent:
|
||||
return DurableAgentStateErrorContent(
|
||||
message=content.message, error_code=content.error_code, details=content.details
|
||||
message=content.message, error_code=content.error_code, details=content.error_details
|
||||
)
|
||||
|
||||
def to_ai_content(self) -> ErrorContent:
|
||||
return ErrorContent(message=self.message, error_code=self.error_code, details=self.details)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_error(message=self.message, error_code=self.error_code, error_details=self.details)
|
||||
|
||||
|
||||
class DurableAgentStateFunctionCallContent(DurableAgentStateContent):
|
||||
@@ -949,7 +939,7 @@ class DurableAgentStateFunctionCallContent(DurableAgentStateContent):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_function_call_content(content: FunctionCallContent) -> DurableAgentStateFunctionCallContent:
|
||||
def from_function_call_content(content: Content) -> DurableAgentStateFunctionCallContent:
|
||||
# Ensure arguments is a dict; parse string if needed
|
||||
arguments: dict[str, Any] = {}
|
||||
if content.arguments:
|
||||
@@ -962,10 +952,10 @@ class DurableAgentStateFunctionCallContent(DurableAgentStateContent):
|
||||
except json.JSONDecodeError:
|
||||
arguments = {}
|
||||
|
||||
return DurableAgentStateFunctionCallContent(call_id=content.call_id, name=content.name, arguments=arguments)
|
||||
return DurableAgentStateFunctionCallContent(call_id=content.call_id, name=content.name, arguments=arguments) # type: ignore[arg-type]
|
||||
|
||||
def to_ai_content(self) -> FunctionCallContent:
|
||||
return FunctionCallContent(call_id=self.call_id, name=self.name, arguments=self.arguments)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_function_call(call_id=self.call_id, name=self.name, arguments=self.arguments)
|
||||
|
||||
|
||||
class DurableAgentStateFunctionResultContent(DurableAgentStateContent):
|
||||
@@ -997,11 +987,11 @@ class DurableAgentStateFunctionResultContent(DurableAgentStateContent):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_function_result_content(content: FunctionResultContent) -> DurableAgentStateFunctionResultContent:
|
||||
return DurableAgentStateFunctionResultContent(call_id=content.call_id, result=content.result)
|
||||
def from_function_result_content(content: Content) -> DurableAgentStateFunctionResultContent:
|
||||
return DurableAgentStateFunctionResultContent(call_id=content.call_id, result=content.result) # type: ignore[arg-type]
|
||||
|
||||
def to_ai_content(self) -> FunctionResultContent:
|
||||
return FunctionResultContent(call_id=self.call_id, result=self.result)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_function_result(call_id=self.call_id, result=self.result)
|
||||
|
||||
|
||||
class DurableAgentStateHostedFileContent(DurableAgentStateContent):
|
||||
@@ -1025,11 +1015,11 @@ class DurableAgentStateHostedFileContent(DurableAgentStateContent):
|
||||
return {DurableStateFields.TYPE_DISCRIMINATOR: self.type, DurableStateFields.FILE_ID: self.file_id}
|
||||
|
||||
@staticmethod
|
||||
def from_hosted_file_content(content: HostedFileContent) -> DurableAgentStateHostedFileContent:
|
||||
return DurableAgentStateHostedFileContent(file_id=content.file_id)
|
||||
def from_hosted_file_content(content: Content) -> DurableAgentStateHostedFileContent:
|
||||
return DurableAgentStateHostedFileContent(file_id=content.file_id) # type: ignore[arg-type]
|
||||
|
||||
def to_ai_content(self) -> HostedFileContent:
|
||||
return HostedFileContent(file_id=self.file_id)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_hosted_file(file_id=self.file_id)
|
||||
|
||||
|
||||
class DurableAgentStateHostedVectorStoreContent(DurableAgentStateContent):
|
||||
@@ -1058,12 +1048,12 @@ class DurableAgentStateHostedVectorStoreContent(DurableAgentStateContent):
|
||||
|
||||
@staticmethod
|
||||
def from_hosted_vector_store_content(
|
||||
content: HostedVectorStoreContent,
|
||||
content: Content,
|
||||
) -> DurableAgentStateHostedVectorStoreContent:
|
||||
return DurableAgentStateHostedVectorStoreContent(vector_store_id=content.vector_store_id)
|
||||
return DurableAgentStateHostedVectorStoreContent(vector_store_id=content.vector_store_id) # type: ignore[arg-type]
|
||||
|
||||
def to_ai_content(self) -> HostedVectorStoreContent:
|
||||
return HostedVectorStoreContent(vector_store_id=self.vector_store_id)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_hosted_vector_store(vector_store_id=self.vector_store_id)
|
||||
|
||||
|
||||
class DurableAgentStateTextContent(DurableAgentStateContent):
|
||||
@@ -1085,11 +1075,11 @@ class DurableAgentStateTextContent(DurableAgentStateContent):
|
||||
return {DurableStateFields.TYPE_DISCRIMINATOR: self.type, DurableStateFields.TEXT: self.text}
|
||||
|
||||
@staticmethod
|
||||
def from_text_content(content: TextContent) -> DurableAgentStateTextContent:
|
||||
def from_text_content(content: Content) -> DurableAgentStateTextContent:
|
||||
return DurableAgentStateTextContent(text=content.text)
|
||||
|
||||
def to_ai_content(self) -> TextContent:
|
||||
return TextContent(text=self.text or "")
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_text(text=self.text or "")
|
||||
|
||||
|
||||
class DurableAgentStateTextReasoningContent(DurableAgentStateContent):
|
||||
@@ -1111,11 +1101,11 @@ class DurableAgentStateTextReasoningContent(DurableAgentStateContent):
|
||||
return {DurableStateFields.TYPE_DISCRIMINATOR: self.type, DurableStateFields.TEXT: self.text}
|
||||
|
||||
@staticmethod
|
||||
def from_text_reasoning_content(content: TextReasoningContent) -> DurableAgentStateTextReasoningContent:
|
||||
def from_text_reasoning_content(content: Content) -> DurableAgentStateTextReasoningContent:
|
||||
return DurableAgentStateTextReasoningContent(text=content.text)
|
||||
|
||||
def to_ai_content(self) -> TextReasoningContent:
|
||||
return TextReasoningContent(text=self.text or "")
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_text_reasoning(text=self.text)
|
||||
|
||||
|
||||
class DurableAgentStateUriContent(DurableAgentStateContent):
|
||||
@@ -1146,11 +1136,11 @@ class DurableAgentStateUriContent(DurableAgentStateContent):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_uri_content(content: UriContent) -> DurableAgentStateUriContent:
|
||||
return DurableAgentStateUriContent(uri=content.uri, media_type=content.media_type)
|
||||
def from_uri_content(content: Content) -> DurableAgentStateUriContent:
|
||||
return DurableAgentStateUriContent(uri=content.uri, media_type=content.media_type) # type: ignore[arg-type]
|
||||
|
||||
def to_ai_content(self) -> UriContent:
|
||||
return UriContent(uri=self.uri, media_type=self.media_type)
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_uri(uri=self.uri, media_type=self.media_type)
|
||||
|
||||
|
||||
class DurableAgentStateUsage:
|
||||
@@ -1204,22 +1194,22 @@ class DurableAgentStateUsage:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_usage(usage: UsageDetails | None) -> DurableAgentStateUsage | None:
|
||||
def from_usage(usage: UsageDetails | dict[str, int] | None) -> DurableAgentStateUsage | None:
|
||||
if usage is None:
|
||||
return None
|
||||
return DurableAgentStateUsage(
|
||||
input_token_count=usage.input_token_count,
|
||||
output_token_count=usage.output_token_count,
|
||||
total_token_count=usage.total_token_count,
|
||||
input_token_count=usage.get("input_token_count"),
|
||||
output_token_count=usage.get("output_token_count"),
|
||||
total_token_count=usage.get("total_token_count"),
|
||||
)
|
||||
|
||||
def to_usage_details(self) -> UsageDetails:
|
||||
# Convert back to AI SDK UsageDetails
|
||||
return UsageDetails(
|
||||
input_token_count=self.input_token_count,
|
||||
output_token_count=self.output_token_count,
|
||||
total_token_count=self.total_token_count,
|
||||
)
|
||||
return {
|
||||
"input_token_count": self.input_token_count,
|
||||
"output_token_count": self.output_token_count,
|
||||
"total_token_count": self.total_token_count,
|
||||
}
|
||||
|
||||
|
||||
class DurableAgentStateUsageContent(DurableAgentStateContent):
|
||||
@@ -1247,11 +1237,11 @@ class DurableAgentStateUsageContent(DurableAgentStateContent):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_usage_content(content: UsageContent) -> DurableAgentStateUsageContent:
|
||||
return DurableAgentStateUsageContent(usage=DurableAgentStateUsage.from_usage(content.details))
|
||||
def from_usage_content(content: Content) -> DurableAgentStateUsageContent:
|
||||
return DurableAgentStateUsageContent(usage=DurableAgentStateUsage.from_usage(content.usage_details))
|
||||
|
||||
def to_ai_content(self) -> UsageContent:
|
||||
return UsageContent(details=self.usage.to_usage_details())
|
||||
def to_ai_content(self) -> Content:
|
||||
return Content.from_usage(usage_details=self.usage.to_usage_details())
|
||||
|
||||
|
||||
class DurableAgentStateUnknownContent(DurableAgentStateContent):
|
||||
@@ -1279,7 +1269,7 @@ class DurableAgentStateUnknownContent(DurableAgentStateContent):
|
||||
def from_unknown_content(content: Any) -> DurableAgentStateUnknownContent:
|
||||
return DurableAgentStateUnknownContent(content=content)
|
||||
|
||||
def to_ai_content(self) -> BaseContent:
|
||||
def to_ai_content(self) -> Content:
|
||||
if not self.content:
|
||||
raise Exception("The content is missing and cannot be converted to valid AI content.")
|
||||
return BaseContent(content=self.content)
|
||||
return Content(type=self.type, additional_properties={"content": self.content}) # type: ignore
|
||||
|
||||
@@ -18,7 +18,7 @@ from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
ChatMessage,
|
||||
ErrorContent,
|
||||
Content,
|
||||
Role,
|
||||
get_logger,
|
||||
)
|
||||
@@ -193,7 +193,7 @@ class AgentEntity:
|
||||
|
||||
# Create error message
|
||||
error_message = ChatMessage(
|
||||
role=Role.ASSISTANT, contents=[ErrorContent(message=str(exc), error_code=type(exc).__name__)]
|
||||
role=Role.ASSISTANT, contents=[Content.from_error(message=str(exc), error_code=type(exc).__name__)]
|
||||
)
|
||||
|
||||
error_response = AgentResponse(messages=[error_message])
|
||||
|
||||
@@ -10,7 +10,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
import azure.durable_functions as df
|
||||
import azure.functions as func
|
||||
import pytest
|
||||
from agent_framework import AgentResponse, ChatMessage, ErrorContent
|
||||
from agent_framework import AgentResponse, ChatMessage
|
||||
|
||||
from agent_framework_azurefunctions import AgentFunctionApp
|
||||
from agent_framework_azurefunctions._app import WAIT_FOR_RESPONSE_FIELD, WAIT_FOR_RESPONSE_HEADER
|
||||
@@ -622,7 +622,7 @@ class TestErrorHandling:
|
||||
assert isinstance(result, AgentResponse)
|
||||
assert len(result.messages) == 1
|
||||
content = result.messages[0].contents[0]
|
||||
assert isinstance(content, ErrorContent)
|
||||
assert content.type == "error"
|
||||
assert "Agent error" in (content.message or "")
|
||||
assert content.error_code == "Exception"
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Any, TypeVar
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, ErrorContent, Role
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, Role
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework_azurefunctions._durable_agent_state import (
|
||||
@@ -608,7 +608,7 @@ class TestErrorHandling:
|
||||
assert isinstance(result, AgentResponse)
|
||||
assert len(result.messages) == 1
|
||||
content = result.messages[0].contents[0]
|
||||
assert isinstance(content, ErrorContent)
|
||||
assert content.type == "error"
|
||||
assert "Agent failed" in (content.message or "")
|
||||
assert content.error_code == "Exception"
|
||||
|
||||
@@ -627,7 +627,7 @@ class TestErrorHandling:
|
||||
assert isinstance(result, AgentResponse)
|
||||
assert len(result.messages) == 1
|
||||
content = result.messages[0].contents[0]
|
||||
assert isinstance(content, ErrorContent)
|
||||
assert content.type == "error"
|
||||
assert content.error_code == "ValueError"
|
||||
assert "Invalid input" in str(content.message)
|
||||
|
||||
@@ -646,7 +646,7 @@ class TestErrorHandling:
|
||||
assert isinstance(result, AgentResponse)
|
||||
assert len(result.messages) == 1
|
||||
content = result.messages[0].contents[0]
|
||||
assert isinstance(content, ErrorContent)
|
||||
assert content.type == "error"
|
||||
assert content.error_code == "TimeoutError"
|
||||
|
||||
def test_entity_function_handles_exception_in_operation(self) -> None:
|
||||
@@ -685,7 +685,7 @@ class TestErrorHandling:
|
||||
assert isinstance(result, AgentResponse)
|
||||
assert len(result.messages) == 1
|
||||
content = result.messages[0].contents[0]
|
||||
assert isinstance(content, ErrorContent)
|
||||
assert content.type == "error"
|
||||
|
||||
|
||||
class TestConversationHistory:
|
||||
|
||||
@@ -16,14 +16,10 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Contents,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
ToolProtocol,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
get_logger,
|
||||
prepare_function_call_results,
|
||||
@@ -328,7 +324,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
response = await self._inner_get_response(messages=messages, options=options, **kwargs)
|
||||
contents = list(response.messages[0].contents if response.messages else [])
|
||||
if response.usage_details:
|
||||
contents.append(UsageContent(details=response.usage_details))
|
||||
contents.append(Content.from_usage(usage_details=response.usage_details)) # type: ignore[arg-type]
|
||||
yield ChatResponseUpdate(
|
||||
response_id=response.response_id,
|
||||
contents=contents,
|
||||
@@ -472,37 +468,41 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
blocks.append(block)
|
||||
return blocks
|
||||
|
||||
def _convert_content_to_bedrock_block(self, content: Contents) -> dict[str, Any] | None:
|
||||
if isinstance(content, TextContent):
|
||||
return {"text": content.text}
|
||||
if isinstance(content, FunctionCallContent):
|
||||
arguments = content.parse_arguments() or {}
|
||||
return {
|
||||
"toolUse": {
|
||||
"toolUseId": content.call_id or self._generate_tool_call_id(),
|
||||
"name": content.name,
|
||||
"input": arguments,
|
||||
def _convert_content_to_bedrock_block(self, content: Content) -> dict[str, Any] | None:
|
||||
match content.type:
|
||||
case "text":
|
||||
return {"text": content.text}
|
||||
case "function_call":
|
||||
arguments = content.parse_arguments() or {}
|
||||
return {
|
||||
"toolUse": {
|
||||
"toolUseId": content.call_id or self._generate_tool_call_id(),
|
||||
"name": content.name,
|
||||
"input": arguments,
|
||||
}
|
||||
}
|
||||
}
|
||||
if isinstance(content, FunctionResultContent):
|
||||
tool_result_block = {
|
||||
"toolResult": {
|
||||
"toolUseId": content.call_id,
|
||||
"content": self._convert_tool_result_to_blocks(content.result),
|
||||
"status": "error" if content.exception else "success",
|
||||
case "function_result":
|
||||
tool_result_block = {
|
||||
"toolResult": {
|
||||
"toolUseId": content.call_id,
|
||||
"content": self._convert_tool_result_to_blocks(content.result),
|
||||
"status": "error" if content.exception else "success",
|
||||
}
|
||||
}
|
||||
}
|
||||
if content.exception:
|
||||
tool_result = tool_result_block["toolResult"]
|
||||
existing_content = tool_result.get("content")
|
||||
content_list: list[dict[str, Any]]
|
||||
if isinstance(existing_content, list):
|
||||
content_list = existing_content
|
||||
else:
|
||||
content_list = []
|
||||
tool_result["content"] = content_list
|
||||
content_list.append({"text": str(content.exception)})
|
||||
return tool_result_block
|
||||
if content.exception:
|
||||
tool_result = tool_result_block["toolResult"]
|
||||
existing_content = tool_result.get("content")
|
||||
content_list: list[dict[str, Any]]
|
||||
if isinstance(existing_content, list):
|
||||
content_list = existing_content
|
||||
else:
|
||||
content_list = []
|
||||
tool_result["content"] = content_list
|
||||
content_list.append({"text": str(content.exception)})
|
||||
return tool_result_block
|
||||
case _:
|
||||
# Bedrock does not support other content types at this time
|
||||
pass
|
||||
return None
|
||||
|
||||
def _convert_tool_result_to_blocks(self, result: Any) -> list[dict[str, Any]]:
|
||||
@@ -531,7 +531,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
return {"text": value}
|
||||
if isinstance(value, (int, float, bool)) or value is None:
|
||||
return {"json": value}
|
||||
if isinstance(value, TextContent) and getattr(value, "text", None):
|
||||
if isinstance(value, Content) and value.type == "text":
|
||||
return {"text": value.text}
|
||||
if hasattr(value, "to_dict"):
|
||||
try:
|
||||
@@ -586,23 +586,23 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
def _parse_usage(self, usage: dict[str, Any] | None) -> UsageDetails | None:
|
||||
if not usage:
|
||||
return None
|
||||
details = UsageDetails()
|
||||
details: UsageDetails = {}
|
||||
if (input_tokens := usage.get("inputTokens")) is not None:
|
||||
details.input_token_count = input_tokens
|
||||
details["input_token_count"] = input_tokens
|
||||
if (output_tokens := usage.get("outputTokens")) is not None:
|
||||
details.output_token_count = output_tokens
|
||||
details["output_token_count"] = output_tokens
|
||||
if (total_tokens := usage.get("totalTokens")) is not None:
|
||||
details.additional_counts["bedrock.total_tokens"] = total_tokens
|
||||
details["total_token_count"] = total_tokens
|
||||
return details
|
||||
|
||||
def _parse_message_contents(self, content_blocks: Sequence[MutableMapping[str, Any]]) -> list[Any]:
|
||||
contents: list[Any] = []
|
||||
for block in content_blocks:
|
||||
if text_value := block.get("text"):
|
||||
contents.append(TextContent(text=text_value, raw_representation=block))
|
||||
contents.append(Content.from_text(text=text_value, raw_representation=block))
|
||||
continue
|
||||
if (json_value := block.get("json")) is not None:
|
||||
contents.append(TextContent(text=json.dumps(json_value), raw_representation=block))
|
||||
contents.append(Content.from_text(text=json.dumps(json_value), raw_representation=block))
|
||||
continue
|
||||
tool_use = block.get("toolUse")
|
||||
if isinstance(tool_use, MutableMapping):
|
||||
@@ -610,7 +610,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
if not tool_name:
|
||||
raise ServiceInvalidResponseError("Bedrock response missing required tool name in toolUse block.")
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=tool_use.get("toolUseId") or self._generate_tool_call_id(),
|
||||
name=tool_name,
|
||||
arguments=tool_use.get("input"),
|
||||
@@ -626,10 +626,10 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
exception = RuntimeError(f"Bedrock tool result status: {status}")
|
||||
result_value = self._convert_bedrock_tool_result_to_value(tool_result.get("content"))
|
||||
contents.append(
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=tool_result.get("toolUseId") or self._generate_tool_call_id(),
|
||||
result=result_value,
|
||||
exception=exception,
|
||||
exception=str(exception) if exception else None, # type: ignore[arg-type]
|
||||
raw_representation=block,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatMessage, Role, TextContent
|
||||
from agent_framework import ChatMessage, Content, Role
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
|
||||
from agent_framework_bedrock import BedrockChatClient
|
||||
@@ -42,8 +42,8 @@ def test_get_response_invokes_bedrock_runtime() -> None:
|
||||
)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are concise.")]),
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="hello")]),
|
||||
ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="You are concise.")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="hello")]),
|
||||
]
|
||||
|
||||
response = asyncio.run(client.get_response(messages=messages, options={"max_tokens": 32}))
|
||||
@@ -53,7 +53,7 @@ def test_get_response_invokes_bedrock_runtime() -> None:
|
||||
assert payload["modelId"] == "amazon.titan-text"
|
||||
assert payload["messages"][0]["content"][0]["text"] == "hello"
|
||||
assert response.messages[0].contents[0].text == "Bedrock says hi"
|
||||
assert response.usage_details and response.usage_details.input_token_count == 10
|
||||
assert response.usage_details and response.usage_details["input_token_count"] == 10
|
||||
|
||||
|
||||
def test_build_request_requires_non_system_messages() -> None:
|
||||
@@ -63,7 +63,7 @@ def test_build_request_requires_non_system_messages() -> None:
|
||||
client=_StubBedrockRuntime(),
|
||||
)
|
||||
|
||||
messages = [ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="Only system text")])]
|
||||
messages = [ChatMessage(role=Role.SYSTEM, contents=[Content.from_text(text="Only system text")])]
|
||||
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
client._prepare_options(messages, {})
|
||||
|
||||
@@ -9,10 +9,8 @@ from agent_framework import (
|
||||
AIFunction,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -49,7 +47,7 @@ def test_build_request_includes_tool_config() -> None:
|
||||
"tools": [tool],
|
||||
"tool_choice": {"mode": "required", "required_function_name": "get_weather"},
|
||||
}
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="hi")])]
|
||||
messages = [ChatMessage(role=Role.USER, contents=[Content.from_text(text="hi")])]
|
||||
|
||||
request = client._prepare_options(messages, options)
|
||||
|
||||
@@ -61,14 +59,16 @@ def test_build_request_serializes_tool_history() -> None:
|
||||
client = _build_client()
|
||||
options: ChatOptions = {}
|
||||
messages = [
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="how's weather?")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="how's weather?")]),
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[FunctionCallContent(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}')
|
||||
],
|
||||
),
|
||||
ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[FunctionResultContent(call_id="call-1", result={"answer": "72F"})],
|
||||
contents=[Content.from_function_result(call_id="call-1", result={"answer": "72F"})],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -101,9 +101,9 @@ def test_process_response_parses_tool_use_and_result() -> None:
|
||||
chat_response = client._process_converse_response(response)
|
||||
contents = chat_response.messages[0].contents
|
||||
|
||||
assert isinstance(contents[0], FunctionCallContent)
|
||||
assert contents[0].type == "function_call"
|
||||
assert contents[0].name == "get_weather"
|
||||
assert isinstance(contents[1], TextContent)
|
||||
assert contents[1].type == "text"
|
||||
assert chat_response.finish_reason == client._map_finish_reason("tool_use")
|
||||
|
||||
|
||||
@@ -131,5 +131,5 @@ def test_process_response_parses_tool_result() -> None:
|
||||
chat_response = client._process_converse_response(response)
|
||||
contents = chat_response.messages[0].contents
|
||||
|
||||
assert isinstance(contents[0], FunctionResultContent)
|
||||
assert contents[0].type == "function_result"
|
||||
assert contents[0].result == {"answer": 42}
|
||||
|
||||
@@ -8,12 +8,8 @@ from collections.abc import Awaitable, Callable, Sequence
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
DataContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from chatkit.types import (
|
||||
AssistantMessageItem,
|
||||
@@ -91,8 +87,8 @@ class ThreadItemConverter:
|
||||
if isinstance(content_part, UserMessageTextContent):
|
||||
text_content += content_part.text
|
||||
|
||||
# Convert attachments to DataContent or UriContent
|
||||
data_contents: list[DataContent | UriContent] = []
|
||||
# Convert attachments to Content
|
||||
data_contents: list[Content] = []
|
||||
if item.attachments:
|
||||
for attachment in item.attachments:
|
||||
content = await self.attachment_to_message_content(attachment)
|
||||
@@ -108,9 +104,9 @@ class ThreadItemConverter:
|
||||
user_message = ChatMessage(role=Role.USER, text=text_content.strip())
|
||||
else:
|
||||
# Build contents list with both text and attachments
|
||||
contents: list[TextContent | DataContent | UriContent] = []
|
||||
contents: list[Content] = []
|
||||
if text_content.strip():
|
||||
contents.append(TextContent(text=text_content.strip()))
|
||||
contents.append(Content.from_text(text=text_content.strip()))
|
||||
contents.extend(data_contents)
|
||||
user_message = ChatMessage(role=Role.USER, contents=contents)
|
||||
|
||||
@@ -126,7 +122,7 @@ class ThreadItemConverter:
|
||||
|
||||
return messages
|
||||
|
||||
async def attachment_to_message_content(self, attachment: Attachment) -> DataContent | UriContent | None:
|
||||
async def attachment_to_message_content(self, attachment: Attachment) -> Content | None:
|
||||
"""Convert a ChatKit attachment to Agent Framework content.
|
||||
|
||||
This method is called internally by `user_message_to_input()` to handle attachments.
|
||||
@@ -169,14 +165,14 @@ class ThreadItemConverter:
|
||||
if self.attachment_data_fetcher is not None:
|
||||
try:
|
||||
data = await self.attachment_data_fetcher(attachment.id)
|
||||
return DataContent(data=data, media_type=attachment.mime_type)
|
||||
return Content.from_data(data=data, media_type=attachment.mime_type)
|
||||
except Exception as e:
|
||||
# If fetch fails, fall through to URL-based approach
|
||||
logger.debug(f"Failed to fetch attachment data for {attachment.id}: {e}")
|
||||
|
||||
# For ImageAttachment, try to use preview_url
|
||||
if isinstance(attachment, ImageAttachment) and attachment.preview_url:
|
||||
return UriContent(uri=str(attachment.preview_url), media_type=attachment.mime_type)
|
||||
return Content.from_uri(uri=str(attachment.preview_url), media_type=attachment.mime_type)
|
||||
|
||||
# For FileAttachment without data fetcher, skip the attachment
|
||||
# Subclasses can override this method to provide custom handling
|
||||
@@ -220,7 +216,7 @@ class ThreadItemConverter:
|
||||
"""
|
||||
return ChatMessage(role=Role.SYSTEM, text=f"<HIDDEN_CONTEXT>{item.content}</HIDDEN_CONTEXT>")
|
||||
|
||||
def tag_to_message_content(self, tag: UserMessageTagContent) -> TextContent:
|
||||
def tag_to_message_content(self, tag: UserMessageTagContent) -> Content:
|
||||
"""Convert a ChatKit tag (@-mention) to Agent Framework content.
|
||||
|
||||
This method is called internally by `user_message_to_input()` to handle tags.
|
||||
@@ -248,10 +244,10 @@ class ThreadItemConverter:
|
||||
type="input_tag", id="tag_1", text="john", data={"name": "John Doe"}, interactive=False
|
||||
)
|
||||
content = converter.tag_to_message_content(tag)
|
||||
# Returns: TextContent(text="<TAG>Name:John Doe</TAG>")
|
||||
# Returns: Content.from_text(text="<TAG>Name:John Doe</TAG>")
|
||||
"""
|
||||
name = getattr(tag.data, "name", tag.text if hasattr(tag, "text") else "unknown")
|
||||
return TextContent(text=f"<TAG>Name:{name}</TAG>")
|
||||
return Content.from_text(text=f"<TAG>Name:{name}</TAG>")
|
||||
|
||||
def task_to_input(self, item: TaskItem) -> ChatMessage | list[ChatMessage] | None:
|
||||
"""Convert a ChatKit TaskItem to Agent Framework ChatMessage(s).
|
||||
@@ -448,7 +444,7 @@ class ThreadItemConverter:
|
||||
function_call_msg = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=item.call_id,
|
||||
name=item.name,
|
||||
arguments=json.dumps(item.arguments),
|
||||
@@ -460,7 +456,7 @@ class ThreadItemConverter:
|
||||
function_result_msg = ChatMessage(
|
||||
role=Role.TOOL,
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=item.call_id,
|
||||
result=json.dumps(item.output) if item.output is not None else "",
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import uuid
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Callable
|
||||
from datetime import datetime
|
||||
|
||||
from agent_framework import AgentResponseUpdate, TextContent
|
||||
from agent_framework import AgentResponseUpdate
|
||||
from chatkit.types import (
|
||||
AssistantMessageContent,
|
||||
AssistantMessageContentPartTextDelta,
|
||||
@@ -77,7 +77,7 @@ async def stream_agent_response(
|
||||
if update.contents:
|
||||
for content in update.contents:
|
||||
# Handle text content - only TextContent has a text attribute
|
||||
if isinstance(content, TextContent) and content.text is not None:
|
||||
if content.type == "text" and content.text is not None:
|
||||
# Yield incremental text delta for streaming display
|
||||
yield ThreadItemUpdated(
|
||||
type="thread.item.updated",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from agent_framework import ChatMessage, Role, TextContent
|
||||
from agent_framework import ChatMessage, Role
|
||||
from chatkit.types import UserMessageTextContent
|
||||
|
||||
from agent_framework_chatkit import ThreadItemConverter, simple_to_agent_input
|
||||
@@ -133,7 +133,7 @@ class TestThreadItemConverter:
|
||||
)
|
||||
|
||||
result = converter.tag_to_message_content(tag)
|
||||
assert isinstance(result, TextContent)
|
||||
assert result.type == "text"
|
||||
# Since data is a dict, getattr won't work, so it will fall back to text
|
||||
assert result.text == "<TAG>Name:john</TAG>"
|
||||
|
||||
@@ -150,7 +150,7 @@ class TestThreadItemConverter:
|
||||
)
|
||||
|
||||
result = converter.tag_to_message_content(tag)
|
||||
assert isinstance(result, TextContent)
|
||||
assert result.type == "text"
|
||||
assert result.text == "<TAG>Name:jane</TAG>"
|
||||
|
||||
async def test_attachment_to_message_content_file_without_fetcher(self, converter):
|
||||
@@ -169,7 +169,6 @@ class TestThreadItemConverter:
|
||||
|
||||
async def test_attachment_to_message_content_image_with_preview_url(self, converter):
|
||||
"""Test that ImageAttachment with preview_url creates UriContent."""
|
||||
from agent_framework import UriContent
|
||||
from chatkit.types import ImageAttachment
|
||||
|
||||
attachment = ImageAttachment(
|
||||
@@ -181,13 +180,12 @@ class TestThreadItemConverter:
|
||||
)
|
||||
|
||||
result = await converter.attachment_to_message_content(attachment)
|
||||
assert isinstance(result, UriContent)
|
||||
assert result.type == "uri"
|
||||
assert result.uri == "https://example.com/photo.jpg"
|
||||
assert result.media_type == "image/jpeg"
|
||||
|
||||
async def test_attachment_to_message_content_with_data_fetcher(self):
|
||||
"""Test attachment conversion with data fetcher."""
|
||||
from agent_framework import DataContent
|
||||
from chatkit.types import FileAttachment
|
||||
|
||||
# Mock data fetcher
|
||||
@@ -204,14 +202,13 @@ class TestThreadItemConverter:
|
||||
)
|
||||
|
||||
result = await converter.attachment_to_message_content(attachment)
|
||||
assert isinstance(result, DataContent)
|
||||
assert result.type == "data"
|
||||
assert result.media_type == "application/pdf"
|
||||
|
||||
async def test_to_agent_input_with_image_attachment(self):
|
||||
"""Test converting user message with text and image attachment."""
|
||||
from datetime import datetime
|
||||
|
||||
from agent_framework import UriContent
|
||||
from chatkit.types import ImageAttachment, UserMessageItem
|
||||
|
||||
attachment = ImageAttachment(
|
||||
@@ -241,11 +238,11 @@ class TestThreadItemConverter:
|
||||
assert len(message.contents) == 2
|
||||
|
||||
# First content should be text
|
||||
assert isinstance(message.contents[0], TextContent)
|
||||
assert message.contents[0].type == "text"
|
||||
assert message.contents[0].text == "Check out this photo!"
|
||||
|
||||
# Second content should be UriContent for the image
|
||||
assert isinstance(message.contents[1], UriContent)
|
||||
assert message.contents[1].type == "uri"
|
||||
assert message.contents[1].uri == "https://example.com/photo.jpg"
|
||||
assert message.contents[1].media_type == "image/jpeg"
|
||||
|
||||
@@ -253,7 +250,6 @@ class TestThreadItemConverter:
|
||||
"""Test converting user message with file attachment using data fetcher."""
|
||||
from datetime import datetime
|
||||
|
||||
from agent_framework import DataContent
|
||||
from chatkit.types import FileAttachment, UserMessageItem
|
||||
|
||||
attachment = FileAttachment(
|
||||
@@ -285,10 +281,10 @@ class TestThreadItemConverter:
|
||||
assert len(message.contents) == 2
|
||||
|
||||
# First content should be text
|
||||
assert isinstance(message.contents[0], TextContent)
|
||||
assert message.contents[0].type == "text"
|
||||
|
||||
# Second content should be DataContent for the file
|
||||
assert isinstance(message.contents[1], DataContent)
|
||||
assert message.contents[1].type == "data"
|
||||
assert message.contents[1].media_type == "application/pdf"
|
||||
|
||||
def test_task_to_input(self, converter):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from agent_framework import AgentResponseUpdate, Role, TextContent
|
||||
from agent_framework import AgentResponseUpdate, Content, Role
|
||||
from chatkit.types import (
|
||||
ThreadItemAddedEvent,
|
||||
ThreadItemDoneEvent,
|
||||
@@ -34,7 +34,7 @@ class TestStreamAgentResponse:
|
||||
"""Test streaming single text update."""
|
||||
|
||||
async def single_update_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Hello world")])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello world")])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(single_update_stream(), thread_id="test_thread"):
|
||||
@@ -59,8 +59,8 @@ class TestStreamAgentResponse:
|
||||
"""Test streaming multiple text updates."""
|
||||
|
||||
async def multiple_updates_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Hello ")])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="world!")])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello ")])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="world!")])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(multiple_updates_stream(), thread_id="test_thread"):
|
||||
@@ -91,7 +91,7 @@ class TestStreamAgentResponse:
|
||||
return f"custom_{item_type}_123"
|
||||
|
||||
async def single_update_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[TextContent(text="Test")])
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[Content.from_text(text="Test")])
|
||||
|
||||
events = []
|
||||
async for event in stream_agent_response(
|
||||
@@ -125,9 +125,10 @@ class TestStreamAgentResponse:
|
||||
async def test_stream_non_text_content(self):
|
||||
"""Test streaming updates with non-text content."""
|
||||
# Mock a content object without text attribute
|
||||
non_text_content = Mock()
|
||||
non_text_content = Mock(spec=Content)
|
||||
non_text_content.type = "image"
|
||||
# Don't set text attribute
|
||||
del non_text_content.text
|
||||
non_text_content.text = None
|
||||
|
||||
async def non_text_stream():
|
||||
yield AgentResponseUpdate(role=Role.ASSISTANT, contents=[non_text_content])
|
||||
|
||||
@@ -10,9 +10,9 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
ContextProvider,
|
||||
Role,
|
||||
TextContent,
|
||||
normalize_messages,
|
||||
)
|
||||
from agent_framework._pydantic import AFBaseSettings
|
||||
@@ -332,7 +332,7 @@ class CopilotStudioAgent(BaseAgent):
|
||||
):
|
||||
yield ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[TextContent(activity.text)],
|
||||
contents=[Content.from_text(activity.text)],
|
||||
author_name=activity.from_property.name if activity.from_property else None,
|
||||
message_id=activity.id,
|
||||
raw_representation=activity,
|
||||
|
||||
@@ -4,14 +4,7 @@ from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
AgentThread,
|
||||
ChatMessage,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content, Role
|
||||
from agent_framework.exceptions import ServiceException, ServiceInitializationError
|
||||
from microsoft_agents.copilotstudio.client import CopilotClient
|
||||
|
||||
@@ -136,7 +129,7 @@ class TestCopilotStudioAgent:
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 1
|
||||
content = response.messages[0].contents[0]
|
||||
assert isinstance(content, TextContent)
|
||||
assert content.type == "text"
|
||||
assert content.text == "Test response"
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
|
||||
@@ -150,13 +143,13 @@ class TestCopilotStudioAgent:
|
||||
mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity])
|
||||
mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity])
|
||||
|
||||
chat_message = ChatMessage(role=Role.USER, contents=[TextContent("test message")])
|
||||
chat_message = ChatMessage(role=Role.USER, contents=[Content.from_text("test message")])
|
||||
response = await agent.run(chat_message)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert len(response.messages) == 1
|
||||
content = response.messages[0].contents[0]
|
||||
assert isinstance(content, TextContent)
|
||||
assert content.type == "text"
|
||||
assert content.text == "Test response"
|
||||
assert response.messages[0].role == Role.ASSISTANT
|
||||
|
||||
@@ -206,7 +199,7 @@ class TestCopilotStudioAgent:
|
||||
async for response in agent.run_stream("test message"):
|
||||
assert isinstance(response, AgentResponseUpdate)
|
||||
content = response.contents[0]
|
||||
assert isinstance(content, TextContent)
|
||||
assert content.type == "text"
|
||||
assert content.text == "Streaming response"
|
||||
response_count += 1
|
||||
|
||||
@@ -233,7 +226,7 @@ class TestCopilotStudioAgent:
|
||||
async for response in agent.run_stream("test message", thread=thread):
|
||||
assert isinstance(response, AgentResponseUpdate)
|
||||
content = response.contents[0]
|
||||
assert isinstance(content, TextContent)
|
||||
assert content.type == "text"
|
||||
assert content.text == "Streaming response"
|
||||
response_count += 1
|
||||
|
||||
|
||||
@@ -1140,9 +1140,9 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
|
||||
# Convert result to MCP content
|
||||
if isinstance(result, str):
|
||||
return [types.TextContent(type="text", text=result)]
|
||||
return [types.TextContent(type="text", text=result)] # type: ignore[attr-defined]
|
||||
|
||||
return [types.TextContent(type="text", text=str(result))]
|
||||
return [types.TextContent(type="text", text=str(result))] # type: ignore[attr-defined]
|
||||
|
||||
@server.set_logging_level() # type: ignore
|
||||
async def _set_logging_level(level: types.LoggingLevel) -> None: # type: ignore
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
@@ -29,13 +30,8 @@ from ._tools import (
|
||||
)
|
||||
from ._types import (
|
||||
ChatMessage,
|
||||
Contents,
|
||||
DataContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from .exceptions import ToolException, ToolExecutionException
|
||||
|
||||
@@ -82,7 +78,7 @@ def _parse_message_from_mcp(
|
||||
|
||||
def _parse_contents_from_mcp_tool_result(
|
||||
mcp_type: types.CallToolResult,
|
||||
) -> list[Contents]:
|
||||
) -> list[Content]:
|
||||
"""Parse an MCP CallToolResult into Agent Framework content types.
|
||||
|
||||
This function extracts the complete _meta field from CallToolResult objects
|
||||
@@ -147,25 +143,27 @@ def _parse_content_from_mcp(
|
||||
| types.ToolUseContent
|
||||
| types.ToolResultContent
|
||||
],
|
||||
) -> list[Contents]:
|
||||
) -> list[Content]:
|
||||
"""Parse an MCP type into an Agent Framework type."""
|
||||
mcp_types = mcp_type if isinstance(mcp_type, Sequence) else [mcp_type]
|
||||
return_types: list[Contents] = []
|
||||
return_types: list[Content] = []
|
||||
for mcp_type in mcp_types:
|
||||
match mcp_type:
|
||||
case types.TextContent():
|
||||
return_types.append(TextContent(text=mcp_type.text, raw_representation=mcp_type))
|
||||
return_types.append(Content.from_text(text=mcp_type.text, raw_representation=mcp_type))
|
||||
case types.ImageContent() | types.AudioContent():
|
||||
# MCP protocol uses base64-encoded strings, convert to bytes
|
||||
data_bytes = base64.b64decode(mcp_type.data) if isinstance(mcp_type.data, str) else mcp_type.data
|
||||
return_types.append(
|
||||
DataContent(
|
||||
data=mcp_type.data,
|
||||
Content.from_data(
|
||||
data=data_bytes,
|
||||
media_type=mcp_type.mimeType,
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
)
|
||||
case types.ResourceLink():
|
||||
return_types.append(
|
||||
UriContent(
|
||||
Content.from_uri(
|
||||
uri=str(mcp_type.uri),
|
||||
media_type=mcp_type.mimeType or "application/json",
|
||||
raw_representation=mcp_type,
|
||||
@@ -173,7 +171,7 @@ def _parse_content_from_mcp(
|
||||
)
|
||||
case types.ToolUseContent():
|
||||
return_types.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=mcp_type.id,
|
||||
name=mcp_type.name,
|
||||
arguments=mcp_type.input,
|
||||
@@ -182,12 +180,12 @@ def _parse_content_from_mcp(
|
||||
)
|
||||
case types.ToolResultContent():
|
||||
return_types.append(
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id=mcp_type.toolUseId,
|
||||
result=_parse_content_from_mcp(mcp_type.content)
|
||||
if mcp_type.content
|
||||
else mcp_type.structuredContent,
|
||||
exception=Exception() if mcp_type.isError else None,
|
||||
exception=str(Exception()) if mcp_type.isError else None, # type: ignore[arg-type]
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
)
|
||||
@@ -195,7 +193,7 @@ def _parse_content_from_mcp(
|
||||
match mcp_type.resource:
|
||||
case types.TextResourceContents():
|
||||
return_types.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=mcp_type.resource.text,
|
||||
raw_representation=mcp_type,
|
||||
additional_properties=(
|
||||
@@ -205,7 +203,7 @@ def _parse_content_from_mcp(
|
||||
)
|
||||
case types.BlobResourceContents():
|
||||
return_types.append(
|
||||
DataContent(
|
||||
Content.from_uri(
|
||||
uri=mcp_type.resource.blob,
|
||||
media_type=mcp_type.resource.mimeType,
|
||||
raw_representation=mcp_type,
|
||||
@@ -218,45 +216,41 @@ def _parse_content_from_mcp(
|
||||
|
||||
|
||||
def _prepare_content_for_mcp(
|
||||
content: Contents,
|
||||
content: Content,
|
||||
) -> types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink | None:
|
||||
"""Prepare an Agent Framework content type for MCP."""
|
||||
match content:
|
||||
case TextContent():
|
||||
return types.TextContent(type="text", text=content.text)
|
||||
case DataContent():
|
||||
if content.media_type and content.media_type.startswith("image/"):
|
||||
return types.ImageContent(type="image", data=content.uri, mimeType=content.media_type)
|
||||
if content.media_type and content.media_type.startswith("audio/"):
|
||||
return types.AudioContent(type="audio", data=content.uri, mimeType=content.media_type)
|
||||
if content.media_type and content.media_type.startswith("application/"):
|
||||
return types.EmbeddedResource(
|
||||
type="resource",
|
||||
resource=types.BlobResourceContents(
|
||||
blob=content.uri,
|
||||
mimeType=content.media_type,
|
||||
# uri's are not limited in MCP but they have to be set.
|
||||
# the uri of data content, contains the data uri, which
|
||||
# is not the uri meant here, UriContent would match this.
|
||||
uri=(
|
||||
content.additional_properties.get("uri", "af://binary")
|
||||
if content.additional_properties
|
||||
else "af://binary"
|
||||
), # type: ignore[reportArgumentType]
|
||||
),
|
||||
)
|
||||
return None
|
||||
case UriContent():
|
||||
return types.ResourceLink(
|
||||
type="resource_link",
|
||||
uri=content.uri, # type: ignore[reportArgumentType]
|
||||
mimeType=content.media_type,
|
||||
name=(
|
||||
content.additional_properties.get("name", "Unknown") if content.additional_properties else "Unknown"
|
||||
if content.type == "text":
|
||||
return types.TextContent(type="text", text=content.text) # type: ignore[attr-defined]
|
||||
if content.type == "data":
|
||||
if content.media_type and content.media_type.startswith("image/"): # type: ignore[attr-defined]
|
||||
return types.ImageContent(type="image", data=content.uri, mimeType=content.media_type) # type: ignore[attr-defined]
|
||||
if content.media_type and content.media_type.startswith("audio/"): # type: ignore[attr-defined]
|
||||
return types.AudioContent(type="audio", data=content.uri, mimeType=content.media_type) # type: ignore[attr-defined]
|
||||
if content.media_type and content.media_type.startswith("application/"): # type: ignore[attr-defined]
|
||||
return types.EmbeddedResource(
|
||||
type="resource",
|
||||
resource=types.BlobResourceContents(
|
||||
blob=content.uri, # type: ignore[attr-defined]
|
||||
mimeType=content.media_type, # type: ignore[attr-defined]
|
||||
# uri's are not limited in MCP but they have to be set.
|
||||
# the uri of data content, contains the data uri, which
|
||||
# is not the uri meant here, UriContent would match this.
|
||||
uri=(
|
||||
content.additional_properties.get("uri", "af://binary")
|
||||
if content.additional_properties
|
||||
else "af://binary"
|
||||
), # type: ignore[reportArgumentType]
|
||||
),
|
||||
)
|
||||
case _:
|
||||
return None
|
||||
return None
|
||||
if content.type == "uri":
|
||||
return types.ResourceLink(
|
||||
type="resource_link",
|
||||
uri=content.uri, # type: ignore[reportArgumentType,attr-defined]
|
||||
mimeType=content.media_type, # type: ignore[attr-defined]
|
||||
name=(content.additional_properties.get("name", "Unknown") if content.additional_properties else "Unknown"),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _prepare_message_for_mcp(
|
||||
@@ -650,7 +644,7 @@ class MCPTool:
|
||||
input_model = _get_input_model_from_mcp_tool(tool)
|
||||
approval_mode = self._determine_approval_mode(local_name)
|
||||
# Create AIFunctions out of each tool
|
||||
func: AIFunction[BaseModel, list[Contents] | Any | types.CallToolResult] = AIFunction(
|
||||
func: AIFunction[BaseModel, list[Content] | Any | types.CallToolResult] = AIFunction(
|
||||
func=partial(self.call_tool, tool.name),
|
||||
name=local_name,
|
||||
description=tool.description or "",
|
||||
@@ -704,7 +698,7 @@ class MCPTool:
|
||||
inner_exception=ex,
|
||||
) from ex
|
||||
|
||||
async def call_tool(self, tool_name: str, **kwargs: Any) -> list[Contents] | Any | types.CallToolResult:
|
||||
async def call_tool(self, tool_name: str, **kwargs: Any) -> list[Content] | Any | types.CallToolResult:
|
||||
"""Call a tool with the given arguments.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -54,9 +54,7 @@ if TYPE_CHECKING:
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Contents,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
Content,
|
||||
)
|
||||
|
||||
from typing import overload
|
||||
@@ -104,15 +102,15 @@ _NOOP_HISTOGRAM = _NoOpHistogram()
|
||||
|
||||
|
||||
def _parse_inputs(
|
||||
inputs: "Contents | dict[str, Any] | str | list[Contents | dict[str, Any] | str] | None",
|
||||
) -> list["Contents"]:
|
||||
"""Parse the inputs for a tool, ensuring they are of type Contents.
|
||||
inputs: "Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None",
|
||||
) -> list["Content"]:
|
||||
"""Parse the inputs for a tool, ensuring they are of type Content.
|
||||
|
||||
Args:
|
||||
inputs: The inputs to parse. Can be a single item or list of Contents, dicts, or strings.
|
||||
inputs: The inputs to parse. Can be a single item or list of Content, dicts, or strings.
|
||||
|
||||
Returns:
|
||||
A list of Contents objects.
|
||||
A list of Content objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If an unsupported input type is encountered.
|
||||
@@ -122,43 +120,39 @@ def _parse_inputs(
|
||||
return []
|
||||
|
||||
from ._types import (
|
||||
BaseContent,
|
||||
DataContent,
|
||||
HostedFileContent,
|
||||
HostedVectorStoreContent,
|
||||
UriContent,
|
||||
Content,
|
||||
)
|
||||
|
||||
parsed_inputs: list["Contents"] = []
|
||||
parsed_inputs: list["Content"] = []
|
||||
if not isinstance(inputs, list):
|
||||
inputs = [inputs]
|
||||
for input_item in inputs:
|
||||
if isinstance(input_item, str):
|
||||
# If it's a string, we assume it's a URI or similar identifier.
|
||||
# Convert it to a UriContent or similar type as needed.
|
||||
parsed_inputs.append(UriContent(uri=input_item, media_type="text/plain"))
|
||||
parsed_inputs.append(Content.from_uri(uri=input_item, media_type="text/plain"))
|
||||
elif isinstance(input_item, dict):
|
||||
# If it's a dict, we assume it contains properties for a specific content type.
|
||||
# we check if the required keys are present to determine the type.
|
||||
# for instance, if it has "uri" and "media_type", we treat it as UriContent.
|
||||
# if is only has uri, then we treat it as DataContent.
|
||||
# if it only has uri and media_type without a specific type indicator, we treat it as DataContent.
|
||||
# etc.
|
||||
if "uri" in input_item:
|
||||
parsed_inputs.append(
|
||||
UriContent(**input_item) if "media_type" in input_item else DataContent(**input_item)
|
||||
)
|
||||
# Use Content.from_uri for proper URI content, DataContent for backwards compatibility
|
||||
parsed_inputs.append(Content.from_uri(**input_item))
|
||||
elif "file_id" in input_item:
|
||||
parsed_inputs.append(HostedFileContent(**input_item))
|
||||
parsed_inputs.append(Content.from_hosted_file(**input_item))
|
||||
elif "vector_store_id" in input_item:
|
||||
parsed_inputs.append(HostedVectorStoreContent(**input_item))
|
||||
parsed_inputs.append(Content.from_hosted_vector_store(**input_item))
|
||||
elif "data" in input_item:
|
||||
parsed_inputs.append(DataContent(**input_item))
|
||||
# DataContent helper handles both uri and data parameters
|
||||
parsed_inputs.append(Content.from_data(**input_item))
|
||||
else:
|
||||
raise ValueError(f"Unsupported input type: {input_item}")
|
||||
elif isinstance(input_item, BaseContent):
|
||||
elif isinstance(input_item, Content):
|
||||
parsed_inputs.append(input_item)
|
||||
else:
|
||||
raise TypeError(f"Unsupported input type: {type(input_item).__name__}. Expected Contents or dict.")
|
||||
raise TypeError(f"Unsupported input type: {type(input_item).__name__}. Expected Content or dict.")
|
||||
return parsed_inputs
|
||||
|
||||
|
||||
@@ -254,7 +248,7 @@ class HostedCodeInterpreterTool(BaseTool):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
inputs: "Contents | dict[str, Any] | str | list[Contents | dict[str, Any] | str] | None" = None,
|
||||
inputs: "Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None" = None,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -266,8 +260,8 @@ class HostedCodeInterpreterTool(BaseTool):
|
||||
This should mostly be HostedFileContent or HostedVectorStoreContent.
|
||||
Can also be DataContent, depending on the service used.
|
||||
When supplying a list, it can contain:
|
||||
- Contents instances
|
||||
- dicts with properties for Contents (e.g., {"uri": "http://example.com", "media_type": "text/html"})
|
||||
- Content instances
|
||||
- dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"})
|
||||
- strings (which will be converted to UriContent with media_type "text/plain").
|
||||
If None, defaults to an empty list.
|
||||
description: A description of the tool.
|
||||
@@ -503,7 +497,7 @@ class HostedFileSearchTool(BaseTool):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
inputs: "Contents | dict[str, Any] | str | list[Contents | dict[str, Any] | str] | None" = None,
|
||||
inputs: "Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None" = None,
|
||||
max_results: int | None = None,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
@@ -515,8 +509,8 @@ class HostedFileSearchTool(BaseTool):
|
||||
inputs: A list of contents that the tool can accept as input. Defaults to None.
|
||||
This should be one or more HostedVectorStoreContents.
|
||||
When supplying a list, it can contain:
|
||||
- Contents instances
|
||||
- dicts with properties for Contents (e.g., {"uri": "http://example.com", "media_type": "text/html"})
|
||||
- Content instances
|
||||
- dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"})
|
||||
- strings (which will be converted to UriContent with media_type "text/plain").
|
||||
If None, defaults to an empty list.
|
||||
max_results: The maximum number of results to return from the file search.
|
||||
@@ -1480,7 +1474,7 @@ class FunctionExecutionResult:
|
||||
|
||||
__slots__ = ("content", "terminate")
|
||||
|
||||
def __init__(self, content: "Contents", terminate: bool = False) -> None:
|
||||
def __init__(self, content: "Content", terminate: bool = False) -> None:
|
||||
"""Initialize FunctionExecutionResult.
|
||||
|
||||
Args:
|
||||
@@ -1492,7 +1486,7 @@ class FunctionExecutionResult:
|
||||
|
||||
|
||||
async def _auto_invoke_function(
|
||||
function_call_content: "FunctionCallContent | FunctionApprovalResponseContent",
|
||||
function_call_content: "Content",
|
||||
custom_args: dict[str, Any] | None = None,
|
||||
*,
|
||||
config: FunctionInvocationConfiguration,
|
||||
@@ -1500,7 +1494,7 @@ async def _auto_invoke_function(
|
||||
sequence_index: int | None = None,
|
||||
request_index: int | None = None,
|
||||
middleware_pipeline: Any = None, # Optional MiddlewarePipeline
|
||||
) -> "FunctionExecutionResult | Contents":
|
||||
) -> "FunctionExecutionResult | Content":
|
||||
"""Invoke a function call requested by the agent, applying middleware that is defined.
|
||||
|
||||
Args:
|
||||
@@ -1516,41 +1510,42 @@ async def _auto_invoke_function(
|
||||
|
||||
Returns:
|
||||
A FunctionExecutionResult wrapping the content and terminate signal,
|
||||
or a Contents object for approval/hosted tool scenarios.
|
||||
or a Content object for approval/hosted tool scenarios.
|
||||
|
||||
Raises:
|
||||
KeyError: If the requested function is not found in the tool map.
|
||||
"""
|
||||
from ._types import Content
|
||||
|
||||
# Note: The scenarios for approval_mode="always_require", declaration_only, and
|
||||
# terminate_on_unknown_calls are all handled in _try_execute_function_calls before
|
||||
# this function is called. This function only handles the actual execution of approved,
|
||||
# non-declaration-only functions.
|
||||
from ._types import FunctionCallContent, FunctionResultContent
|
||||
|
||||
tool: AIFunction[BaseModel, Any] | None = None
|
||||
if function_call_content.type == "function_call":
|
||||
tool = tool_map.get(function_call_content.name)
|
||||
tool = tool_map.get(function_call_content.name) # type: ignore[arg-type]
|
||||
# Tool should exist because _try_execute_function_calls validates this
|
||||
if tool is None:
|
||||
exc = KeyError(f'Function "{function_call_content.name}" not found.')
|
||||
return FunctionExecutionResult(
|
||||
content=FunctionResultContent(
|
||||
call_id=function_call_content.call_id,
|
||||
content=Content.from_function_result(
|
||||
call_id=function_call_content.call_id, # type: ignore[arg-type]
|
||||
result=f'Error: Requested function "{function_call_content.name}" not found.',
|
||||
exception=exc,
|
||||
exception=str(exc), # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Note: Unapproved tools (approved=False) are handled in _replace_approval_contents_with_results
|
||||
# and never reach this function, so we only handle approved=True cases here.
|
||||
inner_call = function_call_content.function_call
|
||||
if not isinstance(inner_call, FunctionCallContent):
|
||||
inner_call = function_call_content.function_call # type: ignore[attr-defined]
|
||||
if inner_call.type != "function_call": # type: ignore[union-attr]
|
||||
return function_call_content
|
||||
tool = tool_map.get(inner_call.name)
|
||||
tool = tool_map.get(inner_call.name) # type: ignore[attr-defined, union-attr, arg-type]
|
||||
if tool is None:
|
||||
# we assume it is a hosted tool
|
||||
return function_call_content
|
||||
function_call_content = inner_call
|
||||
function_call_content = inner_call # type: ignore[assignment]
|
||||
|
||||
parsed_args: dict[str, Any] = dict(function_call_content.parse_arguments() or {})
|
||||
|
||||
@@ -1567,7 +1562,11 @@ async def _auto_invoke_function(
|
||||
if config.include_detailed_errors:
|
||||
message = f"{message} Exception: {exc}"
|
||||
return FunctionExecutionResult(
|
||||
content=FunctionResultContent(call_id=function_call_content.call_id, result=message, exception=exc)
|
||||
content=Content.from_function_result(
|
||||
call_id=function_call_content.call_id, # type: ignore[arg-type]
|
||||
result=message,
|
||||
exception=str(exc), # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
|
||||
if not middleware_pipeline or (
|
||||
@@ -1581,8 +1580,8 @@ async def _auto_invoke_function(
|
||||
**runtime_kwargs if getattr(tool, "_forward_runtime_kwargs", False) else {},
|
||||
)
|
||||
return FunctionExecutionResult(
|
||||
content=FunctionResultContent(
|
||||
call_id=function_call_content.call_id,
|
||||
content=Content.from_function_result(
|
||||
call_id=function_call_content.call_id, # type: ignore[arg-type]
|
||||
result=function_result,
|
||||
)
|
||||
)
|
||||
@@ -1591,7 +1590,11 @@ async def _auto_invoke_function(
|
||||
if config.include_detailed_errors:
|
||||
message = f"{message} Exception: {exc}"
|
||||
return FunctionExecutionResult(
|
||||
content=FunctionResultContent(call_id=function_call_content.call_id, result=message, exception=exc)
|
||||
content=Content.from_function_result(
|
||||
call_id=function_call_content.call_id, # type: ignore[arg-type]
|
||||
result=message,
|
||||
exception=str(exc),
|
||||
)
|
||||
)
|
||||
# Execute through middleware pipeline if available
|
||||
from ._middleware import FunctionInvocationContext
|
||||
@@ -1617,8 +1620,8 @@ async def _auto_invoke_function(
|
||||
final_handler=final_function_handler,
|
||||
)
|
||||
return FunctionExecutionResult(
|
||||
content=FunctionResultContent(
|
||||
call_id=function_call_content.call_id,
|
||||
content=Content.from_function_result(
|
||||
call_id=function_call_content.call_id, # type: ignore[arg-type]
|
||||
result=function_result,
|
||||
),
|
||||
terminate=middleware_context.terminate,
|
||||
@@ -1628,7 +1631,11 @@ async def _auto_invoke_function(
|
||||
if config.include_detailed_errors:
|
||||
message = f"{message} Exception: {exc}"
|
||||
return FunctionExecutionResult(
|
||||
content=FunctionResultContent(call_id=function_call_content.call_id, result=message, exception=exc)
|
||||
content=Content.from_function_result(
|
||||
call_id=function_call_content.call_id, # type: ignore[arg-type]
|
||||
result=message,
|
||||
exception=str(exc), # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1653,14 +1660,14 @@ def _get_tool_map(
|
||||
async def _try_execute_function_calls(
|
||||
custom_args: dict[str, Any],
|
||||
attempt_idx: int,
|
||||
function_calls: Sequence["FunctionCallContent"] | Sequence["FunctionApprovalResponseContent"],
|
||||
function_calls: Sequence["Content"],
|
||||
tools: "ToolProtocol \
|
||||
| Callable[..., Any] \
|
||||
| MutableMapping[str, Any] \
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]",
|
||||
config: FunctionInvocationConfiguration,
|
||||
middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports
|
||||
) -> tuple[Sequence["Contents"], bool]:
|
||||
) -> tuple[Sequence["Content"], bool]:
|
||||
"""Execute multiple function calls concurrently.
|
||||
|
||||
Args:
|
||||
@@ -1673,12 +1680,12 @@ async def _try_execute_function_calls(
|
||||
|
||||
Returns:
|
||||
A tuple of:
|
||||
- A list of Contents containing the results of each function call,
|
||||
- A list of Content containing the results of each function call,
|
||||
or the approval requests if any function requires approval,
|
||||
or the original function calls if any are declaration only.
|
||||
- A boolean indicating whether to terminate the function calling loop.
|
||||
"""
|
||||
from ._types import FunctionApprovalRequestContent, FunctionCallContent
|
||||
from ._types import Content
|
||||
|
||||
tool_map = _get_tool_map(tools)
|
||||
approval_tools = [tool_name for tool_name, tool in tool_map.items() if tool.approval_mode == "always_require"]
|
||||
@@ -1689,27 +1696,27 @@ async def _try_execute_function_calls(
|
||||
approval_needed = False
|
||||
declaration_only_flag = False
|
||||
for fcc in function_calls:
|
||||
if isinstance(fcc, FunctionCallContent) and fcc.name in approval_tools:
|
||||
if fcc.type == "function_call" and fcc.name in approval_tools: # type: ignore[attr-defined]
|
||||
approval_needed = True
|
||||
break
|
||||
if isinstance(fcc, FunctionCallContent) and (fcc.name in declaration_only or fcc.name in additional_tool_names):
|
||||
if fcc.type == "function_call" and (fcc.name in declaration_only or fcc.name in additional_tool_names): # type: ignore[attr-defined]
|
||||
declaration_only_flag = True
|
||||
break
|
||||
if config.terminate_on_unknown_calls and isinstance(fcc, FunctionCallContent) and fcc.name not in tool_map:
|
||||
raise KeyError(f'Error: Requested function "{fcc.name}" not found.')
|
||||
if config.terminate_on_unknown_calls and fcc.type == "function_call" and fcc.name not in tool_map: # type: ignore[attr-defined]
|
||||
raise KeyError(f'Error: Requested function "{fcc.name}" not found.') # type: ignore[attr-defined]
|
||||
if approval_needed:
|
||||
# approval can only be needed for Function Call Contents, not Approval Responses.
|
||||
# approval can only be needed for Function Call Content, not Approval Responses.
|
||||
return (
|
||||
[
|
||||
FunctionApprovalRequestContent(id=fcc.call_id, function_call=fcc)
|
||||
Content.from_function_approval_request(id=fcc.call_id, function_call=fcc) # type: ignore[attr-defined, arg-type]
|
||||
for fcc in function_calls
|
||||
if isinstance(fcc, FunctionCallContent)
|
||||
if fcc.type == "function_call"
|
||||
],
|
||||
False,
|
||||
)
|
||||
if declaration_only_flag:
|
||||
# return the declaration only tools to the user, since we cannot execute them.
|
||||
return ([fcc for fcc in function_calls if isinstance(fcc, FunctionCallContent)], False)
|
||||
return ([fcc for fcc in function_calls if fcc.type == "function_call"], False)
|
||||
|
||||
# Run all function calls concurrently
|
||||
execution_results = await asyncio.gather(*[
|
||||
@@ -1726,7 +1733,7 @@ async def _try_execute_function_calls(
|
||||
])
|
||||
|
||||
# Unpack FunctionExecutionResult wrappers and check for terminate signal
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
should_terminate = False
|
||||
for result in execution_results:
|
||||
if isinstance(result, FunctionExecutionResult):
|
||||
@@ -1734,7 +1741,7 @@ async def _try_execute_function_calls(
|
||||
if result.terminate:
|
||||
should_terminate = True
|
||||
else:
|
||||
# Direct Contents (e.g., from hosted tools)
|
||||
# Direct Content (e.g., from hosted tools)
|
||||
contents.append(result)
|
||||
|
||||
return (contents, should_terminate)
|
||||
@@ -1772,30 +1779,27 @@ def _extract_tools(options: dict[str, Any] | None) -> Any:
|
||||
|
||||
def _collect_approval_responses(
|
||||
messages: "list[ChatMessage]",
|
||||
) -> dict[str, "FunctionApprovalResponseContent"]:
|
||||
) -> dict[str, "Content"]:
|
||||
"""Collect approval responses (both approved and rejected) from messages."""
|
||||
from ._types import ChatMessage, FunctionApprovalResponseContent
|
||||
from ._types import ChatMessage, Content
|
||||
|
||||
fcc_todo: dict[str, FunctionApprovalResponseContent] = {}
|
||||
fcc_todo: dict[str, Content] = {}
|
||||
for msg in messages:
|
||||
for content in msg.contents if isinstance(msg, ChatMessage) else []:
|
||||
# Collect BOTH approved and rejected responses
|
||||
if isinstance(content, FunctionApprovalResponseContent):
|
||||
fcc_todo[content.id] = content
|
||||
if content.type == "function_approval_response":
|
||||
fcc_todo[content.id] = content # type: ignore[attr-defined, index]
|
||||
return fcc_todo
|
||||
|
||||
|
||||
def _replace_approval_contents_with_results(
|
||||
messages: "list[ChatMessage]",
|
||||
fcc_todo: dict[str, "FunctionApprovalResponseContent"],
|
||||
approved_function_results: "list[Contents]",
|
||||
fcc_todo: dict[str, "Content"],
|
||||
approved_function_results: "list[Content]",
|
||||
) -> None:
|
||||
"""Replace approval request/response contents with function call/result contents in-place."""
|
||||
from ._types import (
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
)
|
||||
|
||||
@@ -1803,23 +1807,25 @@ def _replace_approval_contents_with_results(
|
||||
for msg in messages:
|
||||
# First pass - collect existing function call IDs to avoid duplicates
|
||||
existing_call_ids = {
|
||||
content.call_id for content in msg.contents if isinstance(content, FunctionCallContent) and content.call_id
|
||||
content.call_id # type: ignore[union-attr, operator]
|
||||
for content in msg.contents
|
||||
if content.type == "function_call" and content.call_id # type: ignore[attr-defined]
|
||||
}
|
||||
|
||||
# Track approval requests that should be removed (duplicates)
|
||||
contents_to_remove = []
|
||||
|
||||
for content_idx, content in enumerate(msg.contents):
|
||||
if isinstance(content, FunctionApprovalRequestContent):
|
||||
if content.type == "function_approval_request":
|
||||
# Don't add the function call if it already exists (would create duplicate)
|
||||
if content.function_call.call_id in existing_call_ids:
|
||||
if content.function_call.call_id in existing_call_ids: # type: ignore[attr-defined, union-attr, operator]
|
||||
# Just mark for removal - the function call already exists
|
||||
contents_to_remove.append(content_idx)
|
||||
else:
|
||||
# Put back the function call content only if it doesn't exist
|
||||
msg.contents[content_idx] = content.function_call
|
||||
elif isinstance(content, FunctionApprovalResponseContent):
|
||||
if content.approved and content.id in fcc_todo:
|
||||
msg.contents[content_idx] = content.function_call # type: ignore[attr-defined, assignment]
|
||||
elif content.type == "function_approval_response":
|
||||
if content.approved and content.id in fcc_todo: # type: ignore[attr-defined]
|
||||
# Replace with the corresponding result
|
||||
if result_idx < len(approved_function_results):
|
||||
msg.contents[content_idx] = approved_function_results[result_idx]
|
||||
@@ -1828,8 +1834,8 @@ def _replace_approval_contents_with_results(
|
||||
else:
|
||||
# Create a "not approved" result for rejected calls
|
||||
# Use function_call.call_id (the function's ID), not content.id (approval's ID)
|
||||
msg.contents[content_idx] = FunctionResultContent(
|
||||
call_id=content.function_call.call_id,
|
||||
msg.contents[content_idx] = Content.from_function_result(
|
||||
call_id=content.function_call.call_id, # type: ignore[union-attr, arg-type]
|
||||
result="Error: Tool call invocation was rejected by user.",
|
||||
)
|
||||
msg.role = Role.TOOL
|
||||
@@ -1867,9 +1873,6 @@ def _handle_function_calls_response(
|
||||
from ._middleware import extract_and_merge_function_middleware
|
||||
from ._types import (
|
||||
ChatMessage,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
prepare_messages,
|
||||
)
|
||||
|
||||
@@ -1893,7 +1896,7 @@ def _handle_function_calls_response(
|
||||
tools = _extract_tools(options)
|
||||
# Only execute APPROVED function calls, not rejected ones
|
||||
approved_responses = [resp for resp in fcc_todo.values() if resp.approved]
|
||||
approved_function_results: list[Contents] = []
|
||||
approved_function_results: list[Content] = []
|
||||
if approved_responses:
|
||||
results, _ = await _try_execute_function_calls(
|
||||
custom_args=kwargs,
|
||||
@@ -1907,7 +1910,7 @@ def _handle_function_calls_response(
|
||||
if any(
|
||||
fcr.exception is not None
|
||||
for fcr in approved_function_results
|
||||
if isinstance(fcr, FunctionResultContent)
|
||||
if fcr.type == "function_result"
|
||||
):
|
||||
errors_in_a_row += 1
|
||||
# no need to reset the counter here, since this is the start of a new attempt.
|
||||
@@ -1926,13 +1929,11 @@ def _handle_function_calls_response(
|
||||
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in ("thread", "tools", "tool_choice")}
|
||||
response = await func(self, messages=prepped_messages, options=options, **filtered_kwargs)
|
||||
# if there are function calls, we will handle them first
|
||||
function_results = {
|
||||
it.call_id for it in response.messages[0].contents if isinstance(it, FunctionResultContent)
|
||||
}
|
||||
function_results = {it.call_id for it in response.messages[0].contents if it.type == "function_result"}
|
||||
function_calls = [
|
||||
it
|
||||
for it in response.messages[0].contents
|
||||
if isinstance(it, FunctionCallContent) and it.call_id not in function_results
|
||||
if it.type == "function_call" and it.call_id not in function_results
|
||||
]
|
||||
|
||||
if response.conversation_id is not None:
|
||||
@@ -1953,7 +1954,7 @@ def _handle_function_calls_response(
|
||||
config=config,
|
||||
)
|
||||
# Check if we have approval requests or function calls (not results) in the results
|
||||
if any(isinstance(fccr, FunctionApprovalRequestContent) for fccr in function_call_results):
|
||||
if any(fccr.type == "function_approval_request" for fccr in function_call_results):
|
||||
# Add approval requests to the existing assistant message (with tool_calls)
|
||||
# instead of creating a separate tool message
|
||||
from ._types import Role
|
||||
@@ -1965,7 +1966,7 @@ def _handle_function_calls_response(
|
||||
result_message = ChatMessage(role="assistant", contents=function_call_results)
|
||||
response.messages.append(result_message)
|
||||
return response
|
||||
if any(isinstance(fccr, FunctionCallContent) for fccr in function_call_results):
|
||||
if any(fccr.type == "function_call" for fccr in function_call_results):
|
||||
# the function calls are already in the response, so we just continue
|
||||
return response
|
||||
|
||||
@@ -1980,11 +1981,7 @@ def _handle_function_calls_response(
|
||||
response.messages.insert(0, msg)
|
||||
return response
|
||||
|
||||
if any(
|
||||
fcr.exception is not None
|
||||
for fcr in function_call_results
|
||||
if isinstance(fcr, FunctionResultContent)
|
||||
):
|
||||
if any(fcr.exception is not None for fcr in function_call_results if fcr.type == "function_result"):
|
||||
errors_in_a_row += 1
|
||||
if errors_in_a_row >= config.max_consecutive_errors_per_request:
|
||||
logger.warning(
|
||||
@@ -2071,8 +2068,6 @@ def _handle_function_calls_streaming_response(
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
prepare_messages,
|
||||
)
|
||||
|
||||
@@ -2094,7 +2089,7 @@ def _handle_function_calls_streaming_response(
|
||||
tools = _extract_tools(options)
|
||||
# Only execute APPROVED function calls, not rejected ones
|
||||
approved_responses = [resp for resp in fcc_todo.values() if resp.approved]
|
||||
approved_function_results: list[Contents] = []
|
||||
approved_function_results: list[Content] = []
|
||||
if approved_responses:
|
||||
results, _ = await _try_execute_function_calls(
|
||||
custom_args=kwargs,
|
||||
@@ -2108,7 +2103,7 @@ def _handle_function_calls_streaming_response(
|
||||
if any(
|
||||
fcr.exception is not None
|
||||
for fcr in approved_function_results
|
||||
if isinstance(fcr, FunctionResultContent)
|
||||
if fcr.type == "function_result"
|
||||
):
|
||||
errors_in_a_row += 1
|
||||
# no need to reset the counter here, since this is the start of a new attempt.
|
||||
@@ -2124,10 +2119,9 @@ def _handle_function_calls_streaming_response(
|
||||
# efficient check for FunctionCallContent in the updates
|
||||
# if there is at least one, this stops and continuous
|
||||
# if there are no FCC's then it returns
|
||||
from ._types import FunctionApprovalRequestContent
|
||||
|
||||
if not any(
|
||||
isinstance(item, (FunctionCallContent, FunctionApprovalRequestContent))
|
||||
item.type in ("function_call", "function_approval_request")
|
||||
for upd in all_updates
|
||||
for item in upd.contents
|
||||
):
|
||||
@@ -2139,13 +2133,11 @@ def _handle_function_calls_streaming_response(
|
||||
|
||||
response: "ChatResponse" = ChatResponse.from_chat_response_updates(all_updates)
|
||||
# get the function calls (excluding ones that already have results)
|
||||
function_results = {
|
||||
it.call_id for it in response.messages[0].contents if isinstance(it, FunctionResultContent)
|
||||
}
|
||||
function_results = {it.call_id for it in response.messages[0].contents if it.type == "function_result"}
|
||||
function_calls = [
|
||||
it
|
||||
for it in response.messages[0].contents
|
||||
if isinstance(it, FunctionCallContent) and it.call_id not in function_results
|
||||
if it.type == "function_call" and it.call_id not in function_results
|
||||
]
|
||||
|
||||
# When conversation id is present, it means that messages are hosted on the server.
|
||||
@@ -2169,7 +2161,7 @@ def _handle_function_calls_streaming_response(
|
||||
)
|
||||
|
||||
# Check if we have approval requests or function calls (not results) in the results
|
||||
if any(isinstance(fccr, FunctionApprovalRequestContent) for fccr in function_call_results):
|
||||
if any(fccr.type == "function_approval_request" for fccr in function_call_results):
|
||||
# Add approval requests to the existing assistant message (with tool_calls)
|
||||
# instead of creating a separate tool message
|
||||
from ._types import Role
|
||||
@@ -2184,7 +2176,7 @@ def _handle_function_calls_streaming_response(
|
||||
yield ChatResponseUpdate(contents=function_call_results, role="assistant")
|
||||
response.messages.append(result_message)
|
||||
return
|
||||
if any(isinstance(fccr, FunctionCallContent) for fccr in function_call_results):
|
||||
if any(fccr.type == "function_call" for fccr in function_call_results):
|
||||
# the function calls were already yielded.
|
||||
return
|
||||
|
||||
@@ -2195,11 +2187,7 @@ def _handle_function_calls_streaming_response(
|
||||
yield ChatResponseUpdate(contents=function_call_results, role="tool")
|
||||
return
|
||||
|
||||
if any(
|
||||
fcr.exception is not None
|
||||
for fcr in function_call_results
|
||||
if isinstance(fcr, FunctionResultContent)
|
||||
):
|
||||
if any(fcr.exception is not None for fcr in function_call_results if fcr.type == "function_result"):
|
||||
errors_in_a_row += 1
|
||||
if errors_in_a_row >= config.max_consecutive_errors_per_request:
|
||||
logger.warning(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,18 +13,13 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
BaseContent,
|
||||
ChatMessage,
|
||||
Contents,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
UsageDetails,
|
||||
)
|
||||
|
||||
from .._types import add_usage_details
|
||||
from ..exceptions import AgentExecutionException
|
||||
from ._agent_executor import AgentExecutor
|
||||
from ._checkpoint import CheckpointStorage
|
||||
@@ -357,12 +352,12 @@ class WorkflowAgent(BaseAgent):
|
||||
|
||||
args = self.RequestInfoFunctionArgs(request_id=request_id, data=event.data).to_dict()
|
||||
|
||||
function_call = FunctionCallContent(
|
||||
function_call = Content.from_function_call(
|
||||
call_id=request_id,
|
||||
name=self.REQUEST_INFO_FUNCTION_NAME,
|
||||
arguments=args,
|
||||
)
|
||||
approval_request = FunctionApprovalRequestContent(
|
||||
approval_request = Content.from_function_approval_request(
|
||||
id=request_id,
|
||||
function_call=function_call,
|
||||
additional_properties={"request_id": request_id},
|
||||
@@ -385,9 +380,9 @@ class WorkflowAgent(BaseAgent):
|
||||
function_responses: dict[str, Any] = {}
|
||||
for message in input_messages:
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionApprovalResponseContent):
|
||||
if content.type == "function_approval_response":
|
||||
# Parse the function arguments to recover request payload
|
||||
arguments_payload = content.function_call.arguments
|
||||
arguments_payload = content.function_call.arguments # type: ignore[attr-defined, union-attr]
|
||||
if isinstance(arguments_payload, str):
|
||||
try:
|
||||
parsed_args = self.RequestInfoFunctionArgs.from_json(arguments_payload)
|
||||
@@ -402,8 +397,8 @@ class WorkflowAgent(BaseAgent):
|
||||
"FunctionApprovalResponseContent arguments must be a mapping or JSON string."
|
||||
)
|
||||
|
||||
request_id = parsed_args.request_id or content.id
|
||||
if not content.approved:
|
||||
request_id = parsed_args.request_id or content.id # type: ignore[attr-defined]
|
||||
if not content.approved: # type: ignore[attr-defined]
|
||||
raise AgentExecutionException(f"Request '{request_id}' was not approved by the caller.")
|
||||
|
||||
if request_id in self.pending_requests:
|
||||
@@ -412,10 +407,10 @@ class WorkflowAgent(BaseAgent):
|
||||
raise AgentExecutionException(
|
||||
"Only responses for pending requests are allowed when there are outstanding approvals."
|
||||
)
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
request_id = content.call_id
|
||||
elif content.type == "function_result":
|
||||
request_id = content.call_id # type: ignore[attr-defined]
|
||||
if request_id in self.pending_requests:
|
||||
response_data = content.result if hasattr(content, "result") else str(content)
|
||||
response_data = content.result if hasattr(content, "result") else str(content) # type: ignore[attr-defined]
|
||||
function_responses[request_id] = response_data
|
||||
elif bool(self.pending_requests):
|
||||
raise AgentExecutionException(
|
||||
@@ -426,17 +421,17 @@ class WorkflowAgent(BaseAgent):
|
||||
raise AgentExecutionException("Unexpected content type while awaiting request info responses.")
|
||||
return function_responses
|
||||
|
||||
def _extract_contents(self, data: Any) -> list[Contents]:
|
||||
"""Recursively extract Contents from workflow output data."""
|
||||
def _extract_contents(self, data: Any) -> list[Content]:
|
||||
"""Recursively extract Content from workflow output data."""
|
||||
if isinstance(data, ChatMessage):
|
||||
return list(data.contents)
|
||||
if isinstance(data, list):
|
||||
return [c for item in data for c in self._extract_contents(item)]
|
||||
if isinstance(data, BaseContent):
|
||||
return [cast(Contents, data)]
|
||||
if isinstance(data, Content):
|
||||
return [data] # type: ignore[redundant-cast]
|
||||
if isinstance(data, str):
|
||||
return [TextContent(text=data)]
|
||||
return [TextContent(text=str(data))]
|
||||
return [Content.from_text(text=data)]
|
||||
return [Content.from_text(text=str(data))]
|
||||
|
||||
class _ResponseState(TypedDict):
|
||||
"""State for grouping response updates by message_id."""
|
||||
@@ -468,7 +463,7 @@ class WorkflowAgent(BaseAgent):
|
||||
for u in updates:
|
||||
if u.response_id:
|
||||
for content in u.contents:
|
||||
if isinstance(content, FunctionCallContent) and content.call_id:
|
||||
if content.type == "function_call" and content.call_id:
|
||||
call_id_to_response_id[content.call_id] = u.response_id
|
||||
|
||||
# Second pass: group updates, associating FunctionResultContent with their calls
|
||||
@@ -480,7 +475,7 @@ class WorkflowAgent(BaseAgent):
|
||||
# If no response_id, check if this is a FunctionResultContent that matches a call
|
||||
if not effective_response_id:
|
||||
for content in u.contents:
|
||||
if isinstance(content, FunctionResultContent) and content.call_id:
|
||||
if content.type == "function_result" and content.call_id:
|
||||
effective_response_id = call_id_to_response_id.get(content.call_id)
|
||||
if effective_response_id:
|
||||
break
|
||||
@@ -508,13 +503,6 @@ class WorkflowAgent(BaseAgent):
|
||||
except Exception:
|
||||
return (0, v)
|
||||
|
||||
def _sum_usage(a: UsageDetails | None, b: UsageDetails | None) -> UsageDetails | None:
|
||||
if a is None:
|
||||
return b
|
||||
if b is None:
|
||||
return a
|
||||
return a + b
|
||||
|
||||
def _merge_responses(current: AgentResponse | None, incoming: AgentResponse) -> AgentResponse:
|
||||
if current is None:
|
||||
return incoming
|
||||
@@ -534,7 +522,7 @@ class WorkflowAgent(BaseAgent):
|
||||
messages=(current.messages or []) + (incoming.messages or []),
|
||||
response_id=current.response_id or incoming.response_id,
|
||||
created_at=incoming.created_at or current.created_at,
|
||||
usage_details=_sum_usage(current.usage_details, incoming.usage_details),
|
||||
usage_details=add_usage_details(current.usage_details, incoming.usage_details), # type: ignore[arg-type]
|
||||
raw_representation=raw_list if raw_list else None,
|
||||
additional_properties=incoming.additional_properties or current.additional_properties,
|
||||
)
|
||||
@@ -569,7 +557,7 @@ class WorkflowAgent(BaseAgent):
|
||||
if aggregated:
|
||||
final_messages.extend(aggregated.messages)
|
||||
if aggregated.usage_details:
|
||||
merged_usage = _sum_usage(merged_usage, aggregated.usage_details)
|
||||
merged_usage = add_usage_details(merged_usage, aggregated.usage_details) # type: ignore[arg-type]
|
||||
if aggregated.created_at and (
|
||||
not latest_created_at or _parse_dt(aggregated.created_at) > _parse_dt(latest_created_at)
|
||||
):
|
||||
@@ -593,7 +581,7 @@ class WorkflowAgent(BaseAgent):
|
||||
flattened = AgentResponse.from_agent_run_response_updates(global_dangling)
|
||||
final_messages.extend(flattened.messages)
|
||||
if flattened.usage_details:
|
||||
merged_usage = _sum_usage(merged_usage, flattened.usage_details)
|
||||
merged_usage = add_usage_details(merged_usage, flattened.usage_details) # type: ignore[arg-type]
|
||||
if flattened.created_at and (
|
||||
not latest_created_at or _parse_dt(flattened.created_at) > _parse_dt(latest_created_at)
|
||||
):
|
||||
|
||||
@@ -5,7 +5,7 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import FunctionApprovalRequestContent, FunctionApprovalResponseContent
|
||||
from agent_framework import Content
|
||||
|
||||
from .._agents import AgentProtocol, ChatAgent
|
||||
from .._threads import AgentThread
|
||||
@@ -95,8 +95,8 @@ class AgentExecutor(Executor):
|
||||
super().__init__(exec_id)
|
||||
self._agent = agent
|
||||
self._agent_thread = agent_thread or self._agent.get_new_thread()
|
||||
self._pending_agent_requests: dict[str, FunctionApprovalRequestContent] = {}
|
||||
self._pending_responses_to_agent: list[FunctionApprovalResponseContent] = []
|
||||
self._pending_agent_requests: dict[str, Content] = {}
|
||||
self._pending_responses_to_agent: list[Content] = []
|
||||
self._output_response = output_response
|
||||
|
||||
# AgentExecutor maintains an internal cache of messages in between runs
|
||||
@@ -179,8 +179,8 @@ class AgentExecutor(Executor):
|
||||
@response_handler
|
||||
async def handle_user_input_response(
|
||||
self,
|
||||
original_request: FunctionApprovalRequestContent,
|
||||
response: FunctionApprovalResponseContent,
|
||||
original_request: Content,
|
||||
response: Content,
|
||||
ctx: WorkflowContext[AgentExecutorResponse, AgentResponse],
|
||||
) -> None:
|
||||
"""Handle user input responses for function approvals during agent execution.
|
||||
@@ -193,7 +193,7 @@ class AgentExecutor(Executor):
|
||||
ctx: The workflow context for emitting events and outputs.
|
||||
"""
|
||||
self._pending_responses_to_agent.append(response)
|
||||
self._pending_agent_requests.pop(original_request.id, None)
|
||||
self._pending_agent_requests.pop(original_request.id, None) # type: ignore[arg-type]
|
||||
|
||||
if not self._pending_agent_requests:
|
||||
# All pending requests have been resolved; resume agent execution
|
||||
@@ -344,8 +344,8 @@ class AgentExecutor(Executor):
|
||||
# Handle any user input requests
|
||||
if response.user_input_requests:
|
||||
for user_input_request in response.user_input_requests:
|
||||
self._pending_agent_requests[user_input_request.id] = user_input_request
|
||||
await ctx.request_info(user_input_request, FunctionApprovalResponseContent)
|
||||
self._pending_agent_requests[user_input_request.id] = user_input_request # type: ignore[index]
|
||||
await ctx.request_info(user_input_request, Content)
|
||||
return None
|
||||
|
||||
return response
|
||||
@@ -362,7 +362,7 @@ class AgentExecutor(Executor):
|
||||
run_kwargs: dict[str, Any] = await ctx.get_shared_state(WORKFLOW_RUN_KWARGS_KEY)
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
user_input_requests: list[FunctionApprovalRequestContent] = []
|
||||
user_input_requests: list[Content] = []
|
||||
async for update in self._agent.run_stream(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
@@ -387,8 +387,8 @@ class AgentExecutor(Executor):
|
||||
# Handle any user input requests after the streaming completes
|
||||
if user_input_requests:
|
||||
for user_input_request in user_input_requests:
|
||||
self._pending_agent_requests[user_input_request.id] = user_input_request
|
||||
await ctx.request_info(user_input_request, FunctionApprovalResponseContent)
|
||||
self._pending_agent_requests[user_input_request.id] = user_input_request # type: ignore[index]
|
||||
await ctx.request_info(user_input_request, Content)
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
@@ -37,8 +37,6 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat
|
||||
Returns:
|
||||
Cleaned conversation safe for handoff routing
|
||||
"""
|
||||
from agent_framework import FunctionApprovalRequestContent, FunctionCallContent
|
||||
|
||||
cleaned: list[ChatMessage] = []
|
||||
for msg in conversation:
|
||||
# Skip tool response messages entirely
|
||||
@@ -49,7 +47,7 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat
|
||||
has_tool_content = False
|
||||
if msg.contents:
|
||||
has_tool_content = any(
|
||||
isinstance(content, (FunctionApprovalRequestContent, FunctionCallContent)) for content in msg.contents
|
||||
content.type in ("function_approval_request", "function_call") for content in msg.contents
|
||||
)
|
||||
|
||||
# If no tool content, keep original
|
||||
|
||||
@@ -13,10 +13,10 @@ from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
|
||||
from pydantic import ValidationError
|
||||
|
||||
from agent_framework import (
|
||||
Annotation,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CitationAnnotation,
|
||||
TextContent,
|
||||
Content,
|
||||
use_chat_middleware,
|
||||
use_function_invocation,
|
||||
)
|
||||
@@ -267,8 +267,8 @@ class AzureOpenAIChatClient(
|
||||
)
|
||||
|
||||
@override
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
"""Parse the choice into a TextContent object.
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None:
|
||||
"""Parse the choice into a Content object with type='text'.
|
||||
|
||||
Overwritten from OpenAIBaseChatClient to deal with Azure On Your Data function.
|
||||
For docs see:
|
||||
@@ -279,10 +279,10 @@ class AzureOpenAIChatClient(
|
||||
if message is None: # type: ignore
|
||||
return None
|
||||
if hasattr(message, "refusal") and message.refusal:
|
||||
return TextContent(text=message.refusal, raw_representation=choice)
|
||||
return Content.from_text(text=message.refusal, raw_representation=choice)
|
||||
if not message.content:
|
||||
return None
|
||||
text_content = TextContent(text=message.content, raw_representation=choice)
|
||||
text_content = Content.from_text(text=message.content, raw_representation=choice)
|
||||
if not message.model_extra or "context" not in message.model_extra:
|
||||
return text_content
|
||||
|
||||
@@ -304,7 +304,8 @@ class AzureOpenAIChatClient(
|
||||
text_content.annotations = []
|
||||
for citation in citations:
|
||||
text_content.annotations.append(
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
title=citation.get("title", ""),
|
||||
url=citation.get("url", ""),
|
||||
snippet=citation.get("content", ""),
|
||||
|
||||
@@ -40,7 +40,7 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Contents,
|
||||
Content,
|
||||
FinishReason,
|
||||
)
|
||||
|
||||
@@ -1750,8 +1750,10 @@ def _to_otel_message(message: "ChatMessage") -> dict[str, Any]:
|
||||
return {"role": message.role.value, "parts": [_to_otel_part(content) for content in message.contents]}
|
||||
|
||||
|
||||
def _to_otel_part(content: "Contents") -> dict[str, Any] | None:
|
||||
def _to_otel_part(content: "Content") -> dict[str, Any] | None:
|
||||
"""Create a otel representation of a Content."""
|
||||
from ._types import _get_data_bytes_as_str
|
||||
|
||||
match content.type:
|
||||
case "text":
|
||||
return {"type": "text", "content": content.text}
|
||||
@@ -1767,7 +1769,7 @@ def _to_otel_part(content: "Contents") -> dict[str, Any] | None:
|
||||
case "data":
|
||||
return {
|
||||
"type": "blob",
|
||||
"content": content.get_data_bytes_as_str(),
|
||||
"content": _get_data_bytes_as_str(content),
|
||||
"mime_type": content.media_type,
|
||||
"modality": content.media_type.split("/")[0] if content.media_type else None,
|
||||
}
|
||||
@@ -1808,10 +1810,10 @@ def _get_response_attributes(
|
||||
if model_id := getattr(response, "model_id", None):
|
||||
attributes[SpanAttributes.LLM_RESPONSE_MODEL] = model_id
|
||||
if capture_usage and (usage := response.usage_details):
|
||||
if usage.input_token_count:
|
||||
attributes[OtelAttr.INPUT_TOKENS] = usage.input_token_count
|
||||
if usage.output_token_count:
|
||||
attributes[OtelAttr.OUTPUT_TOKENS] = usage.output_token_count
|
||||
if usage.get("input_token_count"):
|
||||
attributes[OtelAttr.INPUT_TOKENS] = usage["input_token_count"]
|
||||
if usage.get("output_token_count"):
|
||||
attributes[OtelAttr.OUTPUT_TOKENS] = usage["output_token_count"]
|
||||
if duration:
|
||||
attributes[Meters.LLM_OPERATION_DURATION] = duration
|
||||
return attributes
|
||||
|
||||
@@ -47,15 +47,8 @@ from .._types import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CodeInterpreterToolCallContent,
|
||||
Contents,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
MCPServerToolCallContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
prepare_function_call_results,
|
||||
)
|
||||
@@ -416,7 +409,7 @@ class OpenAIAssistantsClient(
|
||||
thread_id: str | None,
|
||||
assistant_id: str,
|
||||
run_options: dict[str, Any],
|
||||
tool_results: list[FunctionResultContent] | None,
|
||||
tool_results: list[Content] | None,
|
||||
) -> tuple[Any, str]:
|
||||
"""Create the assistant stream for processing.
|
||||
|
||||
@@ -526,7 +519,7 @@ class OpenAIAssistantsClient(
|
||||
and response.data.usage is not None
|
||||
):
|
||||
usage = response.data.usage
|
||||
usage_content = UsageContent(
|
||||
usage_content = Content.from_usage(
|
||||
UsageDetails(
|
||||
input_token_count=usage.prompt_tokens,
|
||||
output_token_count=usage.completion_tokens,
|
||||
@@ -551,9 +544,9 @@ class OpenAIAssistantsClient(
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
|
||||
def _parse_function_calls_from_assistants(self, event_data: Run, response_id: str | None) -> list[Contents]:
|
||||
def _parse_function_calls_from_assistants(self, event_data: Run, response_id: str | None) -> list[Content]:
|
||||
"""Parse function call contents from an assistants tool action event."""
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
|
||||
if event_data.required_action is not None:
|
||||
for tool_call in event_data.required_action.submit_tool_outputs.tool_calls:
|
||||
@@ -563,10 +556,12 @@ class OpenAIAssistantsClient(
|
||||
if tool_type == "code_interpreter" and getattr(tool_call_any, "code_interpreter", None):
|
||||
code_input = getattr(tool_call_any.code_interpreter, "input", None)
|
||||
inputs = (
|
||||
[TextContent(text=code_input, raw_representation=tool_call)] if code_input is not None else None
|
||||
[Content.from_text(text=code_input, raw_representation=tool_call)]
|
||||
if code_input is not None
|
||||
else None
|
||||
)
|
||||
contents.append(
|
||||
CodeInterpreterToolCallContent(
|
||||
Content.from_code_interpreter_tool_call(
|
||||
call_id=call_id,
|
||||
inputs=inputs,
|
||||
raw_representation=tool_call,
|
||||
@@ -574,7 +569,7 @@ class OpenAIAssistantsClient(
|
||||
)
|
||||
elif tool_type == "mcp":
|
||||
contents.append(
|
||||
MCPServerToolCallContent(
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id=call_id,
|
||||
tool_name=getattr(tool_call, "name", "") or "",
|
||||
server_name=getattr(tool_call, "server_label", None),
|
||||
@@ -586,7 +581,7 @@ class OpenAIAssistantsClient(
|
||||
function_name = tool_call.function.name
|
||||
function_arguments = json.loads(tool_call.function.arguments)
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name=function_name,
|
||||
arguments=function_arguments,
|
||||
@@ -600,7 +595,7 @@ class OpenAIAssistantsClient(
|
||||
messages: MutableSequence[ChatMessage],
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> tuple[dict[str, Any], list[FunctionResultContent] | None]:
|
||||
) -> tuple[dict[str, Any], list[Content] | None]:
|
||||
from .._types import validate_tool_mode
|
||||
|
||||
run_options: dict[str, Any] = {**kwargs}
|
||||
@@ -672,7 +667,7 @@ class OpenAIAssistantsClient(
|
||||
}
|
||||
|
||||
instructions: list[str] = []
|
||||
tool_results: list[FunctionResultContent] | None = None
|
||||
tool_results: list[Content] | None = None
|
||||
|
||||
additional_messages: list[AdditionalMessage] | None = None
|
||||
|
||||
@@ -681,21 +676,23 @@ class OpenAIAssistantsClient(
|
||||
# All other messages are added 1:1.
|
||||
for chat_message in messages:
|
||||
if chat_message.role.value in ["system", "developer"]:
|
||||
for text_content in [content for content in chat_message.contents if isinstance(content, TextContent)]:
|
||||
instructions.append(text_content.text)
|
||||
for text_content in [content for content in chat_message.contents if content.type == "text"]:
|
||||
text = getattr(text_content, "text", None)
|
||||
if text:
|
||||
instructions.append(text)
|
||||
|
||||
continue
|
||||
|
||||
message_contents: list[MessageContentPartParam] = []
|
||||
|
||||
for content in chat_message.contents:
|
||||
if isinstance(content, TextContent):
|
||||
message_contents.append(TextContentBlockParam(type="text", text=content.text))
|
||||
elif isinstance(content, UriContent) and content.has_top_level_media_type("image"):
|
||||
if content.type == "text":
|
||||
message_contents.append(TextContentBlockParam(type="text", text=content.text)) # type: ignore[attr-defined, typeddict-item]
|
||||
elif content.type == "uri" and content.has_top_level_media_type("image"):
|
||||
message_contents.append(
|
||||
ImageURLContentBlockParam(type="image_url", image_url=ImageURLParam(url=content.uri))
|
||||
ImageURLContentBlockParam(type="image_url", image_url=ImageURLParam(url=content.uri)) # type: ignore[attr-defined, typeddict-item]
|
||||
)
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
if tool_results is None:
|
||||
tool_results = []
|
||||
tool_results.append(content)
|
||||
@@ -720,7 +717,7 @@ class OpenAIAssistantsClient(
|
||||
|
||||
def _prepare_tool_outputs_for_assistants(
|
||||
self,
|
||||
tool_results: list[FunctionResultContent] | None,
|
||||
tool_results: list[Content] | None,
|
||||
) -> tuple[str | None, list[ToolOutput] | None]:
|
||||
"""Prepare function results for submission to the assistants API."""
|
||||
run_id: str | None = None
|
||||
@@ -731,7 +728,7 @@ class OpenAIAssistantsClient(
|
||||
# When creating the FunctionCallContent, we created it with a CallId == [runId, callId].
|
||||
# We need to extract the run ID and ensure that the ToolOutput we send back to Azure
|
||||
# is only the call ID.
|
||||
run_and_call_ids: list[str] = json.loads(function_result_content.call_id)
|
||||
run_and_call_ids: list[str] = json.loads(function_result_content.call_id) # type: ignore[arg-type]
|
||||
|
||||
if (
|
||||
not run_and_call_ids
|
||||
|
||||
@@ -25,18 +25,9 @@ from .._types import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Contents,
|
||||
DataContent,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
prepare_function_call_results,
|
||||
)
|
||||
@@ -294,13 +285,13 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
response_metadata.update(self._get_metadata_from_chat_choice(choice))
|
||||
if choice.finish_reason:
|
||||
finish_reason = FinishReason(value=choice.finish_reason)
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
if text_content := self._parse_text_from_openai(choice):
|
||||
contents.append(text_content)
|
||||
if parsed_tool_calls := [tool for tool in self._parse_tool_calls_from_openai(choice)]:
|
||||
contents.extend(parsed_tool_calls)
|
||||
if reasoning_details := getattr(choice.message, "reasoning_details", None):
|
||||
contents.append(TextReasoningContent(None, protected_data=json.dumps(reasoning_details)))
|
||||
contents.append(Content.from_text_reasoning(protected_data=json.dumps(reasoning_details)))
|
||||
messages.append(ChatMessage(role="assistant", contents=contents))
|
||||
return ChatResponse(
|
||||
response_id=response.id,
|
||||
@@ -322,13 +313,17 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
if chunk.usage:
|
||||
return ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[UsageContent(details=self._parse_usage_from_openai(chunk.usage), raw_representation=chunk)],
|
||||
contents=[
|
||||
Content.from_usage(
|
||||
usage_details=self._parse_usage_from_openai(chunk.usage), raw_representation=chunk
|
||||
)
|
||||
],
|
||||
model_id=chunk.model,
|
||||
additional_properties=chunk_metadata,
|
||||
response_id=chunk.id,
|
||||
message_id=chunk.id,
|
||||
)
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
finish_reason: FinishReason | None = None
|
||||
for choice in chunk.choices:
|
||||
chunk_metadata.update(self._get_metadata_from_chat_choice(choice))
|
||||
@@ -339,7 +334,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
if text_content := self._parse_text_from_openai(choice):
|
||||
contents.append(text_content)
|
||||
if reasoning_details := getattr(choice.delta, "reasoning_details", None):
|
||||
contents.append(TextReasoningContent(None, protected_data=json.dumps(reasoning_details)))
|
||||
contents.append(Content.from_text_reasoning(protected_data=json.dumps(reasoning_details)))
|
||||
return ChatResponseUpdate(
|
||||
created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
contents=contents,
|
||||
@@ -360,27 +355,27 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
)
|
||||
if usage.completion_tokens_details:
|
||||
if tokens := usage.completion_tokens_details.accepted_prediction_tokens:
|
||||
details["completion/accepted_prediction_tokens"] = tokens
|
||||
details["completion/accepted_prediction_tokens"] = tokens # type: ignore[typeddict-unknown-key]
|
||||
if tokens := usage.completion_tokens_details.audio_tokens:
|
||||
details["completion/audio_tokens"] = tokens
|
||||
details["completion/audio_tokens"] = tokens # type: ignore[typeddict-unknown-key]
|
||||
if tokens := usage.completion_tokens_details.reasoning_tokens:
|
||||
details["completion/reasoning_tokens"] = tokens
|
||||
details["completion/reasoning_tokens"] = tokens # type: ignore[typeddict-unknown-key]
|
||||
if tokens := usage.completion_tokens_details.rejected_prediction_tokens:
|
||||
details["completion/rejected_prediction_tokens"] = tokens
|
||||
details["completion/rejected_prediction_tokens"] = tokens # type: ignore[typeddict-unknown-key]
|
||||
if usage.prompt_tokens_details:
|
||||
if tokens := usage.prompt_tokens_details.audio_tokens:
|
||||
details["prompt/audio_tokens"] = tokens
|
||||
details["prompt/audio_tokens"] = tokens # type: ignore[typeddict-unknown-key]
|
||||
if tokens := usage.prompt_tokens_details.cached_tokens:
|
||||
details["prompt/cached_tokens"] = tokens
|
||||
details["prompt/cached_tokens"] = tokens # type: ignore[typeddict-unknown-key]
|
||||
return details
|
||||
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
"""Parse the choice into a TextContent object."""
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None:
|
||||
"""Parse the choice into a Content object with type='text'."""
|
||||
message = choice.message if isinstance(choice, Choice) else choice.delta
|
||||
if message.content:
|
||||
return TextContent(text=message.content, raw_representation=choice)
|
||||
return Content.from_text(text=message.content, raw_representation=choice)
|
||||
if hasattr(message, "refusal") and message.refusal:
|
||||
return TextContent(text=message.refusal, raw_representation=choice)
|
||||
return Content.from_text(text=message.refusal, raw_representation=choice)
|
||||
return None
|
||||
|
||||
def _get_metadata_from_chat_response(self, response: ChatCompletion) -> dict[str, Any]:
|
||||
@@ -401,15 +396,15 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
"logprobs": getattr(choice, "logprobs", None),
|
||||
}
|
||||
|
||||
def _parse_tool_calls_from_openai(self, choice: Choice | ChunkChoice) -> list[Contents]:
|
||||
def _parse_tool_calls_from_openai(self, choice: Choice | ChunkChoice) -> list[Content]:
|
||||
"""Parse tool calls from an OpenAI response choice."""
|
||||
resp: list[Contents] = []
|
||||
resp: list[Content] = []
|
||||
content = choice.message if isinstance(choice, Choice) else choice.delta
|
||||
if content and content.tool_calls:
|
||||
for tool in content.tool_calls:
|
||||
if not isinstance(tool, ChatCompletionMessageCustomToolCall) and tool.function:
|
||||
# ignoring tool.custom
|
||||
fcc = FunctionCallContent(
|
||||
fcc = Content.from_function_call(
|
||||
call_id=tool.id if tool.id else "",
|
||||
name=tool.function.name if tool.function.name else "",
|
||||
arguments=tool.function.arguments if tool.function.arguments else "",
|
||||
@@ -455,7 +450,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
all_messages: list[dict[str, Any]] = []
|
||||
for content in message.contents:
|
||||
# Skip approval content - it's internal framework state, not for the LLM
|
||||
if isinstance(content, (FunctionApprovalRequestContent, FunctionApprovalResponseContent)):
|
||||
if content.type in ("function_approval_request", "function_approval_response"):
|
||||
continue
|
||||
|
||||
args: dict[str, Any] = {
|
||||
@@ -467,21 +462,21 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
details := message.additional_properties["reasoning_details"]
|
||||
):
|
||||
args["reasoning_details"] = details
|
||||
match content:
|
||||
case FunctionCallContent():
|
||||
match content.type:
|
||||
case "function_call":
|
||||
if all_messages and "tool_calls" in all_messages[-1]:
|
||||
# If the last message already has tool calls, append to it
|
||||
all_messages[-1]["tool_calls"].append(self._prepare_content_for_openai(content))
|
||||
else:
|
||||
args["tool_calls"] = [self._prepare_content_for_openai(content)] # type: ignore
|
||||
case FunctionResultContent():
|
||||
case "function_result":
|
||||
args["tool_call_id"] = content.call_id
|
||||
# Always include content for tool results - API requires it even if empty
|
||||
# Functions returning None should still have a tool result message
|
||||
args["content"] = (
|
||||
prepare_function_call_results(content.result) if content.result is not None else ""
|
||||
)
|
||||
case TextReasoningContent(protected_data=protected_data) if protected_data is not None:
|
||||
case "text_reasoning" if (protected_data := content.protected_data) is not None:
|
||||
all_messages[-1]["reasoning_details"] = json.loads(protected_data)
|
||||
case _:
|
||||
if "content" not in args:
|
||||
@@ -492,27 +487,27 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
all_messages.append(args)
|
||||
return all_messages
|
||||
|
||||
def _prepare_content_for_openai(self, content: Contents) -> dict[str, Any]:
|
||||
def _prepare_content_for_openai(self, content: Content) -> dict[str, Any]:
|
||||
"""Prepare content for OpenAI."""
|
||||
match content:
|
||||
case FunctionCallContent():
|
||||
match content.type:
|
||||
case "function_call":
|
||||
args = json.dumps(content.arguments) if isinstance(content.arguments, Mapping) else content.arguments
|
||||
return {
|
||||
"id": content.call_id,
|
||||
"type": "function",
|
||||
"function": {"name": content.name, "arguments": args},
|
||||
}
|
||||
case FunctionResultContent():
|
||||
case "function_result":
|
||||
return {
|
||||
"tool_call_id": content.call_id,
|
||||
"content": content.result,
|
||||
}
|
||||
case DataContent() | UriContent() if content.has_top_level_media_type("image"):
|
||||
case "data" | "uri" if content.has_top_level_media_type("image"):
|
||||
return {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": content.uri},
|
||||
}
|
||||
case DataContent() | UriContent() if content.has_top_level_media_type("audio"):
|
||||
case "data" | "uri" if content.has_top_level_media_type("audio"):
|
||||
if content.media_type and "wav" in content.media_type:
|
||||
audio_format = "wav"
|
||||
elif content.media_type and "mp3" in content.media_type:
|
||||
@@ -523,9 +518,9 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
|
||||
# Extract base64 data from data URI
|
||||
audio_data = content.uri
|
||||
if audio_data.startswith("data:"):
|
||||
if audio_data.startswith("data:"): # type: ignore[union-attr]
|
||||
# Extract just the base64 part after "data:audio/format;base64,"
|
||||
audio_data = audio_data.split(",", 1)[-1]
|
||||
audio_data = audio_data.split(",", 1)[-1] # type: ignore[union-attr]
|
||||
|
||||
return {
|
||||
"type": "input_audio",
|
||||
@@ -534,9 +529,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
"format": audio_format,
|
||||
},
|
||||
}
|
||||
case DataContent() | UriContent() if content.has_top_level_media_type(
|
||||
"application"
|
||||
) and content.uri.startswith("data:"):
|
||||
case "data" | "uri" if content.has_top_level_media_type("application") and content.uri.startswith("data:"): # type: ignore[union-attr]
|
||||
# All application/* media types should be treated as files for OpenAI
|
||||
filename = getattr(content, "filename", None) or (
|
||||
content.additional_properties.get("filename")
|
||||
|
||||
@@ -48,33 +48,16 @@ from .._tools import (
|
||||
use_function_invocation,
|
||||
)
|
||||
from .._types import (
|
||||
Annotation,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CitationAnnotation,
|
||||
CodeInterpreterToolCallContent,
|
||||
CodeInterpreterToolResultContent,
|
||||
Contents,
|
||||
DataContent,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedFileContent,
|
||||
HostedVectorStoreContent,
|
||||
ImageGenerationToolCallContent,
|
||||
ImageGenerationToolResultContent,
|
||||
MCPServerToolCallContent,
|
||||
MCPServerToolResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
TextSpanRegion,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
_parse_content,
|
||||
detect_media_type_from_base64,
|
||||
prepare_function_call_results,
|
||||
prepend_instructions_to_messages,
|
||||
validate_tool_mode,
|
||||
@@ -231,7 +214,6 @@ class OpenAIBaseResponsesClient(
|
||||
response = await client.responses.parse(stream=False, **run_options)
|
||||
else:
|
||||
response = await client.responses.create(stream=False, **run_options)
|
||||
return self._parse_response_from_openai(response, options=options)
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
raise OpenAIContentFilterException(
|
||||
@@ -247,6 +229,7 @@ class OpenAIBaseResponsesClient(
|
||||
f"{type(self)} service failed to complete the prompt: {ex}",
|
||||
inner_exception=ex,
|
||||
) from ex
|
||||
return self._parse_response_from_openai(response, options=options)
|
||||
|
||||
@override
|
||||
async def _inner_get_streaming_response(
|
||||
@@ -391,7 +374,7 @@ class OpenAIBaseResponsesClient(
|
||||
if tool.inputs:
|
||||
tool_args["file_ids"] = []
|
||||
for tool_input in tool.inputs:
|
||||
if isinstance(tool_input, HostedFileContent):
|
||||
if tool_input.type == "hosted_file":
|
||||
tool_args["file_ids"].append(tool_input.file_id) # type: ignore[attr-defined]
|
||||
if not tool_args["file_ids"]:
|
||||
tool_args.pop("file_ids")
|
||||
@@ -417,7 +400,9 @@ class OpenAIBaseResponsesClient(
|
||||
if not tool.inputs:
|
||||
raise ValueError("HostedFileSearchTool requires inputs to be specified.")
|
||||
inputs: list[str] = [
|
||||
inp.vector_store_id for inp in tool.inputs if isinstance(inp, HostedVectorStoreContent)
|
||||
inp.vector_store_id # type: ignore[misc]
|
||||
for inp in tool.inputs
|
||||
if inp.type == "hosted_vector_store" # type: ignore[attr-defined]
|
||||
]
|
||||
if not inputs:
|
||||
raise ValueError(
|
||||
@@ -629,11 +614,11 @@ class OpenAIBaseResponsesClient(
|
||||
for message in chat_messages:
|
||||
for content in message.contents:
|
||||
if (
|
||||
isinstance(content, FunctionCallContent)
|
||||
content.type == "function_call"
|
||||
and content.additional_properties
|
||||
and "fc_id" in content.additional_properties
|
||||
):
|
||||
call_id_to_id[content.call_id] = content.additional_properties["fc_id"]
|
||||
call_id_to_id[content.call_id] = content.additional_properties["fc_id"] # type: ignore[attr-defined, index]
|
||||
list_of_list = [self._prepare_message_for_openai(message, call_id_to_id) for message in chat_messages]
|
||||
# Flatten the list of lists into a single list
|
||||
return list(chain.from_iterable(list_of_list))
|
||||
@@ -649,18 +634,18 @@ class OpenAIBaseResponsesClient(
|
||||
"role": message.role.value if isinstance(message.role, Role) else message.role,
|
||||
}
|
||||
for content in message.contents:
|
||||
match content:
|
||||
case TextReasoningContent():
|
||||
match content.type:
|
||||
case "text_reasoning":
|
||||
# Don't send reasoning content back to model
|
||||
continue
|
||||
case FunctionResultContent():
|
||||
case "function_result":
|
||||
new_args: dict[str, Any] = {}
|
||||
new_args.update(self._prepare_content_for_openai(message.role, content, call_id_to_id))
|
||||
all_messages.append(new_args)
|
||||
case FunctionCallContent():
|
||||
case "function_call":
|
||||
function_call = self._prepare_content_for_openai(message.role, content, call_id_to_id)
|
||||
all_messages.append(function_call) # type: ignore
|
||||
case FunctionApprovalResponseContent() | FunctionApprovalRequestContent():
|
||||
case "function_approval_response" | "function_approval_request":
|
||||
all_messages.append(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore
|
||||
case _:
|
||||
if "content" not in args:
|
||||
@@ -673,17 +658,17 @@ class OpenAIBaseResponsesClient(
|
||||
def _prepare_content_for_openai(
|
||||
self,
|
||||
role: Role,
|
||||
content: Contents,
|
||||
content: Content,
|
||||
call_id_to_id: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
"""Prepare content for the OpenAI Responses API format."""
|
||||
match content:
|
||||
case TextContent():
|
||||
match content.type:
|
||||
case "text":
|
||||
return {
|
||||
"type": "output_text" if role == Role.ASSISTANT else "input_text",
|
||||
"text": content.text,
|
||||
}
|
||||
case TextReasoningContent():
|
||||
case "text_reasoning":
|
||||
ret: dict[str, Any] = {
|
||||
"type": "reasoning",
|
||||
"summary": {
|
||||
@@ -703,7 +688,7 @@ class OpenAIBaseResponsesClient(
|
||||
if encrypted_content := props.get("encrypted_content"):
|
||||
ret["encrypted_content"] = encrypted_content
|
||||
return ret
|
||||
case DataContent() | UriContent():
|
||||
case "data" | "uri":
|
||||
if content.has_top_level_media_type("image"):
|
||||
return {
|
||||
"type": "input_image",
|
||||
@@ -744,7 +729,7 @@ class OpenAIBaseResponsesClient(
|
||||
file_obj["filename"] = filename
|
||||
return file_obj
|
||||
return {}
|
||||
case FunctionCallContent():
|
||||
case "function_call":
|
||||
if not content.call_id:
|
||||
logger.warning(f"FunctionCallContent missing call_id for function '{content.name}'")
|
||||
return {}
|
||||
@@ -761,7 +746,7 @@ class OpenAIBaseResponsesClient(
|
||||
"arguments": content.arguments,
|
||||
"status": None,
|
||||
}
|
||||
case FunctionResultContent():
|
||||
case "function_result":
|
||||
# call_id for the result needs to be the same as the call_id for the function call
|
||||
args: dict[str, Any] = {
|
||||
"call_id": content.call_id,
|
||||
@@ -769,29 +754,29 @@ class OpenAIBaseResponsesClient(
|
||||
"output": prepare_function_call_results(content.result),
|
||||
}
|
||||
return args
|
||||
case FunctionApprovalRequestContent():
|
||||
case "function_approval_request":
|
||||
return {
|
||||
"type": "mcp_approval_request",
|
||||
"id": content.id,
|
||||
"arguments": content.function_call.arguments,
|
||||
"name": content.function_call.name,
|
||||
"server_label": content.function_call.additional_properties.get("server_label")
|
||||
if content.function_call.additional_properties
|
||||
"id": content.id, # type: ignore[union-attr]
|
||||
"arguments": content.function_call.arguments, # type: ignore[union-attr]
|
||||
"name": content.function_call.name, # type: ignore[union-attr]
|
||||
"server_label": content.function_call.additional_properties.get("server_label") # type: ignore[union-attr]
|
||||
if content.function_call.additional_properties # type: ignore[union-attr]
|
||||
else None,
|
||||
}
|
||||
case FunctionApprovalResponseContent():
|
||||
case "function_approval_response":
|
||||
return {
|
||||
"type": "mcp_approval_response",
|
||||
"approval_request_id": content.id,
|
||||
"approve": content.approved,
|
||||
}
|
||||
case HostedFileContent():
|
||||
case "hosted_file":
|
||||
return {
|
||||
"type": "input_file",
|
||||
"file_id": content.file_id,
|
||||
}
|
||||
case _: # should catch UsageDetails and ErrorContent and HostedVectorStoreContent
|
||||
logger.debug("Unsupported content type passed (type: %s)", type(content))
|
||||
logger.debug("Unsupported content type passed (type: %s)", content.type)
|
||||
return {}
|
||||
|
||||
# region Parse methods
|
||||
@@ -804,7 +789,7 @@ class OpenAIBaseResponsesClient(
|
||||
structured_response: BaseModel | None = response.output_parsed if isinstance(response, ParsedResponse) else None # type: ignore[reportUnknownMemberType]
|
||||
|
||||
metadata: dict[str, Any] = response.metadata or {}
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
for item in response.output: # type: ignore[reportUnknownMemberType]
|
||||
match item.type:
|
||||
# types:
|
||||
@@ -829,7 +814,7 @@ class OpenAIBaseResponsesClient(
|
||||
for message_content in item.content: # type: ignore[reportMissingTypeArgument]
|
||||
match message_content.type:
|
||||
case "output_text":
|
||||
text_content = TextContent(
|
||||
text_content = Content.from_text(
|
||||
text=message_content.text,
|
||||
raw_representation=message_content, # type: ignore[reportUnknownArgumentType]
|
||||
)
|
||||
@@ -840,7 +825,8 @@ class OpenAIBaseResponsesClient(
|
||||
match annotation.type:
|
||||
case "file_path":
|
||||
text_content.annotations.append(
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
file_id=annotation.file_id,
|
||||
additional_properties={
|
||||
"index": annotation.index,
|
||||
@@ -850,7 +836,8 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "file_citation":
|
||||
text_content.annotations.append(
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
url=annotation.filename,
|
||||
file_id=annotation.file_id,
|
||||
raw_representation=annotation,
|
||||
@@ -861,11 +848,13 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "url_citation":
|
||||
text_content.annotations.append(
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
title=annotation.title,
|
||||
url=annotation.url,
|
||||
annotated_regions=[
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=annotation.start_index,
|
||||
end_index=annotation.end_index,
|
||||
)
|
||||
@@ -875,7 +864,8 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "container_file_citation":
|
||||
text_content.annotations.append(
|
||||
CitationAnnotation(
|
||||
Annotation(
|
||||
type="citation",
|
||||
file_id=annotation.file_id,
|
||||
url=annotation.filename,
|
||||
additional_properties={
|
||||
@@ -883,6 +873,7 @@ class OpenAIBaseResponsesClient(
|
||||
},
|
||||
annotated_regions=[
|
||||
TextSpanRegion(
|
||||
type="text_span",
|
||||
start_index=annotation.start_index,
|
||||
end_index=annotation.end_index,
|
||||
)
|
||||
@@ -898,7 +889,7 @@ class OpenAIBaseResponsesClient(
|
||||
contents.append(text_content)
|
||||
case "refusal":
|
||||
contents.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=message_content.refusal,
|
||||
raw_representation=message_content,
|
||||
)
|
||||
@@ -910,7 +901,7 @@ class OpenAIBaseResponsesClient(
|
||||
if hasattr(item, "summary") and item.summary and index < len(item.summary):
|
||||
additional_properties = {"summary": item.summary[index]}
|
||||
contents.append(
|
||||
TextReasoningContent(
|
||||
Content.from_text_reasoning(
|
||||
text=reasoning_content.text,
|
||||
raw_representation=reasoning_content,
|
||||
additional_properties=additional_properties,
|
||||
@@ -919,23 +910,23 @@ class OpenAIBaseResponsesClient(
|
||||
if hasattr(item, "summary") and item.summary:
|
||||
for summary in item.summary:
|
||||
contents.append(
|
||||
TextReasoningContent(text=summary.text, raw_representation=summary) # type: ignore[arg-type]
|
||||
Content.from_text_reasoning(text=summary.text, raw_representation=summary) # type: ignore[arg-type]
|
||||
)
|
||||
case "code_interpreter_call": # ResponseOutputCodeInterpreterCall
|
||||
call_id = getattr(item, "call_id", None) or getattr(item, "id", None)
|
||||
outputs: list["Contents"] = []
|
||||
outputs: list["Content"] = []
|
||||
if item_outputs := getattr(item, "outputs", None):
|
||||
for code_output in item_outputs:
|
||||
if getattr(code_output, "type", None) == "logs":
|
||||
outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=code_output.logs,
|
||||
raw_representation=code_output,
|
||||
)
|
||||
)
|
||||
elif getattr(code_output, "type", None) == "image":
|
||||
outputs.append(
|
||||
UriContent(
|
||||
Content.from_uri(
|
||||
uri=code_output.url,
|
||||
raw_representation=code_output,
|
||||
media_type="image",
|
||||
@@ -943,14 +934,14 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
if code := getattr(item, "code", None):
|
||||
contents.append(
|
||||
CodeInterpreterToolCallContent(
|
||||
Content.from_code_interpreter_tool_call(
|
||||
call_id=call_id,
|
||||
inputs=[TextContent(text=code, raw_representation=item)],
|
||||
inputs=[Content.from_text(text=code, raw_representation=item)],
|
||||
raw_representation=item,
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
CodeInterpreterToolResultContent(
|
||||
Content.from_code_interpreter_tool_result(
|
||||
call_id=call_id,
|
||||
outputs=outputs,
|
||||
raw_representation=item,
|
||||
@@ -958,7 +949,7 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "function_call": # ResponseOutputFunctionCall
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=item.call_id if hasattr(item, "call_id") and item.call_id else "",
|
||||
name=item.name if hasattr(item, "name") else "",
|
||||
arguments=item.arguments if hasattr(item, "arguments") else "",
|
||||
@@ -968,9 +959,9 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "mcp_approval_request": # ResponseOutputMcpApprovalRequest
|
||||
contents.append(
|
||||
FunctionApprovalRequestContent(
|
||||
Content.from_function_approval_request(
|
||||
id=item.id,
|
||||
function_call=FunctionCallContent(
|
||||
function_call=Content.from_function_call(
|
||||
call_id=item.id,
|
||||
name=item.name,
|
||||
arguments=item.arguments,
|
||||
@@ -982,7 +973,7 @@ class OpenAIBaseResponsesClient(
|
||||
case "mcp_call":
|
||||
call_id = item.id
|
||||
contents.append(
|
||||
MCPServerToolCallContent(
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id=call_id,
|
||||
tool_name=item.name,
|
||||
server_name=item.server_label,
|
||||
@@ -992,31 +983,31 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
if item.output is not None:
|
||||
contents.append(
|
||||
MCPServerToolResultContent(
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id=call_id,
|
||||
output=[TextContent(text=item.output)],
|
||||
output=[Content.from_text(text=item.output)],
|
||||
raw_representation=item,
|
||||
)
|
||||
)
|
||||
case "image_generation_call": # ResponseOutputImageGenerationCall
|
||||
image_output: DataContent | None = None
|
||||
if item.result:
|
||||
base64_data = item.result
|
||||
image_format = DataContent.detect_image_format_from_base64(base64_data)
|
||||
image_output = DataContent(
|
||||
data=base64_data,
|
||||
media_type=f"image/{image_format}" if image_format else "image/png",
|
||||
image_output: Content | None = None
|
||||
if item.result is not None:
|
||||
# item.result contains raw base64 string
|
||||
# so we call detect_media_type_from_base64 to get the media type and fallback to image/png
|
||||
image_output = Content.from_uri(
|
||||
uri=f"data:{detect_media_type_from_base64(data_str=item.result) or 'image/png'}"
|
||||
f";base64,{item.result}",
|
||||
raw_representation=item.result,
|
||||
)
|
||||
image_id = item.id
|
||||
contents.append(
|
||||
ImageGenerationToolCallContent(
|
||||
Content.from_image_generation_tool_call(
|
||||
image_id=image_id,
|
||||
raw_representation=item,
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
ImageGenerationToolResultContent(
|
||||
Content.from_image_generation_tool_result(
|
||||
image_id=image_id,
|
||||
outputs=image_output,
|
||||
raw_representation=item,
|
||||
@@ -1056,11 +1047,10 @@ class OpenAIBaseResponsesClient(
|
||||
) -> ChatResponseUpdate:
|
||||
"""Parse an OpenAI Responses API streaming event into a ChatResponseUpdate."""
|
||||
metadata: dict[str, Any] = {}
|
||||
contents: list[Contents] = []
|
||||
contents: list[Content] = []
|
||||
conversation_id: str | None = None
|
||||
response_id: str | None = None
|
||||
model = self.model_id
|
||||
# TODO(peterychang): Add support for other content types
|
||||
match event.type:
|
||||
# types:
|
||||
# ResponseAudioDeltaEvent,
|
||||
@@ -1120,26 +1110,26 @@ class OpenAIBaseResponsesClient(
|
||||
event_part = event.part
|
||||
match event_part.type:
|
||||
case "output_text":
|
||||
contents.append(TextContent(text=event_part.text, raw_representation=event))
|
||||
contents.append(Content.from_text(text=event_part.text, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event_part))
|
||||
case "refusal":
|
||||
contents.append(TextContent(text=event_part.refusal, raw_representation=event))
|
||||
contents.append(Content.from_text(text=event_part.refusal, raw_representation=event))
|
||||
case _:
|
||||
pass
|
||||
case "response.output_text.delta":
|
||||
contents.append(TextContent(text=event.delta, raw_representation=event))
|
||||
contents.append(Content.from_text(text=event.delta, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.reasoning_text.delta":
|
||||
contents.append(TextReasoningContent(text=event.delta, raw_representation=event))
|
||||
contents.append(Content.from_text_reasoning(text=event.delta, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.reasoning_text.done":
|
||||
contents.append(TextReasoningContent(text=event.text, raw_representation=event))
|
||||
contents.append(Content.from_text_reasoning(text=event.text, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.reasoning_summary_text.delta":
|
||||
contents.append(TextReasoningContent(text=event.delta, raw_representation=event))
|
||||
contents.append(Content.from_text_reasoning(text=event.delta, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.reasoning_summary_text.done":
|
||||
contents.append(TextReasoningContent(text=event.text, raw_representation=event))
|
||||
contents.append(Content.from_text_reasoning(text=event.text, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.created":
|
||||
response_id = event.response.id
|
||||
@@ -1154,7 +1144,7 @@ class OpenAIBaseResponsesClient(
|
||||
if event.response.usage:
|
||||
usage = self._parse_usage_from_openai(event.response.usage)
|
||||
if usage:
|
||||
contents.append(UsageContent(details=usage, raw_representation=event))
|
||||
contents.append(Content.from_usage(usage_details=usage, raw_representation=event))
|
||||
case "response.output_item.added":
|
||||
event_item = event.item
|
||||
match event_item.type:
|
||||
@@ -1179,9 +1169,9 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "mcp_approval_request":
|
||||
contents.append(
|
||||
FunctionApprovalRequestContent(
|
||||
Content.from_function_approval_request(
|
||||
id=event_item.id,
|
||||
function_call=FunctionCallContent(
|
||||
function_call=Content.from_function_call(
|
||||
call_id=event_item.id,
|
||||
name=event_item.name,
|
||||
arguments=event_item.arguments,
|
||||
@@ -1193,7 +1183,7 @@ class OpenAIBaseResponsesClient(
|
||||
case "mcp_call":
|
||||
call_id = getattr(event_item, "id", None) or getattr(event_item, "call_id", None) or ""
|
||||
contents.append(
|
||||
MCPServerToolCallContent(
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id=call_id,
|
||||
tool_name=getattr(event_item, "name", "") or "",
|
||||
server_name=getattr(event_item, "server_label", None),
|
||||
@@ -1206,7 +1196,7 @@ class OpenAIBaseResponsesClient(
|
||||
or getattr(event_item, "output", None)
|
||||
or getattr(event_item, "outputs", None)
|
||||
)
|
||||
parsed_output: list[Contents] | None = None
|
||||
parsed_output: list[Content] | None = None
|
||||
if result_output:
|
||||
normalized = (
|
||||
result_output
|
||||
@@ -1214,9 +1204,9 @@ class OpenAIBaseResponsesClient(
|
||||
and not isinstance(result_output, (str, bytes, MutableMapping))
|
||||
else [result_output]
|
||||
)
|
||||
parsed_output = [_parse_content(output_item) for output_item in normalized]
|
||||
parsed_output = [Content.from_dict(output_item) for output_item in normalized]
|
||||
contents.append(
|
||||
MCPServerToolResultContent(
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id=call_id,
|
||||
output=parsed_output,
|
||||
raw_representation=event_item,
|
||||
@@ -1224,19 +1214,19 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
case "code_interpreter_call": # ResponseOutputCodeInterpreterCall
|
||||
call_id = getattr(event_item, "call_id", None) or getattr(event_item, "id", None)
|
||||
outputs: list[Contents] = []
|
||||
outputs: list[Content] = []
|
||||
if hasattr(event_item, "outputs") and event_item.outputs:
|
||||
for code_output in event_item.outputs:
|
||||
if getattr(code_output, "type", None) == "logs":
|
||||
outputs.append(
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=cast(Any, code_output).logs,
|
||||
raw_representation=code_output,
|
||||
)
|
||||
)
|
||||
elif getattr(code_output, "type", None) == "image":
|
||||
outputs.append(
|
||||
UriContent(
|
||||
Content.from_uri(
|
||||
uri=cast(Any, code_output).url,
|
||||
raw_representation=code_output,
|
||||
media_type="image",
|
||||
@@ -1244,10 +1234,10 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
if hasattr(event_item, "code") and event_item.code:
|
||||
contents.append(
|
||||
CodeInterpreterToolCallContent(
|
||||
Content.from_code_interpreter_tool_call(
|
||||
call_id=call_id,
|
||||
inputs=[
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=event_item.code,
|
||||
raw_representation=event_item,
|
||||
)
|
||||
@@ -1256,7 +1246,7 @@ class OpenAIBaseResponsesClient(
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
CodeInterpreterToolResultContent(
|
||||
Content.from_code_interpreter_tool_result(
|
||||
call_id=call_id,
|
||||
outputs=outputs,
|
||||
raw_representation=event_item,
|
||||
@@ -1273,7 +1263,7 @@ class OpenAIBaseResponsesClient(
|
||||
):
|
||||
additional_properties = {"summary": event_item.summary[index]}
|
||||
contents.append(
|
||||
TextReasoningContent(
|
||||
Content.from_text_reasoning(
|
||||
text=reasoning_content.text,
|
||||
raw_representation=reasoning_content,
|
||||
additional_properties=additional_properties,
|
||||
@@ -1285,7 +1275,7 @@ class OpenAIBaseResponsesClient(
|
||||
call_id, name = function_call_ids.get(event.output_index, (None, None))
|
||||
if call_id and name:
|
||||
contents.append(
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name=name,
|
||||
arguments=event.delta,
|
||||
@@ -1300,13 +1290,9 @@ class OpenAIBaseResponsesClient(
|
||||
# Handle streaming partial image generation
|
||||
image_base64 = event.partial_image_b64
|
||||
partial_index = event.partial_image_index
|
||||
|
||||
# Use helper function to create data URI from base64
|
||||
uri, media_type = DataContent.create_data_uri_from_base64(image_base64)
|
||||
|
||||
image_output = DataContent(
|
||||
uri=uri,
|
||||
media_type=media_type,
|
||||
image_output = Content.from_uri(
|
||||
uri=f"data:{detect_media_type_from_base64(data_str=image_base64) or 'image/png'}"
|
||||
f";base64,{image_base64}",
|
||||
additional_properties={
|
||||
"partial_image_index": partial_index,
|
||||
"is_partial_image": True,
|
||||
@@ -1316,13 +1302,13 @@ class OpenAIBaseResponsesClient(
|
||||
|
||||
image_id = getattr(event, "item_id", None)
|
||||
contents.append(
|
||||
ImageGenerationToolCallContent(
|
||||
Content.from_image_generation_tool_call(
|
||||
image_id=image_id,
|
||||
raw_representation=event,
|
||||
)
|
||||
)
|
||||
contents.append(
|
||||
ImageGenerationToolResultContent(
|
||||
Content.from_image_generation_tool_result(
|
||||
image_id=image_id,
|
||||
outputs=image_output,
|
||||
raw_representation=event,
|
||||
@@ -1343,7 +1329,7 @@ class OpenAIBaseResponsesClient(
|
||||
if ann_type == "file_path":
|
||||
if ann_file_id:
|
||||
contents.append(
|
||||
HostedFileContent(
|
||||
Content.from_hosted_file(
|
||||
file_id=str(ann_file_id),
|
||||
additional_properties={
|
||||
"annotation_index": event.annotation_index,
|
||||
@@ -1355,7 +1341,7 @@ class OpenAIBaseResponsesClient(
|
||||
elif ann_type == "file_citation":
|
||||
if ann_file_id:
|
||||
contents.append(
|
||||
HostedFileContent(
|
||||
Content.from_hosted_file(
|
||||
file_id=str(ann_file_id),
|
||||
additional_properties={
|
||||
"annotation_index": event.annotation_index,
|
||||
@@ -1368,7 +1354,7 @@ class OpenAIBaseResponsesClient(
|
||||
elif ann_type == "container_file_citation":
|
||||
if ann_file_id:
|
||||
contents.append(
|
||||
HostedFileContent(
|
||||
Content.from_hosted_file(
|
||||
file_id=str(ann_file_id),
|
||||
additional_properties={
|
||||
"annotation_index": event.annotation_index,
|
||||
@@ -1402,9 +1388,9 @@ class OpenAIBaseResponsesClient(
|
||||
total_token_count=usage.total_tokens,
|
||||
)
|
||||
if usage.input_tokens_details and usage.input_tokens_details.cached_tokens:
|
||||
details["openai.cached_input_tokens"] = usage.input_tokens_details.cached_tokens
|
||||
details["openai.cached_input_tokens"] = usage.input_tokens_details.cached_tokens # type: ignore[typeddict-unknown-key]
|
||||
if usage.output_tokens_details and usage.output_tokens_details.reasoning_tokens:
|
||||
details["openai.reasoning_tokens"] = usage.output_tokens_details.reasoning_tokens
|
||||
details["openai.reasoning_tokens"] = usage.output_tokens_details.reasoning_tokens # type: ignore[typeddict-unknown-key]
|
||||
return details
|
||||
|
||||
def _get_metadata_from_response(self, output: Any) -> dict[str, Any]:
|
||||
|
||||
@@ -18,7 +18,6 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
HostedCodeInterpreterTool,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIAssistantsClient
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -332,7 +331,7 @@ async def test_azure_assistants_client_streaming() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25", "weather", "seattle"])
|
||||
@@ -358,7 +357,7 @@ async def test_azure_assistants_client_streaming_tools() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25", "weather"])
|
||||
|
||||
@@ -25,7 +25,6 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
@@ -304,9 +303,9 @@ async def test_azure_on_your_data(
|
||||
)
|
||||
assert len(content.messages) == 1
|
||||
assert len(content.messages[0].contents) == 1
|
||||
assert isinstance(content.messages[0].contents[0], TextContent)
|
||||
assert content.messages[0].contents[0].type == "text"
|
||||
assert len(content.messages[0].contents[0].annotations) == 1
|
||||
assert content.messages[0].contents[0].annotations[0].title == "test title"
|
||||
assert content.messages[0].contents[0].annotations[0]["title"] == "test title"
|
||||
assert content.messages[0].contents[0].text == "test"
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
@@ -374,9 +373,9 @@ async def test_azure_on_your_data_string(
|
||||
)
|
||||
assert len(content.messages) == 1
|
||||
assert len(content.messages[0].contents) == 1
|
||||
assert isinstance(content.messages[0].contents[0], TextContent)
|
||||
assert content.messages[0].contents[0].type == "text"
|
||||
assert len(content.messages[0].contents[0].annotations) == 1
|
||||
assert content.messages[0].contents[0].annotations[0].title == "test title"
|
||||
assert content.messages[0].contents[0].annotations[0]["title"] == "test title"
|
||||
assert content.messages[0].contents[0].text == "test"
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
@@ -433,7 +432,7 @@ async def test_azure_on_your_data_fail(
|
||||
)
|
||||
assert len(content.messages) == 1
|
||||
assert len(content.messages[0].contents) == 1
|
||||
assert isinstance(content.messages[0].contents[0], TextContent)
|
||||
assert content.messages[0].contents[0].type == "text"
|
||||
assert content.messages[0].contents[0].text == "test"
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
@@ -628,9 +627,7 @@ async def test_streaming_with_none_delta(
|
||||
results.append(msg)
|
||||
|
||||
assert len(results) > 0
|
||||
assert any(
|
||||
isinstance(content, TextContent) and content.text == "test" for msg in results for content in msg.contents
|
||||
)
|
||||
assert any(content.type == "text" and content.text == "test" for msg in results for content in msg.contents)
|
||||
assert any(msg.contents for msg in results)
|
||||
|
||||
|
||||
@@ -731,7 +728,7 @@ async def test_azure_openai_chat_client_streaming() -> None:
|
||||
assert chunk.message_id is not None
|
||||
assert chunk.response_id is not None
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert "Emily" in full_message or "David" in full_message
|
||||
@@ -757,7 +754,7 @@ async def test_azure_openai_chat_client_streaming_tools() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert "Emily" in full_message or "David" in full_message
|
||||
|
||||
@@ -15,10 +15,10 @@ from agent_framework import (
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
ai_function,
|
||||
)
|
||||
@@ -48,7 +48,7 @@ async def get_weather(location: Annotated[str, "The location as a city name"]) -
|
||||
return f"The weather in {location} is sunny and 72°F."
|
||||
|
||||
|
||||
async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, HostedVectorStoreContent]:
|
||||
async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str, Content]:
|
||||
"""Create a vector store with sample documents for testing."""
|
||||
file = await client.client.files.create(
|
||||
file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."), purpose="assistants"
|
||||
@@ -61,7 +61,7 @@ async def create_vector_store(client: AzureOpenAIResponsesClient) -> tuple[str,
|
||||
if result.last_error is not None:
|
||||
raise Exception(f"Vector store file processing failed with status: {result.last_error.message}")
|
||||
|
||||
return file.id, HostedVectorStoreContent(vector_store_id=vector_store.id)
|
||||
return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id)
|
||||
|
||||
|
||||
async def delete_vector_store(client: AzureOpenAIResponsesClient, file_id: str, vector_store_id: str) -> None:
|
||||
|
||||
@@ -20,8 +20,8 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
ToolProtocol,
|
||||
ai_function,
|
||||
use_chat_middleware,
|
||||
@@ -108,8 +108,8 @@ class MockChatClient:
|
||||
for update in self.streaming_responses.pop(0):
|
||||
yield update
|
||||
else:
|
||||
yield ChatResponseUpdate(text=TextContent(text="test streaming response "), role="assistant")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="another update")], role="assistant")
|
||||
yield ChatResponseUpdate(text=Content.from_text(text="test streaming response "), role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="another update")], role="assistant")
|
||||
|
||||
|
||||
@use_chat_middleware
|
||||
@@ -233,7 +233,7 @@ class MockAgent(AgentProtocol):
|
||||
**kwargs: Any,
|
||||
) -> AgentResponse:
|
||||
logger.debug(f"Running mock agent, with: {messages=}, {thread=}, {kwargs=}")
|
||||
return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent("Response")])])
|
||||
return AgentResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("Response")])])
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
@@ -243,7 +243,7 @@ class MockAgent(AgentProtocol):
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
logger.debug(f"Running mock agent stream, with: {messages=}, {thread=}, {kwargs=}")
|
||||
yield AgentResponseUpdate(contents=[TextContent("Response")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text("Response")])
|
||||
|
||||
def get_new_thread(self) -> AgentThread:
|
||||
return MockAgentThread()
|
||||
|
||||
@@ -18,12 +18,11 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatMessageStore,
|
||||
ChatResponse,
|
||||
Content,
|
||||
Context,
|
||||
ContextProvider,
|
||||
FunctionCallContent,
|
||||
HostedCodeInterpreterTool,
|
||||
Role,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework._mcp import MCPTool
|
||||
@@ -136,7 +135,7 @@ async def test_prepare_thread_does_not_mutate_agent_chat_options(chat_client: Ch
|
||||
|
||||
async def test_chat_client_agent_update_thread_id(chat_client_base: ChatClientProtocol) -> None:
|
||||
mock_response = ChatResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent("test response")])],
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("test response")])],
|
||||
conversation_id="123",
|
||||
)
|
||||
chat_client_base.run_responses = [mock_response]
|
||||
@@ -200,7 +199,9 @@ async def test_chat_client_agent_author_name_is_used_from_response(chat_client_b
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=[
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[TextContent("test response")], author_name="TestAuthor")
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT, contents=[Content.from_text("test response")], author_name="TestAuthor"
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
@@ -264,7 +265,7 @@ async def test_chat_agent_context_providers_thread_created(chat_client_base: Cha
|
||||
mock_provider = MockContextProvider()
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent("test response")])],
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("test response")])],
|
||||
conversation_id="test-thread-id",
|
||||
)
|
||||
]
|
||||
@@ -345,7 +346,7 @@ async def test_chat_agent_context_providers_with_thread_service_id(chat_client_b
|
||||
mock_provider = MockContextProvider()
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent("test response")])],
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("test response")])],
|
||||
conversation_id="service-thread-123",
|
||||
)
|
||||
]
|
||||
@@ -575,7 +576,9 @@ async def test_agent_tool_receives_thread_in_kwargs(chat_client_base: Any) -> No
|
||||
ChatResponse(
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="1", name="echo_thread_info", arguments='{"text": "hello"}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="1", name="echo_thread_info", arguments='{"text": "hello"}')
|
||||
],
|
||||
)
|
||||
),
|
||||
ChatResponse(messages=ChatMessage(role="assistant", text="done")),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatMessage, ChatResponse, FunctionCallContent, agent_middleware
|
||||
from agent_framework import ChatAgent, ChatMessage, ChatResponse, Content, agent_middleware
|
||||
from agent_framework._middleware import AgentRunContext
|
||||
|
||||
from .conftest import MockChatClient
|
||||
@@ -113,7 +113,7 @@ class TestAsToolKwargsPropagation:
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_c_1",
|
||||
name="call_c",
|
||||
arguments='{"task": "Please execute agent_c"}',
|
||||
@@ -170,10 +170,10 @@ class TestAsToolKwargsPropagation:
|
||||
await next(context)
|
||||
|
||||
# Setup mock streaming responses
|
||||
from agent_framework import ChatResponseUpdate, TextContent
|
||||
from agent_framework import ChatResponseUpdate
|
||||
|
||||
chat_client.streaming_responses = [
|
||||
[ChatResponseUpdate(text=TextContent(text="Streaming response"), role="assistant")],
|
||||
[ChatResponseUpdate(text=Content.from_text(text="Streaming response"), role="assistant")],
|
||||
]
|
||||
|
||||
sub_agent = ChatAgent(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,7 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
TextContent,
|
||||
Content,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework._tools import _handle_function_calls_response, _handle_function_calls_streaming_response
|
||||
@@ -42,7 +41,9 @@ class TestKwargsPropagationToAIFunction:
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="capture_kwargs_tool", arguments='{"x": 42}')
|
||||
Content.from_function_call(
|
||||
call_id="call_1", name="capture_kwargs_tool", arguments='{"x": 42}'
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
@@ -94,7 +95,9 @@ class TestKwargsPropagationToAIFunction:
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="call_1", name="simple_tool", arguments='{"x": 99}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_1", name="simple_tool", arguments='{"x": 99}')
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -136,10 +139,10 @@ class TestKwargsPropagationToAIFunction:
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_1", name="tracking_tool", arguments='{"name": "first"}'
|
||||
),
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_2", name="tracking_tool", arguments='{"name": "second"}'
|
||||
),
|
||||
],
|
||||
@@ -187,7 +190,7 @@ class TestKwargsPropagationToAIFunction:
|
||||
yield ChatResponseUpdate(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="stream_call_1",
|
||||
name="streaming_capture_tool",
|
||||
arguments='{"value": "streaming-test"}',
|
||||
@@ -197,7 +200,9 @@ class TestKwargsPropagationToAIFunction:
|
||||
)
|
||||
else:
|
||||
# Second call: return final response
|
||||
yield ChatResponseUpdate(text=TextContent(text="Stream complete!"), role="assistant", is_finished=True)
|
||||
yield ChatResponseUpdate(
|
||||
text=Content.from_text(text="Stream complete!"), role="assistant", is_finished=True
|
||||
)
|
||||
|
||||
wrapped = _handle_function_calls_streaming_response(mock_get_streaming_response)
|
||||
|
||||
|
||||
@@ -13,14 +13,12 @@ from pydantic import AnyUrl, BaseModel, ValidationError
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
DataContent,
|
||||
Content,
|
||||
MCPStdioTool,
|
||||
MCPStreamableHTTPTool,
|
||||
MCPWebsocketTool,
|
||||
Role,
|
||||
TextContent,
|
||||
ToolProtocol,
|
||||
UriContent,
|
||||
)
|
||||
from agent_framework._mcp import (
|
||||
MCPTool,
|
||||
@@ -65,7 +63,7 @@ def test_mcp_prompt_message_to_ai_content():
|
||||
assert isinstance(ai_content, ChatMessage)
|
||||
assert ai_content.role.value == "user"
|
||||
assert len(ai_content.contents) == 1
|
||||
assert isinstance(ai_content.contents[0], TextContent)
|
||||
assert ai_content.contents[0].type == "text"
|
||||
assert ai_content.contents[0].text == "Hello, world!"
|
||||
assert ai_content.raw_representation == mcp_message
|
||||
|
||||
@@ -75,20 +73,20 @@ def test_parse_contents_from_mcp_tool_result():
|
||||
mcp_result = types.CallToolResult(
|
||||
content=[
|
||||
types.TextContent(type="text", text="Result text"),
|
||||
types.ImageContent(type="image", data="xyz", mimeType="image/png"),
|
||||
types.ImageContent(type="image", data=b"abc", mimeType="image/webp"),
|
||||
types.ImageContent(type="image", data="eHl6", mimeType="image/png"), # base64 for "xyz"
|
||||
types.ImageContent(type="image", data="YWJj", mimeType="image/webp"), # base64 for "abc"
|
||||
]
|
||||
)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 3
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
assert ai_contents[0].type == "text"
|
||||
assert ai_contents[0].text == "Result text"
|
||||
assert isinstance(ai_contents[1], DataContent)
|
||||
assert ai_contents[1].uri == "data:image/png;base64,xyz"
|
||||
assert ai_contents[1].type == "data"
|
||||
assert ai_contents[1].uri == "data:image/png;base64,eHl6"
|
||||
assert ai_contents[1].media_type == "image/png"
|
||||
assert isinstance(ai_contents[2], DataContent)
|
||||
assert ai_contents[2].uri == "data:image/webp;base64,abc"
|
||||
assert ai_contents[2].type == "data"
|
||||
assert ai_contents[2].uri == "data:image/webp;base64,YWJj"
|
||||
assert ai_contents[2].media_type == "image/webp"
|
||||
|
||||
|
||||
@@ -103,7 +101,7 @@ def test_mcp_call_tool_result_with_meta_error():
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
assert ai_contents[0].type == "text"
|
||||
assert ai_contents[0].text == "Error occurred"
|
||||
|
||||
# Check that _meta data is merged into additional_properties
|
||||
@@ -134,7 +132,7 @@ def test_mcp_call_tool_result_with_meta_arbitrary_data():
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
assert ai_contents[0].type == "text"
|
||||
assert ai_contents[0].text == "Success result"
|
||||
|
||||
# Check that _meta data is preserved in additional_properties
|
||||
@@ -172,7 +170,7 @@ def test_mcp_call_tool_result_with_meta_none():
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
assert ai_contents[0].type == "text"
|
||||
assert ai_contents[0].text == "No meta test"
|
||||
|
||||
# Should handle gracefully when no _meta field exists
|
||||
@@ -187,7 +185,7 @@ def test_mcp_call_tool_result_regression_successful_workflow():
|
||||
mcp_result = types.CallToolResult(
|
||||
content=[
|
||||
types.TextContent(type="text", text="Success message"),
|
||||
types.ImageContent(type="image", data="abc123", mimeType="image/jpeg"),
|
||||
types.ImageContent(type="image", data="YWJjMTIz", mimeType="image/jpeg"), # base64 for "abc123"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -197,12 +195,12 @@ def test_mcp_call_tool_result_regression_successful_workflow():
|
||||
assert len(ai_contents) == 2
|
||||
|
||||
text_content = ai_contents[0]
|
||||
assert isinstance(text_content, TextContent)
|
||||
assert text_content.type == "text"
|
||||
assert text_content.text == "Success message"
|
||||
|
||||
image_content = ai_contents[1]
|
||||
assert isinstance(image_content, DataContent)
|
||||
assert image_content.uri == "data:image/jpeg;base64,abc123"
|
||||
assert image_content.type == "data"
|
||||
assert image_content.uri == "data:image/jpeg;base64,YWJjMTIz"
|
||||
assert image_content.media_type == "image/jpeg"
|
||||
|
||||
# Should have no additional_properties when no _meta field
|
||||
@@ -215,30 +213,31 @@ def test_mcp_content_types_to_ai_content_text():
|
||||
mcp_content = types.TextContent(type="text", text="Sample text")
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, TextContent)
|
||||
assert ai_content.type == "text"
|
||||
assert ai_content.text == "Sample text"
|
||||
assert ai_content.raw_representation == mcp_content
|
||||
|
||||
|
||||
def test_mcp_content_types_to_ai_content_image():
|
||||
"""Test conversion of MCP image content to AI content."""
|
||||
mcp_content = types.ImageContent(type="image", data="abc", mimeType="image/jpeg")
|
||||
mcp_content = types.ImageContent(type="image", data=b"abc", mimeType="image/jpeg")
|
||||
# MCP can send data as base64 string or as bytes
|
||||
mcp_content = types.ImageContent(type="image", data="YWJj", mimeType="image/jpeg") # base64 for b"abc"
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:image/jpeg;base64,abc"
|
||||
assert ai_content.type == "data"
|
||||
assert ai_content.uri == "data:image/jpeg;base64,YWJj"
|
||||
assert ai_content.media_type == "image/jpeg"
|
||||
assert ai_content.raw_representation == mcp_content
|
||||
|
||||
|
||||
def test_mcp_content_types_to_ai_content_audio():
|
||||
"""Test conversion of MCP audio content to AI content."""
|
||||
mcp_content = types.AudioContent(type="audio", data="def", mimeType="audio/wav")
|
||||
# Use properly padded base64
|
||||
mcp_content = types.AudioContent(type="audio", data="ZGVm", mimeType="audio/wav") # base64 for b"def"
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:audio/wav;base64,def"
|
||||
assert ai_content.type == "data"
|
||||
assert ai_content.uri == "data:audio/wav;base64,ZGVm"
|
||||
assert ai_content.media_type == "audio/wav"
|
||||
assert ai_content.raw_representation == mcp_content
|
||||
|
||||
@@ -253,7 +252,7 @@ def test_mcp_content_types_to_ai_content_resource_link():
|
||||
)
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, UriContent)
|
||||
assert ai_content.type == "uri"
|
||||
assert ai_content.uri == "https://example.com/resource"
|
||||
assert ai_content.media_type == "application/json"
|
||||
assert ai_content.raw_representation == mcp_content
|
||||
@@ -269,7 +268,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_text():
|
||||
mcp_content = types.EmbeddedResource(type="resource", resource=text_resource)
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, TextContent)
|
||||
assert ai_content.type == "text"
|
||||
assert ai_content.text == "Embedded text content"
|
||||
assert ai_content.raw_representation == mcp_content
|
||||
|
||||
@@ -285,7 +284,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_blob():
|
||||
mcp_content = types.EmbeddedResource(type="resource", resource=blob_resource)
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.type == "data"
|
||||
assert ai_content.uri == "data:application/octet-stream;base64,dGVzdCBkYXRh"
|
||||
assert ai_content.media_type == "application/octet-stream"
|
||||
assert ai_content.raw_representation == mcp_content
|
||||
@@ -293,7 +292,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_blob():
|
||||
|
||||
def test_ai_content_to_mcp_content_types_text():
|
||||
"""Test conversion of AI text content to MCP content."""
|
||||
ai_content = TextContent(text="Sample text")
|
||||
ai_content = Content.from_text(text="Sample text")
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.TextContent)
|
||||
@@ -303,7 +302,7 @@ def test_ai_content_to_mcp_content_types_text():
|
||||
|
||||
def test_ai_content_to_mcp_content_types_data_image():
|
||||
"""Test conversion of AI data content to MCP content."""
|
||||
ai_content = DataContent(uri="data:image/png;base64,xyz", media_type="image/png")
|
||||
ai_content = Content.from_uri(uri="data:image/png;base64,xyz", media_type="image/png")
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.ImageContent)
|
||||
@@ -314,7 +313,7 @@ def test_ai_content_to_mcp_content_types_data_image():
|
||||
|
||||
def test_ai_content_to_mcp_content_types_data_audio():
|
||||
"""Test conversion of AI data content to MCP content."""
|
||||
ai_content = DataContent(uri="data:audio/mpeg;base64,xyz", media_type="audio/mpeg")
|
||||
ai_content = Content.from_uri(uri="data:audio/mpeg;base64,xyz", media_type="audio/mpeg")
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.AudioContent)
|
||||
@@ -325,7 +324,7 @@ def test_ai_content_to_mcp_content_types_data_audio():
|
||||
|
||||
def test_ai_content_to_mcp_content_types_data_binary():
|
||||
"""Test conversion of AI data content to MCP content."""
|
||||
ai_content = DataContent(
|
||||
ai_content = Content.from_uri(
|
||||
uri="data:application/octet-stream;base64,xyz",
|
||||
media_type="application/octet-stream",
|
||||
)
|
||||
@@ -339,7 +338,7 @@ def test_ai_content_to_mcp_content_types_data_binary():
|
||||
|
||||
def test_ai_content_to_mcp_content_types_uri():
|
||||
"""Test conversion of AI URI content to MCP content."""
|
||||
ai_content = UriContent(uri="https://example.com/resource", media_type="application/json")
|
||||
ai_content = Content.from_uri(uri="https://example.com/resource", media_type="application/json")
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.ResourceLink)
|
||||
@@ -352,8 +351,8 @@ def test_prepare_message_for_mcp():
|
||||
message = ChatMessage(
|
||||
role="user",
|
||||
contents=[
|
||||
TextContent(text="test"),
|
||||
DataContent(uri="data:image/png;base64,xyz", media_type="image/png"),
|
||||
Content.from_text(text="test"),
|
||||
Content.from_uri(uri="data:image/png;base64,xyz", media_type="image/png"),
|
||||
],
|
||||
)
|
||||
mcp_contents = _prepare_message_for_mcp(message)
|
||||
@@ -871,7 +870,7 @@ async def test_mcp_tool_call_tool_with_meta_integration():
|
||||
result = await func.invoke(param="test_value")
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].type == "text"
|
||||
assert result[0].text == "Tool executed with metadata"
|
||||
|
||||
# Verify that _meta data is present in additional_properties
|
||||
@@ -920,7 +919,7 @@ async def test_local_mcp_server_function_execution():
|
||||
result = await func.invoke(param="test_value")
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].type == "text"
|
||||
assert result[0].text == "Tool executed successfully"
|
||||
|
||||
|
||||
@@ -969,7 +968,7 @@ async def test_local_mcp_server_function_execution_with_nested_object():
|
||||
result = await func.invoke(params={"customer_id": 251})
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].type == "text"
|
||||
|
||||
# Verify the session.call_tool was called with the correct nested structure
|
||||
server.session.call_tool.assert_called_once()
|
||||
@@ -1413,7 +1412,7 @@ async def test_mcp_tool_sampling_callback_chat_client_exception():
|
||||
|
||||
async def test_mcp_tool_sampling_callback_no_valid_content():
|
||||
"""Test sampling callback when response has no valid content types."""
|
||||
from agent_framework import ChatMessage, DataContent, Role
|
||||
from agent_framework import ChatMessage, Role
|
||||
|
||||
tool = MCPStdioTool(name="test_tool", command="python")
|
||||
|
||||
@@ -1424,7 +1423,7 @@ async def test_mcp_tool_sampling_callback_no_valid_content():
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
DataContent(
|
||||
Content.from_uri(
|
||||
uri="data:application/json;base64,e30K",
|
||||
media_type="application/json",
|
||||
)
|
||||
|
||||
@@ -14,8 +14,8 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework._middleware import (
|
||||
AgentMiddleware,
|
||||
@@ -217,8 +217,8 @@ class TestAgentMiddlewarePipeline:
|
||||
context = AgentRunContext(agent=mock_agent, messages=messages)
|
||||
|
||||
async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
async for update in pipeline.execute_stream(mock_agent, messages, context, final_handler):
|
||||
@@ -250,8 +250,8 @@ class TestAgentMiddlewarePipeline:
|
||||
|
||||
async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
execution_order.append("handler_start")
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
execution_order.append("handler_end")
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
@@ -313,8 +313,8 @@ class TestAgentMiddlewarePipeline:
|
||||
async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
# Handler should not be executed when terminated before next()
|
||||
execution_order.append("handler_start")
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
execution_order.append("handler_end")
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
@@ -336,8 +336,8 @@ class TestAgentMiddlewarePipeline:
|
||||
|
||||
async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
execution_order.append("handler_start")
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
execution_order.append("handler_end")
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
@@ -609,8 +609,8 @@ class TestChatMiddlewarePipeline:
|
||||
context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options)
|
||||
|
||||
async def final_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
async for update in pipeline.execute_stream(mock_chat_client, messages, chat_options, context, final_handler):
|
||||
@@ -641,8 +641,8 @@ class TestChatMiddlewarePipeline:
|
||||
|
||||
async def final_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
execution_order.append("handler_start")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
execution_order.append("handler_end")
|
||||
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
@@ -706,8 +706,8 @@ class TestChatMiddlewarePipeline:
|
||||
async def final_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
# Handler should not be executed when terminated before next()
|
||||
execution_order.append("handler_start")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
execution_order.append("handler_end")
|
||||
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
@@ -730,8 +730,8 @@ class TestChatMiddlewarePipeline:
|
||||
|
||||
async def final_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
execution_order.append("handler_start")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
execution_order.append("handler_end")
|
||||
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
@@ -1264,7 +1264,7 @@ class TestStreamingScenarios:
|
||||
|
||||
async def final_stream_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
streaming_flags.append(ctx.is_streaming)
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk")])
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
async for update in pipeline.execute_stream(mock_agent, messages, context_stream, final_stream_handler):
|
||||
@@ -1292,9 +1292,9 @@ class TestStreamingScenarios:
|
||||
|
||||
async def final_stream_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
chunks_processed.append("stream_start")
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
chunks_processed.append("chunk1_yielded")
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
chunks_processed.append("chunk2_yielded")
|
||||
chunks_processed.append("stream_end")
|
||||
|
||||
@@ -1342,7 +1342,7 @@ class TestStreamingScenarios:
|
||||
|
||||
async def final_stream_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
streaming_flags.append(ctx.is_streaming)
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk")])
|
||||
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
async for update in pipeline.execute_stream(
|
||||
@@ -1371,9 +1371,9 @@ class TestStreamingScenarios:
|
||||
|
||||
async def final_stream_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
chunks_processed.append("stream_start")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk1")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk1")])
|
||||
chunks_processed.append("chunk1_yielded")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="chunk2")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="chunk2")])
|
||||
chunks_processed.append("chunk2_yielded")
|
||||
chunks_processed.append("stream_end")
|
||||
|
||||
@@ -1486,7 +1486,7 @@ class TestMiddlewareExecutionControl:
|
||||
async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
nonlocal handler_called
|
||||
handler_called = True
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="should not execute")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="should not execute")])
|
||||
|
||||
# When middleware doesn't call next(), streaming should yield no updates
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
@@ -1617,7 +1617,7 @@ class TestMiddlewareExecutionControl:
|
||||
async def final_handler(ctx: ChatContext) -> AsyncIterable[ChatResponseUpdate]:
|
||||
nonlocal handler_called
|
||||
handler_called = True
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="should not execute")])
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="should not execute")])
|
||||
|
||||
# When middleware doesn't call next(), streaming should yield no updates
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
|
||||
@@ -13,8 +13,8 @@ from agent_framework import (
|
||||
AgentResponseUpdate,
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework._middleware import (
|
||||
AgentMiddleware,
|
||||
@@ -75,8 +75,8 @@ class TestResultOverrideMiddleware:
|
||||
"""Test that agent middleware can override response for streaming execution."""
|
||||
|
||||
async def override_stream() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="overridden")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=" stream")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="overridden")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=" stream")])
|
||||
|
||||
class StreamResponseOverrideMiddleware(AgentMiddleware):
|
||||
async def process(
|
||||
@@ -92,7 +92,7 @@ class TestResultOverrideMiddleware:
|
||||
context = AgentRunContext(agent=mock_agent, messages=messages)
|
||||
|
||||
async def final_handler(ctx: AgentRunContext) -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="original")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="original")])
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
async for update in pipeline.execute_stream(mock_agent, messages, context, final_handler):
|
||||
@@ -175,9 +175,9 @@ class TestResultOverrideMiddleware:
|
||||
mock_chat_client = MockChatClient()
|
||||
|
||||
async def custom_stream() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(contents=[TextContent(text="Custom")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=" streaming")])
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=" response!")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text="Custom")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=" streaming")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=" response!")])
|
||||
|
||||
class ChatAgentStreamOverrideMiddleware(AgentMiddleware):
|
||||
async def process(
|
||||
|
||||
@@ -13,10 +13,8 @@ from agent_framework import (
|
||||
ChatMiddleware,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
Role,
|
||||
TextContent,
|
||||
agent_middleware,
|
||||
chat_middleware,
|
||||
function_middleware,
|
||||
@@ -201,7 +199,9 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(call_id="test_call", name="test_function", arguments={"text": "test"})
|
||||
Content.from_function_call(
|
||||
call_id="test_call", name="test_function", arguments={"text": "test"}
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
@@ -256,7 +256,9 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(call_id="test_call", name="test_function", arguments={"text": "test"})
|
||||
Content.from_function_call(
|
||||
call_id="test_call", name="test_function", arguments={"text": "test"}
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
@@ -365,8 +367,8 @@ class TestChatAgentStreamingMiddleware:
|
||||
# Set up mock streaming responses
|
||||
chat_client.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(contents=[TextContent(text="Streaming")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[TextContent(text=" response")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text="Streaming")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text=" response")], role=Role.ASSISTANT),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -550,7 +552,7 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_123",
|
||||
name="sample_tool_function",
|
||||
arguments='{"location": "Seattle"}',
|
||||
@@ -585,8 +587,8 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
# Verify function call and result are in the response
|
||||
all_contents = [content for message in response.messages for content in message.contents]
|
||||
function_calls = [c for c in all_contents if isinstance(c, FunctionCallContent)]
|
||||
function_results = [c for c in all_contents if isinstance(c, FunctionResultContent)]
|
||||
function_calls = [c for c in all_contents if c.type == "function_call"]
|
||||
function_results = [c for c in all_contents if c.type == "function_result"]
|
||||
|
||||
assert len(function_calls) == 1
|
||||
assert len(function_results) == 1
|
||||
@@ -610,7 +612,7 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_456",
|
||||
name="sample_tool_function",
|
||||
arguments='{"location": "San Francisco"}',
|
||||
@@ -644,8 +646,8 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
# Verify function call and result are in the response
|
||||
all_contents = [content for message in response.messages for content in message.contents]
|
||||
function_calls = [c for c in all_contents if isinstance(c, FunctionCallContent)]
|
||||
function_results = [c for c in all_contents if isinstance(c, FunctionResultContent)]
|
||||
function_calls = [c for c in all_contents if c.type == "function_call"]
|
||||
function_results = [c for c in all_contents if c.type == "function_result"]
|
||||
|
||||
assert len(function_calls) == 1
|
||||
assert len(function_results) == 1
|
||||
@@ -682,7 +684,7 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_789",
|
||||
name="sample_tool_function",
|
||||
arguments='{"location": "New York"}',
|
||||
@@ -723,8 +725,8 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
# Verify function call and result are in the response
|
||||
all_contents = [content for message in response.messages for content in message.contents]
|
||||
function_calls = [c for c in all_contents if isinstance(c, FunctionCallContent)]
|
||||
function_results = [c for c in all_contents if isinstance(c, FunctionResultContent)]
|
||||
function_calls = [c for c in all_contents if c.type == "function_call"]
|
||||
function_results = [c for c in all_contents if c.type == "function_result"]
|
||||
|
||||
assert len(function_calls) == 1
|
||||
assert len(function_results) == 1
|
||||
@@ -769,14 +771,16 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="test_call", name="sample_tool_function", arguments={"location": "Seattle"}
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent("Function completed")])]),
|
||||
ChatResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text("Function completed")])]
|
||||
),
|
||||
]
|
||||
|
||||
# Create ChatAgent with function middleware
|
||||
@@ -1076,8 +1080,8 @@ class TestRunLevelMiddleware:
|
||||
# Set up mock streaming responses
|
||||
chat_client.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(contents=[TextContent(text="Stream")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[TextContent(text=" response")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text=" response")], role=Role.ASSISTANT),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1159,7 +1163,7 @@ class TestRunLevelMiddleware:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="test_call",
|
||||
name="custom_tool",
|
||||
arguments='{"message": "test"}',
|
||||
@@ -1204,8 +1208,8 @@ class TestRunLevelMiddleware:
|
||||
|
||||
# Verify function call and result are in the response
|
||||
all_contents = [content for message in response.messages for content in message.contents]
|
||||
function_calls = [c for c in all_contents if isinstance(c, FunctionCallContent)]
|
||||
function_results = [c for c in all_contents if isinstance(c, FunctionResultContent)]
|
||||
function_calls = [c for c in all_contents if c.type == "function_call"]
|
||||
function_results = [c for c in all_contents if c.type == "function_result"]
|
||||
|
||||
assert len(function_calls) == 1
|
||||
assert len(function_results) == 1
|
||||
@@ -1248,7 +1252,7 @@ class TestMiddlewareDecoratorLogic:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="test_call",
|
||||
name="custom_tool",
|
||||
arguments='{"message": "test"}',
|
||||
@@ -1315,7 +1319,7 @@ class TestMiddlewareDecoratorLogic:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="test_call",
|
||||
name="custom_tool",
|
||||
arguments='{"message": "test"}',
|
||||
@@ -1365,7 +1369,7 @@ class TestMiddlewareDecoratorLogic:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="test_call",
|
||||
name="custom_tool",
|
||||
arguments='{"message": "test"}',
|
||||
@@ -1704,8 +1708,8 @@ class TestChatAgentChatMiddleware:
|
||||
# Set up mock streaming responses
|
||||
chat_client.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(contents=[TextContent(text="Stream")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[TextContent(text=" response")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role=Role.ASSISTANT),
|
||||
ChatResponseUpdate(contents=[Content.from_text(text=" response")], role=Role.ASSISTANT),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1806,7 +1810,7 @@ class TestChatAgentChatMiddleware:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_456",
|
||||
name="sample_tool_function",
|
||||
arguments='{"location": "San Francisco"}',
|
||||
@@ -1850,8 +1854,8 @@ class TestChatAgentChatMiddleware:
|
||||
|
||||
# Verify function call and result are in the response
|
||||
all_contents = [content for message in response.messages for content in message.contents]
|
||||
function_calls = [c for c in all_contents if isinstance(c, FunctionCallContent)]
|
||||
function_results = [c for c in all_contents if isinstance(c, FunctionResultContent)]
|
||||
function_calls = [c for c in all_contents if c.type == "function_call"]
|
||||
function_results = [c for c in all_contents if c.type == "function_result"]
|
||||
|
||||
assert len(function_calls) == 1
|
||||
assert len(function_results) == 1
|
||||
|
||||
@@ -9,7 +9,7 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatMiddleware,
|
||||
ChatResponse,
|
||||
FunctionCallContent,
|
||||
Content,
|
||||
FunctionInvocationContext,
|
||||
Role,
|
||||
chat_middleware,
|
||||
@@ -349,7 +349,7 @@ class TestChatMiddleware:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_1",
|
||||
name="sample_tool",
|
||||
arguments={"location": "San Francisco"},
|
||||
@@ -405,7 +405,7 @@ class TestChatMiddleware:
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_2",
|
||||
name="sample_tool",
|
||||
arguments={"location": "New York"},
|
||||
|
||||
@@ -9,6 +9,7 @@ from pydantic import BaseModel, ValidationError
|
||||
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
@@ -639,24 +640,22 @@ def test_parse_inputs_none():
|
||||
|
||||
def test_parse_inputs_string():
|
||||
"""Test _parse_inputs with string input."""
|
||||
from agent_framework import UriContent
|
||||
|
||||
result = _parse_inputs("http://example.com")
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], UriContent)
|
||||
assert result[0].type == "uri"
|
||||
assert result[0].uri == "http://example.com"
|
||||
assert result[0].media_type == "text/plain"
|
||||
|
||||
|
||||
def test_parse_inputs_list_of_strings():
|
||||
"""Test _parse_inputs with list of strings."""
|
||||
from agent_framework import UriContent
|
||||
|
||||
inputs = ["http://example.com", "https://test.org"]
|
||||
result = _parse_inputs(inputs)
|
||||
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(item, UriContent) for item in result)
|
||||
assert all(item.type == "uri" for item in result)
|
||||
assert result[0].uri == "http://example.com"
|
||||
assert result[1].uri == "https://test.org"
|
||||
assert all(item.media_type == "text/plain" for item in result)
|
||||
@@ -664,88 +663,84 @@ def test_parse_inputs_list_of_strings():
|
||||
|
||||
def test_parse_inputs_uri_dict():
|
||||
"""Test _parse_inputs with URI dictionary."""
|
||||
from agent_framework import UriContent
|
||||
|
||||
input_dict = {"uri": "http://example.com", "media_type": "application/json"}
|
||||
result = _parse_inputs(input_dict)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], UriContent)
|
||||
assert result[0].type == "uri"
|
||||
assert result[0].uri == "http://example.com"
|
||||
assert result[0].media_type == "application/json"
|
||||
|
||||
|
||||
def test_parse_inputs_hosted_file_dict():
|
||||
"""Test _parse_inputs with hosted file dictionary."""
|
||||
from agent_framework import HostedFileContent
|
||||
|
||||
input_dict = {"file_id": "file-123"}
|
||||
result = _parse_inputs(input_dict)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedFileContent)
|
||||
assert result[0].type == "hosted_file"
|
||||
assert result[0].file_id == "file-123"
|
||||
|
||||
|
||||
def test_parse_inputs_hosted_vector_store_dict():
|
||||
"""Test _parse_inputs with hosted vector store dictionary."""
|
||||
from agent_framework import HostedVectorStoreContent
|
||||
from agent_framework import Content
|
||||
|
||||
input_dict = {"vector_store_id": "vs-789"}
|
||||
result = _parse_inputs(input_dict)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedVectorStoreContent)
|
||||
assert isinstance(result[0], Content)
|
||||
assert result[0].type == "hosted_vector_store"
|
||||
assert result[0].vector_store_id == "vs-789"
|
||||
|
||||
|
||||
def test_parse_inputs_data_dict():
|
||||
"""Test _parse_inputs with data dictionary."""
|
||||
from agent_framework import DataContent
|
||||
|
||||
input_dict = {"data": b"test data", "media_type": "application/octet-stream"}
|
||||
result = _parse_inputs(input_dict)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], DataContent)
|
||||
assert result[0].type == "data"
|
||||
assert result[0].uri == "data:application/octet-stream;base64,dGVzdCBkYXRh"
|
||||
assert result[0].media_type == "application/octet-stream"
|
||||
|
||||
|
||||
def test_parse_inputs_ai_contents_instance():
|
||||
"""Test _parse_inputs with Contents instance."""
|
||||
from agent_framework import TextContent
|
||||
"""Test _parse_inputs with Content instance."""
|
||||
|
||||
text_content = TextContent(text="Hello, world!")
|
||||
text_content = Content.from_text(text="Hello, world!")
|
||||
result = _parse_inputs(text_content)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert result[0].type == "text"
|
||||
assert result[0].text == "Hello, world!"
|
||||
|
||||
|
||||
def test_parse_inputs_mixed_list():
|
||||
"""Test _parse_inputs with mixed input types."""
|
||||
from agent_framework import HostedFileContent, TextContent, UriContent
|
||||
|
||||
inputs = [
|
||||
"http://example.com", # string
|
||||
{"uri": "https://test.org", "media_type": "text/html"}, # URI dict
|
||||
{"file_id": "file-456"}, # hosted file dict
|
||||
TextContent(text="Hello"), # Contents instance
|
||||
Content.from_text(text="Hello"), # Content instance
|
||||
]
|
||||
|
||||
result = _parse_inputs(inputs)
|
||||
|
||||
assert len(result) == 4
|
||||
assert isinstance(result[0], UriContent)
|
||||
assert result[0].type == "uri"
|
||||
assert result[0].uri == "http://example.com"
|
||||
assert isinstance(result[1], UriContent)
|
||||
assert result[1].type == "uri"
|
||||
assert result[1].uri == "https://test.org"
|
||||
assert result[1].media_type == "text/html"
|
||||
assert isinstance(result[2], HostedFileContent)
|
||||
assert result[2].type == "hosted_file"
|
||||
assert result[2].file_id == "file-456"
|
||||
assert isinstance(result[3], TextContent)
|
||||
assert result[3].type == "text"
|
||||
assert result[3].text == "Hello"
|
||||
|
||||
|
||||
@@ -765,55 +760,51 @@ def test_parse_inputs_unsupported_type():
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_string_input():
|
||||
"""Test HostedCodeInterpreterTool with string input."""
|
||||
from agent_framework import UriContent
|
||||
|
||||
tool = HostedCodeInterpreterTool(inputs="http://example.com")
|
||||
|
||||
assert len(tool.inputs) == 1
|
||||
assert isinstance(tool.inputs[0], UriContent)
|
||||
assert tool.inputs[0].type == "uri"
|
||||
assert tool.inputs[0].uri == "http://example.com"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_dict_inputs():
|
||||
"""Test HostedCodeInterpreterTool with dictionary inputs."""
|
||||
from agent_framework import HostedFileContent, UriContent
|
||||
|
||||
inputs = [{"uri": "http://example.com", "media_type": "text/html"}, {"file_id": "file-123"}]
|
||||
|
||||
tool = HostedCodeInterpreterTool(inputs=inputs)
|
||||
|
||||
assert len(tool.inputs) == 2
|
||||
assert isinstance(tool.inputs[0], UriContent)
|
||||
assert tool.inputs[0].type == "uri"
|
||||
assert tool.inputs[0].uri == "http://example.com"
|
||||
assert tool.inputs[0].media_type == "text/html"
|
||||
assert isinstance(tool.inputs[1], HostedFileContent)
|
||||
assert tool.inputs[1].type == "hosted_file"
|
||||
assert tool.inputs[1].file_id == "file-123"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_ai_contents():
|
||||
"""Test HostedCodeInterpreterTool with Contents instances."""
|
||||
from agent_framework import DataContent, TextContent
|
||||
"""Test HostedCodeInterpreterTool with Content instances."""
|
||||
|
||||
inputs = [TextContent(text="Hello, world!"), DataContent(data=b"test", media_type="text/plain")]
|
||||
inputs = [Content.from_text(text="Hello, world!"), Content.from_data(data=b"test", media_type="text/plain")]
|
||||
|
||||
tool = HostedCodeInterpreterTool(inputs=inputs)
|
||||
|
||||
assert len(tool.inputs) == 2
|
||||
assert isinstance(tool.inputs[0], TextContent)
|
||||
assert tool.inputs[0].type == "text"
|
||||
assert tool.inputs[0].text == "Hello, world!"
|
||||
assert isinstance(tool.inputs[1], DataContent)
|
||||
assert tool.inputs[1].type == "data"
|
||||
assert tool.inputs[1].media_type == "text/plain"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_single_input():
|
||||
"""Test HostedCodeInterpreterTool with single input (not in list)."""
|
||||
from agent_framework import HostedFileContent
|
||||
|
||||
input_dict = {"file_id": "file-single"}
|
||||
tool = HostedCodeInterpreterTool(inputs=input_dict)
|
||||
|
||||
assert len(tool.inputs) == 1
|
||||
assert isinstance(tool.inputs[0], HostedFileContent)
|
||||
assert tool.inputs[0].type == "hosted_file"
|
||||
assert tool.inputs[0].file_id == "file-single"
|
||||
|
||||
|
||||
@@ -983,7 +974,7 @@ def mock_chat_client():
|
||||
yield ChatResponseUpdate(contents=[content], role=msg.role)
|
||||
else:
|
||||
# Default response
|
||||
yield ChatResponseUpdate(contents=["Default response"], role="assistant")
|
||||
yield ChatResponseUpdate(text="Default response", role="assistant")
|
||||
|
||||
return MockChatClient()
|
||||
|
||||
@@ -1006,7 +997,7 @@ def requires_approval_tool(x: int) -> int:
|
||||
|
||||
async def test_non_streaming_single_function_no_approval():
|
||||
"""Test non-streaming handler with single function call that doesn't require approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework import ChatMessage, ChatResponse
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
# Create mock client
|
||||
@@ -1017,11 +1008,11 @@ async def test_non_streaming_single_function_no_approval():
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
contents=[Content.from_function_call(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
)
|
||||
]
|
||||
)
|
||||
final_response = ChatResponse(messages=[ChatMessage(role="assistant", contents=["The result is 10"])])
|
||||
final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="The result is 10")])
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response, final_response]
|
||||
@@ -1039,17 +1030,16 @@ async def test_non_streaming_single_function_no_approval():
|
||||
|
||||
# Verify: should have 3 messages: function call, function result, final answer
|
||||
assert len(result.messages) == 3
|
||||
assert isinstance(result.messages[0].contents[0], FunctionCallContent)
|
||||
from agent_framework import FunctionResultContent
|
||||
assert result.messages[0].contents[0].type == "function_call"
|
||||
|
||||
assert isinstance(result.messages[1].contents[0], FunctionResultContent)
|
||||
assert result.messages[1].contents[0].type == "function_result"
|
||||
assert result.messages[1].contents[0].result == 10 # 5 * 2
|
||||
assert result.messages[2].contents[0] == "The result is 10"
|
||||
assert result.messages[2].text == "The result is 10"
|
||||
|
||||
|
||||
async def test_non_streaming_single_function_requires_approval():
|
||||
"""Test non-streaming handler with single function call that requires approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework import ChatMessage, ChatResponse
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1059,7 +1049,9 @@ async def test_non_streaming_single_function_requires_approval():
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -1078,18 +1070,17 @@ async def test_non_streaming_single_function_requires_approval():
|
||||
result = await wrapped(mock_client, messages=[], options={"tools": [requires_approval_tool]})
|
||||
|
||||
# Verify: should return 1 message with function call and approval request
|
||||
from agent_framework import FunctionApprovalRequestContent
|
||||
|
||||
assert len(result.messages) == 1
|
||||
assert len(result.messages[0].contents) == 2
|
||||
assert isinstance(result.messages[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(result.messages[0].contents[1], FunctionApprovalRequestContent)
|
||||
assert result.messages[0].contents[0].type == "function_call"
|
||||
assert result.messages[0].contents[1].type == "function_approval_request"
|
||||
assert result.messages[0].contents[1].function_call.name == "requires_approval_tool"
|
||||
|
||||
|
||||
async def test_non_streaming_two_functions_both_no_approval():
|
||||
"""Test non-streaming handler with two function calls, neither requiring approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework import ChatMessage, ChatResponse
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1100,15 +1091,13 @@ async def test_non_streaming_two_functions_both_no_approval():
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
FunctionCallContent(call_id="call_2", name="no_approval_tool", arguments='{"x": 3}'),
|
||||
Content.from_function_call(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
Content.from_function_call(call_id="call_2", name="no_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
final_response = ChatResponse(
|
||||
messages=[ChatMessage(role="assistant", contents=["Both tools executed successfully"])]
|
||||
)
|
||||
final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Both tools executed successfully")])
|
||||
|
||||
call_count = [0]
|
||||
responses = [initial_response, final_response]
|
||||
@@ -1124,21 +1113,20 @@ async def test_non_streaming_two_functions_both_no_approval():
|
||||
result = await wrapped(mock_client, messages=[], options={"tools": [no_approval_tool]})
|
||||
|
||||
# Verify: should have function calls, results, and final answer
|
||||
from agent_framework import FunctionResultContent
|
||||
|
||||
assert len(result.messages) == 3
|
||||
# First message has both function calls
|
||||
assert len(result.messages[0].contents) == 2
|
||||
# Second message has both results
|
||||
assert len(result.messages[1].contents) == 2
|
||||
assert all(isinstance(c, FunctionResultContent) for c in result.messages[1].contents)
|
||||
assert all(c.type == "function_result" for c in result.messages[1].contents)
|
||||
assert result.messages[1].contents[0].result == 10 # 5 * 2
|
||||
assert result.messages[1].contents[1].result == 6 # 3 * 2
|
||||
|
||||
|
||||
async def test_non_streaming_two_functions_both_require_approval():
|
||||
"""Test non-streaming handler with two function calls, both requiring approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework import ChatMessage, ChatResponse
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1149,8 +1137,8 @@ async def test_non_streaming_two_functions_both_require_approval():
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}'),
|
||||
FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}'),
|
||||
Content.from_function_call(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}'),
|
||||
Content.from_function_call(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
)
|
||||
]
|
||||
@@ -1170,12 +1158,11 @@ async def test_non_streaming_two_functions_both_require_approval():
|
||||
result = await wrapped(mock_client, messages=[], options={"tools": [requires_approval_tool]})
|
||||
|
||||
# Verify: should return 1 message with function calls and approval requests
|
||||
from agent_framework import FunctionApprovalRequestContent
|
||||
|
||||
assert len(result.messages) == 1
|
||||
assert len(result.messages[0].contents) == 4 # 2 function calls + 2 approval requests
|
||||
function_calls = [c for c in result.messages[0].contents if isinstance(c, FunctionCallContent)]
|
||||
approval_requests = [c for c in result.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)]
|
||||
function_calls = [c for c in result.messages[0].contents if c.type == "function_call"]
|
||||
approval_requests = [c for c in result.messages[0].contents if c.type == "function_approval_request"]
|
||||
assert len(function_calls) == 2
|
||||
assert len(approval_requests) == 2
|
||||
assert approval_requests[0].function_call.name == "requires_approval_tool"
|
||||
@@ -1184,7 +1171,7 @@ async def test_non_streaming_two_functions_both_require_approval():
|
||||
|
||||
async def test_non_streaming_two_functions_mixed_approval():
|
||||
"""Test non-streaming handler with two function calls, one requiring approval."""
|
||||
from agent_framework import ChatMessage, ChatResponse, FunctionCallContent
|
||||
from agent_framework import ChatMessage, ChatResponse
|
||||
from agent_framework._tools import _handle_function_calls_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1195,8 +1182,8 @@ async def test_non_streaming_two_functions_mixed_approval():
|
||||
ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}'),
|
||||
Content.from_function_call(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
Content.from_function_call(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
)
|
||||
]
|
||||
@@ -1216,17 +1203,16 @@ async def test_non_streaming_two_functions_mixed_approval():
|
||||
result = await wrapped(mock_client, messages=[], options={"tools": [no_approval_tool, requires_approval_tool]})
|
||||
|
||||
# Verify: should return approval requests for both (when one needs approval, all are sent for approval)
|
||||
from agent_framework import FunctionApprovalRequestContent
|
||||
|
||||
assert len(result.messages) == 1
|
||||
assert len(result.messages[0].contents) == 4 # 2 function calls + 2 approval requests
|
||||
approval_requests = [c for c in result.messages[0].contents if isinstance(c, FunctionApprovalRequestContent)]
|
||||
approval_requests = [c for c in result.messages[0].contents if c.type == "function_approval_request"]
|
||||
assert len(approval_requests) == 2
|
||||
|
||||
|
||||
async def test_streaming_single_function_no_approval():
|
||||
"""Test streaming handler with single function call that doesn't require approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework import ChatResponseUpdate
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1234,11 +1220,11 @@ async def test_streaming_single_function_no_approval():
|
||||
# Initial response with function call, then final response after function execution
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
contents=[Content.from_function_call(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
)
|
||||
]
|
||||
final_updates = [ChatResponseUpdate(contents=["The result is 10"], role="assistant")]
|
||||
final_updates = [ChatResponseUpdate(text="The result is 10", role="assistant")]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates, final_updates]
|
||||
@@ -1257,22 +1243,23 @@ async def test_streaming_single_function_no_approval():
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should have function call update, tool result update (injected), and final update
|
||||
from agent_framework import FunctionResultContent, Role
|
||||
from agent_framework import Role
|
||||
|
||||
assert len(updates) >= 3
|
||||
# First update is the function call
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert updates[0].contents[0].type == "function_call"
|
||||
# Second update should be the tool result (injected by the wrapper)
|
||||
assert updates[1].role == Role.TOOL
|
||||
assert isinstance(updates[1].contents[0], FunctionResultContent)
|
||||
assert updates[1].contents[0].type == "function_result"
|
||||
assert updates[1].contents[0].result == 10 # 5 * 2
|
||||
# Last update is the final message
|
||||
assert updates[-1].contents[0] == "The result is 10"
|
||||
assert updates[-1].contents[0].type == "text"
|
||||
assert updates[-1].contents[0].text == "The result is 10"
|
||||
|
||||
|
||||
async def test_streaming_single_function_requires_approval():
|
||||
"""Test streaming handler with single function call that requires approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework import ChatResponseUpdate
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1280,7 +1267,9 @@ async def test_streaming_single_function_requires_approval():
|
||||
# Initial response with function call
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')
|
||||
],
|
||||
role="assistant",
|
||||
)
|
||||
]
|
||||
@@ -1302,17 +1291,17 @@ async def test_streaming_single_function_requires_approval():
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should yield function call and then approval request
|
||||
from agent_framework import FunctionApprovalRequestContent, Role
|
||||
from agent_framework import Role
|
||||
|
||||
assert len(updates) == 2
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert updates[0].contents[0].type == "function_call"
|
||||
assert updates[1].role == Role.ASSISTANT
|
||||
assert isinstance(updates[1].contents[0], FunctionApprovalRequestContent)
|
||||
assert updates[1].contents[0].type == "function_approval_request"
|
||||
|
||||
|
||||
async def test_streaming_two_functions_both_no_approval():
|
||||
"""Test streaming handler with two function calls, neither requiring approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework import ChatResponseUpdate
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1320,15 +1309,14 @@ async def test_streaming_two_functions_both_no_approval():
|
||||
# Initial response with two function calls to the same tool
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_2", name="no_approval_tool", arguments='{"x": 3}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}'),
|
||||
Content.from_function_call(call_id="call_2", name="no_approval_tool", arguments='{"x": 3}'),
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
final_updates = [ChatResponseUpdate(contents=["Both tools executed successfully"], role="assistant")]
|
||||
final_updates = [ChatResponseUpdate(text="Both tools executed successfully", role="assistant")]
|
||||
|
||||
call_count = [0]
|
||||
updates_list = [initial_updates, final_updates]
|
||||
@@ -1347,22 +1335,23 @@ async def test_streaming_two_functions_both_no_approval():
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should have both function calls, one tool result update with both results, and final message
|
||||
from agent_framework import FunctionResultContent, Role
|
||||
from agent_framework import Role
|
||||
|
||||
assert len(updates) >= 3
|
||||
# First two updates are function calls
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(updates[1].contents[0], FunctionCallContent)
|
||||
assert len(updates) >= 2
|
||||
# First update has both function calls
|
||||
assert len(updates[0].contents) == 2
|
||||
assert updates[0].contents[0].type == "function_call"
|
||||
assert updates[0].contents[1].type == "function_call"
|
||||
# Should have a tool result update with both results
|
||||
tool_updates = [u for u in updates if u.role == Role.TOOL]
|
||||
assert len(tool_updates) == 1
|
||||
assert len(tool_updates[0].contents) == 2
|
||||
assert all(isinstance(c, FunctionResultContent) for c in tool_updates[0].contents)
|
||||
assert all(c.type == "function_result" for c in tool_updates[0].contents)
|
||||
|
||||
|
||||
async def test_streaming_two_functions_both_require_approval():
|
||||
"""Test streaming handler with two function calls, both requiring approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework import ChatResponseUpdate
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1370,11 +1359,15 @@ async def test_streaming_two_functions_both_require_approval():
|
||||
# Initial response with two function calls to the same tool
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_1", name="requires_approval_tool", arguments='{"x": 5}')
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}')
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
@@ -1396,20 +1389,20 @@ async def test_streaming_two_functions_both_require_approval():
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should yield both function calls and then approval requests
|
||||
from agent_framework import FunctionApprovalRequestContent, Role
|
||||
from agent_framework import Role
|
||||
|
||||
assert len(updates) == 3
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(updates[1].contents[0], FunctionCallContent)
|
||||
assert updates[0].contents[0].type == "function_call"
|
||||
assert updates[1].contents[0].type == "function_call"
|
||||
# Assistant update with both approval requests
|
||||
assert updates[2].role == Role.ASSISTANT
|
||||
assert len(updates[2].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in updates[2].contents)
|
||||
assert all(c.type == "function_approval_request" for c in updates[2].contents)
|
||||
|
||||
|
||||
async def test_streaming_two_functions_mixed_approval():
|
||||
"""Test streaming handler with two function calls, one requiring approval."""
|
||||
from agent_framework import ChatResponseUpdate, FunctionCallContent
|
||||
from agent_framework import ChatResponseUpdate
|
||||
from agent_framework._tools import _handle_function_calls_streaming_response
|
||||
|
||||
mock_client = type("MockClient", (), {})()
|
||||
@@ -1417,11 +1410,13 @@ async def test_streaming_two_functions_mixed_approval():
|
||||
# Initial response with two function calls
|
||||
initial_updates = [
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
contents=[Content.from_function_call(call_id="call_1", name="no_approval_tool", arguments='{"x": 5}')],
|
||||
role="assistant",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_2", name="requires_approval_tool", arguments='{"x": 3}')
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
@@ -1445,15 +1440,15 @@ async def test_streaming_two_functions_mixed_approval():
|
||||
updates.append(update)
|
||||
|
||||
# Verify: should yield both function calls and then approval requests (when one needs approval, all wait)
|
||||
from agent_framework import FunctionApprovalRequestContent, Role
|
||||
from agent_framework import Role
|
||||
|
||||
assert len(updates) == 3
|
||||
assert isinstance(updates[0].contents[0], FunctionCallContent)
|
||||
assert isinstance(updates[1].contents[0], FunctionCallContent)
|
||||
assert updates[0].contents[0].type == "function_call"
|
||||
assert updates[1].contents[0].type == "function_call"
|
||||
# Assistant update with both approval requests
|
||||
assert updates[2].role == Role.ASSISTANT
|
||||
assert len(updates[2].contents) == 2
|
||||
assert all(isinstance(c, FunctionApprovalRequestContent) for c in updates[2].contents)
|
||||
assert all(c.type == "function_approval_request" for c in updates[2].contents)
|
||||
|
||||
|
||||
async def test_ai_function_with_kwargs_injection():
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,15 +19,10 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedVectorStoreContent,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -68,7 +63,7 @@ def create_test_openai_assistants_client(
|
||||
return client
|
||||
|
||||
|
||||
async def create_vector_store(client: OpenAIAssistantsClient) -> tuple[str, HostedVectorStoreContent]:
|
||||
async def create_vector_store(client: OpenAIAssistantsClient) -> tuple[str, Content]:
|
||||
"""Create a vector store with sample documents for testing."""
|
||||
file = await client.client.files.create(
|
||||
file=("todays_weather.txt", b"The weather today is sunny with a high of 25C."), purpose="user_data"
|
||||
@@ -81,7 +76,7 @@ async def create_vector_store(client: OpenAIAssistantsClient) -> tuple[str, Host
|
||||
if result.last_error is not None:
|
||||
raise Exception(f"Vector store file processing failed with status: {result.last_error.message}")
|
||||
|
||||
return file.id, HostedVectorStoreContent(vector_store_id=vector_store.id)
|
||||
return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id)
|
||||
|
||||
|
||||
async def delete_vector_store(client: OpenAIAssistantsClient, file_id: str, vector_store_id: str) -> None:
|
||||
@@ -464,7 +459,7 @@ async def test_process_stream_events_requires_action(mock_async_openai: MagicMoc
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Mock the _parse_function_calls_from_assistants method to return test content
|
||||
test_function_content = FunctionCallContent(call_id="call-123", name="test_func", arguments={"arg": "value"})
|
||||
test_function_content = Content.from_function_call(call_id="call-123", name="test_func", arguments={"arg": "value"})
|
||||
chat_client._parse_function_calls_from_assistants = MagicMock(return_value=[test_function_content]) # type: ignore
|
||||
|
||||
# Create a mock Run object
|
||||
@@ -578,10 +573,10 @@ async def test_process_stream_events_run_completed_with_usage(
|
||||
|
||||
# Check the usage content
|
||||
usage_content = update.contents[0]
|
||||
assert isinstance(usage_content, UsageContent)
|
||||
assert usage_content.details.input_token_count == 100
|
||||
assert usage_content.details.output_token_count == 50
|
||||
assert usage_content.details.total_token_count == 150
|
||||
assert usage_content.type == "usage"
|
||||
assert usage_content.usage_details["input_token_count"] == 100
|
||||
assert usage_content.usage_details["output_token_count"] == 50
|
||||
assert usage_content.usage_details["total_token_count"] == 150
|
||||
assert update.raw_representation == mock_run
|
||||
|
||||
|
||||
@@ -609,7 +604,7 @@ def test_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock
|
||||
|
||||
# Test that one function call content was created
|
||||
assert len(contents) == 1
|
||||
assert isinstance(contents[0], FunctionCallContent)
|
||||
assert contents[0].type == "function_call"
|
||||
assert contents[0].name == "get_weather"
|
||||
assert contents[0].arguments == {"location": "Seattle"}
|
||||
|
||||
@@ -830,7 +825,7 @@ def test_prepare_options_with_image_content(mock_async_openai: MagicMock) -> Non
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Create message with image content
|
||||
image_content = UriContent(uri="https://example.com/image.jpg", media_type="image/jpeg")
|
||||
image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg")
|
||||
messages = [ChatMessage(role=Role.USER, contents=[image_content])]
|
||||
|
||||
# Call the method
|
||||
@@ -861,7 +856,7 @@ def test_prepare_tool_outputs_for_assistants_valid(mock_async_openai: MagicMock)
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
call_id = json.dumps(["run-123", "call-456"])
|
||||
function_result = FunctionResultContent(call_id=call_id, result="Function executed successfully")
|
||||
function_result = Content.from_function_result(call_id=call_id, result="Function executed successfully")
|
||||
|
||||
run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([function_result]) # type: ignore
|
||||
|
||||
@@ -881,8 +876,8 @@ def test_prepare_tool_outputs_for_assistants_mismatched_run_ids(
|
||||
# Create function results with different run IDs
|
||||
call_id1 = json.dumps(["run-123", "call-456"])
|
||||
call_id2 = json.dumps(["run-789", "call-xyz"]) # Different run ID
|
||||
function_result1 = FunctionResultContent(call_id=call_id1, result="Result 1")
|
||||
function_result2 = FunctionResultContent(call_id=call_id2, result="Result 2")
|
||||
function_result1 = Content.from_function_result(call_id=call_id1, result="Result 1")
|
||||
function_result2 = Content.from_function_result(call_id=call_id2, result="Result 2")
|
||||
|
||||
run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([function_result1, function_result2]) # type: ignore
|
||||
|
||||
@@ -1006,7 +1001,7 @@ async def test_streaming() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25", "weather", "seattle"])
|
||||
@@ -1035,7 +1030,7 @@ async def test_streaming_tools() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25", "weather"])
|
||||
@@ -1121,7 +1116,7 @@ async def test_file_search_streaming() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
await delete_vector_store(openai_assistants_client, file_id, vector_store.vector_store_id)
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ from agent_framework import (
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
DataContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
ai_function,
|
||||
@@ -282,7 +281,9 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Test with empty list (falsy but not None)
|
||||
message_with_empty_list = ChatMessage(role="tool", contents=[FunctionResultContent(call_id="call-123", result=[])])
|
||||
message_with_empty_list = ChatMessage(
|
||||
role="tool", contents=[Content.from_function_result(call_id="call-123", result=[])]
|
||||
)
|
||||
|
||||
openai_messages = client._prepare_message_for_openai(message_with_empty_list)
|
||||
assert len(openai_messages) == 1
|
||||
@@ -290,7 +291,7 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s
|
||||
|
||||
# Test with empty string (falsy but not None)
|
||||
message_with_empty_string = ChatMessage(
|
||||
role="tool", contents=[FunctionResultContent(call_id="call-456", result="")]
|
||||
role="tool", contents=[Content.from_function_result(call_id="call-456", result="")]
|
||||
)
|
||||
|
||||
openai_messages = client._prepare_message_for_openai(message_with_empty_string)
|
||||
@@ -298,7 +299,9 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s
|
||||
assert openai_messages[0]["content"] == "" # Empty string should be preserved
|
||||
|
||||
# Test with False (falsy but not None)
|
||||
message_with_false = ChatMessage(role="tool", contents=[FunctionResultContent(call_id="call-789", result=False)])
|
||||
message_with_false = ChatMessage(
|
||||
role="tool", contents=[Content.from_function_result(call_id="call-789", result=False)]
|
||||
)
|
||||
|
||||
openai_messages = client._prepare_message_for_openai(message_with_false)
|
||||
assert len(openai_messages) == 1
|
||||
@@ -317,7 +320,7 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str]
|
||||
message_with_exception = ChatMessage(
|
||||
role="tool",
|
||||
contents=[
|
||||
FunctionResultContent(call_id="call-123", result="Error: Function failed.", exception=test_exception)
|
||||
Content.from_function_result(call_id="call-123", result="Error: Function failed.", exception=test_exception)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -339,7 +342,7 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Test DataContent with image media type
|
||||
image_data_content = DataContent(
|
||||
image_data_content = Content.from_uri(
|
||||
uri="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||
media_type="image/png",
|
||||
)
|
||||
@@ -351,7 +354,7 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic
|
||||
assert result["image_url"]["url"] == image_data_content.uri
|
||||
|
||||
# Test DataContent with non-image media type should use default model_dump
|
||||
text_data_content = DataContent(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
|
||||
text_data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
|
||||
|
||||
result = client._prepare_content_for_openai(text_data_content) # type: ignore
|
||||
|
||||
@@ -361,7 +364,7 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic
|
||||
assert result["media_type"] == "text/plain"
|
||||
|
||||
# Test DataContent with audio media type
|
||||
audio_data_content = DataContent(
|
||||
audio_data_content = Content.from_uri(
|
||||
uri="data:audio/wav;base64,UklGRjBEAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQwEAAAAAAAAAAAA",
|
||||
media_type="audio/wav",
|
||||
)
|
||||
@@ -375,7 +378,9 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic
|
||||
assert result["input_audio"]["format"] == "wav"
|
||||
|
||||
# Test DataContent with MP3 audio
|
||||
mp3_data_content = DataContent(uri="data:audio/mp3;base64,//uQAAAAWGluZwAAAA8AAAACAAACcQ==", media_type="audio/mp3")
|
||||
mp3_data_content = Content.from_uri(
|
||||
uri="data:audio/mp3;base64,//uQAAAAWGluZwAAAA8AAAACAAACcQ==", media_type="audio/mp3"
|
||||
)
|
||||
|
||||
result = client._prepare_content_for_openai(mp3_data_content) # type: ignore
|
||||
|
||||
@@ -391,7 +396,7 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Test PDF without filename - should omit filename in OpenAI payload
|
||||
pdf_data_content = DataContent(
|
||||
pdf_data_content = Content.from_uri(
|
||||
uri="data:application/pdf;base64,JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXS9QYXJlbnQgMiAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDQgMCBSPj4+Pi9Db250ZW50cyA1IDAgUj4+CmVuZG9iago0IDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1R5cGUxL0Jhc2VGb250L0hlbHZldGljYT4+CmVuZG9iago1IDAgb2JqCjw8L0xlbmd0aCA0ND4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSA4IFRmCihIZWxsbyBXb3JsZCEpIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQ1IDAwMDAwIG4gCjAwMDAwMDAzMDcgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDYvUm9vdCAxIDAgUj4+CnN0YXJ0eHJlZgo0MDUKJSVFT0Y=",
|
||||
media_type="application/pdf",
|
||||
)
|
||||
@@ -407,7 +412,7 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
|
||||
assert result["file"]["file_data"] == pdf_data_content.uri
|
||||
|
||||
# Test PDF with custom filename via additional_properties
|
||||
pdf_with_filename = DataContent(
|
||||
pdf_with_filename = Content.from_uri(
|
||||
uri="data:application/pdf;base64,JVBERi0xLjQ=",
|
||||
media_type="application/pdf",
|
||||
additional_properties={"filename": "report.pdf"},
|
||||
@@ -441,7 +446,7 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
|
||||
|
||||
for case in test_cases:
|
||||
# Test without filename
|
||||
doc_content = DataContent(
|
||||
doc_content = Content.from_uri(
|
||||
uri=f"data:{case['media_type']};base64,{case['base64']}",
|
||||
media_type=case["media_type"],
|
||||
)
|
||||
@@ -454,7 +459,7 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
|
||||
assert result["file"]["file_data"] == doc_content.uri
|
||||
|
||||
# Test with filename - should now use file format with filename
|
||||
doc_with_filename = DataContent(
|
||||
doc_with_filename = Content.from_uri(
|
||||
uri=f"data:{case['media_type']};base64,{case['base64']}",
|
||||
media_type=case["media_type"],
|
||||
additional_properties={"filename": case["filename"]},
|
||||
@@ -468,7 +473,7 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
|
||||
assert result["file"]["file_data"] == doc_with_filename.uri
|
||||
|
||||
# Test edge case: empty additional_properties dict
|
||||
pdf_empty_props = DataContent(
|
||||
pdf_empty_props = Content.from_uri(
|
||||
uri="data:application/pdf;base64,JVBERi0xLjQ=",
|
||||
media_type="application/pdf",
|
||||
additional_properties={},
|
||||
@@ -480,7 +485,7 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
|
||||
assert "filename" not in result["file"]
|
||||
|
||||
# Test edge case: None filename in additional_properties
|
||||
pdf_none_filename = DataContent(
|
||||
pdf_none_filename = Content.from_uri(
|
||||
uri="data:application/pdf;base64,JVBERi0xLjQ=",
|
||||
media_type="application/pdf",
|
||||
additional_properties={"filename": None},
|
||||
|
||||
@@ -33,26 +33,13 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
CodeInterpreterToolCallContent,
|
||||
CodeInterpreterToolResultContent,
|
||||
DataContent,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
ImageGenerationToolCallContent,
|
||||
ImageGenerationToolResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
TextReasoningContent,
|
||||
UriContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.exceptions import (
|
||||
@@ -81,7 +68,7 @@ class OutputStruct(BaseModel):
|
||||
|
||||
async def create_vector_store(
|
||||
client: OpenAIResponsesClient,
|
||||
) -> tuple[str, HostedVectorStoreContent]:
|
||||
) -> tuple[str, Content]:
|
||||
"""Create a vector store with sample documents for testing."""
|
||||
file = await client.client.files.create(
|
||||
file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."),
|
||||
@@ -99,7 +86,7 @@ async def create_vector_store(
|
||||
if result.last_error is not None:
|
||||
raise Exception(f"Vector store file processing failed with status: {result.last_error.message}")
|
||||
|
||||
return file.id, HostedVectorStoreContent(vector_store_id=vector_store.id)
|
||||
return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id)
|
||||
|
||||
|
||||
async def delete_vector_store(client: OpenAIResponsesClient, file_id: str, vector_store_id: str) -> None:
|
||||
@@ -285,7 +272,7 @@ def test_file_search_tool_with_invalid_inputs() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test with invalid inputs type (should trigger ValueError)
|
||||
file_search_tool = HostedFileSearchTool(inputs=[HostedFileContent(file_id="invalid")])
|
||||
file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_file(file_id="invalid")])
|
||||
|
||||
# Should raise an error due to invalid inputs
|
||||
with pytest.raises(ValueError, match="HostedFileSearchTool requires inputs to be of type"):
|
||||
@@ -314,7 +301,7 @@ def test_code_interpreter_tool_variations() -> None:
|
||||
|
||||
# Test code interpreter with files
|
||||
code_tool_with_files = HostedCodeInterpreterTool(
|
||||
inputs=[HostedFileContent(file_id="file1"), HostedFileContent(file_id="file2")]
|
||||
inputs=[Content.from_hosted_file(file_id="file1"), Content.from_hosted_file(file_id="file2")]
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceResponseException):
|
||||
@@ -367,14 +354,14 @@ def test_chat_message_parsing_with_function_calls() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Create messages with function call and result content
|
||||
function_call = FunctionCallContent(
|
||||
function_call = Content.from_function_call(
|
||||
call_id="test-call-id",
|
||||
name="test_function",
|
||||
arguments='{"param": "value"}',
|
||||
additional_properties={"fc_id": "test-fc-id"},
|
||||
)
|
||||
|
||||
function_result = FunctionResultContent(call_id="test-call-id", result="Function executed successfully")
|
||||
function_result = Content.from_function_result(call_id="test-call-id", result="Function executed successfully")
|
||||
|
||||
messages = [
|
||||
ChatMessage(role="user", text="Call a function"),
|
||||
@@ -516,7 +503,7 @@ def test_response_content_creation_with_annotations() -> None:
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) >= 1
|
||||
assert isinstance(response.messages[0].contents[0], TextContent)
|
||||
assert response.messages[0].contents[0].type == "text"
|
||||
assert response.messages[0].contents[0].text == "Text with annotations."
|
||||
assert response.messages[0].contents[0].annotations is not None
|
||||
|
||||
@@ -547,7 +534,7 @@ def test_response_content_creation_with_refusal() -> None:
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 1
|
||||
assert isinstance(response.messages[0].contents[0], TextContent)
|
||||
assert response.messages[0].contents[0].type == "text"
|
||||
assert response.messages[0].contents[0].text == "I cannot provide that information."
|
||||
|
||||
|
||||
@@ -577,7 +564,7 @@ def test_response_content_creation_with_reasoning() -> None:
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 2
|
||||
assert isinstance(response.messages[0].contents[0], TextReasoningContent)
|
||||
assert response.messages[0].contents[0].type == "text_reasoning"
|
||||
assert response.messages[0].contents[0].text == "Reasoning step"
|
||||
|
||||
|
||||
@@ -614,13 +601,13 @@ def test_response_content_creation_with_code_interpreter() -> None:
|
||||
|
||||
assert len(response.messages[0].contents) == 2
|
||||
call_content, result_content = response.messages[0].contents
|
||||
assert isinstance(call_content, CodeInterpreterToolCallContent)
|
||||
assert call_content.type == "code_interpreter_tool_call"
|
||||
assert call_content.inputs is not None
|
||||
assert isinstance(call_content.inputs[0], TextContent)
|
||||
assert isinstance(result_content, CodeInterpreterToolResultContent)
|
||||
assert call_content.inputs[0].type == "text"
|
||||
assert result_content.type == "code_interpreter_tool_result"
|
||||
assert result_content.outputs is not None
|
||||
assert any(isinstance(out, TextContent) for out in result_content.outputs)
|
||||
assert any(isinstance(out, UriContent) for out in result_content.outputs)
|
||||
assert any(out.type == "text" for out in result_content.outputs)
|
||||
assert any(out.type == "uri" for out in result_content.outputs)
|
||||
|
||||
|
||||
def test_response_content_creation_with_function_call() -> None:
|
||||
@@ -648,7 +635,7 @@ def test_response_content_creation_with_function_call() -> None:
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 1
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
assert response.messages[0].contents[0].type == "function_call"
|
||||
function_call = response.messages[0].contents[0]
|
||||
assert function_call.call_id == "call_123"
|
||||
assert function_call.name == "get_weather"
|
||||
@@ -708,7 +695,7 @@ def test_parse_response_from_openai_with_mcp_approval_request() -> None:
|
||||
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert isinstance(response.messages[0].contents[0], FunctionApprovalRequestContent)
|
||||
assert response.messages[0].contents[0].type == "function_approval_request"
|
||||
req = response.messages[0].contents[0]
|
||||
assert req.id == "approval-1"
|
||||
assert req.function_call.name == "do_sensitive_action"
|
||||
@@ -874,8 +861,8 @@ def test_parse_chunk_from_openai_with_mcp_approval_request() -> None:
|
||||
mock_event.item = mock_item
|
||||
|
||||
update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
|
||||
assert any(isinstance(c, FunctionApprovalRequestContent) for c in update.contents)
|
||||
fa = next(c for c in update.contents if isinstance(c, FunctionApprovalRequestContent))
|
||||
assert any(c.type == "function_approval_request" for c in update.contents)
|
||||
fa = next(c for c in update.contents if c.type == "function_approval_request")
|
||||
assert fa.id == "approval-stream-1"
|
||||
assert fa.function_call.name == "do_stream_action"
|
||||
|
||||
@@ -925,12 +912,12 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
|
||||
with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create:
|
||||
# First call: get the approval request
|
||||
response = await client.get_response(messages=[ChatMessage(role="user", text="Trigger approval")])
|
||||
assert isinstance(response.messages[0].contents[0], FunctionApprovalRequestContent)
|
||||
assert response.messages[0].contents[0].type == "function_approval_request"
|
||||
req = response.messages[0].contents[0]
|
||||
assert req.id == "approval-1"
|
||||
|
||||
# Build a user approval and send it (include required function_call)
|
||||
approval = FunctionApprovalResponseContent(approved=True, id=req.id, function_call=req.function_call)
|
||||
approval = Content.from_function_approval_response(approved=True, id=req.id, function_call=req.function_call)
|
||||
approval_message = ChatMessage(role="user", contents=[approval])
|
||||
_ = await client.get_response(messages=[approval_message])
|
||||
|
||||
@@ -961,9 +948,9 @@ def test_usage_details_basic() -> None:
|
||||
|
||||
details = client._parse_usage_from_openai(mock_usage) # type: ignore
|
||||
assert details is not None
|
||||
assert details.input_token_count == 100
|
||||
assert details.output_token_count == 50
|
||||
assert details.total_token_count == 150
|
||||
assert details["input_token_count"] == 100
|
||||
assert details["output_token_count"] == 50
|
||||
assert details["total_token_count"] == 150
|
||||
|
||||
|
||||
def test_usage_details_with_cached_tokens() -> None:
|
||||
@@ -980,8 +967,8 @@ def test_usage_details_with_cached_tokens() -> None:
|
||||
|
||||
details = client._parse_usage_from_openai(mock_usage) # type: ignore
|
||||
assert details is not None
|
||||
assert details.input_token_count == 200
|
||||
assert details.additional_counts["openai.cached_input_tokens"] == 25
|
||||
assert details["input_token_count"] == 200
|
||||
assert details["openai.cached_input_tokens"] == 25
|
||||
|
||||
|
||||
def test_usage_details_with_reasoning_tokens() -> None:
|
||||
@@ -998,8 +985,8 @@ def test_usage_details_with_reasoning_tokens() -> None:
|
||||
|
||||
details = client._parse_usage_from_openai(mock_usage) # type: ignore
|
||||
assert details is not None
|
||||
assert details.output_token_count == 80
|
||||
assert details.additional_counts["openai.reasoning_tokens"] == 30
|
||||
assert details["output_token_count"] == 80
|
||||
assert details["openai.reasoning_tokens"] == 30
|
||||
|
||||
|
||||
def test_get_metadata_from_response() -> None:
|
||||
@@ -1098,7 +1085,7 @@ def test_streaming_annotation_added_with_file_path() -> None:
|
||||
|
||||
assert len(response.contents) == 1
|
||||
content = response.contents[0]
|
||||
assert isinstance(content, HostedFileContent)
|
||||
assert content.type == "hosted_file"
|
||||
assert content.file_id == "file-abc123"
|
||||
assert content.additional_properties is not None
|
||||
assert content.additional_properties.get("annotation_index") == 0
|
||||
@@ -1125,7 +1112,7 @@ def test_streaming_annotation_added_with_file_citation() -> None:
|
||||
|
||||
assert len(response.contents) == 1
|
||||
content = response.contents[0]
|
||||
assert isinstance(content, HostedFileContent)
|
||||
assert content.type == "hosted_file"
|
||||
assert content.file_id == "file-xyz789"
|
||||
assert content.additional_properties is not None
|
||||
assert content.additional_properties.get("filename") == "sample.txt"
|
||||
@@ -1154,7 +1141,7 @@ def test_streaming_annotation_added_with_container_file_citation() -> None:
|
||||
|
||||
assert len(response.contents) == 1
|
||||
content = response.contents[0]
|
||||
assert isinstance(content, HostedFileContent)
|
||||
assert content.type == "hosted_file"
|
||||
assert content.file_id == "file-container123"
|
||||
assert content.additional_properties is not None
|
||||
assert content.additional_properties.get("container_id") == "container-456"
|
||||
@@ -1228,7 +1215,7 @@ def test_prepare_content_for_openai_image_content() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test image content with detail parameter and file_id
|
||||
image_content_with_detail = UriContent(
|
||||
image_content_with_detail = Content.from_uri(
|
||||
uri="https://example.com/image.jpg",
|
||||
media_type="image/jpeg",
|
||||
additional_properties={"detail": "high", "file_id": "file_123"},
|
||||
@@ -1240,7 +1227,7 @@ def test_prepare_content_for_openai_image_content() -> None:
|
||||
assert result["file_id"] == "file_123"
|
||||
|
||||
# Test image content without additional properties (defaults)
|
||||
image_content_basic = UriContent(uri="https://example.com/basic.png", media_type="image/png")
|
||||
image_content_basic = Content.from_uri(uri="https://example.com/basic.png", media_type="image/png")
|
||||
result = client._prepare_content_for_openai(Role.USER, image_content_basic, {}) # type: ignore
|
||||
assert result["type"] == "input_image"
|
||||
assert result["detail"] == "auto"
|
||||
@@ -1252,14 +1239,14 @@ def test_prepare_content_for_openai_audio_content() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test WAV audio content
|
||||
wav_content = UriContent(uri="data:audio/wav;base64,abc123", media_type="audio/wav")
|
||||
wav_content = Content.from_uri(uri="data:audio/wav;base64,abc123", media_type="audio/wav")
|
||||
result = client._prepare_content_for_openai(Role.USER, wav_content, {}) # type: ignore
|
||||
assert result["type"] == "input_audio"
|
||||
assert result["input_audio"]["data"] == "data:audio/wav;base64,abc123"
|
||||
assert result["input_audio"]["format"] == "wav"
|
||||
|
||||
# Test MP3 audio content
|
||||
mp3_content = UriContent(uri="data:audio/mp3;base64,def456", media_type="audio/mp3")
|
||||
mp3_content = Content.from_uri(uri="data:audio/mp3;base64,def456", media_type="audio/mp3")
|
||||
result = client._prepare_content_for_openai(Role.USER, mp3_content, {}) # type: ignore
|
||||
assert result["type"] == "input_audio"
|
||||
assert result["input_audio"]["format"] == "mp3"
|
||||
@@ -1270,12 +1257,12 @@ def test_prepare_content_for_openai_unsupported_content() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test unsupported audio format
|
||||
unsupported_audio = UriContent(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg")
|
||||
unsupported_audio = Content.from_uri(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg")
|
||||
result = client._prepare_content_for_openai(Role.USER, unsupported_audio, {}) # type: ignore
|
||||
assert result == {}
|
||||
|
||||
# Test non-media content
|
||||
text_uri_content = UriContent(uri="https://example.com/document.txt", media_type="text/plain")
|
||||
text_uri_content = Content.from_uri(uri="https://example.com/document.txt", media_type="text/plain")
|
||||
result = client._prepare_content_for_openai(Role.USER, text_uri_content, {}) # type: ignore
|
||||
assert result == {}
|
||||
|
||||
@@ -1299,11 +1286,9 @@ def test_parse_chunk_from_openai_code_interpreter() -> None:
|
||||
|
||||
result = client._parse_chunk_from_openai(mock_event_image, chat_options, function_call_ids) # type: ignore
|
||||
assert len(result.contents) == 1
|
||||
assert isinstance(result.contents[0], CodeInterpreterToolResultContent)
|
||||
assert result.contents[0].type == "code_interpreter_tool_result"
|
||||
assert result.contents[0].outputs
|
||||
assert any(
|
||||
isinstance(out, UriContent) and out.uri == "https://example.com/plot.png" for out in result.contents[0].outputs
|
||||
)
|
||||
assert any(out.type == "uri" and out.uri == "https://example.com/plot.png" for out in result.contents[0].outputs)
|
||||
|
||||
|
||||
def test_parse_chunk_from_openai_reasoning() -> None:
|
||||
@@ -1324,7 +1309,7 @@ def test_parse_chunk_from_openai_reasoning() -> None:
|
||||
|
||||
result = client._parse_chunk_from_openai(mock_event_reasoning, chat_options, function_call_ids) # type: ignore
|
||||
assert len(result.contents) == 1
|
||||
assert isinstance(result.contents[0], TextReasoningContent)
|
||||
assert result.contents[0].type == "text_reasoning"
|
||||
assert result.contents[0].text == "Analyzing the problem step by step..."
|
||||
if result.contents[0].additional_properties:
|
||||
assert result.contents[0].additional_properties["summary"] == "Problem analysis summary"
|
||||
@@ -1335,7 +1320,7 @@ def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test TextReasoningContent with all additional properties
|
||||
comprehensive_reasoning = TextReasoningContent(
|
||||
comprehensive_reasoning = Content.from_text_reasoning(
|
||||
text="Comprehensive reasoning summary",
|
||||
additional_properties={
|
||||
"status": "in_progress",
|
||||
@@ -1371,7 +1356,7 @@ def test_streaming_reasoning_text_delta_event() -> None:
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
assert response.contents[0].type == "text_reasoning"
|
||||
assert response.contents[0].text == "reasoning delta"
|
||||
assert response.contents[0].raw_representation == event
|
||||
mock_metadata.assert_called_once_with(event)
|
||||
@@ -1396,7 +1381,7 @@ def test_streaming_reasoning_text_done_event() -> None:
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
assert response.contents[0].type == "text_reasoning"
|
||||
assert response.contents[0].text == "complete reasoning"
|
||||
assert response.contents[0].raw_representation == event
|
||||
mock_metadata.assert_called_once_with(event)
|
||||
@@ -1422,7 +1407,7 @@ def test_streaming_reasoning_summary_text_delta_event() -> None:
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
assert response.contents[0].type == "text_reasoning"
|
||||
assert response.contents[0].text == "summary delta"
|
||||
assert response.contents[0].raw_representation == event
|
||||
mock_metadata.assert_called_once_with(event)
|
||||
@@ -1447,7 +1432,7 @@ def test_streaming_reasoning_summary_text_done_event() -> None:
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
assert response.contents[0].type == "text_reasoning"
|
||||
assert response.contents[0].text == "complete summary"
|
||||
assert response.contents[0].raw_representation == event
|
||||
mock_metadata.assert_called_once_with(event)
|
||||
@@ -1488,8 +1473,8 @@ def test_streaming_reasoning_events_preserve_metadata() -> None:
|
||||
assert reasoning_response.additional_properties == {"test": "metadata"}
|
||||
|
||||
# Content types should be different
|
||||
assert isinstance(text_response.contents[0], TextContent)
|
||||
assert isinstance(reasoning_response.contents[0], TextReasoningContent)
|
||||
assert text_response.contents[0].type == "text"
|
||||
assert reasoning_response.contents[0].type == "text_reasoning"
|
||||
|
||||
|
||||
def test_parse_response_from_openai_image_generation_raw_base64():
|
||||
@@ -1521,11 +1506,11 @@ def test_parse_response_from_openai_image_generation_raw_base64():
|
||||
# Verify the response contains call + result with DataContent output
|
||||
assert len(response.messages[0].contents) == 2
|
||||
call_content, result_content = response.messages[0].contents
|
||||
assert isinstance(call_content, ImageGenerationToolCallContent)
|
||||
assert isinstance(result_content, ImageGenerationToolResultContent)
|
||||
assert call_content.type == "image_generation_tool_call"
|
||||
assert result_content.type == "image_generation_tool_result"
|
||||
assert result_content.outputs
|
||||
data_out = result_content.outputs
|
||||
assert isinstance(data_out, DataContent)
|
||||
assert data_out.type == "data"
|
||||
assert data_out.uri.startswith("data:image/png;base64,")
|
||||
assert data_out.media_type == "image/png"
|
||||
|
||||
@@ -1558,11 +1543,11 @@ def test_parse_response_from_openai_image_generation_existing_data_uri():
|
||||
# Verify the response contains call + result with DataContent output
|
||||
assert len(response.messages[0].contents) == 2
|
||||
call_content, result_content = response.messages[0].contents
|
||||
assert isinstance(call_content, ImageGenerationToolCallContent)
|
||||
assert isinstance(result_content, ImageGenerationToolResultContent)
|
||||
assert call_content.type == "image_generation_tool_call"
|
||||
assert result_content.type == "image_generation_tool_result"
|
||||
assert result_content.outputs
|
||||
data_out = result_content.outputs
|
||||
assert isinstance(data_out, DataContent)
|
||||
assert data_out.type == "data"
|
||||
assert data_out.uri == f"data:image/webp;base64,{valid_webp_base64}"
|
||||
assert data_out.media_type == "image/webp"
|
||||
|
||||
@@ -1591,9 +1576,9 @@ def test_parse_response_from_openai_image_generation_format_detection():
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response_jpeg = client._parse_response_from_openai(mock_response_jpeg, options={}) # type: ignore
|
||||
result_contents = response_jpeg.messages[0].contents
|
||||
assert isinstance(result_contents[1], ImageGenerationToolResultContent)
|
||||
assert result_contents[1].type == "image_generation_tool_result"
|
||||
outputs = result_contents[1].outputs
|
||||
assert outputs and isinstance(outputs, DataContent)
|
||||
assert outputs and outputs.type == "data"
|
||||
assert outputs.media_type == "image/jpeg"
|
||||
assert "data:image/jpeg;base64," in outputs.uri
|
||||
|
||||
@@ -1617,7 +1602,7 @@ def test_parse_response_from_openai_image_generation_format_detection():
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response_webp = client._parse_response_from_openai(mock_response_webp, options={}) # type: ignore
|
||||
outputs_webp = response_webp.messages[0].contents[1].outputs
|
||||
assert outputs_webp and isinstance(outputs_webp, DataContent)
|
||||
assert outputs_webp and outputs_webp.type == "data"
|
||||
assert outputs_webp.media_type == "image/webp"
|
||||
assert "data:image/webp;base64," in outputs_webp.uri
|
||||
|
||||
@@ -1650,7 +1635,7 @@ def test_parse_response_from_openai_image_generation_fallback():
|
||||
# Verify it falls back to PNG format for unrecognized binary data
|
||||
assert len(response.messages[0].contents) == 2
|
||||
result_content = response.messages[0].contents[1]
|
||||
assert isinstance(result_content, ImageGenerationToolResultContent)
|
||||
assert result_content.type == "image_generation_tool_result"
|
||||
assert result_content.outputs
|
||||
content = result_content.outputs
|
||||
assert content.media_type == "image/png"
|
||||
@@ -1944,7 +1929,7 @@ async def test_integration_streaming_file_search() -> None:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
full_message += content.text
|
||||
|
||||
await delete_vector_store(openai_responses_client, file_id, vector_store.vector_store_id)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from agent_framework._types import FunctionResultContent
|
||||
from agent_framework import Content
|
||||
from agent_framework.observability import _to_otel_part
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_datetime_in_tool_results() -> None:
|
||||
|
||||
Reproduces issue #2219 where datetime objects caused TypeError.
|
||||
"""
|
||||
content = FunctionResultContent(
|
||||
content = Content.from_function_result(
|
||||
call_id="test-call",
|
||||
result={"timestamp": datetime(2025, 11, 16, 10, 30, 0)},
|
||||
)
|
||||
|
||||
@@ -11,9 +11,9 @@ from agent_framework import (
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
ChatMessageStore,
|
||||
Content,
|
||||
Role,
|
||||
SequentialBuilder,
|
||||
TextContent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
@@ -49,7 +49,7 @@ class _CountingAgent(BaseAgent):
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
self.call_count += 1
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=f"Response #{self.call_count}: {self.name}")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=f"Response #{self.call_count}: {self.name}")])
|
||||
|
||||
|
||||
async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
|
||||
|
||||
@@ -19,12 +19,9 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
TextContent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
@@ -60,14 +57,14 @@ class _ToolCallingAgent(BaseAgent):
|
||||
"""Simulate streaming with tool calls and results."""
|
||||
# First update: some text
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text="Let me search for that...")],
|
||||
contents=[Content.from_text(text="Let me search for that...")],
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
|
||||
# Second update: tool call (no text!)
|
||||
yield AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="call_123",
|
||||
name="search",
|
||||
arguments={"query": "weather"},
|
||||
@@ -79,7 +76,7 @@ class _ToolCallingAgent(BaseAgent):
|
||||
# Third update: tool result (no text!)
|
||||
yield AgentResponseUpdate(
|
||||
contents=[
|
||||
FunctionResultContent(
|
||||
Content.from_function_result(
|
||||
call_id="call_123",
|
||||
result={"temperature": 72, "condition": "sunny"},
|
||||
)
|
||||
@@ -89,7 +86,7 @@ class _ToolCallingAgent(BaseAgent):
|
||||
|
||||
# Fourth update: final text response
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text="The weather is sunny, 72°F.")],
|
||||
contents=[Content.from_text(text="The weather is sunny, 72°F.")],
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
|
||||
@@ -113,25 +110,25 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
|
||||
|
||||
# First event: text update
|
||||
assert events[0].data is not None
|
||||
assert isinstance(events[0].data.contents[0], TextContent)
|
||||
assert events[0].data.contents[0].type == "text"
|
||||
assert "Let me search" in events[0].data.contents[0].text
|
||||
|
||||
# Second event: function call
|
||||
assert events[1].data is not None
|
||||
assert isinstance(events[1].data.contents[0], FunctionCallContent)
|
||||
assert events[1].data.contents[0].type == "function_call"
|
||||
func_call = events[1].data.contents[0]
|
||||
assert func_call.call_id == "call_123"
|
||||
assert func_call.name == "search"
|
||||
|
||||
# Third event: function result
|
||||
assert events[2].data is not None
|
||||
assert isinstance(events[2].data.contents[0], FunctionResultContent)
|
||||
assert events[2].data.contents[0].type == "function_result"
|
||||
func_result = events[2].data.contents[0]
|
||||
assert func_result.call_id == "call_123"
|
||||
|
||||
# Fourth event: final text
|
||||
assert events[3].data is not None
|
||||
assert isinstance(events[3].data.contents[0], TextContent)
|
||||
assert events[3].data.contents[0].type == "text"
|
||||
assert "sunny" in events[3].data.contents[0].text
|
||||
|
||||
|
||||
@@ -161,10 +158,10 @@ class MockChatClient:
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="2", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
],
|
||||
@@ -175,7 +172,7 @@ class MockChatClient:
|
||||
messages=ChatMessage(
|
||||
role="assistant",
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
)
|
||||
],
|
||||
@@ -196,10 +193,10 @@ class MockChatClient:
|
||||
if self._parallel_request:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="2", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
),
|
||||
],
|
||||
@@ -208,15 +205,15 @@ class MockChatClient:
|
||||
else:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[
|
||||
FunctionCallContent(
|
||||
Content.from_function_call(
|
||||
call_id="1", name="mock_tool_requiring_approval", arguments='{"query": "test"}'
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
)
|
||||
else:
|
||||
yield ChatResponseUpdate(text=TextContent(text="Tool executed "), role="assistant")
|
||||
yield ChatResponseUpdate(contents=[TextContent(text="successfully.")], role="assistant")
|
||||
yield ChatResponseUpdate(text=Content.from_text(text="Tool executed "), role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="successfully.")], role="assistant")
|
||||
|
||||
self._iteration += 1
|
||||
|
||||
@@ -243,12 +240,14 @@ async def test_agent_executor_tool_call_with_approval() -> None:
|
||||
# Assert
|
||||
assert len(events.get_request_info_events()) == 1
|
||||
approval_request = events.get_request_info_events()[0]
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.type == "function_approval_request"
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
events = await workflow.send_responses({approval_request.request_id: approval_request.data.create_response(True)})
|
||||
events = await workflow.send_responses({
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True)
|
||||
})
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
@@ -276,14 +275,14 @@ async def test_agent_executor_tool_call_with_approval_streaming() -> None:
|
||||
# Assert
|
||||
assert len(request_info_events) == 1
|
||||
approval_request = request_info_events[0]
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.type == "function_approval_request"
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming({
|
||||
approval_request.request_id: approval_request.data.create_response(True)
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True)
|
||||
}):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = event.data
|
||||
@@ -310,13 +309,13 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None:
|
||||
# Assert
|
||||
assert len(events.get_request_info_events()) == 2
|
||||
for approval_request in events.get_request_info_events():
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.type == "function_approval_request"
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
responses = {
|
||||
approval_request.request_id: approval_request.data.create_response(True) # type: ignore
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True) # type: ignore
|
||||
for approval_request in events.get_request_info_events()
|
||||
}
|
||||
events = await workflow.send_responses(responses)
|
||||
@@ -347,13 +346,13 @@ async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> No
|
||||
# Assert
|
||||
assert len(request_info_events) == 2
|
||||
for approval_request in request_info_events:
|
||||
assert isinstance(approval_request.data, FunctionApprovalRequestContent)
|
||||
assert approval_request.data.type == "function_approval_request"
|
||||
assert approval_request.data.function_call.name == "mock_tool_requiring_approval"
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
responses = {
|
||||
approval_request.request_id: approval_request.data.create_response(True) # type: ignore
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True) # type: ignore
|
||||
for approval_request in request_info_events
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
Role,
|
||||
SequentialBuilder,
|
||||
TextContent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowRunState,
|
||||
@@ -50,7 +50,7 @@ class _SimpleAgent(BaseAgent):
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
# This agent does not support streaming; yield a single complete response
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=self._reply_text)])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=self._reply_text)])
|
||||
|
||||
|
||||
class _CaptureFullConversation(Executor):
|
||||
@@ -136,7 +136,7 @@ class _CaptureAgent(BaseAgent):
|
||||
elif isinstance(m, str):
|
||||
norm.append(ChatMessage(role=Role.USER, text=m))
|
||||
self._last_messages = norm
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=self._reply_text)])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=self._reply_text)])
|
||||
|
||||
|
||||
async def test_sequential_adapter_uses_full_conversation() -> None:
|
||||
|
||||
@@ -17,6 +17,7 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
GroupChatBuilder,
|
||||
GroupChatState,
|
||||
MagenticContext,
|
||||
@@ -25,7 +26,6 @@ from agent_framework import (
|
||||
MagenticProgressLedgerItem,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
TextContent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
@@ -57,7 +57,7 @@ class StubAgent(BaseAgent):
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text=self._reply_text)], role=Role.ASSISTANT, author_name=self.name
|
||||
contents=[Content.from_text(text=self._reply_text)], role=Role.ASSISTANT, author_name=self.name
|
||||
)
|
||||
|
||||
return _stream()
|
||||
@@ -141,7 +141,7 @@ class StubManagerAgent(ChatAgent):
|
||||
async def _stream_initial() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(
|
||||
contents=[
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=(
|
||||
'{"terminate": false, "reason": "Selecting agent", '
|
||||
'"next_speaker": "agent", "final_message": null}'
|
||||
@@ -157,7 +157,7 @@ class StubManagerAgent(ChatAgent):
|
||||
async def _stream_final() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(
|
||||
contents=[
|
||||
TextContent(
|
||||
Content.from_text(
|
||||
text=(
|
||||
'{"terminate": true, "reason": "Task complete", '
|
||||
'"next_speaker": null, "final_message": "agent manager final"}'
|
||||
|
||||
@@ -11,12 +11,11 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionCallContent,
|
||||
Content,
|
||||
HandoffAgentUserRequest,
|
||||
HandoffBuilder,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
TextContent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
resolve_agent_id,
|
||||
@@ -74,14 +73,16 @@ def _build_reply_contents(
|
||||
agent_name: str,
|
||||
handoff_to: str | None,
|
||||
call_id: str | None,
|
||||
) -> list[TextContent | FunctionCallContent]:
|
||||
contents: list[TextContent | FunctionCallContent] = []
|
||||
) -> list[Content]:
|
||||
contents: list[Content] = []
|
||||
if handoff_to and call_id:
|
||||
contents.append(
|
||||
FunctionCallContent(call_id=call_id, name=f"handoff_to_{handoff_to}", arguments={"handoff_to": handoff_to})
|
||||
Content.from_function_call(
|
||||
call_id=call_id, name=f"handoff_to_{handoff_to}", arguments={"handoff_to": handoff_to}
|
||||
)
|
||||
)
|
||||
text = f"{agent_name} reply"
|
||||
contents.append(TextContent(text=text))
|
||||
contents.append(Content.from_text(text=text))
|
||||
return contents
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
GroupChatRequestMessage,
|
||||
MagenticBuilder,
|
||||
@@ -28,7 +29,6 @@ from agent_framework import (
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
StandardMagenticManager,
|
||||
TextContent,
|
||||
Workflow,
|
||||
WorkflowCheckpoint,
|
||||
WorkflowCheckpointException,
|
||||
@@ -172,7 +172,7 @@ class StubAgent(BaseAgent):
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text=self._reply_text)], role=Role.ASSISTANT, author_name=self.name
|
||||
contents=[Content.from_text(text=self._reply_text)], role=Role.ASSISTANT, author_name=self.name
|
||||
)
|
||||
|
||||
return _stream()
|
||||
@@ -541,7 +541,7 @@ class StubThreadAgent(BaseAgent):
|
||||
|
||||
async def run_stream(self, messages=None, *, thread=None, **kwargs): # type: ignore[override]
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text="thread-ok")],
|
||||
contents=[Content.from_text(text="thread-ok")],
|
||||
author_name=self.name,
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
@@ -563,7 +563,7 @@ class StubAssistantsAgent(BaseAgent):
|
||||
|
||||
async def run_stream(self, messages=None, *, thread=None, **kwargs): # type: ignore[override]
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text="assistants-ok")],
|
||||
contents=[Content.from_text(text="assistants-ok")],
|
||||
author_name=self.name,
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
Role,
|
||||
SequentialBuilder,
|
||||
TextContent,
|
||||
TypeCompatibilityError,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
@@ -46,7 +46,7 @@ class _EchoAgent(BaseAgent):
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
# Minimal async generator with one assistant update
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=f"{self.name} reply")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=f"{self.name} reply")])
|
||||
|
||||
|
||||
class _SummarizerExec(Executor):
|
||||
|
||||
@@ -18,12 +18,12 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Executor,
|
||||
FileCheckpointStorage,
|
||||
Message,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
TextContent,
|
||||
WorkflowBuilder,
|
||||
WorkflowCheckpointException,
|
||||
WorkflowContext,
|
||||
@@ -881,7 +881,7 @@ class _StreamingTestAgent(BaseAgent):
|
||||
"""Streaming run - yields incremental updates."""
|
||||
# Simulate streaming by yielding character by character
|
||||
for char in self._reply_text:
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=char)])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=char)])
|
||||
|
||||
|
||||
async def test_agent_streaming_vs_non_streaming() -> None:
|
||||
|
||||
@@ -14,16 +14,9 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
ChatMessage,
|
||||
ChatMessageStore,
|
||||
DataContent,
|
||||
Content,
|
||||
Executor,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
UsageContent,
|
||||
UsageDetails,
|
||||
WorkflowAgent,
|
||||
WorkflowBuilder,
|
||||
@@ -44,17 +37,15 @@ class SimpleExecutor(Executor):
|
||||
|
||||
@handler
|
||||
async def handle_message(self, message: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
input_text = (
|
||||
message[0].contents[0].text if message and isinstance(message[0].contents[0], TextContent) else "no input"
|
||||
)
|
||||
input_text = message[0].contents[0].text if message and message[0].contents[0].type == "text" else "no input"
|
||||
response_text = f"{self.response_text}: {input_text}"
|
||||
|
||||
# Create response message for both streaming and non-streaming cases
|
||||
response_message = ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=response_text)])
|
||||
response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)])
|
||||
|
||||
# Emit update event.
|
||||
streaming_update = AgentResponseUpdate(
|
||||
contents=[TextContent(text=response_text)], role=Role.ASSISTANT, message_id=str(uuid.uuid4())
|
||||
contents=[Content.from_text(text=response_text)], role=Role.ASSISTANT, message_id=str(uuid.uuid4())
|
||||
)
|
||||
await ctx.add_event(AgentRunUpdateEvent(executor_id=self.id, data=streaming_update))
|
||||
|
||||
@@ -76,7 +67,7 @@ class RequestingExecutor(Executor):
|
||||
) -> None:
|
||||
# Handle the response and emit completion response
|
||||
update = AgentResponseUpdate(
|
||||
contents=[TextContent(text="Request completed successfully")],
|
||||
contents=[Content.from_text(text="Request completed successfully")],
|
||||
role=Role.ASSISTANT,
|
||||
message_id=str(uuid.uuid4()),
|
||||
)
|
||||
@@ -99,10 +90,10 @@ class ConversationHistoryCapturingExecutor(Executor):
|
||||
message_count = len(messages)
|
||||
response_text = f"Received {message_count} messages"
|
||||
|
||||
response_message = ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=response_text)])
|
||||
response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)])
|
||||
|
||||
streaming_update = AgentResponseUpdate(
|
||||
contents=[TextContent(text=response_text)], role=Role.ASSISTANT, message_id=str(uuid.uuid4())
|
||||
contents=[Content.from_text(text=response_text)], role=Role.ASSISTANT, message_id=str(uuid.uuid4())
|
||||
)
|
||||
await ctx.add_event(AgentRunUpdateEvent(executor_id=self.id, data=streaming_update))
|
||||
await ctx.send_message([response_message])
|
||||
@@ -134,7 +125,7 @@ class TestWorkflowAgent:
|
||||
|
||||
for message in result.messages:
|
||||
first_content = message.contents[0]
|
||||
if isinstance(first_content, TextContent):
|
||||
if first_content.type == "text":
|
||||
text = first_content.text
|
||||
if text.startswith("Step1:"):
|
||||
step1_messages.append(message)
|
||||
@@ -172,11 +163,11 @@ class TestWorkflowAgent:
|
||||
|
||||
# Verify we got a streaming update
|
||||
assert updates[0].contents is not None
|
||||
first_content: TextContent = updates[0].contents[0] # type: ignore[assignment]
|
||||
second_content: TextContent = updates[1].contents[0] # type: ignore[assignment]
|
||||
assert isinstance(first_content, TextContent)
|
||||
first_content: Content = updates[0].contents[0] # type: ignore[assignment]
|
||||
second_content: Content = updates[1].contents[0] # type: ignore[assignment]
|
||||
assert first_content.type == "text"
|
||||
assert "Streaming1: Test input" in first_content.text
|
||||
assert isinstance(second_content, TextContent)
|
||||
assert second_content.type == "text"
|
||||
assert "Streaming2: Streaming1: Test input" in second_content.text
|
||||
|
||||
async def test_end_to_end_request_info_handling(self):
|
||||
@@ -200,17 +191,15 @@ class TestWorkflowAgent:
|
||||
|
||||
approval_update: AgentResponseUpdate | None = None
|
||||
for update in updates:
|
||||
if any(isinstance(content, FunctionApprovalRequestContent) for content in update.contents):
|
||||
if any(content.type == "function_approval_request" for content in update.contents):
|
||||
approval_update = update
|
||||
break
|
||||
|
||||
assert approval_update is not None, "Should have received a request_info approval request"
|
||||
|
||||
function_call = next(
|
||||
content for content in approval_update.contents if isinstance(content, FunctionCallContent)
|
||||
)
|
||||
function_call = next(content for content in approval_update.contents if content.type == "function_call")
|
||||
approval_request = next(
|
||||
content for content in approval_update.contents if isinstance(content, FunctionApprovalRequestContent)
|
||||
content for content in approval_update.contents if content.type == "function_approval_request"
|
||||
)
|
||||
|
||||
# Verify the function call has expected structure
|
||||
@@ -233,10 +222,10 @@ class TestWorkflowAgent:
|
||||
data="User provided answer",
|
||||
).to_dict()
|
||||
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
approval_response = Content.from_function_approval_response(
|
||||
approved=True,
|
||||
id=approval_request.id,
|
||||
function_call=FunctionCallContent(
|
||||
function_call=Content.from_function_call(
|
||||
call_id=function_call.call_id,
|
||||
name=function_call.name,
|
||||
arguments=response_args,
|
||||
@@ -306,7 +295,7 @@ class TestWorkflowAgent:
|
||||
workflow = WorkflowBuilder().set_start_executor(yielding_executor).build()
|
||||
|
||||
# Run directly - should return WorkflowOutputEvent in result
|
||||
direct_result = await workflow.run([ChatMessage(role=Role.USER, contents=[TextContent(text="hello")])])
|
||||
direct_result = await workflow.run([ChatMessage(role=Role.USER, contents=[Content.from_text(text="hello")])])
|
||||
direct_outputs = direct_result.get_outputs()
|
||||
assert len(direct_outputs) == 1
|
||||
assert direct_outputs[0] == "processed: hello"
|
||||
@@ -340,14 +329,14 @@ class TestWorkflowAgent:
|
||||
assert "second output" in texts
|
||||
|
||||
async def test_workflow_as_agent_yield_output_with_content_types(self) -> None:
|
||||
"""Test that yield_output preserves different content types (TextContent, DataContent, etc.)."""
|
||||
"""Test that yield_output preserves different content types (Content, Content, etc.)."""
|
||||
|
||||
@executor
|
||||
async def content_yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext) -> None:
|
||||
# Yield different content types
|
||||
await ctx.yield_output(TextContent(text="text content"))
|
||||
await ctx.yield_output(DataContent(data=b"binary data", media_type="application/octet-stream"))
|
||||
await ctx.yield_output(UriContent(uri="https://example.com/image.png", media_type="image/png"))
|
||||
await ctx.yield_output(Content.from_text(text="text content"))
|
||||
await ctx.yield_output(Content.from_data(data=b"binary data", media_type="application/octet-stream"))
|
||||
await ctx.yield_output(Content.from_uri(uri="https://example.com/image.png", media_type="image/png"))
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(content_yielding_executor).build()
|
||||
agent = workflow.as_agent("content-test-agent")
|
||||
@@ -358,13 +347,13 @@ class TestWorkflowAgent:
|
||||
assert len(result.messages) == 3
|
||||
|
||||
# Verify each content type is preserved
|
||||
assert isinstance(result.messages[0].contents[0], TextContent)
|
||||
assert result.messages[0].contents[0].type == "text"
|
||||
assert result.messages[0].contents[0].text == "text content"
|
||||
|
||||
assert isinstance(result.messages[1].contents[0], DataContent)
|
||||
assert result.messages[1].contents[0].type == "data"
|
||||
assert result.messages[1].contents[0].media_type == "application/octet-stream"
|
||||
|
||||
assert isinstance(result.messages[2].contents[0], UriContent)
|
||||
assert result.messages[2].contents[0].type == "uri"
|
||||
assert result.messages[2].contents[0].uri == "https://example.com/image.png"
|
||||
|
||||
async def test_workflow_as_agent_yield_output_with_chat_message(self) -> None:
|
||||
@@ -374,7 +363,7 @@ class TestWorkflowAgent:
|
||||
async def chat_message_executor(messages: list[ChatMessage], ctx: WorkflowContext) -> None:
|
||||
msg = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[TextContent(text="response text")],
|
||||
contents=[Content.from_text(text="response text")],
|
||||
author_name="custom-author",
|
||||
)
|
||||
await ctx.yield_output(msg)
|
||||
@@ -404,7 +393,7 @@ class TestWorkflowAgent:
|
||||
async def raw_yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext) -> None:
|
||||
# Yield different types of data
|
||||
await ctx.yield_output("simple string")
|
||||
await ctx.yield_output(TextContent(text="text content"))
|
||||
await ctx.yield_output(Content.from_text(text="text content"))
|
||||
custom = CustomData(42)
|
||||
await ctx.yield_output(custom)
|
||||
|
||||
@@ -420,7 +409,7 @@ class TestWorkflowAgent:
|
||||
|
||||
# Verify raw_representation is set for each update
|
||||
assert updates[0].raw_representation == "simple string"
|
||||
assert isinstance(updates[1].raw_representation, TextContent)
|
||||
assert updates[1].raw_representation.type == "text"
|
||||
assert updates[1].raw_representation.text == "text content"
|
||||
assert isinstance(updates[2].raw_representation, CustomData)
|
||||
assert updates[2].raw_representation.value == 42
|
||||
@@ -428,19 +417,19 @@ class TestWorkflowAgent:
|
||||
async def test_workflow_as_agent_yield_output_with_list_of_chat_messages(self) -> None:
|
||||
"""Test that yield_output with list[ChatMessage] extracts contents from all messages.
|
||||
|
||||
Note: TextContent items are coalesced by _finalize_response, so multiple text contents
|
||||
become a single merged TextContent in the final response.
|
||||
Note: Content items are coalesced by _finalize_response, so multiple text contents
|
||||
become a single merged Content in the final response.
|
||||
"""
|
||||
|
||||
@executor
|
||||
async def list_yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext) -> None:
|
||||
# Yield a list of ChatMessages (as SequentialBuilder does)
|
||||
msg_list = [
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="first message")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="second message")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="first message")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="second message")]),
|
||||
ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[TextContent(text="third"), TextContent(text="fourth")],
|
||||
contents=[Content.from_text(text="third"), Content.from_text(text="fourth")],
|
||||
),
|
||||
]
|
||||
await ctx.yield_output(msg_list)
|
||||
@@ -455,7 +444,7 @@ class TestWorkflowAgent:
|
||||
|
||||
assert len(updates) == 1
|
||||
assert len(updates[0].contents) == 4
|
||||
texts = [c.text for c in updates[0].contents if isinstance(c, TextContent)]
|
||||
texts = [c.text for c in updates[0].contents if c.type == "text"]
|
||||
assert texts == ["first message", "second message", "third", "fourth"]
|
||||
|
||||
# Verify run() coalesces text contents (expected behavior)
|
||||
@@ -463,7 +452,7 @@ class TestWorkflowAgent:
|
||||
|
||||
assert isinstance(result, AgentResponse)
|
||||
assert len(result.messages) == 1
|
||||
# TextContent items are coalesced into one
|
||||
# Content items are coalesced into one
|
||||
assert len(result.messages[0].contents) == 1
|
||||
assert result.messages[0].text == "first messagesecond messagethirdfourth"
|
||||
|
||||
@@ -599,7 +588,7 @@ class TestWorkflowAgent:
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
for word in self._response_text.split():
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text=word + " ")],
|
||||
contents=[Content.from_text(text=word + " ")],
|
||||
role=Role.ASSISTANT,
|
||||
author_name=self._name,
|
||||
)
|
||||
@@ -672,7 +661,7 @@ class TestWorkflowAgent:
|
||||
self, messages: Any, *, thread: AgentThread | None = None, **kwargs: Any
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(
|
||||
contents=[TextContent(text=self._response_text)],
|
||||
contents=[Content.from_text(text=self._response_text)],
|
||||
role=Role.ASSISTANT,
|
||||
author_name=self._name,
|
||||
)
|
||||
@@ -738,7 +727,7 @@ class TestWorkflowAgentAuthorName:
|
||||
async def handle_message(self, message: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
# Emit update with explicit author_name
|
||||
update = AgentResponseUpdate(
|
||||
contents=[TextContent(text="Response with author")],
|
||||
contents=[Content.from_text(text="Response with author")],
|
||||
role=Role.ASSISTANT,
|
||||
author_name="custom_author_name", # Explicitly set
|
||||
message_id=str(uuid.uuid4()),
|
||||
@@ -790,7 +779,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
updates = [
|
||||
# Response B, Message 2 (latest in resp B)
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="RespB-Msg2")],
|
||||
contents=[Content.from_text(text="RespB-Msg2")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-b",
|
||||
message_id="msg-2",
|
||||
@@ -798,7 +787,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Response A, Message 1 (earliest overall)
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="RespA-Msg1")],
|
||||
contents=[Content.from_text(text="RespA-Msg1")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-a",
|
||||
message_id="msg-1",
|
||||
@@ -806,7 +795,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Response B, Message 1 (earlier in resp B)
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="RespB-Msg1")],
|
||||
contents=[Content.from_text(text="RespB-Msg1")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-b",
|
||||
message_id="msg-1",
|
||||
@@ -814,7 +803,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Response A, Message 2 (later in resp A)
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="RespA-Msg2")],
|
||||
contents=[Content.from_text(text="RespA-Msg2")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-a",
|
||||
message_id="msg-2",
|
||||
@@ -822,7 +811,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Global dangling update (no response_id) - should go at end
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="Global-Dangling")],
|
||||
contents=[Content.from_text(text="Global-Dangling")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id=None,
|
||||
message_id="msg-global",
|
||||
@@ -841,9 +830,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
# Verify ordering: responses are processed by response_id groups,
|
||||
# within each group messages are chronologically ordered,
|
||||
# global dangling goes at the end
|
||||
message_texts = [
|
||||
msg.contents[0].text if isinstance(msg.contents[0], TextContent) else "" for msg in result.messages
|
||||
]
|
||||
message_texts = [msg.contents[0].text if msg.contents[0].type == "text" else "" for msg in result.messages]
|
||||
|
||||
# The exact order depends on dict iteration order for response_ids,
|
||||
# but within each response group, chronological order should be maintained
|
||||
@@ -894,9 +881,9 @@ class TestWorkflowAgentMergeUpdates:
|
||||
updates = [
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
TextContent(text="First"),
|
||||
UsageContent(
|
||||
details=UsageDetails(input_token_count=10, output_token_count=5, total_token_count=15)
|
||||
Content.from_text(text="First"),
|
||||
Content.from_usage(
|
||||
usage_details={"input_token_count": 10, "output_token_count": 5, "total_token_count": 15}
|
||||
),
|
||||
],
|
||||
role=Role.ASSISTANT,
|
||||
@@ -907,9 +894,9 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
TextContent(text="Second"),
|
||||
UsageContent(
|
||||
details=UsageDetails(input_token_count=20, output_token_count=8, total_token_count=28)
|
||||
Content.from_text(text="Second"),
|
||||
Content.from_usage(
|
||||
usage_details={"input_token_count": 20, "output_token_count": 8, "total_token_count": 28}
|
||||
),
|
||||
],
|
||||
role=Role.ASSISTANT,
|
||||
@@ -920,8 +907,10 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
TextContent(text="Third"),
|
||||
UsageContent(details=UsageDetails(input_token_count=5, output_token_count=3, total_token_count=8)),
|
||||
Content.from_text(text="Third"),
|
||||
Content.from_usage(
|
||||
usage_details={"input_token_count": 5, "output_token_count": 3, "total_token_count": 8}
|
||||
),
|
||||
],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1", # Same response_id as first
|
||||
@@ -985,7 +974,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
updates = [
|
||||
# User question
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="What is the weather?")],
|
||||
contents=[Content.from_text(text="What is the weather?")],
|
||||
role=Role.USER,
|
||||
response_id="resp-1",
|
||||
message_id="msg-1",
|
||||
@@ -993,7 +982,9 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Assistant with function call
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id=call_id, name="get_weather", arguments='{"location": "NYC"}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id=call_id, name="get_weather", arguments='{"location": "NYC"}')
|
||||
],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1",
|
||||
message_id="msg-2",
|
||||
@@ -1002,7 +993,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
# Function result: no response_id previously caused this to go to global_dangling
|
||||
# and be placed at the end (the bug); fix now correctly associates via call_id
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionResultContent(call_id=call_id, result="Sunny, 72F")],
|
||||
contents=[Content.from_function_result(call_id=call_id, result="Sunny, 72F")],
|
||||
role=Role.TOOL,
|
||||
response_id=None,
|
||||
message_id="msg-3",
|
||||
@@ -1010,7 +1001,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Final assistant answer
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="The weather in NYC is sunny and 72F.")],
|
||||
contents=[Content.from_text(text="The weather in NYC is sunny and 72F.")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1",
|
||||
message_id="msg-4",
|
||||
@@ -1026,11 +1017,11 @@ class TestWorkflowAgentMergeUpdates:
|
||||
content_sequence = []
|
||||
for msg in result.messages:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, TextContent):
|
||||
if content.type == "text":
|
||||
content_sequence.append(("text", msg.role))
|
||||
elif isinstance(content, FunctionCallContent):
|
||||
elif content.type == "function_call":
|
||||
content_sequence.append(("function_call", msg.role))
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
content_sequence.append(("function_result", msg.role))
|
||||
|
||||
# Verify correct ordering: user -> function_call -> function_result -> assistant_answer
|
||||
@@ -1051,10 +1042,10 @@ class TestWorkflowAgentMergeUpdates:
|
||||
function_result_idx = None
|
||||
for i, msg in enumerate(result.messages):
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
if content.type == "function_call":
|
||||
function_call_idx = i
|
||||
assert content.call_id == call_id
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
function_result_idx = i
|
||||
assert content.call_id == call_id
|
||||
|
||||
@@ -1081,7 +1072,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
updates = [
|
||||
# User question
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="What's the weather and time?")],
|
||||
contents=[Content.from_text(text="What's the weather and time?")],
|
||||
role=Role.USER,
|
||||
response_id="resp-1",
|
||||
message_id="msg-1",
|
||||
@@ -1089,7 +1080,9 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Assistant with first function call
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id=call_id_1, name="get_weather", arguments='{"location": "NYC"}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id=call_id_1, name="get_weather", arguments='{"location": "NYC"}')
|
||||
],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1",
|
||||
message_id="msg-2",
|
||||
@@ -1097,7 +1090,9 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Assistant with second function call
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionCallContent(call_id=call_id_2, name="get_time", arguments='{"timezone": "EST"}')],
|
||||
contents=[
|
||||
Content.from_function_call(call_id=call_id_2, name="get_time", arguments='{"timezone": "EST"}')
|
||||
],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1",
|
||||
message_id="msg-3",
|
||||
@@ -1105,7 +1100,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Second function result arrives first (no response_id)
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionResultContent(call_id=call_id_2, result="3:00 PM EST")],
|
||||
contents=[Content.from_function_result(call_id=call_id_2, result="3:00 PM EST")],
|
||||
role=Role.TOOL,
|
||||
response_id=None,
|
||||
message_id="msg-4",
|
||||
@@ -1113,7 +1108,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# First function result arrives second (no response_id)
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionResultContent(call_id=call_id_1, result="Sunny, 72F")],
|
||||
contents=[Content.from_function_result(call_id=call_id_1, result="Sunny, 72F")],
|
||||
role=Role.TOOL,
|
||||
response_id=None,
|
||||
message_id="msg-5",
|
||||
@@ -1121,7 +1116,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Final assistant answer
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="It's sunny (72F) and 3 PM in NYC.")],
|
||||
contents=[Content.from_text(text="It's sunny (72F) and 3 PM in NYC.")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1",
|
||||
message_id="msg-6",
|
||||
@@ -1137,11 +1132,11 @@ class TestWorkflowAgentMergeUpdates:
|
||||
content_sequence = []
|
||||
for msg in result.messages:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, TextContent):
|
||||
if content.type == "text":
|
||||
content_sequence.append(("text", None))
|
||||
elif isinstance(content, FunctionCallContent):
|
||||
elif content.type == "function_call":
|
||||
content_sequence.append(("function_call", content.call_id))
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
content_sequence.append(("function_result", content.call_id))
|
||||
|
||||
# Verify all function results appear before the final assistant text
|
||||
@@ -1172,7 +1167,7 @@ class TestWorkflowAgentMergeUpdates:
|
||||
"""
|
||||
updates = [
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="Hello")],
|
||||
contents=[Content.from_text(text="Hello")],
|
||||
role=Role.USER,
|
||||
response_id="resp-1",
|
||||
message_id="msg-1",
|
||||
@@ -1180,14 +1175,14 @@ class TestWorkflowAgentMergeUpdates:
|
||||
),
|
||||
# Function result with no matching call
|
||||
AgentResponseUpdate(
|
||||
contents=[FunctionResultContent(call_id="orphan_call_id", result="orphan result")],
|
||||
contents=[Content.from_function_result(call_id="orphan_call_id", result="orphan result")],
|
||||
role=Role.TOOL,
|
||||
response_id=None,
|
||||
message_id="msg-2",
|
||||
created_at="2024-01-01T12:00:01Z",
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[TextContent(text="Goodbye")],
|
||||
contents=[Content.from_text(text="Goodbye")],
|
||||
role=Role.ASSISTANT,
|
||||
response_id="resp-1",
|
||||
message_id="msg-3",
|
||||
@@ -1203,9 +1198,9 @@ class TestWorkflowAgentMergeUpdates:
|
||||
content_types = []
|
||||
for msg in result.messages:
|
||||
for content in msg.contents:
|
||||
if isinstance(content, TextContent):
|
||||
if content.type == "text":
|
||||
content_types.append("text")
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
elif content.type == "function_result":
|
||||
content_types.append("function_result")
|
||||
|
||||
# Order: text (user), text (assistant), function_result (orphan at end)
|
||||
|
||||
@@ -12,12 +12,12 @@ from agent_framework import (
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
ConcurrentBuilder,
|
||||
Content,
|
||||
GroupChatBuilder,
|
||||
GroupChatState,
|
||||
HandoffBuilder,
|
||||
Role,
|
||||
SequentialBuilder,
|
||||
TextContent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
ai_function,
|
||||
@@ -67,7 +67,7 @@ class _KwargsCapturingAgent(BaseAgent):
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
self.captured_kwargs.append(dict(kwargs))
|
||||
yield AgentResponseUpdate(contents=[TextContent(text=f"{self.name} response")])
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=f"{self.name} response")])
|
||||
|
||||
|
||||
# region Sequential Builder Tests
|
||||
|
||||
@@ -9,12 +9,11 @@ from agent_framework import (
|
||||
AIFunction,
|
||||
ChatAgent,
|
||||
ChatClientProtocol,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPSpecificApproval,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
)
|
||||
@@ -739,14 +738,14 @@ class AgentFactory:
|
||||
if tool_resource.filters:
|
||||
add_props["filters"] = tool_resource.filters
|
||||
return HostedFileSearchTool(
|
||||
inputs=[HostedVectorStoreContent(id) for id in tool_resource.vectorStoreIds or []],
|
||||
inputs=[Content.from_hosted_vector_store(id) for id in tool_resource.vectorStoreIds or []],
|
||||
description=tool_resource.description,
|
||||
max_results=tool_resource.maximumResultCount,
|
||||
additional_properties=add_props,
|
||||
)
|
||||
case CodeInterpreterTool():
|
||||
return HostedCodeInterpreterTool(
|
||||
inputs=[HostedFileContent(file_id=file) for file in tool_resource.fileIds or []],
|
||||
inputs=[Content.from_hosted_file(file_id=file) for file in tool_resource.fileIds or []],
|
||||
description=tool_resource.description,
|
||||
)
|
||||
case McpTool():
|
||||
|
||||
+8
-11
@@ -21,8 +21,7 @@ from typing import Any, cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Content,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
response_handler,
|
||||
@@ -191,7 +190,7 @@ def _validate_conversation_history(messages: list[ChatMessage], agent_name: str)
|
||||
if not hasattr(msg, "contents") or msg.contents is None:
|
||||
continue
|
||||
for content in msg.contents:
|
||||
if isinstance(content, FunctionCallContent) and content.call_id:
|
||||
if content.type == "function_call" and content.call_id:
|
||||
tool_call_ids.add(content.call_id)
|
||||
logger.debug(
|
||||
"Agent '%s': Found tool call '%s' (id=%s) in message %d",
|
||||
@@ -200,7 +199,7 @@ def _validate_conversation_history(messages: list[ChatMessage], agent_name: str)
|
||||
content.call_id,
|
||||
i,
|
||||
)
|
||||
elif isinstance(content, FunctionResultContent) and content.call_id:
|
||||
elif content.type == "function_result" and content.call_id:
|
||||
tool_result_ids.add(content.call_id)
|
||||
logger.debug(
|
||||
"Agent '%s': Found tool result for call_id=%s in message %d",
|
||||
@@ -265,7 +264,7 @@ class AgentResult:
|
||||
response: str
|
||||
agent_name: str
|
||||
messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], []))
|
||||
tool_calls: list[FunctionCallContent] = field(default_factory=lambda: cast(list[FunctionCallContent], []))
|
||||
tool_calls: list[Content] = field(default_factory=lambda: cast(list[Content], []))
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@@ -311,7 +310,7 @@ class AgentExternalInputRequest:
|
||||
agent_response: str
|
||||
iteration: int = 0
|
||||
messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], []))
|
||||
function_calls: list[FunctionCallContent] = field(default_factory=lambda: cast(list[FunctionCallContent], []))
|
||||
function_calls: list[Content] = field(default_factory=lambda: cast(list[Content], []))
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -342,9 +341,7 @@ class AgentExternalInputResponse:
|
||||
|
||||
user_input: str
|
||||
messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], []))
|
||||
function_results: dict[str, FunctionResultContent] = field(
|
||||
default_factory=lambda: cast(dict[str, FunctionResultContent], {})
|
||||
)
|
||||
function_results: dict[str, Content] = field(default_factory=lambda: cast(dict[str, Content], {}))
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -641,7 +638,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
"""
|
||||
accumulated_response = ""
|
||||
all_messages: list[ChatMessage] = []
|
||||
tool_calls: list[FunctionCallContent] = []
|
||||
tool_calls: list[Content] = []
|
||||
|
||||
# Add user input to conversation history first (via state.append only)
|
||||
if input_text:
|
||||
@@ -679,7 +676,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
||||
all_messages = list(cast(list[ChatMessage], result_messages))
|
||||
result_tool_calls: Any = getattr(result, "tool_calls", None)
|
||||
if result_tool_calls is not None:
|
||||
tool_calls = list(cast(list[FunctionCallContent], result_tool_calls))
|
||||
tool_calls = list(cast(list[Content], result_tool_calls))
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"Agent '{agent_name}' has no run or run_stream method")
|
||||
|
||||
@@ -321,7 +321,7 @@ class InMemoryConversationStore(ConversationStore):
|
||||
# Convert ChatMessage contents to OpenAI TextContent format
|
||||
message_content = []
|
||||
for content_item in msg.contents:
|
||||
if hasattr(content_item, "type") and content_item.type == "text":
|
||||
if content_item.type == "text":
|
||||
# Extract text from TextContent object
|
||||
text_value = getattr(content_item, "text", "")
|
||||
message_content.append(TextContent(type="text", text=text_value))
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentProtocol
|
||||
from agent_framework import AgentProtocol, Content
|
||||
from agent_framework._workflows._events import RequestInfoEvent
|
||||
|
||||
from ._conversations import ConversationStore, InMemoryConversationStore
|
||||
@@ -602,7 +602,7 @@ class AgentFrameworkExecutor:
|
||||
"""
|
||||
# Import Agent Framework types
|
||||
try:
|
||||
from agent_framework import ChatMessage, DataContent, Role, TextContent
|
||||
from agent_framework import ChatMessage, Role
|
||||
except ImportError:
|
||||
# Fallback to string extraction if Agent Framework not available
|
||||
return self._extract_user_message_fallback(input_data)
|
||||
@@ -613,14 +613,12 @@ class AgentFrameworkExecutor:
|
||||
|
||||
# Handle OpenAI ResponseInputParam (List[ResponseInputItemParam])
|
||||
if isinstance(input_data, list):
|
||||
return self._convert_openai_input_to_chat_message(input_data, ChatMessage, TextContent, DataContent, Role)
|
||||
return self._convert_openai_input_to_chat_message(input_data, ChatMessage, Role)
|
||||
|
||||
# Fallback for other formats
|
||||
return self._extract_user_message_fallback(input_data)
|
||||
|
||||
def _convert_openai_input_to_chat_message(
|
||||
self, input_items: list[Any], ChatMessage: Any, TextContent: Any, DataContent: Any, Role: Any
|
||||
) -> Any:
|
||||
def _convert_openai_input_to_chat_message(self, input_items: list[Any], ChatMessage: Any, Role: Any) -> Any:
|
||||
"""Convert OpenAI ResponseInputParam to Agent Framework ChatMessage.
|
||||
|
||||
Processes text, images, files, and other content types from OpenAI format
|
||||
@@ -629,14 +627,12 @@ class AgentFrameworkExecutor:
|
||||
Args:
|
||||
input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects)
|
||||
ChatMessage: ChatMessage class for creating chat messages
|
||||
TextContent: TextContent class for text content
|
||||
DataContent: DataContent class for data/media content
|
||||
Role: Role enum for message roles
|
||||
|
||||
Returns:
|
||||
ChatMessage with converted content
|
||||
"""
|
||||
contents = []
|
||||
contents: list[Content] = []
|
||||
|
||||
# Process each input item
|
||||
for item in input_items:
|
||||
@@ -649,7 +645,7 @@ class AgentFrameworkExecutor:
|
||||
|
||||
# Handle both string content and list content
|
||||
if isinstance(message_content, str):
|
||||
contents.append(TextContent(text=message_content))
|
||||
contents.append(Content.from_text(text=message_content))
|
||||
elif isinstance(message_content, list):
|
||||
for content_item in message_content:
|
||||
# Handle dict content items
|
||||
@@ -658,7 +654,7 @@ class AgentFrameworkExecutor:
|
||||
|
||||
if content_type == "input_text":
|
||||
text = content_item.get("text", "")
|
||||
contents.append(TextContent(text=text))
|
||||
contents.append(Content.from_text(text=text))
|
||||
|
||||
elif content_type == "input_image":
|
||||
image_url = content_item.get("image_url", "")
|
||||
@@ -676,7 +672,7 @@ class AgentFrameworkExecutor:
|
||||
media_type = "image/png"
|
||||
else:
|
||||
media_type = "image/png"
|
||||
contents.append(DataContent(uri=image_url, media_type=media_type))
|
||||
contents.append(Content.from_uri(uri=image_url, media_type=media_type))
|
||||
|
||||
elif content_type == "input_file":
|
||||
# Handle file input
|
||||
@@ -710,7 +706,7 @@ class AgentFrameworkExecutor:
|
||||
# Assume file_data is base64, create data URI
|
||||
data_uri = f"data:{media_type};base64,{file_data}"
|
||||
contents.append(
|
||||
DataContent(
|
||||
Content.from_uri(
|
||||
uri=data_uri,
|
||||
media_type=media_type,
|
||||
additional_properties=additional_props,
|
||||
@@ -718,7 +714,7 @@ class AgentFrameworkExecutor:
|
||||
)
|
||||
elif file_url:
|
||||
contents.append(
|
||||
DataContent(
|
||||
Content.from_uri(
|
||||
uri=file_url,
|
||||
media_type=media_type,
|
||||
additional_properties=additional_props,
|
||||
@@ -728,21 +724,19 @@ class AgentFrameworkExecutor:
|
||||
elif content_type == "function_approval_response":
|
||||
# Handle function approval response (DevUI extension)
|
||||
try:
|
||||
from agent_framework import FunctionApprovalResponseContent, FunctionCallContent
|
||||
|
||||
request_id = content_item.get("request_id", "")
|
||||
approved = content_item.get("approved", False)
|
||||
function_call_data = content_item.get("function_call", {})
|
||||
|
||||
# Create FunctionCallContent from the function_call data
|
||||
function_call = FunctionCallContent(
|
||||
function_call = Content.from_function_call(
|
||||
call_id=function_call_data.get("id", ""),
|
||||
name=function_call_data.get("name", ""),
|
||||
arguments=function_call_data.get("arguments", {}),
|
||||
)
|
||||
|
||||
# Create FunctionApprovalResponseContent with correct signature
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
approval_response = Content.from_function_approval_response(
|
||||
approved, # positional argument
|
||||
id=request_id, # keyword argument 'id', NOT 'request_id'
|
||||
function_call=function_call, # FunctionCallContent object
|
||||
@@ -764,7 +758,7 @@ class AgentFrameworkExecutor:
|
||||
|
||||
# If no contents found, create a simple text message
|
||||
if not contents:
|
||||
contents.append(TextContent(text=""))
|
||||
contents.append(Content.from_text(text=""))
|
||||
|
||||
chat_message = ChatMessage(role=Role.USER, contents=contents)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
||||
from typing import Any, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from agent_framework import ChatMessage, Content
|
||||
from openai.types.responses import (
|
||||
Response,
|
||||
ResponseContentPartAddedEvent,
|
||||
@@ -92,7 +92,7 @@ def _serialize_content_recursive(value: Any) -> Any:
|
||||
if isinstance(value, (list, tuple)):
|
||||
serialized = [_serialize_content_recursive(item) for item in value]
|
||||
# For single-item lists containing text Content, extract just the text
|
||||
# This handles the MCP case where result = [TextContent(text="Hello")]
|
||||
# This handles the MCP case where result = [Content.from_text(text="Hello")]
|
||||
# and we want output = "Hello" not output = '[{"type": "text", "text": "Hello"}]'
|
||||
if len(serialized) == 1 and isinstance(serialized[0], dict) and serialized[0].get("type") == "text":
|
||||
return serialized[0].get("text", "")
|
||||
@@ -127,18 +127,18 @@ class MessageMapper:
|
||||
|
||||
# Register content type mappers for all 12 Agent Framework content types
|
||||
self.content_mappers = {
|
||||
"TextContent": self._map_text_content,
|
||||
"TextReasoningContent": self._map_reasoning_content,
|
||||
"FunctionCallContent": self._map_function_call_content,
|
||||
"FunctionResultContent": self._map_function_result_content,
|
||||
"ErrorContent": self._map_error_content,
|
||||
"UsageContent": self._map_usage_content,
|
||||
"DataContent": self._map_data_content,
|
||||
"UriContent": self._map_uri_content,
|
||||
"HostedFileContent": self._map_hosted_file_content,
|
||||
"HostedVectorStoreContent": self._map_hosted_vector_store_content,
|
||||
"FunctionApprovalRequestContent": self._map_approval_request_content,
|
||||
"FunctionApprovalResponseContent": self._map_approval_response_content,
|
||||
"text": self._map_text_content,
|
||||
"text_reasoning": self._map_reasoning_content,
|
||||
"function_call": self._map_function_call_content,
|
||||
"function_result": self._map_function_result_content,
|
||||
"error": self._map_error_content,
|
||||
"usage": self._map_usage_content,
|
||||
"data": self._map_data_content,
|
||||
"uri": self._map_uri_content,
|
||||
"hosted_file": self._map_hosted_file_content,
|
||||
"hosted_vector_store": self._map_hosted_vector_store_content,
|
||||
"function_approval_request": self._map_approval_request_content,
|
||||
"function_approval_response": self._map_approval_response_content,
|
||||
}
|
||||
|
||||
async def convert_event(self, raw_event: Any, request: AgentFrameworkRequest) -> Sequence[Any]:
|
||||
@@ -603,7 +603,7 @@ class MessageMapper:
|
||||
return events
|
||||
|
||||
# Check if we're streaming text content
|
||||
has_text_content = any(isinstance(content, TextContent) for content in update.contents)
|
||||
has_text_content = any(content.type == "text" for content in update.contents)
|
||||
|
||||
# Check if we're in an executor context with an existing item
|
||||
executor_id = context.get("current_executor_id")
|
||||
@@ -647,10 +647,8 @@ class MessageMapper:
|
||||
|
||||
# Process each content item
|
||||
for content in update.contents:
|
||||
content_type = content.__class__.__name__
|
||||
|
||||
# Special handling for TextContent to use proper delta events
|
||||
if content_type == "TextContent" and "current_message_id" in context:
|
||||
if content.type == "text" and "current_message_id" in context:
|
||||
# Stream text content via proper delta events
|
||||
events.append(
|
||||
ResponseTextDeltaEvent(
|
||||
@@ -663,9 +661,9 @@ class MessageMapper:
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
)
|
||||
elif content_type in self.content_mappers:
|
||||
elif content.type in self.content_mappers:
|
||||
# Use existing mappers for other content types
|
||||
mapped_events = await self.content_mappers[content_type](content, context)
|
||||
mapped_events = await self.content_mappers[content.type](content, context)
|
||||
if mapped_events is not None: # Handle None returns (e.g., UsageContent)
|
||||
if isinstance(mapped_events, list):
|
||||
events.extend(mapped_events)
|
||||
@@ -676,7 +674,7 @@ class MessageMapper:
|
||||
events.append(await self._create_unknown_content_event(content, context))
|
||||
|
||||
# Don't increment content_index for text deltas within the same part
|
||||
if content_type != "TextContent":
|
||||
if content.type != "text":
|
||||
context["content_index"] = context.get("content_index", 0) + 1
|
||||
|
||||
except Exception as e:
|
||||
@@ -708,10 +706,8 @@ class MessageMapper:
|
||||
for message in messages:
|
||||
if hasattr(message, "contents") and message.contents:
|
||||
for content in message.contents:
|
||||
content_type = content.__class__.__name__
|
||||
|
||||
if content_type in self.content_mappers:
|
||||
mapped_events = await self.content_mappers[content_type](content, context)
|
||||
if content.type in self.content_mappers:
|
||||
mapped_events = await self.content_mappers[content.type](content, context)
|
||||
if mapped_events is not None: # Handle None returns (e.g., UsageContent)
|
||||
if isinstance(mapped_events, list):
|
||||
events.extend(mapped_events)
|
||||
@@ -726,9 +722,7 @@ class MessageMapper:
|
||||
# Add usage information if present
|
||||
usage_details = getattr(response, "usage_details", None)
|
||||
if usage_details:
|
||||
from agent_framework import UsageContent
|
||||
|
||||
usage_content = UsageContent(details=usage_details)
|
||||
usage_content = Content.from_usage(usage_details=usage_details)
|
||||
await self._map_usage_content(usage_content, context)
|
||||
# Note: _map_usage_content returns None - it accumulates usage for final Response.usage
|
||||
|
||||
@@ -1421,11 +1415,11 @@ class MessageMapper:
|
||||
Returns:
|
||||
None - no event emitted (usage goes in final Response.usage)
|
||||
"""
|
||||
# Extract usage from UsageContent.details (UsageDetails object)
|
||||
details = getattr(content, "details", None)
|
||||
total_tokens = getattr(details, "total_token_count", 0) or 0
|
||||
prompt_tokens = getattr(details, "input_token_count", 0) or 0
|
||||
completion_tokens = getattr(details, "output_token_count", 0) or 0
|
||||
# Extract usage from UsageContent.usage_details (UsageDetails object)
|
||||
details = content.usage_details or {}
|
||||
total_tokens = details.get("total_token_count", 0)
|
||||
prompt_tokens = details.get("input_token_count", 0)
|
||||
completion_tokens = details.get("output_token_count", 0)
|
||||
|
||||
# Accumulate for final Response.usage
|
||||
request_id = context.get("request_id", "default")
|
||||
|
||||
@@ -187,7 +187,7 @@ export interface HostedVectorStoreContent extends BaseContent {
|
||||
}
|
||||
|
||||
// Union type for all content
|
||||
export type Contents =
|
||||
export type Content =
|
||||
| TextContent
|
||||
| FunctionCallContent
|
||||
| FunctionResultContent
|
||||
@@ -209,7 +209,7 @@ export interface UsageDetails {
|
||||
|
||||
// Agent run response update (streaming)
|
||||
export interface AgentResponseUpdate {
|
||||
contents: Contents[];
|
||||
contents: Content[];
|
||||
role?: Role;
|
||||
author_name?: string;
|
||||
response_id?: string;
|
||||
@@ -233,7 +233,7 @@ export interface AgentResponse {
|
||||
|
||||
// Chat message
|
||||
export interface ChatMessage {
|
||||
contents: Contents[];
|
||||
contents: Content[];
|
||||
role?: Role;
|
||||
author_name?: string;
|
||||
message_id?: string;
|
||||
@@ -244,7 +244,7 @@ export interface ChatMessage {
|
||||
|
||||
// Chat response update (model client streaming)
|
||||
export interface ChatResponseUpdate {
|
||||
contents: Contents[];
|
||||
contents: Content[];
|
||||
role?: Role;
|
||||
author_name?: string;
|
||||
response_id?: string;
|
||||
@@ -330,18 +330,18 @@ export interface TraceSpan {
|
||||
}
|
||||
|
||||
// Helper type guards for Agent Framework content types
|
||||
export function isTextContent(content: Contents): content is TextContent {
|
||||
export function isTextContent(content: Content): content is TextContent {
|
||||
return content.type === "text";
|
||||
}
|
||||
|
||||
export function isFunctionCallContent(
|
||||
content: Contents
|
||||
content: Content
|
||||
): content is FunctionCallContent {
|
||||
return content.type === "function_call";
|
||||
}
|
||||
|
||||
export function isFunctionResultContent(
|
||||
content: Contents
|
||||
content: Content
|
||||
): content is FunctionResultContent {
|
||||
return content.type === "function_result";
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ export interface MetaResponse {
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant" | "system" | "tool";
|
||||
contents: import("./agent-framework").Contents[];
|
||||
contents: import("./agent-framework").Content[];
|
||||
timestamp: string;
|
||||
streaming?: boolean;
|
||||
author_name?: string;
|
||||
|
||||
@@ -7,7 +7,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentResponse, ChatMessage, Role, TextContent
|
||||
from agent_framework import AgentResponse, ChatMessage, Content, Role
|
||||
|
||||
from agent_framework_devui import register_cleanup
|
||||
from agent_framework_devui._discovery import EntityDiscovery
|
||||
@@ -36,7 +36,7 @@ class MockAgent:
|
||||
async def run_stream(self, messages=None, *, thread=None, **kwargs):
|
||||
"""Mock streaming run method."""
|
||||
yield AgentResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Test response")])],
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Test response")])],
|
||||
)
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ async def test_cleanup_with_file_based_discovery():
|
||||
# Write agent module with cleanup registration
|
||||
agent_file = agent_dir / "__init__.py"
|
||||
agent_file.write_text("""
|
||||
from agent_framework import AgentResponse, ChatMessage, Role, TextContent
|
||||
from agent_framework import AgentResponse, ChatMessage, Role, Content
|
||||
from agent_framework_devui import register_cleanup
|
||||
|
||||
class MockCredential:
|
||||
@@ -279,7 +279,7 @@ class TestAgent:
|
||||
|
||||
async def run_stream(self, messages=None, *, thread=None, **kwargs):
|
||||
yield AgentResponse(
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, content=[TextContent(text="Test")])],
|
||||
messages=[ChatMessage(role=Role.ASSISTANT, content=[Content.from_text(text="Test")])],
|
||||
inner_messages=[],
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user