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:
Eduard van Valkenburg
2026-01-20 22:09:39 +00:00
committed by GitHub
parent 73761aa4a3
commit 83e6229c11
132 changed files with 3949 additions and 4741 deletions
@@ -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"}',
+2 -2
View File
@@ -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}
)