Skip to content
59 changes: 34 additions & 25 deletions src/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
fastapi
uvicorn
autogen-agentchat==0.7.5
azure-cosmos
azure-monitor-opentelemetry
azure-monitor-events-extension
azure-identity
python-dotenv
python-multipart
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-grpc
opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-openai
opentelemetry-exporter-otlp-proto-http
fastapi==0.116.1
uvicorn==0.35.0
azure-cosmos==4.9.0
azure-monitor-opentelemetry==1.8.5
azure-monitor-events-extension==0.1.0
azure-identity==1.24.0
python-dotenv==1.1.1
python-multipart==0.0.22
opentelemetry-api==1.39.0
opentelemetry-sdk==1.39.0
opentelemetry-exporter-otlp-proto-grpc==1.39.0
opentelemetry-exporter-otlp-proto-http==1.39.0
opentelemetry-instrumentation-fastapi==0.60b0
opentelemetry-instrumentation-openai==0.46.2

semantic-kernel[azure]==1.39.4
azure-ai-projects==1.0.0
openai==1.105.0
azure-ai-inference==1.0.0b9
azure-search-documents
azure-ai-evaluation
azure-ai-projects==2.0.0
openai==2.16.0
azure-ai-inference==1.0.0b9
azure-search-documents==11.5.3
azure-ai-evaluation==1.11.0
azure-core==1.38.0

opentelemetry-exporter-otlp-proto-grpc
agent-framework-azure-ai==1.0.0rc4
agent-framework-core==1.0.0rc4
agent-framework-orchestrations==1.0.0b260311

# Date and internationalization
babel>=2.9.0
mcp==1.26.0
werkzeug==3.1.5
pylint-pydantic==0.3.5
pexpect==4.9.0
urllib3==2.6.3
protobuf==5.29.6
cryptography==46.0.5
aiohttp==3.13.3
pyasn1==0.6.2
nltk==3.9.3

# Testing tools
pytest>=8.2,<9 # Compatible version for pytest-asyncio
pytest==8.4.1
pytest-asyncio==0.24.0
pytest-cov==5.0.0

47 changes: 43 additions & 4 deletions src/tests/backend/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,26 @@ def _setup_agent_framework_mock():
# Names used as base classes or in Union type hints MUST be real classes
# to avoid SyntaxError from typing module's forward reference evaluation.
_class_names = [
'AgentResponse', 'AgentResponseUpdate', 'AgentRunUpdateEvent',
'AgentThread', 'BaseAgent', 'ChatAgent', 'ChatMessage',
'Agent', 'AgentResponse', 'AgentResponseUpdate', 'AgentRunUpdateEvent',
'AgentSession', 'AgentThread', 'BaseAgent', 'ChatAgent', 'ChatMessage',
'ChatOptions', 'Content', 'ExecutorCompletedEvent',
'GroupChatRequestSentEvent', 'GroupChatResponseReceivedEvent',
'HostedCodeInterpreterTool', 'HostedMCPTool',
'InMemoryCheckpointStorage', 'MCPStreamableHTTPTool',
'MagenticBuilder', 'MagenticOrchestratorEvent',
'MagenticProgressLedger', 'Role', 'UsageDetails',
'MagenticProgressLedger', 'Message', 'Role', 'UsageDetails',
'WorkflowOutputEvent',
]
for name in _class_names:
setattr(mock_af, name, type(name, (), {}))
setattr(mock_af, name, type(name, (), {
'__init__': lambda self, *args, **kwargs: None,
}))

# Sub-module: agent_framework._types
mock_af_types = ModuleType('agent_framework._types')
mock_af_types.ResponseStream = type('ResponseStream', (), {})
mock_af._types = mock_af_types
sys.modules['agent_framework._types'] = mock_af_types

# Sub-module: agent_framework.azure
mock_af_azure = ModuleType('agent_framework.azure')
Expand Down Expand Up @@ -91,6 +99,37 @@ def _setup_agent_framework_mock():
sys.modules['agent_framework._workflows'] = mock_af_workflows
sys.modules['agent_framework._workflows._magentic'] = mock_af_magentic

if 'agent_framework_orchestrations' not in sys.modules:
mock_af_orch = ModuleType('agent_framework_orchestrations')
mock_af_orch.MagenticBuilder = type('MagenticBuilder', (), {
'__init__': lambda self, *args, **kwargs: None,
'build': lambda self: Mock(),
})
sys.modules['agent_framework_orchestrations'] = mock_af_orch

