|
- """Comprehensive tests for AssistantAgent functionality."""
-
- # Standard library imports
- import asyncio
- import json
- import os
- from typing import Any, List, Optional, Union, cast
- from unittest.mock import AsyncMock, MagicMock, patch
-
- # Third-party imports
- import pytest
-
- # First-party imports
- from autogen_agentchat.agents import AssistantAgent
- from autogen_agentchat.agents._assistant_agent import AssistantAgentConfig
- from autogen_agentchat.base import Handoff, Response, TaskResult
- from autogen_agentchat.messages import (
- BaseAgentEvent,
- BaseChatMessage,
- HandoffMessage,
- MemoryQueryEvent,
- ModelClientStreamingChunkEvent,
- StructuredMessage,
- TextMessage,
- ThoughtEvent,
- ToolCallExecutionEvent,
- ToolCallRequestEvent,
- ToolCallSummaryMessage,
- )
- from autogen_core import CancellationToken, ComponentModel, FunctionCall
- from autogen_core.memory import Memory, MemoryContent, UpdateContextResult
- from autogen_core.memory import MemoryQueryResult as MemoryQueryResultSet
- from autogen_core.model_context import BufferedChatCompletionContext
- from autogen_core.models import (
- AssistantMessage,
- CreateResult,
- FunctionExecutionResult,
- ModelFamily,
- RequestUsage,
- SystemMessage,
- UserMessage,
- )
- from autogen_ext.models.anthropic import AnthropicChatCompletionClient
- from autogen_ext.models.openai import OpenAIChatCompletionClient
- from autogen_ext.models.replay import ReplayChatCompletionClient
- from autogen_ext.tools.mcp import McpWorkbench, SseServerParams
- from pydantic import BaseModel, ValidationError
-
-
- def mock_tool_function(param: str) -> str:
- """Mock tool function for testing.
-
- Args:
- param: Input parameter to process
-
- Returns:
- Formatted string with the input parameter
- """
- return f"Tool executed with: {param}"
-
-
- async def async_mock_tool_function(param: str) -> str:
- """Async mock tool function for testing.
-
- Args:
- param: Input parameter to process
-
- Returns:
- Formatted string with the input parameter
- """
- return f"Async tool executed with: {param}"
-
-
- def _pass_function(input: str) -> str:
- """Pass through function for testing.
-
- Args:
- input: Input to pass through
-
- Returns:
- The string "pass"
- """
- return "pass"
-
-
- def _echo_function(input: str) -> str:
- """Echo function for testing.
-
- Args:
- input: Input to echo
-
- Returns:
- The input string
- """
- return input
-
-
- class MockMemory(Memory):
- """Mock memory implementation for testing.
-
- A simple memory implementation that stores strings and provides basic memory operations
- for testing purposes.
-
- Args:
- contents: Optional list of initial memory contents
- """
-
- def __init__(self, contents: Optional[List[str]] = None) -> None:
- """Initialize mock memory.
-
- Args:
- contents: Optional list of initial memory contents
- """
- self._contents: List[str] = contents or []
-
- async def add(self, content: MemoryContent, cancellation_token: Optional[CancellationToken] = None) -> None:
- """Add content to memory.
-
- Args:
- content: Content to add to memory
- cancellation_token: Optional token for cancelling operation
- """
- self._contents.append(str(content))
-
- async def query(
- self, query: Union[str, MemoryContent], cancellation_token: Optional[CancellationToken] = None, **kwargs: Any
- ) -> MemoryQueryResultSet:
- """Query memory contents.
-
- Args:
- query: Search query
- cancellation_token: Optional token for cancelling operation
- kwargs: Additional query parameters
-
- Returns:
- Query results containing all memory contents
- """
- results = [MemoryContent(content=content, mime_type="text/plain") for content in self._contents]
- return MemoryQueryResultSet(results=results)
-
- async def clear(self, cancellation_token: Optional[CancellationToken] = None) -> None:
- """Clear all memory contents.
-
- Args:
- cancellation_token: Optional token for cancelling operation
- """
- self._contents.clear()
-
- async def close(self) -> None:
- """Close memory resources."""
- pass
-
- async def update_context(self, model_context: Any) -> UpdateContextResult:
- """Update model context with memory contents.
-
- Args:
- model_context: Context to update
-
- Returns:
- Update result containing memory contents
- """
- if self._contents:
- results = [MemoryContent(content=content, mime_type="text/plain") for content in self._contents]
- return UpdateContextResult(memories=MemoryQueryResultSet(results=results))
- return UpdateContextResult(memories=MemoryQueryResultSet(results=[]))
-
- def dump_component(self) -> ComponentModel:
- """Dump memory state as component model.
-
- Returns:
- Component model representing memory state
- """
- return ComponentModel(provider="test", config={"type": "mock_memory"})
-
-
- class StructuredOutput(BaseModel):
- """Test structured output model.
-
- Attributes:
- content: Main content string
- confidence: Confidence score between 0 and 1
- """
-
- content: str
- confidence: float
-
-
- @pytest.mark.asyncio
- async def test_model_client_stream() -> None:
- mock_client = ReplayChatCompletionClient(
- [
- "Response to message 3",
- ]
- )
- agent = AssistantAgent(
- "test_agent",
- model_client=mock_client,
- model_client_stream=True,
- )
- chunks: List[str] = []
- async for message in agent.run_stream(task="task"):
- if isinstance(message, TaskResult):
- assert isinstance(message.messages[-1], TextMessage)
- assert message.messages[-1].content == "Response to message 3"
- elif isinstance(message, ModelClientStreamingChunkEvent):
- chunks.append(message.content)
- assert "".join(chunks) == "Response to message 3"
-
-
- @pytest.mark.asyncio
- async def test_model_client_stream_with_tool_calls() -> None:
- mock_client = ReplayChatCompletionClient(
- [
- CreateResult(
- content=[
- FunctionCall(id="1", name="_pass_function", arguments=r'{"input": "task"}'),
- FunctionCall(id="3", name="_echo_function", arguments=r'{"input": "task"}'),
- ],
- finish_reason="function_calls",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- "Example response 2 to task",
- ]
- )
- mock_client._model_info["function_calling"] = True # pyright: ignore
- agent = AssistantAgent(
- "test_agent",
- model_client=mock_client,
- model_client_stream=True,
- reflect_on_tool_use=True,
- tools=[_pass_function, _echo_function],
- )
- chunks: List[str] = []
- async for message in agent.run_stream(task="task"):
- if isinstance(message, TaskResult):
- assert isinstance(message.messages[-1], TextMessage)
- assert isinstance(message.messages[1], ToolCallRequestEvent)
- assert message.messages[-1].content == "Example response 2 to task"
- assert message.messages[1].content == [
- FunctionCall(id="1", name="_pass_function", arguments=r'{"input": "task"}'),
- FunctionCall(id="3", name="_echo_function", arguments=r'{"input": "task"}'),
- ]
- assert isinstance(message.messages[2], ToolCallExecutionEvent)
- assert message.messages[2].content == [
- FunctionExecutionResult(call_id="1", content="pass", is_error=False, name="_pass_function"),
- FunctionExecutionResult(call_id="3", content="task", is_error=False, name="_echo_function"),
- ]
- elif isinstance(message, ModelClientStreamingChunkEvent):
- chunks.append(message.content)
- assert "".join(chunks) == "Example response 2 to task"
-
-
- @pytest.mark.asyncio
- async def test_invalid_structured_output_format() -> None:
- class AgentResponse(BaseModel):
- response: str
- status: str
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content='{"response": "Hello"}',
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ]
- )
-
- agent = AssistantAgent(
- name="assistant",
- model_client=model_client,
- output_content_type=AgentResponse,
- )
-
- with pytest.raises(ValidationError):
- await agent.run()
-
-
- @pytest.mark.asyncio
- async def test_structured_message_factory_serialization() -> None:
- class AgentResponse(BaseModel):
- result: str
- status: str
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content=AgentResponse(result="All good", status="ok").model_dump_json(),
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- ]
- )
-
- agent = AssistantAgent(
- name="structured_agent",
- model_client=model_client,
- output_content_type=AgentResponse,
- output_content_type_format="{result} - {status}",
- )
-
- dumped = agent.dump_component()
- restored_agent = AssistantAgent.load_component(dumped)
- result = await restored_agent.run()
-
- assert isinstance(result.messages[0], StructuredMessage)
- assert result.messages[0].content.result == "All good" # type: ignore
- assert result.messages[0].content.status == "ok" # type: ignore
-
-
- @pytest.mark.asyncio
- async def test_structured_message_format_string() -> None:
- class AgentResponse(BaseModel):
- field1: str
- field2: str
-
- expected = AgentResponse(field1="foo", field2="bar")
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content=expected.model_dump_json(),
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- ]
- )
-
- agent = AssistantAgent(
- name="formatted_agent",
- model_client=model_client,
- output_content_type=AgentResponse,
- output_content_type_format="{field1} - {field2}",
- )
-
- result = await agent.run()
-
- assert len(result.messages) == 1
- message = result.messages[0]
-
- # Check that it's a StructuredMessage with the correct content model
- assert isinstance(message, StructuredMessage)
- assert isinstance(message.content, AgentResponse) # type: ignore[reportUnknownMemberType]
- assert message.content == expected
-
- # Check that the format_string was applied correctly
- assert message.to_model_text() == "foo - bar"
-
-
- @pytest.mark.asyncio
- async def test_tools_serialize_and_deserialize() -> None:
- def test() -> str:
- return "hello world"
-
- client = OpenAIChatCompletionClient(
- model="gpt-4o",
- api_key="API_KEY",
- )
-
- agent = AssistantAgent(
- name="test",
- model_client=client,
- tools=[test],
- )
-
- serialize = agent.dump_component()
- deserialize = AssistantAgent.load_component(serialize)
-
- assert deserialize.name == agent.name
- for original, restored in zip(agent._workbench, deserialize._workbench, strict=True): # type: ignore
- assert await original.list_tools() == await restored.list_tools() # type: ignore
- assert agent.component_version == deserialize.component_version
-
-
- @pytest.mark.asyncio
- async def test_workbench_serialize_and_deserialize() -> None:
- workbench = McpWorkbench(server_params=SseServerParams(url="http://test-url"))
-
- client = OpenAIChatCompletionClient(
- model="gpt-4o",
- api_key="API_KEY",
- )
-
- agent = AssistantAgent(
- name="test",
- model_client=client,
- workbench=workbench,
- )
-
- serialize = agent.dump_component()
- deserialize = AssistantAgent.load_component(serialize)
-
- assert deserialize.name == agent.name
- for original, restored in zip(agent._workbench, deserialize._workbench, strict=True): # type: ignore
- assert isinstance(original, McpWorkbench)
- assert isinstance(restored, McpWorkbench)
- assert original._to_config() == restored._to_config() # type: ignore
-
-
- @pytest.mark.asyncio
- async def test_multiple_workbenches_serialize_and_deserialize() -> None:
- workbenches: List[McpWorkbench] = [
- McpWorkbench(server_params=SseServerParams(url="http://test-url-1")),
- McpWorkbench(server_params=SseServerParams(url="http://test-url-2")),
- ]
-
- client = OpenAIChatCompletionClient(
- model="gpt-4o",
- api_key="API_KEY",
- )
-
- agent = AssistantAgent(
- name="test_multi",
- model_client=client,
- workbench=workbenches,
- )
-
- serialize = agent.dump_component()
- deserialized_agent: AssistantAgent = AssistantAgent.load_component(serialize)
-
- assert deserialized_agent.name == agent.name
- assert isinstance(deserialized_agent._workbench, list) # type: ignore
- assert len(deserialized_agent._workbench) == len(workbenches) # type: ignore
-
- for original, restored in zip(agent._workbench, deserialized_agent._workbench, strict=True): # type: ignore
- assert isinstance(original, McpWorkbench)
- assert isinstance(restored, McpWorkbench)
- assert original._to_config() == restored._to_config() # type: ignore
-
-
- @pytest.mark.asyncio
- async def test_tools_deserialize_aware() -> None:
- dump = """
- {
- "provider": "autogen_agentchat.agents.AssistantAgent",
- "component_type": "agent",
- "version": 1,
- "component_version": 2,
- "description": "An agent that provides assistance with tool use.",
- "label": "AssistantAgent",
- "config": {
- "name": "TestAgent",
- "model_client":{
- "provider": "autogen_ext.models.replay.ReplayChatCompletionClient",
- "component_type": "replay_chat_completion_client",
- "version": 1,
- "component_version": 1,
- "description": "A mock chat completion client that replays predefined responses using an index-based approach.",
- "label": "ReplayChatCompletionClient",
- "config": {
- "chat_completions": [
- {
- "finish_reason": "function_calls",
- "content": [
- {
- "id": "hello",
- "arguments": "{}",
- "name": "hello"
- }
- ],
- "usage": {
- "prompt_tokens": 0,
- "completion_tokens": 0
- },
- "cached": false
- }
- ],
- "model_info": {
- "vision": false,
- "function_calling": true,
- "json_output": false,
- "family": "unknown",
- "structured_output": false
- }
- }
- },
- "tools": [
- {
- "provider": "autogen_core.tools.FunctionTool",
- "component_type": "tool",
- "version": 1,
- "component_version": 1,
- "description": "Create custom tools by wrapping standard Python functions.",
- "label": "FunctionTool",
- "config": {
- "source_code": "def hello():\\n return 'Hello, World!'\\n",
- "name": "hello",
- "description": "",
- "global_imports": [],
- "has_cancellation_support": false
- }
- }
- ],
- "model_context": {
- "provider": "autogen_core.model_context.UnboundedChatCompletionContext",
- "component_type": "chat_completion_context",
- "version": 1,
- "component_version": 1,
- "description": "An unbounded chat completion context that keeps a view of the all the messages.",
- "label": "UnboundedChatCompletionContext",
- "config": {}
- },
- "description": "An agent that provides assistance with ability to use tools.",
- "system_message": "You are a helpful assistant.",
- "model_client_stream": false,
- "reflect_on_tool_use": false,
- "tool_call_summary_format": "{result}",
- "metadata": {}
- }
- }
-
- """
-
- # Test that agent can be deserialized from configuration
- config = json.loads(dump)
- agent = AssistantAgent.load_component(config)
-
- # Verify the agent was loaded correctly
- assert agent.name == "TestAgent"
- assert agent.description == "An agent that provides assistance with ability to use tools."
-
-
- class TestAssistantAgentToolCallLoop:
- """Test suite for tool call loop functionality.
-
- Tests the behavior of AssistantAgent's tool call loop feature, which allows
- multiple sequential tool calls before producing a final response.
- """
-
- @pytest.mark.asyncio
- async def test_tool_call_loop_enabled(self) -> None:
- """Test that tool call loop works when enabled.
-
- Verifies that:
- 1. Multiple tool calls are executed in sequence
- 2. Loop continues until non-tool response
- 3. Final response is correct type
- """
- # Create mock client with multiple tool calls followed by text response
- model_client = ReplayChatCompletionClient(
- [
- # First tool call
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "first"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- # Second tool call (loop continues)
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="2", arguments=json.dumps({"param": "second"}), name="mock_tool_function")
- ],
- usage=RequestUsage(prompt_tokens=12, completion_tokens=5),
- cached=False,
- ),
- # Final text response (loop ends)
- CreateResult(
- finish_reason="stop",
- content="Task completed successfully!",
- usage=RequestUsage(prompt_tokens=15, completion_tokens=10),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- max_tool_iterations=3,
- )
-
- result = await agent.run(task="Execute multiple tool calls")
-
- # Verify multiple model calls were made
- assert len(model_client.create_calls) == 3, f"Expected 3 calls, got {len(model_client.create_calls)}"
-
- # Verify final response is text
- final_message = result.messages[-1]
- assert isinstance(final_message, TextMessage)
- assert final_message.content == "Task completed successfully!"
-
- @pytest.mark.asyncio
- async def test_tool_call_loop_disabled_default(self) -> None:
- """Test that tool call loop is disabled by default.
-
- Verifies that:
- 1. Only one tool call is made when loop is disabled
- 2. Agent returns after first tool call
- """
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- max_tool_iterations=1,
- )
-
- result = await agent.run(task="Execute single tool call")
-
- # Should only make one model call
- assert len(model_client.create_calls) == 1, f"Expected 1 call, got {len(model_client.create_calls)}"
- assert result is not None
-
- @pytest.mark.asyncio
- async def test_tool_call_loop_max_iterations(self) -> None:
- """Test that tool call loop respects max_iterations limit."""
- # Create responses that would continue forever without max_iterations
- responses: List[CreateResult] = []
- for i in range(15): # More than default max_iterations (10)
- responses.append(
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id=str(i), arguments=json.dumps({"param": f"call_{i}"}), name="mock_tool_function")
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- )
-
- model_client = ReplayChatCompletionClient(
- responses,
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- max_tool_iterations=5, # Set max iterations to 5
- )
-
- result = await agent.run(task="Test max iterations")
-
- # Should stop at max_iterations
- assert len(model_client.create_calls) == 5, f"Expected 5 calls, got {len(model_client.create_calls)}"
- # Verify result is not None
- assert result is not None
-
- @pytest.mark.asyncio
- async def test_tool_call_loop_with_handoff(self) -> None:
- """Test that tool call loop stops on handoff."""
- model_client = ReplayChatCompletionClient(
- [
- # Tool call followed by handoff
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function"),
- FunctionCall(
- id="2", arguments=json.dumps({"target": "other_agent"}), name="transfer_to_other_agent"
- ),
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- handoffs=["other_agent"],
- max_tool_iterations=1,
- )
-
- result = await agent.run(task="Test handoff in loop")
-
- # Should stop at handoff
- assert len(model_client.create_calls) == 1, f"Expected 1 call, got {len(model_client.create_calls)}"
-
- # Should return HandoffMessage
- assert isinstance(result.messages[-1], HandoffMessage)
-
- @pytest.mark.asyncio
- async def test_tool_call_config_validation(self) -> None:
- """Test that ToolCallConfig validation works correctly."""
- # Test that max_iterations must be >= 1
- with pytest.raises(
- ValueError, match="Maximum number of tool iterations must be greater than or equal to 1, got 0"
- ):
- AssistantAgent(
- name="test_agent",
- model_client=MagicMock(),
- max_tool_iterations=0, # Should raise error
- )
-
-
- class TestAssistantAgentInitialization:
- """Test suite for AssistantAgent initialization.
-
- Tests various initialization scenarios and configurations of the AssistantAgent class.
- """
-
- @pytest.mark.asyncio
- async def test_basic_initialization(self) -> None:
- """Test basic agent initialization with minimal parameters.
-
- Verifies that:
- 1. Agent initializes with required parameters
- 2. Default values are set correctly
- 3. Basic functionality works
- """
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Hello!",
- usage=RequestUsage(prompt_tokens=5, completion_tokens=2),
- cached=False,
- )
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(name="test_agent", model_client=model_client)
- result = await agent.run(task="Say hello")
-
- assert isinstance(result.messages[-1], TextMessage)
- assert result.messages[-1].content == "Hello!"
-
- @pytest.mark.asyncio
- async def test_initialization_with_tools(self) -> None:
- """Test agent initialization with tools.
-
- Verifies that:
- 1. Agent accepts tool configurations
- 2. Tools are properly registered
- 3. Tool calls work correctly
- """
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- )
-
- result = await agent.run(task="Use the tool")
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- assert "Tool executed with: test" in result.messages[-1].content
-
- @pytest.mark.asyncio
- async def test_initialization_with_memory(self) -> None:
- """Test agent initialization with memory.
-
- Verifies that:
- 1. Memory is properly integrated
- 2. Memory contents affect responses
- 3. Memory updates work correctly
- """
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Using memory content",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- memory = MockMemory(contents=["Test memory content"])
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- memory=[memory],
- )
-
- result = await agent.run(task="Use memory")
- assert isinstance(result.messages[-1], TextMessage)
- assert result.messages[-1].content == "Using memory content"
-
- @pytest.mark.asyncio
- async def test_initialization_with_handoffs(self) -> None:
- """Test agent initialization with handoffs."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent1", Handoff(target="agent2")],
- )
-
- assert len(agent._handoffs) == 2 # type: ignore[reportPrivateUsage]
- assert "transfer_to_agent1" in agent._handoffs # type: ignore[reportPrivateUsage]
- assert "transfer_to_agent2" in agent._handoffs # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_initialization_with_custom_model_context(self) -> None:
- """Test agent initialization with custom model context."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- model_context = BufferedChatCompletionContext(buffer_size=5)
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_context=model_context,
- )
-
- assert agent._model_context == model_context # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_initialization_with_structured_output(self) -> None:
- """Test agent initialization with structured output."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- output_content_type=StructuredOutput,
- )
-
- assert agent._output_content_type == StructuredOutput # type: ignore[reportPrivateUsage]
- assert agent._reflect_on_tool_use is True # type: ignore[reportPrivateUsage] # Should be True by default with structured output
-
- @pytest.mark.asyncio
- async def test_initialization_with_metadata(self) -> None:
- """Test agent initialization with metadata."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- metadata = {"key1": "value1", "key2": "value2"}
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- metadata=metadata,
- )
-
- assert agent._metadata == metadata # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_output_task_messages_false(self) -> None:
- """Test agent with output_task_messages=False.
-
- Verifies that:
- 1. Task messages are excluded from result when output_task_messages=False
- 2. Only agent response messages are included in output
- 3. Both run and run_stream respect the parameter
- """
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Agent response without task message",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=8),
- cached=False,
- ),
- CreateResult(
- finish_reason="stop",
- content="Second agent response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(name="test_agent", model_client=model_client)
-
- # Test run() with output_task_messages=False
- result = await agent.run(task="Test task message", output_task_messages=False)
-
- # Should only contain the agent's response, not the task message
- assert len(result.messages) == 1
- assert isinstance(result.messages[0], TextMessage)
- assert result.messages[0].content == "Agent response without task message"
- assert result.messages[0].source == "test_agent" # Test run_stream() with output_task_messages=False
- # Create a new model client for streaming test to avoid response conflicts
- stream_model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Stream agent response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- stream_agent = AssistantAgent(name="test_agent", model_client=stream_model_client)
- streamed_messages: List[BaseAgentEvent | BaseChatMessage] = []
- final_result: TaskResult | None = None
-
- async for message in stream_agent.run_stream(task="Test task message", output_task_messages=False):
- if isinstance(message, TaskResult):
- final_result = message
- else:
- streamed_messages.append(message)
-
- # Verify streaming behavior
- assert final_result is not None
- assert len(final_result.messages) == 1
- assert isinstance(final_result.messages[0], TextMessage)
- assert final_result.messages[0].content == "Stream agent response"
-
- # Verify that no task message was streamed
- task_messages = [msg for msg in streamed_messages if isinstance(msg, TextMessage) and msg.source == "user"]
- assert len(task_messages) == 0 # Test with multiple task messages
- multi_model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Multi task response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- multi_agent = AssistantAgent(name="test_agent", model_client=multi_model_client)
- task_messages_list = [
- TextMessage(content="First task", source="user"),
- TextMessage(content="Second task", source="user"),
- ]
-
- result_multi = await multi_agent.run(task=task_messages_list, output_task_messages=False)
-
- # Should only contain the agent's response, not the multiple task messages
- assert len(result_multi.messages) == 1
- assert isinstance(result_multi.messages[0], TextMessage)
- assert result_multi.messages[0].source == "test_agent"
- assert result_multi.messages[0].content == "Multi task response"
-
-
- class TestAssistantAgentValidation:
- """Test suite for AssistantAgent validation.
-
- Tests various validation scenarios to ensure proper error handling and input validation.
- """
-
- @pytest.mark.asyncio
- async def test_tool_names_must_be_unique(self) -> None:
- """Test validation of unique tool names.
-
- Verifies that:
- 1. Duplicate tool names are detected
- 2. Appropriate error is raised
- """
-
- def duplicate_tool(param: str) -> str:
- """Test tool with duplicate name.
-
- Args:
- param: Input parameter
-
- Returns:
- Formatted string with parameter
- """
- return f"Duplicate tool: {param}"
-
- model_client = ReplayChatCompletionClient(
- [],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- with pytest.raises(ValueError, match="Tool names must be unique"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function, duplicate_tool, mock_tool_function],
- )
-
- @pytest.mark.asyncio
- async def test_handoff_names_must_be_unique(self) -> None:
- """Test validation of unique handoff names.
-
- Verifies that:
- 1. Duplicate handoff names are detected
- 2. Appropriate error is raised
- """
- model_client = ReplayChatCompletionClient(
- [],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- with pytest.raises(ValueError, match="Handoff names must be unique"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent1", "agent2", "agent1"],
- )
-
- @pytest.mark.asyncio
- async def test_handoff_names_must_be_unique_from_tool_names(self) -> None:
- """Test validation of handoff names against tool names.
-
- Verifies that:
- 1. Handoff names cannot conflict with tool names
- 2. Appropriate error is raised
- """
-
- def test_tool() -> str:
- """Test tool with name that conflicts with handoff.
-
- Returns:
- Static test string
- """
- return "test"
-
- model_client = ReplayChatCompletionClient(
- [],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- with pytest.raises(ValueError, match="Handoff names must be unique from tool names"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[test_tool],
- handoffs=["test_tool"],
- )
-
- @pytest.mark.asyncio
- async def test_function_calling_required_for_tools(self) -> None:
- """Test that function calling is required for tools."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- with pytest.raises(ValueError, match="The model does not support function calling"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- )
-
- @pytest.mark.asyncio
- async def test_function_calling_required_for_handoffs(self) -> None:
- """Test that function calling is required for handoffs."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- with pytest.raises(
- ValueError, match="The model does not support function calling, which is needed for handoffs"
- ):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent1"],
- )
-
- @pytest.mark.asyncio
- async def test_memory_type_validation(self) -> None:
- """Test memory type validation."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- with pytest.raises(TypeError, match="Expected Memory, List\\[Memory\\], or None"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- memory="invalid_memory", # type: ignore
- )
-
- @pytest.mark.asyncio
- async def test_tools_and_workbench_mutually_exclusive(self) -> None:
- """Test that tools and workbench are mutually exclusive."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- workbench = MagicMock()
-
- with pytest.raises(ValueError, match="Tools cannot be used with a workbench"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- workbench=workbench,
- )
-
- @pytest.mark.asyncio
- async def test_unsupported_tool_type(self) -> None:
- """Test error handling for unsupported tool types."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- with pytest.raises(ValueError, match="Unsupported tool type"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=["invalid_tool"], # type: ignore
- )
-
- @pytest.mark.asyncio
- async def test_unsupported_handoff_type(self) -> None:
- """Test error handling for unsupported handoff types."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- with pytest.raises(ValueError, match="Unsupported handoff type"):
- AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=[123], # type: ignore
- )
-
-
- class TestAssistantAgentStateManagement:
- """Test suite for AssistantAgent state management."""
-
- @pytest.mark.asyncio
- async def test_save_and_load_state(self) -> None:
- """Test saving and loading agent state."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- # Mock model context state
- mock_context = MagicMock()
- mock_context.save_state = AsyncMock(return_value={"context": "state"})
- mock_context.load_state = AsyncMock()
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_context=mock_context,
- )
-
- # Test save state
- state = await agent.save_state()
- assert "llm_context" in state
-
- # Test load state
- await agent.load_state(state)
- mock_context.load_state.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_on_reset(self) -> None:
- """Test agent reset functionality."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- mock_context = MagicMock()
- mock_context.clear = AsyncMock()
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_context=mock_context,
- )
-
- cancellation_token = CancellationToken()
- await agent.on_reset(cancellation_token)
-
- mock_context.clear.assert_called_once()
-
-
- class TestAssistantAgentProperties:
- """Test suite for AssistantAgent properties."""
-
- @pytest.mark.asyncio
- async def test_produced_message_types_text_only(self) -> None:
- """Test produced message types for text-only agent."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- )
-
- message_types = agent.produced_message_types
- assert TextMessage in message_types
-
- @pytest.mark.asyncio
- async def test_produced_message_types_with_tools(self) -> None:
- """Test produced message types for agent with tools."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- )
-
- message_types = agent.produced_message_types
- assert ToolCallSummaryMessage in message_types
-
- @pytest.mark.asyncio
- async def test_produced_message_types_with_handoffs(self) -> None:
- """Test produced message types for agent with handoffs."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent1"],
- )
-
- message_types = agent.produced_message_types
- assert HandoffMessage in message_types
-
- @pytest.mark.asyncio
- async def test_model_context_property(self) -> None:
- """Test model_context property access."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- custom_context = BufferedChatCompletionContext(buffer_size=3)
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_context=custom_context,
- )
-
- assert agent.model_context == custom_context
-
-
- class TestAssistantAgentErrorHandling:
- """Test suite for error handling scenarios."""
-
- @pytest.mark.asyncio
- async def test_invalid_json_in_tool_arguments(self) -> None:
- """Test handling of invalid JSON in tool arguments."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments="invalid json", name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- )
-
- result = await agent.run(task="Execute tool with invalid JSON")
-
- # Should handle JSON parsing error
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
-
-
- class TestAssistantAgentMemoryIntegration:
- """Test suite for AssistantAgent memory integration.
-
- Tests the integration between AssistantAgent and memory components, including:
- - Memory initialization
- - Context updates
- - Query operations
- - Memory persistence
- """
-
- @pytest.mark.asyncio
- async def test_memory_updates_context(self) -> None:
- """Test that memory properly updates model context.
-
- Verifies that:
- 1. Memory contents are added to context
- 2. Context updates trigger appropriate events
- 3. Memory query results are properly handled
- """
- # Setup test memory with initial content
- memory = MockMemory(contents=["Previous conversation about topic A"])
-
- # Configure model client with expected response
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Response incorporating memory content",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- # Create agent with memory
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- memory=[memory],
- description="Agent with memory integration",
- )
-
- # Track memory events during execution
- memory_events: List[MemoryQueryEvent] = []
-
- async def event_handler(event: MemoryQueryEvent) -> None:
- """Handle memory query events.
-
- Args:
- event: Memory query event to process
- """
- memory_events.append(event)
-
- # Create a handler function to capture memory events
- async def handle_memory_events(result: Any) -> None:
- messages: List[BaseChatMessage] = result.messages if hasattr(result, "messages") else []
- for msg in messages:
- if isinstance(msg, MemoryQueryEvent):
- await event_handler(msg)
-
- # Run agent
- result = await agent.run(task="Respond using memory context")
-
- # Process the events
- await handle_memory_events(result)
-
- # Verify memory integration
- assert len(memory_events) > 0, "No memory events were generated"
- assert isinstance(result.messages[-1], TextMessage)
- assert "Response incorporating memory content" in result.messages[-1].content
-
- @pytest.mark.asyncio
- async def test_memory_persistence(self) -> None:
- """Test memory persistence across multiple sessions.
-
- Verifies:
- 1. Memory content persists between sessions
- 2. Memory updates are preserved
- 3. Context is properly restored
- 4. Memory query events are generated correctly
- """
- # Create memory with initial content
- memory = MockMemory(contents=["Initial memory"])
-
- # Create model client
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Response using memory",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- CreateResult(
- finish_reason="stop",
- content="Response with updated memory",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- # Create agent with memory
- agent = AssistantAgent(name="memory_test_agent", model_client=model_client, memory=[memory])
-
- # First session
- result1 = await agent.run(task="First task")
- state = await agent.save_state()
-
- # Add new memory content
- await memory.add(MemoryContent(content="New memory", mime_type="text/plain"))
-
- # Create new agent and restore state
- new_agent = AssistantAgent(name="memory_test_agent", model_client=model_client, memory=[memory])
- await new_agent.load_state(state)
-
- # Second session
- result2 = await new_agent.run(task="Second task")
-
- # Verify memory persistence
- assert isinstance(result1.messages[-1], TextMessage)
- assert isinstance(result2.messages[-1], TextMessage)
- assert result1.messages[-1].content == "Response using memory"
- assert result2.messages[-1].content == "Response with updated memory"
-
- # Verify memory events
- memory_events = [msg for msg in result2.messages if isinstance(msg, MemoryQueryEvent)]
- assert len(memory_events) > 0
- assert any("New memory" in str(event.content) for event in memory_events)
-
-
- class TestAssistantAgentSystemMessage:
- """Test suite for system message functionality."""
-
- @pytest.mark.asyncio
- async def test_system_message_none(self) -> None:
- """Test agent with system_message=None."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- system_message=None,
- )
-
- assert agent._system_messages == [] # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_custom_system_message(self) -> None:
- """Test agent with custom system message."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- custom_message = "You are a specialized assistant."
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- system_message=custom_message,
- )
-
- assert len(agent._system_messages) == 1 # type: ignore[reportPrivateUsage]
- assert agent._system_messages[0].content == custom_message # type: ignore[reportPrivateUsage]
-
-
- class TestAssistantAgentModelCompatibility:
- """Test suite for model compatibility functionality."""
-
- @pytest.mark.asyncio
- async def test_vision_compatibility(self) -> None:
- """Test vision model compatibility."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": True, "family": ModelFamily.GPT_4O}
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- )
-
- # Test _get_compatible_context with vision model
- from autogen_core.models import LLMMessage
-
- messages: List[LLMMessage] = [SystemMessage(content="Test")]
- compatible_messages = agent._get_compatible_context(model_client, messages) # type: ignore[reportPrivateUsage]
-
- # Should return original messages for vision models
- assert compatible_messages == messages
-
-
- class TestAssistantAgentComponentSerialization:
- """Test suite for component serialization functionality."""
-
- @pytest.mark.asyncio
- async def test_to_config_basic_agent(self) -> None:
- """Test _to_config method with basic agent configuration."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- description="Test description",
- system_message="Test system message",
- model_context=mock_context,
- metadata={"key": "value"},
- )
-
- config = agent._to_config() # type: ignore[reportPrivateUsage]
-
- assert config.name == "test_agent"
- assert config.description == "Test description"
- assert config.system_message == "Test system message"
- assert config.model_client_stream is False
- assert config.reflect_on_tool_use is False
- assert config.max_tool_iterations == 1
- assert config.metadata == {"key": "value"}
- model_client.dump_component.assert_called_once()
- mock_context.dump_component.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_to_config_agent_with_handoffs(self) -> None:
- """Test _to_config method with agent having handoffs."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent1", Handoff(target="agent2")],
- model_context=mock_context,
- )
-
- config = agent._to_config() # type: ignore[reportPrivateUsage]
-
- assert config.handoffs is not None
- assert len(config.handoffs) == 2
- handoff_targets: List[str] = [h.target if hasattr(h, "target") else str(h) for h in config.handoffs] # type: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
- assert "agent1" in handoff_targets
- assert "agent2" in handoff_targets
-
- @pytest.mark.asyncio
- async def test_to_config_agent_with_memory(self) -> None:
- """Test _to_config method with agent having memory modules."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- mock_memory = MockMemory()
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- memory=[mock_memory],
- model_context=mock_context,
- )
-
- config = agent._to_config() # type: ignore[reportPrivateUsage]
-
- assert config.memory is not None
- assert len(config.memory) == 1
- assert config.memory[0].provider == "test"
- assert config.memory[0].config == {"type": "mock_memory"}
-
- @pytest.mark.asyncio
- async def test_to_config_agent_with_workbench(self) -> None:
- """Test _to_config method with agent having workbench."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- mock_workbench = MagicMock()
- mock_workbench.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_workbench"})
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- model_context=mock_context,
- )
-
- # Replace the workbench with our mock
- agent._workbench = [mock_workbench] # type: ignore[reportPrivateUsage]
-
- config = agent._to_config() # type: ignore[reportPrivateUsage]
-
- assert config.workbench is not None
- assert len(config.workbench) == 1
- mock_workbench.dump_component.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_to_config_agent_with_structured_output(self) -> None:
- """Test _to_config method with agent having structured output."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- output_content_type=StructuredOutput,
- model_context=mock_context,
- )
-
- config = agent._to_config() # type: ignore[reportPrivateUsage]
-
- assert config.structured_message_factory is not None
- assert config.reflect_on_tool_use is True # Should be True with structured output
-
- @pytest.mark.asyncio
- async def test_to_config_system_message_none(self) -> None:
- """Test _to_config method with system_message=None."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- system_message=None,
- model_context=mock_context,
- )
-
- config = agent._to_config() # type: ignore[reportPrivateUsage]
-
- assert config.system_message is None
-
- @pytest.mark.asyncio
- async def test_from_config_basic_agent(self) -> None:
- """Test _from_config method with basic agent configuration."""
- mock_model_client = MagicMock()
- mock_model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- with patch("autogen_core.models.ChatCompletionClient.load_component", return_value=mock_model_client):
- config = AssistantAgentConfig(
- name="test_agent",
- model_client=ComponentModel(provider="test", config={"type": "mock_client"}),
- description="Test description",
- system_message="Test system",
- model_client_stream=True,
- reflect_on_tool_use=False,
- tool_call_summary_format="{tool_name}: {result}",
- metadata={"test": "value"},
- )
-
- agent = AssistantAgent._from_config(config) # type: ignore[reportPrivateUsage]
-
- assert agent.name == "test_agent"
- assert agent.description == "Test description"
- assert agent._model_client_stream is True # type: ignore[reportPrivateUsage]
- assert agent._reflect_on_tool_use is False # type: ignore[reportPrivateUsage]
- assert agent._tool_call_summary_format == "{tool_name}: {result}" # type: ignore[reportPrivateUsage]
- assert agent._metadata == {"test": "value"} # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_from_config_with_structured_output(self) -> None:
- """Test _from_config method with structured output configuration."""
- mock_model_client = MagicMock()
- mock_model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- mock_structured_factory = MagicMock()
- mock_structured_factory.format_string = "Test format"
- mock_structured_factory.ContentModel = StructuredOutput
-
- with (
- patch("autogen_core.models.ChatCompletionClient.load_component", return_value=mock_model_client),
- patch(
- "autogen_agentchat.messages.StructuredMessageFactory.load_component",
- return_value=mock_structured_factory,
- ),
- ):
- config = AssistantAgentConfig(
- name="test_agent",
- model_client=ComponentModel(provider="test", config={"type": "mock_client"}),
- description="Test description",
- reflect_on_tool_use=True,
- tool_call_summary_format="{result}",
- structured_message_factory=ComponentModel(provider="test", config={"type": "mock_factory"}),
- )
-
- agent = AssistantAgent._from_config(config) # type: ignore[reportPrivateUsage]
-
- assert agent._reflect_on_tool_use is True # type: ignore[reportPrivateUsage]
- assert agent._output_content_type == StructuredOutput # type: ignore[reportPrivateUsage]
- assert agent._output_content_type_format == "Test format" # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_from_config_with_workbench_and_memory(self) -> None:
- """Test _from_config method with workbench and memory."""
- mock_model_client = MagicMock()
- mock_model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- mock_workbench = MagicMock()
- mock_memory = MockMemory()
- mock_context = MagicMock()
-
- with (
- patch("autogen_core.models.ChatCompletionClient.load_component", return_value=mock_model_client),
- patch("autogen_core.tools.Workbench.load_component", return_value=mock_workbench),
- patch("autogen_core.memory.Memory.load_component", return_value=mock_memory),
- patch("autogen_core.model_context.ChatCompletionContext.load_component", return_value=mock_context),
- ):
- config = AssistantAgentConfig(
- name="test_agent",
- model_client=ComponentModel(provider="test", config={"type": "mock_client"}),
- description="Test description",
- workbench=[ComponentModel(provider="test", config={"type": "mock_workbench"})],
- memory=[ComponentModel(provider="test", config={"type": "mock_memory"})],
- model_context=ComponentModel(provider="test", config={"type": "mock_context"}),
- reflect_on_tool_use=True,
- tool_call_summary_format="{result}",
- )
-
- agent = AssistantAgent._from_config(config) # type: ignore[reportPrivateUsage]
-
- assert len(agent._workbench) == 1 # type: ignore[reportPrivateUsage]
- assert agent._memory is not None # type: ignore[reportPrivateUsage]
- assert len(agent._memory) == 1 # type: ignore[reportPrivateUsage]
- assert agent._model_context == mock_context # type: ignore[reportPrivateUsage]
-
- @pytest.mark.asyncio
- async def test_config_roundtrip_consistency(self) -> None:
- """Test that converting to config and back preserves agent properties."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_client"})
- )
-
- mock_context = MagicMock()
- mock_context.dump_component = MagicMock(
- return_value=ComponentModel(provider="test", config={"type": "mock_context"})
- )
-
- original_agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- description="Test description",
- system_message="Test system message",
- model_client_stream=True,
- reflect_on_tool_use=True,
- max_tool_iterations=5,
- tool_call_summary_format="{tool_name}: {result}",
- handoffs=["agent1"],
- model_context=mock_context,
- metadata={"test": "value"},
- )
-
- # Convert to config
- config = original_agent._to_config() # type: ignore[reportPrivateUsage]
-
- # Verify config properties
- assert config.name == "test_agent"
- assert config.description == "Test description"
- assert config.system_message == "Test system message"
- assert config.model_client_stream is True
- assert config.reflect_on_tool_use is True
- assert config.max_tool_iterations == 5
- assert config.tool_call_summary_format == "{tool_name}: {result}"
- assert config.metadata == {"test": "value"}
-
-
- class TestAssistantAgentThoughtHandling:
- """Test suite for thought handling functionality."""
-
- @pytest.mark.asyncio
- async def test_thought_event_yielded_from_model_result(self) -> None:
- """Test that thought events are yielded when model result contains thoughts."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Final response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- thought="This is my internal thought process",
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have ThoughtEvent in the stream
- thought_events = [msg for msg in messages if isinstance(msg, ThoughtEvent)]
- assert len(thought_events) == 1
- assert thought_events[0].content == "This is my internal thought process"
- assert thought_events[0].source == "test_agent"
-
- @pytest.mark.asyncio
- async def test_thought_event_with_tool_calls(self) -> None:
- """Test that thought events are yielded when tool calls have thoughts."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- thought="I need to use this tool to help the user",
- ),
- CreateResult(
- finish_reason="stop",
- content="Tool execution completed",
- usage=RequestUsage(prompt_tokens=15, completion_tokens=10),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- max_tool_iterations=1,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have ThoughtEvent in the stream
- thought_events = [msg for msg in messages if isinstance(msg, ThoughtEvent)]
- assert len(thought_events) == 1
- assert thought_events[0].content == "I need to use this tool to help the user"
- assert thought_events[0].source == "test_agent"
-
- @pytest.mark.asyncio
- async def test_thought_event_with_reflection(self) -> None:
- """Test that thought events are yielded during reflection."""
- model_client = ReplayChatCompletionClient(
- [
- # Initial tool call with thought
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- thought="Initial thought before tool call",
- ),
- # Reflection with thought
- CreateResult(
- finish_reason="stop",
- content="Based on the tool result, here's my response",
- usage=RequestUsage(prompt_tokens=15, completion_tokens=10),
- cached=False,
- thought="Reflection thought after tool execution",
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- reflect_on_tool_use=True,
- model_client_stream=True, # Enable streaming
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have two ThoughtEvents - one for initial call, one for reflection
- thought_events = [msg for msg in messages if isinstance(msg, ThoughtEvent)]
- assert len(thought_events) == 2
-
- thought_contents = [event.content for event in thought_events]
- assert "Initial thought before tool call" in thought_contents
- assert "Reflection thought after tool execution" in thought_contents
-
- @pytest.mark.asyncio
- async def test_thought_event_with_tool_call_loop(self) -> None:
- """Test that thought events are yielded in tool call loops."""
- model_client = ReplayChatCompletionClient(
- [
- # First tool call with thought
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "first"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- thought="First iteration thought",
- ),
- # Second tool call with thought
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="2", arguments=json.dumps({"param": "second"}), name="mock_tool_function")
- ],
- usage=RequestUsage(prompt_tokens=12, completion_tokens=5),
- cached=False,
- thought="Second iteration thought",
- ),
- # Final response with thought
- CreateResult(
- finish_reason="stop",
- content="Loop completed",
- usage=RequestUsage(prompt_tokens=15, completion_tokens=10),
- cached=False,
- thought="Final completion thought",
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- max_tool_iterations=3,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have three ThoughtEvents - one for each iteration
- thought_events = [msg for msg in messages if isinstance(msg, ThoughtEvent)]
- assert len(thought_events) == 3
-
- thought_contents = [event.content for event in thought_events]
- assert "First iteration thought" in thought_contents
- assert "Second iteration thought" in thought_contents
- assert "Final completion thought" in thought_contents
-
- @pytest.mark.asyncio
- async def test_thought_event_with_handoff(self) -> None:
- """Test that thought events are included in handoff context."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(
- id="1", arguments=json.dumps({"target": "other_agent"}), name="transfer_to_other_agent"
- )
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- thought="I need to hand this off to another agent",
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["other_agent"],
- max_tool_iterations=1,
- )
-
- result = await agent.run(task="Test handoff with thought")
-
- # Should have ThoughtEvent in inner messages
- thought_events = [msg for msg in result.messages if isinstance(msg, ThoughtEvent)]
- assert len(thought_events) == 1
- assert thought_events[0].content == "I need to hand this off to another agent"
-
- # Should have handoff message with thought in context
- handoff_message = result.messages[-1]
- assert isinstance(handoff_message, HandoffMessage)
- assert len(handoff_message.context) == 1
- assert isinstance(handoff_message.context[0], AssistantMessage)
- assert handoff_message.context[0].content == "I need to hand this off to another agent"
-
- @pytest.mark.asyncio
- async def test_no_thought_event_when_no_thought(self) -> None:
- """Test that no thought events are yielded when model result has no thoughts."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Simple response without thought",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- # No thought field
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have no ThoughtEvents
- thought_events = [msg for msg in messages if isinstance(msg, ThoughtEvent)]
- assert len(thought_events) == 0
-
- @pytest.mark.asyncio
- async def test_thought_event_context_preservation(self) -> None:
- """Test that thoughts are properly preserved in model context."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content="Response with thought",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- thought="Internal reasoning",
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- )
-
- await agent.run(task="Test thought preservation")
-
- # Check that the model context contains the thought
- messages = await agent.model_context.get_messages()
- assistant_messages = [msg for msg in messages if isinstance(msg, AssistantMessage)]
- assert len(assistant_messages) > 0
-
- # The last assistant message should have the thought
- last_assistant_msg = assistant_messages[-1]
- # Fix line 2730 - properly check for thought attribute with type checking
- if hasattr(last_assistant_msg, "thought"):
- thought_content = cast(str, last_assistant_msg.thought)
- assert thought_content == "Internal reasoning"
-
-
- class TestAssistantAgentAdvancedScenarios:
- """Test suite for advanced usage scenarios."""
-
- @pytest.mark.asyncio
- async def test_handoff_without_tool_calls(self) -> None:
- """Test handoff without any tool calls."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"target": "agent2"}), name="transfer_to_agent2")
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent2"],
- )
-
- result = await agent.run(task="Handoff to agent2")
-
- # Should return HandoffMessage
- assert isinstance(result.messages[-1], HandoffMessage)
- assert result.messages[-1].target == "agent2"
-
- @pytest.mark.asyncio
- async def test_multiple_handoff_warning(self) -> None:
- """Test warning for multiple handoffs."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"target": "agent2"}), name="transfer_to_agent2"),
- FunctionCall(id="2", arguments=json.dumps({"target": "agent3"}), name="transfer_to_agent3"),
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- handoffs=["agent2", "agent3"],
- )
-
- with pytest.warns(UserWarning, match="Multiple handoffs detected"):
- result = await agent.run(task="Multiple handoffs")
-
- # Should only execute first handoff
- assert isinstance(result.messages[-1], HandoffMessage)
- assert result.messages[-1].target == "agent2"
-
- @pytest.mark.asyncio
- async def test_structured_output_with_reflection(self) -> None:
- """Test structured output with reflection enabled."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- CreateResult(
- finish_reason="stop",
- content='{"content": "Structured response", "confidence": 0.95}',
- usage=RequestUsage(prompt_tokens=15, completion_tokens=10),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- output_content_type=StructuredOutput,
- reflect_on_tool_use=True,
- )
-
- result = await agent.run(task="Test structured output with reflection")
-
- # Should return StructuredMessage
- from autogen_agentchat.messages import StructuredMessage
-
- final_message = result.messages[-1]
- assert isinstance(final_message, StructuredMessage)
- # Fix line 1710 - properly access structured content with explicit type annotation
- structured_message: StructuredMessage[StructuredOutput] = cast(
- StructuredMessage[StructuredOutput], final_message
- )
- assert structured_message.content.content == "Structured response"
- assert structured_message.content.confidence == 0.95
-
-
- class TestAssistantAgentAdvancedToolFeatures:
- """Test suite for advanced tool features including custom formatters."""
-
- @pytest.mark.asyncio
- async def test_custom_tool_call_summary_formatter(self) -> None:
- """Test custom tool call summary formatter functionality."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"param": "success"}), name="mock_tool_function"),
- FunctionCall(id="2", arguments=json.dumps({"param": "error"}), name="mock_tool_function"),
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- def custom_formatter(call: FunctionCall, result: FunctionExecutionResult) -> str:
- if result.is_error:
- return f"ERROR in {call.name}: {result.content} (args: {call.arguments})"
- else:
- return f"SUCCESS: {call.name} completed"
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- tool_call_summary_formatter=custom_formatter,
- reflect_on_tool_use=False,
- )
-
- result = await agent.run(task="Test custom formatter")
-
- # Should return ToolCallSummaryMessage with custom formatting
- final_message = result.messages[-1]
- assert isinstance(final_message, ToolCallSummaryMessage)
- # Fix line 1875 - properly access content with type checking
- assert hasattr(final_message, "content"), "ToolCallSummaryMessage should have content attribute"
- content = final_message.content
- assert "SUCCESS: mock_tool_function completed" in content
- assert "SUCCESS: mock_tool_function completed" in content # Both calls should be successful
-
- @pytest.mark.asyncio
- async def test_custom_tool_call_summary_format_string(self) -> None:
- """Test custom tool call summary format string."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- tool_call_summary_format="Tool {tool_name} called with {arguments} -> {result}",
- reflect_on_tool_use=False,
- )
-
- result = await agent.run(task="Test custom format string")
-
- # Should return ToolCallSummaryMessage with custom format
- final_message = result.messages[-1]
- assert isinstance(final_message, ToolCallSummaryMessage)
- content = final_message.content
- assert "Tool mock_tool_function called with" in content
- assert "Tool executed with: test" in content
-
- @pytest.mark.asyncio
- async def test_tool_call_summary_formatter_overrides_format_string(self) -> None:
- """Test that tool_call_summary_formatter overrides format string."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- def custom_formatter(call: FunctionCall, result: FunctionExecutionResult) -> str:
- return f"CUSTOM: {call.name} -> {result.content}"
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- tool_call_summary_format="This should be ignored: {result}",
- tool_call_summary_formatter=custom_formatter,
- reflect_on_tool_use=False,
- )
-
- result = await agent.run(task="Test formatter override")
-
- # Should use custom formatter, not format string
- final_message = result.messages[-1]
- assert isinstance(final_message, ToolCallSummaryMessage)
- content = final_message.content
- assert "CUSTOM: mock_tool_function" in content
- assert "This should be ignored" not in content
-
- @pytest.mark.asyncio
- async def test_output_content_type_format_string(self) -> None:
- """Test structured output with custom format string."""
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="stop",
- content='{"content": "Test response", "confidence": 0.8}',
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- output_content_type=StructuredOutput,
- output_content_type_format="Response: {content} (Confidence: {confidence})",
- )
-
- result = await agent.run(task="Test structured output format")
-
- # Should return StructuredMessage with custom format
- final_message = result.messages[-1]
- assert isinstance(final_message, StructuredMessage)
- # Fix line 1880 - properly access structured content with explicit type annotation
- structured_message: StructuredMessage[StructuredOutput] = cast(
- StructuredMessage[StructuredOutput], final_message
- )
- assert structured_message.content.content == "Test response"
- assert structured_message.content.confidence == 0.8
- # The format string should be stored in the agent
- assert hasattr(agent, "_output_content_type_format")
- output_format = getattr(agent, "_output_content_type_format", None)
- assert output_format == "Response: {content} (Confidence: {confidence})"
-
- @pytest.mark.asyncio
- async def test_tool_call_error_handling_with_custom_formatter(self) -> None:
- """Test error handling in tool calls with custom formatter."""
-
- def error_tool(param: str) -> str:
- raise ValueError(f"Tool error with param: {param}")
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="error_tool")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- def error_formatter(call: FunctionCall, result: FunctionExecutionResult) -> str:
- if result.is_error:
- return f"ERROR in {call.name}: {result.content}"
- else:
- return f"SUCCESS: {result.content}"
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[error_tool],
- tool_call_summary_formatter=error_formatter,
- reflect_on_tool_use=False,
- )
-
- result = await agent.run(task="Test error handling")
-
- # Should return ToolCallSummaryMessage with error formatting
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- content = result.messages[-1].content
- assert "ERROR in error_tool" in content
-
- @pytest.mark.asyncio
- async def test_multiple_tools_with_different_formats(self) -> None:
- """Test multiple tool calls with different return formats."""
-
- def json_tool(data: str) -> str:
- return json.dumps({"result": data, "status": "success"})
-
- def simple_tool(text: str) -> str:
- return f"Processed: {text}"
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"data": "json_data"}), name="json_tool"),
- FunctionCall(id="2", arguments=json.dumps({"text": "simple_text"}), name="simple_tool"),
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- def smart_formatter(call: FunctionCall, result: FunctionExecutionResult) -> str:
- try:
- # Try to parse as JSON
- parsed = json.loads(result.content)
- return f"{call.name}: {parsed}"
- except json.JSONDecodeError:
- # Plain text
- return f"{call.name}: {result.content}"
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[json_tool, simple_tool],
- tool_call_summary_formatter=smart_formatter,
- reflect_on_tool_use=False,
- )
-
- result = await agent.run(task="Test multiple tool formats")
-
- # Should handle both JSON and plain text tools
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- content = result.messages[-1].content
- assert "json_tool:" in content
- assert "simple_tool:" in content
- assert "Processed: simple_text" in content
-
-
- class TestAssistantAgentCancellationToken:
- """Test suite for cancellation token handling."""
-
- @pytest.mark.asyncio
- async def test_cancellation_during_model_inference(self) -> None:
- """Test cancellation token during model inference."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- # Mock create method to check cancellation token
- model_client.create = AsyncMock()
- model_client.create.return_value = CreateResult(
- finish_reason="stop",
- content="Response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- )
-
- cancellation_token = CancellationToken()
- result = await agent.on_messages([TextMessage(content="Test", source="user")], cancellation_token)
-
- # Verify cancellation token was passed to model client
- model_client.create.assert_called_once()
- call_args = model_client.create.call_args
- assert call_args.kwargs["cancellation_token"] == cancellation_token
- # Verify result is not None
- assert result is not None
-
- @pytest.mark.asyncio
- async def test_cancellation_during_streaming_inference(self) -> None:
- """Test cancellation token during streaming model inference."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- # Mock create_stream method
- async def mock_create_stream(*args: Any, **kwargs: Any) -> Any:
- yield "chunk1" # First chunk
- yield "chunk2" # Second chunk
- yield CreateResult(
- finish_reason="stop",
- content="chunk1chunk2",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
-
- model_client.create_stream = mock_create_stream
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_client_stream=True,
- )
-
- cancellation_token = CancellationToken()
- messages: List[Any] = []
- async for message in agent.on_messages_stream([TextMessage(content="Test", source="user")], cancellation_token):
- messages.append(message)
-
- # Should have received streaming chunks and final response
- chunk_events = [msg for msg in messages if isinstance(msg, ModelClientStreamingChunkEvent)]
- assert len(chunk_events) == 2
- assert chunk_events[0].content == "chunk1"
- assert chunk_events[1].content == "chunk2"
-
- @pytest.mark.asyncio
- async def test_cancellation_during_tool_execution(self) -> None:
- """Test cancellation token during tool execution."""
-
- async def slow_tool(param: str) -> str:
- await asyncio.sleep(0.1) # Simulate slow operation
- return f"Slow result: {param}"
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="slow_tool")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[slow_tool],
- )
-
- cancellation_token = CancellationToken()
- result = await agent.on_messages([TextMessage(content="Test", source="user")], cancellation_token)
-
- # Tool should execute successfully with cancellation token
- assert isinstance(result.chat_message, ToolCallSummaryMessage)
- assert "Slow result: test" in result.chat_message.content
-
- @pytest.mark.asyncio
- async def test_cancellation_during_workbench_tool_execution(self) -> None:
- """Test cancellation token during workbench tool execution."""
- mock_workbench = MagicMock()
- mock_workbench.list_tools = AsyncMock(return_value=[{"name": "test_tool", "description": "Test tool"}])
-
- # Mock tool execution result
- mock_result = MagicMock()
- mock_result.to_text.return_value = "Workbench tool result"
- mock_result.is_error = False
- mock_workbench.call_tool = AsyncMock(return_value=mock_result)
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="test_tool")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- workbench=[mock_workbench],
- )
-
- cancellation_token = CancellationToken()
- result = await agent.on_messages([TextMessage(content="Test", source="user")], cancellation_token)
-
- # Verify cancellation token was passed to workbench
- mock_workbench.call_tool.assert_called_once()
- call_args = mock_workbench.call_tool.call_args
- assert call_args.kwargs["cancellation_token"] == cancellation_token
- # Verify result is not None
- assert result is not None
-
- @pytest.mark.asyncio
- async def test_cancellation_during_memory_operations(self) -> None:
- """Test cancellation token during memory operations."""
- mock_memory = MagicMock()
- mock_memory.update_context = AsyncMock(return_value=None)
-
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
- model_client.create = AsyncMock(
- return_value=CreateResult(
- finish_reason="stop",
- content="Response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- memory=[mock_memory],
- )
-
- cancellation_token = CancellationToken()
- await agent.on_messages([TextMessage(content="Test", source="user")], cancellation_token)
-
- # Memory update_context should be called
- mock_memory.update_context.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_reset_with_cancellation_token(self) -> None:
- """Test agent reset with cancellation token."""
- mock_context = MagicMock()
- mock_context.clear = AsyncMock()
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=MagicMock(),
- model_context=mock_context,
- )
-
- cancellation_token = CancellationToken()
- await agent.on_reset(cancellation_token)
-
- # Context clear should be called
- mock_context.clear.assert_called_once()
-
-
- class TestAssistantAgentStreamingEdgeCases:
- """Test suite for streaming edge cases and error scenarios."""
-
- @pytest.mark.asyncio
- async def test_streaming_with_empty_chunks(self) -> None:
- """Test streaming with empty chunks."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- async def mock_create_stream(*args: Any, **kwargs: Any) -> Any:
- yield "" # Empty chunk
- yield "content"
- yield "" # Another empty chunk
- yield CreateResult(
- finish_reason="stop",
- content="content",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
-
- model_client.create_stream = mock_create_stream
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_client_stream=True,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should handle empty chunks gracefully
- chunk_events = [msg for msg in messages if isinstance(msg, ModelClientStreamingChunkEvent)]
- assert len(chunk_events) == 3 # Including empty chunks
- assert chunk_events[0].content == ""
- assert chunk_events[1].content == "content"
- assert chunk_events[2].content == ""
-
- @pytest.mark.asyncio
- async def test_streaming_with_invalid_chunk_type(self) -> None:
- """Test streaming with invalid chunk type raises error."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- async def mock_create_stream(*args: Any, **kwargs: Any) -> Any:
- yield "valid_chunk"
- yield 123 # Invalid chunk type
- yield CreateResult(
- finish_reason="stop",
- content="content",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
-
- model_client.create_stream = mock_create_stream
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_client_stream=True,
- )
-
- with pytest.raises(RuntimeError, match="Invalid chunk type"):
- async for _ in agent.on_messages_stream([TextMessage(content="Test", source="user")], CancellationToken()):
- pass
-
- @pytest.mark.asyncio
- async def test_streaming_without_final_result(self) -> None:
- """Test streaming without final CreateResult raises error."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- async def mock_create_stream(*args: Any, **kwargs: Any) -> Any:
- yield "chunk1"
- yield "chunk2"
- # No final CreateResult
-
- model_client.create_stream = mock_create_stream
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_client_stream=True,
- )
-
- with pytest.raises(RuntimeError, match="No final model result in streaming mode"):
- async for _ in agent.on_messages_stream([TextMessage(content="Test", source="user")], CancellationToken()):
- pass
-
- @pytest.mark.asyncio
- async def test_streaming_with_tool_calls_and_reflection(self) -> None:
- """Test streaming with tool calls followed by reflection."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": True, "vision": False, "family": ModelFamily.GPT_4O}
-
- call_count = 0
-
- async def mock_create_stream(*args: Any, **kwargs: Any) -> Any:
- nonlocal call_count
- call_count += 1
-
- if call_count == 1:
- # First call: tool call
- yield CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="mock_tool_function")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
- else:
- # Second call: reflection streaming
- yield "Reflection "
- yield "response "
- yield "complete"
- yield CreateResult(
- finish_reason="stop",
- content="Reflection response complete",
- usage=RequestUsage(prompt_tokens=15, completion_tokens=10),
- cached=False,
- )
-
- model_client.create_stream = mock_create_stream
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- reflect_on_tool_use=True,
- model_client_stream=True,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have tool call events, execution events, and streaming chunks for reflection
- tool_call_events = [msg for msg in messages if isinstance(msg, ToolCallRequestEvent)]
- tool_exec_events = [msg for msg in messages if isinstance(msg, ToolCallExecutionEvent)]
- chunk_events = [msg for msg in messages if isinstance(msg, ModelClientStreamingChunkEvent)]
-
- assert len(tool_call_events) == 1
- assert len(tool_exec_events) == 1
- assert len(chunk_events) == 3 # Three reflection chunks
- assert chunk_events[0].content == "Reflection "
- assert chunk_events[1].content == "response "
- assert chunk_events[2].content == "complete"
-
- @pytest.mark.asyncio
- async def test_streaming_with_large_chunks(self) -> None:
- """Test streaming with large chunks."""
- model_client = MagicMock()
- model_client.model_info = {"function_calling": False, "vision": False, "family": ModelFamily.GPT_4O}
-
- large_chunk = "x" * 10000 # 10KB chunk
-
- async def mock_create_stream(*args: Any, **kwargs: Any) -> Any:
- yield large_chunk
- yield CreateResult(
- finish_reason="stop",
- content=large_chunk,
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- )
-
- model_client.create_stream = mock_create_stream
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- model_client_stream=True,
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Test", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should handle large chunks
- chunk_events = [msg for msg in messages if isinstance(msg, ModelClientStreamingChunkEvent)]
- assert len(chunk_events) == 1
- assert len(chunk_events[0].content) == 10000
-
-
- class TestAssistantAgentWorkbenchIntegration:
- """Test suite for comprehensive workbench testing."""
-
- @pytest.mark.asyncio
- async def test_multiple_workbenches(self) -> None:
- """Test agent with multiple workbenches."""
- mock_workbench1 = MagicMock()
- mock_workbench1.list_tools = AsyncMock(return_value=[{"name": "tool1", "description": "Tool from workbench 1"}])
- mock_result1 = MagicMock()
- mock_result1.to_text.return_value = "Result from workbench 1"
- mock_result1.is_error = False
- mock_workbench1.call_tool = AsyncMock(return_value=mock_result1)
-
- mock_workbench2 = MagicMock()
- mock_workbench2.list_tools = AsyncMock(return_value=[{"name": "tool2", "description": "Tool from workbench 2"}])
- mock_result2 = MagicMock()
- mock_result2.to_text.return_value = "Result from workbench 2"
- mock_result2.is_error = False
- mock_workbench2.call_tool = AsyncMock(return_value=mock_result2)
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"param": "test1"}), name="tool1"),
- FunctionCall(id="2", arguments=json.dumps({"param": "test2"}), name="tool2"),
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- workbench=[mock_workbench1, mock_workbench2],
- )
-
- result = await agent.run(task="Test multiple workbenches")
-
- # Both workbenches should be called
- mock_workbench1.call_tool.assert_called_once()
- mock_workbench2.call_tool.assert_called_once()
-
- # Should return summary with both results
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- content = result.messages[-1].content
- assert "Result from workbench 1" in content
- assert "Result from workbench 2" in content
-
- @pytest.mark.asyncio
- async def test_workbench_tool_not_found(self) -> None:
- """Test handling when tool is not found in any workbench."""
- mock_workbench = MagicMock()
- mock_workbench.list_tools = AsyncMock(
- return_value=[{"name": "available_tool", "description": "Available tool"}]
- )
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[FunctionCall(id="1", arguments=json.dumps({"param": "test"}), name="missing_tool")],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- workbench=[mock_workbench],
- )
-
- result = await agent.run(task="Test missing tool")
-
- # Should return error message for missing tool
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- content = result.messages[-1].content
- assert "tool 'missing_tool' not found" in content
-
- @pytest.mark.asyncio
- async def test_workbench_concurrent_tool_execution(self) -> None:
- """Test concurrent execution of multiple workbench tools."""
- mock_workbench = MagicMock()
- mock_workbench.list_tools = AsyncMock(
- return_value=[
- {"name": "concurrent_tool1", "description": "Concurrent tool 1"},
- {"name": "concurrent_tool2", "description": "Concurrent tool 2"},
- ]
- )
-
- call_order: List[str] = []
-
- async def mock_call_tool(name: str, **kwargs: Any) -> Any:
- call_order.append(f"start_{name}")
- await asyncio.sleep(0.01) # Simulate work
- call_order.append(f"end_{name}")
-
- mock_result = MagicMock()
- mock_result.to_text.return_value = f"Result from {name}"
- mock_result.is_error = False
- return mock_result
-
- mock_workbench.call_tool = mock_call_tool
-
- model_client = ReplayChatCompletionClient(
- [
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"param": "test1"}), name="concurrent_tool1"),
- FunctionCall(id="2", arguments=json.dumps({"param": "test2"}), name="concurrent_tool2"),
- ],
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="test_agent",
- model_client=model_client,
- workbench=[mock_workbench],
- )
-
- result = await agent.run(task="Test concurrent execution")
-
- # Should execute both tools concurrently (both start before either ends)
- assert "start_concurrent_tool1" in call_order
- assert "start_concurrent_tool2" in call_order
-
- # Both results should be present
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- content = result.messages[-1].content
- assert "Result from concurrent_tool1" in content
- assert "Result from concurrent_tool2" in content
-
-
- class TestAssistantAgentComplexIntegration:
- """Test suite for complex integration scenarios."""
-
- @pytest.mark.asyncio
- async def test_complete_workflow_with_all_features(self) -> None:
- """Test agent with tools, handoffs, memory, streaming, and reflection."""
- # Setup memory
- memory = MockMemory(["User prefers detailed explanations"])
-
- # Setup model client with complex workflow
- model_client = ReplayChatCompletionClient(
- [
- # Initial tool call
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"param": "analysis"}), name="mock_tool_function")
- ],
- usage=RequestUsage(prompt_tokens=20, completion_tokens=10),
- cached=False,
- thought="I need to analyze this first",
- ),
- # Reflection result
- CreateResult(
- finish_reason="stop",
- content="Based on the analysis, I can provide a detailed response. The user prefers comprehensive explanations.",
- usage=RequestUsage(prompt_tokens=30, completion_tokens=15),
- cached=False,
- thought="I should be thorough based on user preference",
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="comprehensive_agent",
- model_client=model_client,
- tools=[mock_tool_function],
- handoffs=["specialist_agent"],
- memory=[memory],
- reflect_on_tool_use=True,
- model_client_stream=True,
- tool_call_summary_format="Analysis: {result}",
- metadata={"test": "comprehensive"},
- )
-
- messages: List[Any] = []
- async for message in agent.on_messages_stream(
- [TextMessage(content="Analyze this complex scenario", source="user")], CancellationToken()
- ):
- messages.append(message)
-
- # Should have all types of events
- memory_events = [msg for msg in messages if isinstance(msg, MemoryQueryEvent)]
- thought_events = [msg for msg in messages if isinstance(msg, ThoughtEvent)]
- tool_events = [msg for msg in messages if isinstance(msg, ToolCallRequestEvent)]
- execution_events = [msg for msg in messages if isinstance(msg, ToolCallExecutionEvent)]
- chunk_events = [msg for msg in messages if isinstance(msg, ModelClientStreamingChunkEvent)]
-
- assert len(memory_events) > 0
- assert len(thought_events) == 2 # Initial and reflection thoughts
- assert len(tool_events) == 1
- assert len(execution_events) == 1
- assert len(chunk_events) == 0 # No streaming chunks since we removed the string responses
-
- # Final response should be TextMessage from reflection
- final_response = None
- for msg in reversed(messages):
- if isinstance(msg, Response):
- final_response = msg
- break
-
- assert final_response is not None
- assert isinstance(final_response.chat_message, TextMessage)
- assert "comprehensive explanations" in final_response.chat_message.content
-
- @pytest.mark.asyncio
- async def test_error_recovery_in_complex_workflow(self) -> None:
- """Test error recovery in complex workflow with multiple failures."""
-
- def failing_tool(param: str) -> str:
- if param == "fail":
- raise ValueError("Tool failure")
- return f"Success: {param}"
-
- model_client = ReplayChatCompletionClient(
- [
- # Multiple tool calls, some failing
- CreateResult(
- finish_reason="function_calls",
- content=[
- FunctionCall(id="1", arguments=json.dumps({"param": "success"}), name="failing_tool"),
- FunctionCall(id="2", arguments=json.dumps({"param": "fail"}), name="failing_tool"),
- FunctionCall(id="3", arguments=json.dumps({"param": "success2"}), name="failing_tool"),
- ],
- usage=RequestUsage(prompt_tokens=20, completion_tokens=10),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": True,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- def error_aware_formatter(call: FunctionCall, result: FunctionExecutionResult) -> str:
- if result.is_error:
- return f"⚠️ {call.name} failed: {result.content}"
- else:
- return f"✅ {call.name}: {result.content}"
-
- agent = AssistantAgent(
- name="error_recovery_agent",
- model_client=model_client,
- tools=[failing_tool],
- tool_call_summary_formatter=error_aware_formatter,
- reflect_on_tool_use=False,
- )
-
- result = await agent.run(task="Test error recovery")
-
- # Should handle mixed success/failure gracefully
- assert isinstance(result.messages[-1], ToolCallSummaryMessage)
- content = result.messages[-1].content
- assert "✅ failing_tool: Success: success" in content
- assert "⚠️ failing_tool failed:" in content
- assert "✅ failing_tool: Success: success2" in content
-
- @pytest.mark.asyncio
- async def test_state_persistence_across_interactions(self) -> None:
- """Test that agent state persists correctly across multiple interactions."""
- model_client = ReplayChatCompletionClient(
- [
- # First interaction
- CreateResult(
- finish_reason="stop",
- content="First response",
- usage=RequestUsage(prompt_tokens=10, completion_tokens=5),
- cached=False,
- ),
- # Second interaction
- CreateResult(
- finish_reason="stop",
- content="Second response, remembering context",
- usage=RequestUsage(prompt_tokens=15, completion_tokens=8),
- cached=False,
- ),
- ],
- model_info={
- "function_calling": False,
- "vision": False,
- "json_output": False,
- "family": ModelFamily.GPT_4O,
- "structured_output": False,
- },
- )
-
- agent = AssistantAgent(
- name="stateful_agent",
- model_client=model_client,
- system_message="Remember previous conversations",
- )
-
- # First interaction
- result1 = await agent.run(task="First task")
- final_message_1 = result1.messages[-1]
- assert isinstance(final_message_1, TextMessage)
- assert final_message_1.content == "First response"
-
- # Save state
- state = await agent.save_state()
- assert "llm_context" in state
-
- # Second interaction
- result2 = await agent.run(task="Second task, referring to first")
- # Fix line 2730 - properly access content on TextMessage
- final_message_2 = result2.messages[-1]
- assert isinstance(final_message_2, TextMessage)
- assert final_message_2.content == "Second response, remembering context"
-
- # Verify context contains both interactions
- context_messages = await agent.model_context.get_messages()
- user_messages = [
- msg for msg in context_messages if hasattr(msg, "source") and getattr(msg, "source", None) == "user"
- ]
- assert len(user_messages) == 2
-
-
- class TestAssistantAgentMessageContext:
- """Test suite for message context handling in AssistantAgent.
-
- Tests various scenarios of message handling, context updates, and state management.
- """
-
- @pytest.mark.asyncio
- async def test_add_messages_to_context(self) -> None:
- """Test adding different message types to context.
-
- Verifies:
- 1. Regular messages are added correctly
- 2. Handoff messages with context are handled properly
- 3. Message order is preserved
- 4. Model messages are converted correctly
- """
- # Setup test context
- model_context = BufferedChatCompletionContext(buffer_size=10)
-
- # Create test messages
- regular_msg = TextMessage(content="Regular message", source="user")
- handoff_msg = HandoffMessage(content="Handoff message", source="agent1", target="agent2")
-
- # Add messages to context
- await AssistantAgent._add_messages_to_context(model_context=model_context, messages=[regular_msg, handoff_msg]) # type: ignore[reportPrivateUsage]
-
- # Verify context contents
- context_messages = await model_context.get_messages()
-
- # Should have: regular + handoff = 2 messages (now that handoff doesn't have context)
- assert len(context_messages) == 2
-
- # Verify message order and content - only the added messages should be present
- assert isinstance(context_messages[0], UserMessage)
- assert context_messages[0].content == "Regular message"
-
- assert isinstance(context_messages[1], UserMessage)
- assert context_messages[1].content == "Handoff message"
-
- # No more assertions needed for context_messages since we already verified both
-
- @pytest.mark.asyncio
- async def test_complex_model_context(self) -> None:
- """Test complex model context management scenarios.
-
- Verifies:
- 1. Large context handling
- 2. Mixed message type handling
- 3. Context size limits
- 4. Message filtering
- """
- # Setup test context with limited size
- model_context = BufferedChatCompletionContext(buffer_size=5)
-
- # Create a mix of message types
- messages: List[BaseChatMessage] = [
- TextMessage(content="First message", source="user"),
- StructuredMessage[StructuredOutput](
- content=StructuredOutput(content="Structured data", confidence=0.9), source="agent"
- ),
- ToolCallSummaryMessage(content="Tool result", source="agent", tool_calls=[], results=[]),
- HandoffMessage(content="Handoff", source="agent1", target="agent2"),
- ]
-
- # Add messages to context
- await AssistantAgent._add_messages_to_context(model_context=model_context, messages=messages) # type: ignore[reportPrivateUsage]
-
- # Verify context management
- context_messages = await model_context.get_messages()
-
- # Should respect buffer size limit
- assert len(context_messages) <= 5
-
- # Verify message conversion
- for msg in context_messages:
- assert isinstance(msg, (SystemMessage, UserMessage, AssistantMessage))
-
-
- class TestAnthropicIntegration:
- """Test suite for Anthropic model API integration."""
-
- def _get_anthropic_client(self) -> AnthropicChatCompletionClient:
- """Create an Anthropic client for testing."""
- api_key = os.getenv("ANTHROPIC_API_KEY")
- if not api_key:
- pytest.skip("ANTHROPIC_API_KEY not found in environment variables")
-
- return AnthropicChatCompletionClient(
- model="claude-3-haiku-20240307", # Use haiku for faster/cheaper testing
- api_key=api_key,
- temperature=0.0,
- )
-
- @pytest.mark.asyncio
- async def test_anthropic_tool_call_loop_max_iterations_10(self) -> None:
- """Test Anthropic integration with tool call loop and max_tool_iterations=10."""
- api_key = os.getenv("ANTHROPIC_API_KEY")
- if not api_key:
- pytest.skip("ANTHROPIC_API_KEY not found in environment variables")
-
- client = self._get_anthropic_client()
-
- agent = AssistantAgent(
- name="anthropic_test_agent",
- model_client=client,
- tools=[mock_tool_function],
- max_tool_iterations=10,
- )
-
- # Test with a task that might require tool calls
- result = await agent.run(
- task="Use the mock_tool_function to process the text 'hello world'. Then provide a summary."
- )
-
- # Verify that we got a result
- assert result is not None
- assert isinstance(result, TaskResult)
- assert len(result.messages) > 0
- # Check that the last message is a non-tool call.
- assert isinstance(result.messages[-1], TextMessage)
- # Check that a tool call was made
- tool_calls = [msg for msg in result.messages if isinstance(msg, ToolCallRequestEvent)]
- assert len(tool_calls) > 0
-
- # Check that usage was tracked
- usage = client.total_usage()
- assert usage.prompt_tokens > 0
- assert usage.completion_tokens > 0
-
- @pytest.mark.asyncio
- async def test_anthropic_tool_call_loop_max_iterations_1_with_reflection(self) -> None:
- """Test Anthropic integration with max_tool_iterations=1 and reflect_on_tool_use=True."""
- api_key = os.getenv("ANTHROPIC_API_KEY")
- if not api_key:
- pytest.skip("ANTHROPIC_API_KEY not found in environment variables")
-
- client = self._get_anthropic_client()
-
- agent = AssistantAgent(
- name="anthropic_reflection_agent",
- model_client=client,
- tools=[mock_tool_function],
- max_tool_iterations=1,
- reflect_on_tool_use=True,
- )
-
- # Test with a task that might require tool calls but should be limited to 1 iteration
- result = await agent.run(
- task="Use the mock_tool_function to process the text 'test input' and then explain what happened."
- )
-
- # Verify that we got a result
- assert result is not None
- assert isinstance(result, TaskResult)
- assert len(result.messages) > 0
- # Check that the last message is a reflection
- assert isinstance(result.messages[-1], TextMessage)
- # Check that a tool call was made
- tool_calls = [msg for msg in result.messages if isinstance(msg, ToolCallRequestEvent)]
- assert len(tool_calls) > 0
-
- # Check that usage was tracked
- usage = client.total_usage()
- assert usage.prompt_tokens > 0
- assert usage.completion_tokens > 0
-
- @pytest.mark.asyncio
- async def test_anthropic_basic_text_response(self) -> None:
- """Test basic Anthropic integration without tools."""
- api_key = os.getenv("ANTHROPIC_API_KEY")
- if not api_key:
- pytest.skip("ANTHROPIC_API_KEY not found in environment variables")
-
- client = self._get_anthropic_client()
-
- agent = AssistantAgent(
- name="anthropic_basic_agent",
- model_client=client,
- )
-
- # Test with a simple task that doesn't require tools
- result = await agent.run(task="What is 2 + 2? Just answer with the number.")
-
- # Verify that we got a result
- assert result is not None
- assert isinstance(result, TaskResult)
- # Check that we got a text message with content
- assert isinstance(result.messages[-1], TextMessage)
- assert "4" in result.messages[-1].content
-
- # Check that usage was tracked
- usage = client.total_usage()
- assert usage.prompt_tokens > 0
- assert usage.completion_tokens > 0
|