mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add DevUI to AgentFramework (#781)
* add initial backend service code for devui * add tests * add frontendcode * ui updates * update readme * ui updates and tweaks * update ui bundle * improve ui, add react flow base * add react flow ui, fix background * update ui, fix introspection bug * update readme * update ui build * add support for multimodal input - both backend and frontend * update ui build * refactor as main framework package * backend and tests refactor * ui build update * ui build update and refactor * update pyproject.toml, update uv.lock * update ui build * ui update to fit oai responses types * add backend updat and readme update * mypy and other fixes * add intial dev guide * update ui and fix workflow bug * update ui build, add thread support * type fixes * update workflow view * update uv.lock * fix workflow iport errors * lint and other fixes * mypy fixes * minor update * update ui build * refactor to use oai dependencies directly, update examples to samples, improve typing * readme update * update ui and ui build * fix workflow pyright error * update ui, fix issues with run workflow placement, miniamp menu, etc * make samples integrate serve --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
adb6dcd2af
commit
1ef24d3e91
@@ -0,0 +1,284 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Message Capture Script - Debug message flow
|
||||
- This script is intended to provide a reference for the types of events
|
||||
that are emitted by the server when agents and workflows are executed
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import http.client
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
from openai import OpenAI
|
||||
|
||||
from agent_framework_devui import DevServer
|
||||
|
||||
|
||||
def start_server() -> tuple[str, Any]:
|
||||
"""Start server with samples directory."""
|
||||
# Get samples directory
|
||||
current_dir = Path(__file__).parent
|
||||
samples_dir = current_dir.parent / "samples"
|
||||
|
||||
# Create and start server with simplified parameters
|
||||
server = DevServer(
|
||||
entities_dir=str(samples_dir.resolve()),
|
||||
host="127.0.0.1",
|
||||
port=8085, # Use different port
|
||||
ui_enabled=False,
|
||||
)
|
||||
|
||||
app = server.get_app()
|
||||
|
||||
server_config = uvicorn.Config(
|
||||
app=app,
|
||||
host="127.0.0.1",
|
||||
port=8085,
|
||||
log_level="info", # More verbose to see tracing setup
|
||||
)
|
||||
server_instance = uvicorn.Server(server_config)
|
||||
|
||||
def run_server():
|
||||
asyncio.run(server_instance.serve())
|
||||
|
||||
server_thread = threading.Thread(target=run_server, daemon=True)
|
||||
server_thread.start()
|
||||
|
||||
# Wait for server to start
|
||||
time.sleep(5) # Increased wait time
|
||||
|
||||
# Verify server is running with retries
|
||||
max_retries = 10
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
conn = http.client.HTTPConnection("127.0.0.1", 8085, timeout=5)
|
||||
try:
|
||||
conn.request("GET", "/health")
|
||||
response = conn.getresponse()
|
||||
if response.status == 200:
|
||||
break
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(2)
|
||||
else:
|
||||
raise RuntimeError(f"Server failed to start after {max_retries} attempts: {e}") from e
|
||||
|
||||
return "http://127.0.0.1:8085", server_instance
|
||||
|
||||
|
||||
def capture_agent_stream_with_tracing(client: OpenAI, agent_id: str, scenario: str = "success") -> list[dict[str, Any]]:
|
||||
"""Capture agent streaming events."""
|
||||
|
||||
try:
|
||||
stream = client.responses.create(
|
||||
model="agent-framework",
|
||||
input="Tell me about the weather in Tokyo. I want details.",
|
||||
stream=True,
|
||||
extra_body={"entity_id": agent_id},
|
||||
)
|
||||
|
||||
events = []
|
||||
for event in stream:
|
||||
# Serialize the entire event object
|
||||
try:
|
||||
event_dict = json.loads(event.model_dump_json())
|
||||
except Exception:
|
||||
# Fallback to dict conversion if model_dump_json fails
|
||||
event_dict = event.__dict__ if hasattr(event, "__dict__") else str(event)
|
||||
|
||||
events.append(event_dict)
|
||||
|
||||
# Just capture everything as-is
|
||||
if len(events) >= 200: # Increased limit
|
||||
break
|
||||
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
# Return error information as events
|
||||
error_event = {
|
||||
"type": "error",
|
||||
"scenario": scenario,
|
||||
"error_message": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
return [error_event]
|
||||
|
||||
|
||||
def capture_workflow_stream_with_tracing(
|
||||
client: OpenAI, workflow_id: str, scenario: str = "success"
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Capture workflow streaming events."""
|
||||
|
||||
try:
|
||||
stream = client.responses.create(
|
||||
model="agent-framework",
|
||||
input=(
|
||||
"Process this spam detection workflow with multiple emails: "
|
||||
"'Buy now!', 'Hello mom', 'URGENT: Click here!'"
|
||||
),
|
||||
stream=True,
|
||||
extra_body={"entity_id": workflow_id},
|
||||
)
|
||||
|
||||
events = []
|
||||
for event in stream:
|
||||
# Serialize the entire event object
|
||||
try:
|
||||
event_dict = json.loads(event.model_dump_json())
|
||||
except Exception:
|
||||
# Fallback to dict conversion if model_dump_json fails
|
||||
event_dict = event.__dict__ if hasattr(event, "__dict__") else str(event)
|
||||
|
||||
events.append(event_dict)
|
||||
|
||||
# Just capture everything as-is
|
||||
if len(events) >= 200: # Increased limit
|
||||
break
|
||||
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
# Return error information as events
|
||||
error_event = {
|
||||
"type": "error",
|
||||
"scenario": scenario,
|
||||
"error_message": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"timestamp": time.time(),
|
||||
"entity_type": "workflow",
|
||||
}
|
||||
return [error_event]
|
||||
|
||||
|
||||
def capture_agent_with_bad_config(base_url: str, agent_id: str) -> list[dict[str, Any]]:
|
||||
"""Capture agent events with intentionally bad configuration to test error handling."""
|
||||
|
||||
# Test with invalid API key
|
||||
bad_client = OpenAI(base_url=f"{base_url}/v1", api_key="invalid-api-key-123")
|
||||
|
||||
try:
|
||||
return capture_agent_stream_with_tracing(bad_client, agent_id, "bad_api_key")
|
||||
except Exception as e:
|
||||
return [
|
||||
{
|
||||
"type": "error",
|
||||
"scenario": "bad_api_key",
|
||||
"error_message": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def capture_agent_with_wrong_model(base_url: str, agent_id: str) -> list[dict[str, Any]]:
|
||||
"""Capture agent events with wrong model name to test error handling."""
|
||||
|
||||
client = OpenAI(
|
||||
base_url=f"{base_url}/v1",
|
||||
api_key="dummy-key", # Use the same key as success case
|
||||
)
|
||||
|
||||
try:
|
||||
stream = client.responses.create(
|
||||
model="gpt-4-nonexistent-model", # Wrong model name
|
||||
input="Tell me about the weather in Tokyo. I want details.",
|
||||
stream=True,
|
||||
extra_body={"entity_id": agent_id},
|
||||
)
|
||||
|
||||
events = []
|
||||
for event in stream:
|
||||
# Serialize the entire event object
|
||||
try:
|
||||
event_dict = json.loads(event.model_dump_json())
|
||||
except Exception:
|
||||
# Fallback to dict conversion if model_dump_json fails
|
||||
event_dict = event.__dict__ if hasattr(event, "__dict__") else str(event)
|
||||
|
||||
events.append(event_dict)
|
||||
|
||||
if len(events) >= 200:
|
||||
break
|
||||
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
return [
|
||||
{
|
||||
"type": "error",
|
||||
"scenario": "wrong_model",
|
||||
"error_message": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
"""Main capture script - testing both success and failure scenarios."""
|
||||
|
||||
# Setup
|
||||
output_dir = Path(__file__).parent / "captured_messages"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Start server
|
||||
base_url, server_instance = start_server()
|
||||
|
||||
try:
|
||||
# Create OpenAI client for success scenario
|
||||
client = OpenAI(base_url=f"{base_url}/v1", api_key="dummy-key")
|
||||
|
||||
# Discover entities
|
||||
conn = http.client.HTTPConnection("127.0.0.1", 8085, timeout=10)
|
||||
try:
|
||||
conn.request("GET", "/v1/entities")
|
||||
response = conn.getresponse()
|
||||
response_data = response.read().decode("utf-8")
|
||||
entities = json.loads(response_data)["entities"]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
all_results = {}
|
||||
|
||||
# Test each entity
|
||||
for entity in entities:
|
||||
entity_type = entity["type"]
|
||||
entity_id = entity["id"]
|
||||
|
||||
if entity_type == "agent":
|
||||
events = capture_agent_stream_with_tracing(client, entity_id, "success")
|
||||
elif entity_type == "workflow":
|
||||
events = capture_workflow_stream_with_tracing(client, entity_id, "success")
|
||||
else:
|
||||
continue
|
||||
|
||||
all_results[f"{entity_type}_{entity_id}"] = {"entity_info": entity, "events": events}
|
||||
# Save results
|
||||
file_path = output_dir / "entities_stream_events.json"
|
||||
with open(file_path, "w") as f:
|
||||
json.dump(
|
||||
{"timestamp": time.time(), "server_type": "DevServer", "entities_tested": all_results},
|
||||
f,
|
||||
indent=2,
|
||||
default=str,
|
||||
)
|
||||
|
||||
finally:
|
||||
# Cleanup server
|
||||
with contextlib.suppress(Exception):
|
||||
server_instance.should_exit = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Focused tests for entity discovery functionality."""
|
||||
|
||||
import asyncio
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework_devui._discovery import EntityDiscovery
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_entities_dir():
|
||||
"""Use the samples directory which has proper entity structure."""
|
||||
# Get the samples directory relative to the current test file
|
||||
current_dir = Path(__file__).parent
|
||||
samples_dir = current_dir.parent / "samples"
|
||||
return str(samples_dir.resolve())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discover_agents(test_entities_dir):
|
||||
"""Test that agent discovery works and returns valid agent entities."""
|
||||
discovery = EntityDiscovery(test_entities_dir)
|
||||
entities = await discovery.discover_entities()
|
||||
|
||||
agents = [e for e in entities if e.type == "agent"]
|
||||
|
||||
# Test that we can discover agents (not specific count)
|
||||
assert len(agents) > 0, "Should discover at least one agent"
|
||||
|
||||
# Test agent structure/properties
|
||||
for agent in agents:
|
||||
assert agent.id, "Agent should have an ID"
|
||||
assert agent.name, "Agent should have a name"
|
||||
assert agent.type == "agent", "Should be identified as agent type"
|
||||
assert hasattr(agent, "description"), "Agent should have description attribute"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discover_workflows(test_entities_dir):
|
||||
"""Test that workflow discovery works and returns valid workflow entities."""
|
||||
discovery = EntityDiscovery(test_entities_dir)
|
||||
entities = await discovery.discover_entities()
|
||||
|
||||
workflows = [e for e in entities if e.type == "workflow"]
|
||||
|
||||
# Test that we can discover workflows (not specific count)
|
||||
assert len(workflows) > 0, "Should discover at least one workflow"
|
||||
|
||||
# Test workflow structure/properties
|
||||
for workflow in workflows:
|
||||
assert workflow.id, "Workflow should have an ID"
|
||||
assert workflow.name, "Workflow should have a name"
|
||||
assert workflow.type == "workflow", "Should be identified as workflow type"
|
||||
assert hasattr(workflow, "description"), "Workflow should have description attribute"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_directory():
|
||||
"""Test discovery with empty directory."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
discovery = EntityDiscovery(temp_dir)
|
||||
entities = await discovery.discover_entities()
|
||||
|
||||
assert len(entities) == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Simple test runner
|
||||
async def run_tests():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test files
|
||||
agent_file = temp_path / "test_agent.py"
|
||||
agent_file.write_text("""
|
||||
class WeatherAgent:
|
||||
name = "Weather Agent"
|
||||
description = "Gets weather information"
|
||||
|
||||
def run_stream(self, input_str):
|
||||
return f"Weather in {input_str}"
|
||||
""")
|
||||
|
||||
workflow_file = temp_path / "test_workflow.py"
|
||||
workflow_file.write_text("""
|
||||
class DataWorkflow:
|
||||
name = "Data Processing Workflow"
|
||||
description = "Processes data"
|
||||
|
||||
def run(self, data):
|
||||
return f"Processed {data}"
|
||||
""")
|
||||
|
||||
discovery = EntityDiscovery(str(temp_path))
|
||||
await discovery.discover_entities()
|
||||
|
||||
asyncio.run(run_tests())
|
||||
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Focused tests for execution flow functionality."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework_devui._discovery import EntityDiscovery
|
||||
from agent_framework_devui._executor import AgentFrameworkExecutor, EntityNotFoundError
|
||||
from agent_framework_devui._mapper import MessageMapper
|
||||
from agent_framework_devui.models._openai_custom import AgentFrameworkExtraBody, AgentFrameworkRequest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_entities_dir():
|
||||
"""Use the samples directory which has proper entity structure."""
|
||||
current_dir = Path(__file__).parent
|
||||
samples_dir = current_dir.parent / "samples"
|
||||
return str(samples_dir.resolve())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def executor(test_entities_dir):
|
||||
"""Create configured executor."""
|
||||
discovery = EntityDiscovery(test_entities_dir)
|
||||
mapper = MessageMapper()
|
||||
executor = AgentFrameworkExecutor(discovery, mapper)
|
||||
|
||||
# Discover entities
|
||||
await executor.discover_entities()
|
||||
|
||||
return executor
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_entity_discovery(executor):
|
||||
"""Test executor entity discovery."""
|
||||
entities = await executor.discover_entities()
|
||||
|
||||
# Should find entities from samples directory
|
||||
assert len(entities) > 0, "Should discover at least one entity"
|
||||
|
||||
entity_types = [e.type for e in entities]
|
||||
assert "agent" in entity_types, "Should find at least one agent"
|
||||
assert "workflow" in entity_types, "Should find at least one workflow"
|
||||
|
||||
# Test entity structure
|
||||
for entity in entities:
|
||||
assert entity.id, "Entity should have an ID"
|
||||
assert entity.name, "Entity should have a name"
|
||||
assert entity.type in ["agent", "workflow"], "Entity should have valid type"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_get_entity_info(executor):
|
||||
"""Test getting entity info by ID."""
|
||||
entities = await executor.discover_entities()
|
||||
entity_id = entities[0].id
|
||||
|
||||
entity_info = executor.get_entity_info(entity_id)
|
||||
assert entity_info is not None
|
||||
assert entity_info.id == entity_id
|
||||
assert entity_info.type in ["agent", "workflow"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="requires OpenAI API key")
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_sync_execution(executor):
|
||||
"""Test synchronous execution."""
|
||||
entities = await executor.discover_entities()
|
||||
# Find an agent entity to test with
|
||||
agents = [e for e in entities if e.type == "agent"]
|
||||
assert len(agents) > 0, "No agent entities found for testing"
|
||||
agent_id = agents[0].id
|
||||
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework", input="test data", stream=False, extra_body=AgentFrameworkExtraBody(entity_id=agent_id)
|
||||
)
|
||||
|
||||
response = await executor.execute_sync(request)
|
||||
|
||||
assert response.model == "agent-framework"
|
||||
assert response.object == "response"
|
||||
assert len(response.output) > 0
|
||||
assert response.usage.total_tokens > 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="requires OpenAI API key")
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_streaming_execution(executor):
|
||||
"""Test streaming execution."""
|
||||
entities = await executor.discover_entities()
|
||||
# Find an agent entity to test with
|
||||
agents = [e for e in entities if e.type == "agent"]
|
||||
assert len(agents) > 0, "No agent entities found for testing"
|
||||
agent_id = agents[0].id
|
||||
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework",
|
||||
input="streaming test",
|
||||
stream=True,
|
||||
extra_body=AgentFrameworkExtraBody(entity_id=agent_id),
|
||||
)
|
||||
|
||||
event_count = 0
|
||||
text_events = []
|
||||
|
||||
async for event in executor.execute_streaming(request):
|
||||
event_count += 1
|
||||
if hasattr(event, "type") and event.type == "response.output_text.delta":
|
||||
text_events.append(event.delta)
|
||||
|
||||
if event_count > 10: # Limit for testing
|
||||
break
|
||||
|
||||
assert event_count > 0
|
||||
assert len(text_events) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_invalid_entity_id(executor):
|
||||
"""Test execution with invalid entity ID."""
|
||||
with pytest.raises(EntityNotFoundError):
|
||||
executor.get_entity_info("nonexistent_agent")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executor_missing_entity_id(executor):
|
||||
"""Test execution without entity ID."""
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework",
|
||||
input="test",
|
||||
stream=False,
|
||||
extra_body=None, # Test case for missing entity_id
|
||||
)
|
||||
|
||||
entity_id = request.get_entity_id()
|
||||
assert entity_id is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Simple test runner
|
||||
async def run_tests():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test agent
|
||||
agent_file = temp_path / "streaming_agent.py"
|
||||
agent_file.write_text("""
|
||||
class StreamingAgent:
|
||||
name = "Streaming Test Agent"
|
||||
description = "Test agent for streaming"
|
||||
|
||||
async def run_stream(self, input_str):
|
||||
for i, word in enumerate(f"Processing {input_str}".split()):
|
||||
yield f"word_{i}: {word} "
|
||||
""")
|
||||
|
||||
discovery = EntityDiscovery(str(temp_path))
|
||||
mapper = MessageMapper()
|
||||
executor = AgentFrameworkExecutor(discovery, mapper)
|
||||
|
||||
# Test discovery
|
||||
entities = await executor.discover_entities()
|
||||
|
||||
if entities:
|
||||
# Test sync execution
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework",
|
||||
input="test input",
|
||||
stream=False,
|
||||
extra_body=AgentFrameworkExtraBody(entity_id=entities[0].id),
|
||||
)
|
||||
|
||||
await executor.execute_sync(request)
|
||||
|
||||
# Test streaming execution
|
||||
request.stream = True
|
||||
event_count = 0
|
||||
async for _event in executor.execute_streaming(request):
|
||||
event_count += 1
|
||||
if event_count > 5: # Limit for testing
|
||||
break
|
||||
|
||||
asyncio.run(run_tests())
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Clean focused tests for message mapping functionality."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
# Add the main agent_framework package for real types
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "main"))
|
||||
|
||||
# Import Agent Framework types (assuming they are always available)
|
||||
from agent_framework._types import AgentRunResponseUpdate, ErrorContent, FunctionCallContent, Role, TextContent
|
||||
|
||||
from agent_framework_devui._mapper import MessageMapper
|
||||
from agent_framework_devui.models._openai_custom import AgentFrameworkExtraBody, AgentFrameworkRequest
|
||||
|
||||
|
||||
def create_test_content(content_type: str, **kwargs: Any) -> Any:
|
||||
"""Create test content objects."""
|
||||
if content_type == "text":
|
||||
return TextContent(text=kwargs.get("text", "Hello, world!"))
|
||||
if content_type == "function_call":
|
||||
return FunctionCallContent(
|
||||
call_id=kwargs.get("call_id", "test_call_id"),
|
||||
name=kwargs.get("name", "test_func"),
|
||||
arguments=kwargs.get("arguments", {"param": "value"}),
|
||||
)
|
||||
if content_type == "error":
|
||||
return ErrorContent(message=kwargs.get("message", "Test error"), error_code=kwargs.get("code", "test_error"))
|
||||
raise ValueError(f"Unknown content type: {content_type}")
|
||||
|
||||
|
||||
def create_test_agent_update(contents: list[Any]) -> Any:
|
||||
"""Create test AgentRunResponseUpdate - NO fake attributes!"""
|
||||
return AgentRunResponseUpdate(
|
||||
contents=contents, role=Role.ASSISTANT, message_id="test_msg", response_id="test_resp"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mapper() -> MessageMapper:
|
||||
return MessageMapper()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_request() -> AgentFrameworkRequest:
|
||||
return AgentFrameworkRequest(
|
||||
model="agent-framework",
|
||||
input="Test input",
|
||||
stream=True,
|
||||
extra_body=AgentFrameworkExtraBody(entity_id="test_agent"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_critical_isinstance_bug_detection(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""CRITICAL: Test that would have caught the isinstance vs hasattr bug."""
|
||||
|
||||
content = create_test_content("text", text="Bug detection test")
|
||||
update = create_test_agent_update([content])
|
||||
|
||||
# Key assertions that would have caught the bug
|
||||
assert hasattr(update, "contents") # Real attribute ✅
|
||||
assert not hasattr(update, "response") # Fake attribute should not exist ✅
|
||||
|
||||
# Test isinstance works with real types
|
||||
assert isinstance(update, AgentRunResponseUpdate)
|
||||
|
||||
# Test mapper conversion - should NOT produce "Unknown event"
|
||||
events = await mapper.convert_event(update, test_request)
|
||||
|
||||
assert len(events) > 0
|
||||
assert all(hasattr(event, "type") for event in events)
|
||||
# Should never get unknown events with proper types
|
||||
assert all(event.type != "unknown" for event in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_text_content_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test TextContent mapping."""
|
||||
content = create_test_content("text", text="Hello, clean test!")
|
||||
update = create_test_agent_update([content])
|
||||
|
||||
events = await mapper.convert_event(update, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.output_text.delta"
|
||||
assert events[0].delta == "Hello, clean test!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_function_call_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test FunctionCallContent mapping."""
|
||||
content = create_test_content("function_call", name="test_func", arguments={"location": "TestCity"})
|
||||
update = create_test_agent_update([content])
|
||||
|
||||
events = await mapper.convert_event(update, test_request)
|
||||
|
||||
assert len(events) >= 1
|
||||
assert all(event.type == "response.function_call_arguments.delta" for event in events)
|
||||
|
||||
# Check JSON is chunked
|
||||
full_json = "".join(event.delta for event in events)
|
||||
assert "TestCity" in full_json
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_content_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ErrorContent mapping."""
|
||||
content = create_test_content("error", message="Test error", code="test_code")
|
||||
update = create_test_agent_update([content])
|
||||
|
||||
events = await mapper.convert_event(update, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "error"
|
||||
assert events[0].message == "Test error"
|
||||
assert events[0].code == "test_code"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mixed_content_types(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test multiple content types together."""
|
||||
contents = [
|
||||
create_test_content("text", text="Starting..."),
|
||||
create_test_content("function_call", name="process", arguments={"data": "test"}),
|
||||
create_test_content("text", text="Done!"),
|
||||
]
|
||||
update = create_test_agent_update(contents)
|
||||
|
||||
events = await mapper.convert_event(update, test_request)
|
||||
|
||||
assert len(events) >= 3
|
||||
|
||||
# Should have both types of events
|
||||
event_types = {event.type for event in events}
|
||||
assert "response.output_text.delta" in event_types
|
||||
assert "response.function_call_arguments.delta" in event_types
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_content_fallback(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test graceful handling of unknown content types."""
|
||||
# Test the fallback path directly since we can't create invalid AgentRunResponseUpdate
|
||||
# due to Pydantic validation. Instead, test the content mapper's unknown content handling.
|
||||
|
||||
class MockUnknownContent:
|
||||
def __init__(self):
|
||||
self.__class__.__name__ = "WeirdUnknownContent" # Not in content_mappers
|
||||
|
||||
# Test the content mapper directly
|
||||
context = mapper._get_or_create_context(test_request)
|
||||
unknown_content = MockUnknownContent()
|
||||
|
||||
# This should trigger the unknown content fallback in _convert_agent_update
|
||||
event = await mapper._create_unknown_content_event(unknown_content, context)
|
||||
|
||||
assert event.type == "response.output_text.delta"
|
||||
assert "Unknown content type" in event.delta
|
||||
assert "WeirdUnknownContent" in event.delta
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Simple test runner
|
||||
async def run_all_tests() -> None:
|
||||
mapper = MessageMapper()
|
||||
test_request = AgentFrameworkRequest(
|
||||
model="agent-framework", input="Test", stream=True, extra_body=AgentFrameworkExtraBody(entity_id="test")
|
||||
)
|
||||
|
||||
tests = [
|
||||
("Critical isinstance bug detection", test_critical_isinstance_bug_detection),
|
||||
("Text content mapping", test_text_content_mapping),
|
||||
("Function call mapping", test_function_call_mapping),
|
||||
("Error content mapping", test_error_content_mapping),
|
||||
("Mixed content types", test_mixed_content_types),
|
||||
("Unknown content fallback", test_unknown_content_fallback),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
for _test_name, test_func in tests:
|
||||
try:
|
||||
await test_func(mapper, test_request)
|
||||
passed += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
asyncio.run(run_all_tests())
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Focused tests for server functionality."""
|
||||
|
||||
import asyncio
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework_devui import DevServer
|
||||
from agent_framework_devui.models._openai_custom import AgentFrameworkExtraBody, AgentFrameworkRequest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_entities_dir():
|
||||
"""Use the samples directory which has proper entity structure."""
|
||||
current_dir = Path(__file__).parent
|
||||
samples_dir = current_dir.parent / "samples"
|
||||
return str(samples_dir.resolve())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_health_endpoint(test_entities_dir):
|
||||
"""Test /health endpoint."""
|
||||
server = DevServer(entities_dir=test_entities_dir)
|
||||
executor = await server._ensure_executor()
|
||||
|
||||
# Test entity count
|
||||
entities = await executor.discover_entities()
|
||||
assert len(entities) > 0
|
||||
# Framework name is now hardcoded since we simplified to single framework
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_entities_endpoint(test_entities_dir):
|
||||
"""Test /v1/entities endpoint."""
|
||||
server = DevServer(entities_dir=test_entities_dir)
|
||||
executor = await server._ensure_executor()
|
||||
|
||||
entities = await executor.discover_entities()
|
||||
assert len(entities) >= 1
|
||||
# Should find at least the weather agent
|
||||
agent_entities = [e for e in entities if e.type == "agent"]
|
||||
assert len(agent_entities) >= 1
|
||||
agent_names = [e.name for e in agent_entities]
|
||||
assert "WeatherAgent" in agent_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_execution_sync(test_entities_dir):
|
||||
"""Test sync execution endpoint."""
|
||||
server = DevServer(entities_dir=test_entities_dir)
|
||||
executor = await server._ensure_executor()
|
||||
|
||||
entities = await executor.discover_entities()
|
||||
agent_id = entities[0].id
|
||||
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework",
|
||||
input="San Francisco",
|
||||
stream=False,
|
||||
extra_body=AgentFrameworkExtraBody(entity_id=agent_id),
|
||||
)
|
||||
|
||||
response = await executor.execute_sync(request)
|
||||
assert response.model == "agent-framework"
|
||||
assert len(response.output) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_execution_streaming(test_entities_dir):
|
||||
"""Test streaming execution endpoint."""
|
||||
server = DevServer(entities_dir=test_entities_dir)
|
||||
executor = await server._ensure_executor()
|
||||
|
||||
entities = await executor.discover_entities()
|
||||
agent_id = entities[0].id
|
||||
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework", input="New York", stream=True, extra_body=AgentFrameworkExtraBody(entity_id=agent_id)
|
||||
)
|
||||
|
||||
event_count = 0
|
||||
async for _event in executor.execute_streaming(request):
|
||||
event_count += 1
|
||||
if event_count > 5: # Limit for testing
|
||||
break
|
||||
|
||||
assert event_count > 0
|
||||
|
||||
|
||||
def test_configuration():
|
||||
"""Test basic configuration."""
|
||||
server = DevServer(entities_dir="test", port=9000, host="localhost")
|
||||
assert server.port == 9000
|
||||
assert server.host == "localhost"
|
||||
assert server.entities_dir == "test"
|
||||
assert server.cors_origins == ["*"]
|
||||
assert server.ui_enabled
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Simple test runner
|
||||
async def run_tests():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Create test agent
|
||||
agent_file = temp_path / "weather_agent.py"
|
||||
agent_file.write_text("""
|
||||
class WeatherAgent:
|
||||
name = "Weather Agent"
|
||||
description = "Gets weather information"
|
||||
|
||||
def run_stream(self, input_str):
|
||||
return f"Weather in {input_str} is sunny"
|
||||
""")
|
||||
|
||||
server = DevServer(entities_dir=str(temp_path))
|
||||
executor = await server._ensure_executor()
|
||||
|
||||
entities = await executor.discover_entities()
|
||||
|
||||
if entities:
|
||||
request = AgentFrameworkRequest(
|
||||
model="agent-framework",
|
||||
input="test location",
|
||||
stream=False,
|
||||
extra_body=AgentFrameworkExtraBody(entity_id=entities[0].id),
|
||||
)
|
||||
|
||||
await executor.execute_sync(request)
|
||||
|
||||
asyncio.run(run_tests())
|
||||
Reference in New Issue
Block a user