mock_af_orch_base = ModuleType('agent_framework_orchestrations._base_group_chat_orchestrator')
for name in ['GroupChatRequestSentEvent', 'GroupChatResponseReceivedEvent']:
setattr(mock_af_orch_base, name, type(name, (), {}))
sys.modules['agent_framework_orchestrations._base_group_chat_orchestrator'] = mock_af_orch_base

mock_af_orch_mag = ModuleType('agent_framework_orchestrations._magentic')
for name in ['MagenticContext', 'MagenticProgressLedger']:
setattr(mock_af_orch_mag, name, type(name, (), {}))
# StandardMagenticManager needs a proper __init__ that accepts args/kwargs
# because HumanApprovalMagenticManager calls super().__init__(agent, *args, **kwargs)
setattr(mock_af_orch_mag, 'StandardMagenticManager',
type('StandardMagenticManager', (), {
'__init__': lambda self, *args, **kwargs: None
}))
for name in [
'ORCHESTRATOR_FINAL_ANSWER_PROMPT',
'ORCHESTRATOR_PROGRESS_LEDGER_PROMPT',
'ORCHESTRATOR_TASK_LEDGER_PLAN_PROMPT',
'ORCHESTRATOR_TASK_LEDGER_PLAN_UPDATE_PROMPT',
]:
setattr(mock_af_orch_mag, name, 'mock_prompt_string')
sys.modules['agent_framework_orchestrations._magentic'] = mock_af_orch_mag

if 'agent_framework_azure_ai' not in sys.modules:
mock_af_ai = ModuleType('agent_framework_azure_ai')
mock_af_ai.AzureAIClient = type('AzureAIClient', (), {})
Expand Down
22 changes: 15 additions & 7 deletions src/tests/backend/v4/callbacks/test_response_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,19 @@ def __init__(self):
self.author_name = "TestAgent"
self.role = "assistant"

class MockMessage:
"""Mock Message class for isinstance checks."""
def __init__(self, text="", role="assistant", author_name=""):
self.text = text
self.author_name = author_name
self.role = role

mock_chat_message = MockChatMessage
mock_agent_response_update = Mock()
mock_agent_response_update.text = "Sample update text"
mock_agent_response_update.contents = []

sys.modules['agent_framework'] = Mock(ChatMessage=mock_chat_message)
sys.modules['agent_framework'] = Mock(ChatMessage=mock_chat_message, Message=MockMessage)
sys.modules['agent_framework._workflows'] = Mock()
sys.modules['agent_framework._workflows._magentic'] = Mock(AgentRunResponseUpdate=mock_agent_response_update)
sys.modules['agent_framework.azure'] = Mock(AzureOpenAIChatClient=Mock())
Expand Down Expand Up @@ -388,14 +395,15 @@ def test_agent_response_callback_no_user_id(self):
@patch('backend.v4.callbacks.response_handlers.asyncio.create_task')
@patch('backend.v4.callbacks.response_handlers.time.time')
def test_agent_response_callback_with_chat_message(self, mock_time, mock_create_task):
"""Test agent_response_callback with ChatMessage object."""
"""Test agent_response_callback with Message object."""
mock_time.return_value = 1234567890.0

# Create an instance of our MockChatMessage
mock_message = MockChatMessage()
mock_message.text = "Test message with citations [1:2|source]"
mock_message.author_name = "TestAgent"
mock_message.role = "assistant"
# Create an instance of our MockMessage (source checks isinstance(message, Message))
mock_message = MockMessage(
text="Test message with citations [1:2|source]",
author_name="TestAgent",
role="assistant",
)

with patch('backend.v4.callbacks.response_handlers.AgentMessage') as mock_agent_message:
mock_agent_msg = Mock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
sys.modules['azure.identity'] = Mock()
sys.modules['azure.identity.aio'] = Mock()
sys.modules['common'] = Mock()
sys.modules['common.config'] = Mock()
sys.modules['common.config.app_config'] = Mock(config=Mock())
sys.modules['common.database'] = Mock()
sys.modules['common.database.database_base'] = Mock()
sys.modules['common.models'] = Mock()
Expand Down Expand Up @@ -327,7 +329,7 @@ def test_get_chat_client_with_existing_client(self):
base = MCPEnabledBase()
mock_agent = Mock()
mock_chat_client = Mock()
mock_agent.chat_client = mock_chat_client
mock_agent.client = mock_chat_client
base._agent = mock_agent

