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:
@@ -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}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user