result = base.get_chat_client()
Expand All @@ -339,7 +341,7 @@ def test_get_chat_client_from_agent(self):
base = MCPEnabledBase()
mock_agent = Mock()
mock_chat_client = Mock()
mock_agent.chat_client = mock_chat_client
mock_agent.client = mock_chat_client
base._agent = mock_agent

result = base.get_chat_client()
Expand Down
68 changes: 30 additions & 38 deletions src/tests/backend/v4/magentic_agents/test_foundry_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@
sys.modules['azure.core'] = Mock()
sys.modules['azure.core.exceptions'] = Mock()
sys.modules['azure.identity'] = Mock()
sys.modules['azure.identity.aio'] = Mock()
sys.modules['azure.cosmos'] = Mock(CosmosClient=Mock)
sys.modules['agent_framework'] = Mock(ChatAgent=Mock, ChatMessage=Mock, HostedCodeInterpreterTool=Mock, Role=Mock)
sys.modules['agent_framework'] = Mock(Agent=Mock, Message=Mock, ChatOptions=Mock, ChatMessage=Mock, Role=Mock)
sys.modules['agent_framework_azure_ai'] = Mock(AzureAIClient=Mock)

# Mock additional Azure modules that may be needed
Expand Down Expand Up @@ -303,17 +304,13 @@ def test_is_azure_search_requested_no_index_name(self, mock_get_logger, mock_con
assert result is False

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.HostedCodeInterpreterTool')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
async def test_collect_tools_with_code_interpreter(self, mock_get_logger, mock_config, mock_code_tool_class):
"""Test _collect_tools with code interpreter enabled."""
async def test_collect_tools_with_code_interpreter(self, mock_get_logger, mock_config):
"""Test _collect_tools with code interpreter enabled - now handled server-side."""
mock_logger = Mock()
mock_get_logger.return_value = mock_logger

mock_code_tool = Mock()
mock_code_tool_class.return_value = mock_code_tool

agent = FoundryAgentTemplate(
agent_name="TestAgent",
agent_description="Test Description",
Expand All @@ -329,23 +326,19 @@ async def test_collect_tools_with_code_interpreter(self, mock_get_logger, mock_c

tools = await agent._collect_tools()

assert len(tools) == 1
assert tools[0] == mock_code_tool
mock_code_tool_class.assert_called_once()
mock_logger.info.assert_any_call("Added Code Interpreter tool.")
mock_logger.info.assert_any_call("Total tools collected (MCP path): %d", 1)
# HostedCodeInterpreterTool was removed in rc4; code interpreter is now server-side
assert len(tools) == 0
mock_logger.info.assert_any_call("Code Interpreter requested \u2014 handled server-side by AzureAIClient.")
mock_logger.info.assert_any_call("Total tools collected (MCP path): %d", 0)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.HostedCodeInterpreterTool')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
async def test_collect_tools_code_interpreter_exception(self, mock_get_logger, mock_config, mock_code_tool_class):
"""Test _collect_tools when code interpreter creation fails."""
async def test_collect_tools_code_interpreter_server_side(self, mock_get_logger, mock_config):
"""Test _collect_tools when code interpreter is enabled - handled server-side in rc4."""
mock_logger = Mock()
mock_get_logger.return_value = mock_logger

mock_code_tool_class.side_effect = Exception("Code interpreter failed")

agent = FoundryAgentTemplate(
agent_name="TestAgent",
agent_description="Test Description",
Expand All @@ -361,8 +354,9 @@ async def test_collect_tools_code_interpreter_exception(self, mock_get_logger, m

tools = await agent._collect_tools()

# No tools created locally; code interpreter is handled server-side
assert len(tools) == 0
mock_logger.error.assert_called_with("Code Interpreter tool creation failed: %s", mock_code_tool_class.side_effect)
mock_logger.info.assert_any_call("Code Interpreter requested \u2014 handled server-side by AzureAIClient.")

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.config')
Expand Down Expand Up @@ -639,7 +633,7 @@ class SimpleCreds:
# Verify error was logged (removed specific assertion due to mock corruption issues)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.ChatAgent')
@patch('backend.v4.magentic_agents.foundry_agent.Agent')
@patch('backend.v4.magentic_agents.foundry_agent.agent_registry')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
Expand Down Expand Up @@ -676,11 +670,11 @@ async def test_after_open_reasoning_mode_azure_search(self, mock_get_logger, moc
"TestAgent",
"test-index"
)
mock_logger.info.assert_any_call("Initialized ChatAgent '%s'", "TestAgent")
mock_logger.info.assert_any_call("Initialized Agent '%s'", "TestAgent")
mock_registry.register_agent.assert_called_once_with(agent)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.ChatAgent')
@patch('backend.v4.magentic_agents.foundry_agent.Agent')
@patch('backend.v4.magentic_agents.foundry_agent.agent_registry')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
Expand Down Expand Up @@ -712,11 +706,11 @@ async def test_after_open_foundry_mode_mcp(self, mock_get_logger, mock_config, m

mock_logger.info.assert_any_call("Initializing agent in Foundry mode.")
mock_logger.info.assert_any_call("Initializing agent in MCP mode.")
mock_logger.info.assert_any_call("Initialized ChatAgent '%s'", "TestAgent")
mock_logger.info.assert_any_call("Initialized Agent '%s'", "TestAgent")
mock_registry.register_agent.assert_called_once_with(agent)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.ChatAgent')
@patch('backend.v4.magentic_agents.foundry_agent.Agent')
@patch('backend.v4.magentic_agents.foundry_agent.agent_registry')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
Expand Down Expand Up @@ -745,16 +739,16 @@ async def test_after_open_azure_search_setup_failure(self, mock_get_logger, mock
assert "Azure AI Search mode requested but setup failed." in str(exc_info.value)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.ChatAgent')
@patch('backend.v4.magentic_agents.foundry_agent.Agent')
@patch('backend.v4.magentic_agents.foundry_agent.agent_registry')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
async def test_after_open_chat_agent_creation_error(self, mock_get_logger, mock_config, mock_registry, mock_chat_agent_class):
"""Test _after_open when ChatAgent creation fails."""
"""Test _after_open when Agent creation fails."""
mock_logger = Mock()
mock_get_logger.return_value = mock_logger

mock_chat_agent_class.side_effect = Exception("ChatAgent creation failed")
mock_chat_agent_class.side_effect = Exception("Agent creation failed")

agent = FoundryAgentTemplate(
agent_name="TestAgent",
Expand All @@ -774,11 +768,11 @@ async def test_after_open_chat_agent_creation_error(self, mock_get_logger, mock_
with pytest.raises(Exception) as exc_info:
await agent._after_open()

assert "ChatAgent creation failed" in str(exc_info.value)
mock_logger.error.assert_called_with("Failed to initialize ChatAgent: %s", mock_chat_agent_class.side_effect)
assert "Agent creation failed" in str(exc_info.value)
mock_logger.error.assert_called_with("Failed to initialize Agent: %s", mock_chat_agent_class.side_effect)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.ChatAgent')
@patch('backend.v4.magentic_agents.foundry_agent.Agent')
@patch('backend.v4.magentic_agents.foundry_agent.agent_registry')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
Expand Down Expand Up @@ -817,11 +811,10 @@ async def test_after_open_registry_failure(self, mock_get_logger, mock_config, m
)

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.ChatMessage')
@patch('backend.v4.magentic_agents.foundry_agent.Role')
@patch('backend.v4.magentic_agents.foundry_agent.Message')
@patch('backend.v4.magentic_agents.foundry_agent.config')
@patch('backend.v4.magentic_agents.foundry_agent.logging.getLogger')
async def test_invoke_success(self, mock_get_logger, mock_config, mock_role, mock_chat_message_class):
async def test_invoke_success(self, mock_get_logger, mock_config, mock_message_class):
"""Test invoke method successfully streams responses."""
mock_logger = Mock()
mock_get_logger.return_value = mock_logger
Expand All @@ -830,15 +823,14 @@ async def test_invoke_success(self, mock_get_logger, mock_config, mock_role, moc
mock_update1 = Mock()
mock_update2 = Mock()

# Mock run_stream to return an async iterator
async def mock_run_stream(messages):
# Mock run to return an async iterator (source uses self._agent.run, not run_stream)
async def mock_run(messages, stream=True):
yield mock_update1
yield mock_update2
mock_agent.run_stream = mock_run_stream
mock_agent.run = mock_run

mock_message = Mock()
mock_chat_message_class.return_value = mock_message
mock_role.USER = "user"
mock_message_class.return_value = mock_message

agent = FoundryAgentTemplate(
agent_name="TestAgent",
Expand All @@ -857,7 +849,7 @@ async def mock_run_stream(messages):
updates.append(update)

assert updates == [mock_update1, mock_update2]
mock_chat_message_class.assert_called_once_with(role=mock_role.USER, text="Test prompt")
mock_message_class.assert_called_once_with(role="user", text="Test prompt")

@pytest.mark.asyncio
@patch('backend.v4.magentic_agents.foundry_agent.config')
Expand Down
Loading
Loading