Python: DevUI - Internal Refactor, Conversations API support, and per… (#1235)

* Python: DevUI - Internal Refactor, Conversations API support, and performance improvements

Comprehensive refactor of DevUI package including samples relocation,
frontend reorganization, OpenAI Conversations API support, and critical
performance and code quality improvements.

Key Changes:

Architecture & Organization
- Moved DevUI samples to python/samples/getting_started/devui/
- Consolidated with other framework samples for better discoverability
- Added .env.example files and comprehensive README
- Restructured frontend components into feature-based folders (agent, workflow, gallery, layout)
- Created new OpenAI-compliant message renderers (devui should render oai responses types primarily)

New Features
- Added _conversations.py (467 lines) - Full conversation storage abstraction, replaces the /threads endpoint to better match oai conversations api
- Implements OpenAI Conversations API for thread management, Supports in-memory and extensible storage backends

API Simplification
- Use 'model' field as entity_id (agent/workflow name) instead of extra_body
- Use standard OpenAI 'conversation' field for conversation context.

Performance & Quality Improvements
- Improved context management in MessageMapper with bounded memory (~500KB max)
- Implemented hybrid LRU + cleanup approach to prevent unbounded memory growth
- General QOL improvement - Eliminated ~150 lines of dead/duplicate code, Consolidated helper functions into _utils.py, Extracted magic numbers to module-level constants, Optimized conversation item lookups with index-based approach

Testing
- Added test_conversations.py (13 tests)
- Added test_performance_fixes.py (9 tests)
- Updated existing tests for code consolidation
- 53 tests passing

Impact: 76 files changed: +4,106 insertions, -2,373 deletions
All linting and formatting checks passing. No breaking changes - backward compatible.

Migration: Samples moved to python/samples/getting_started/devui/

* readme lint fixes

* initial support for function approval and minor ui fixes
This commit is contained in:
Victor Dibia
2025-10-08 12:34:30 -07:00
committed by GitHub
Unverified
parent f5abbc67ae
commit c341ee7ed2
75 changed files with 4605 additions and 2646 deletions
+72 -15
View File
@@ -12,7 +12,7 @@ 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
from agent_framework_devui.models._openai_custom import AgentFrameworkRequest
class _DummyStartExecutor:
@@ -38,8 +38,10 @@ class _DummyWorkflow:
@pytest.fixture
def test_entities_dir():
"""Use the samples directory which has proper entity structure."""
# Get the samples directory from the main python samples folder
current_dir = Path(__file__).parent
samples_dir = current_dir.parent / "samples"
# Navigate to python/samples/getting_started/devui
samples_dir = current_dir.parent.parent.parent / "samples" / "getting_started" / "devui"
return str(samples_dir.resolve())
@@ -94,13 +96,17 @@ async def test_executor_sync_execution(executor):
assert len(agents) > 0, "No agent entities found for testing"
agent_id = agents[0].id
# Use simplified routing: model = entity_id
request = AgentFrameworkRequest(
model="agent-framework", input="test data", stream=False, extra_body=AgentFrameworkExtraBody(entity_id=agent_id)
model=agent_id, # Model IS the entity_id
input="test data",
stream=False,
)
response = await executor.execute_sync(request)
assert response.model == "agent-framework"
# With simplified routing, response.model reflects the actual agent_id
assert response.model == agent_id
assert response.object == "response"
assert len(response.output) > 0
assert response.usage.total_tokens > 0
@@ -116,11 +122,11 @@ async def test_executor_streaming_execution(executor):
assert len(agents) > 0, "No agent entities found for testing"
agent_id = agents[0].id
# Use simplified routing: model = entity_id
request = AgentFrameworkRequest(
model="agent-framework",
model=agent_id, # Model IS the entity_id
input="streaming test",
stream=True,
extra_body=AgentFrameworkExtraBody(entity_id=agent_id),
)
event_count = 0
@@ -145,16 +151,16 @@ async def test_executor_invalid_entity_id(executor):
async def test_executor_missing_entity_id(executor):
"""Test execution without entity ID."""
"""Test get_entity_id returns model field (simplified routing)."""
request = AgentFrameworkRequest(
model="agent-framework",
model="my_agent",
input="test",
stream=False,
extra_body=None, # Test case for missing entity_id
)
# With simplified routing, model field IS the entity_id
entity_id = request.get_entity_id()
assert entity_id is None
assert entity_id == "my_agent"
def test_executor_get_start_executor_message_types_uses_handlers():
@@ -171,10 +177,11 @@ def test_executor_get_start_executor_message_types_uses_handlers():
def test_executor_select_primary_input_prefers_string():
"""Select string input even when discovered after other handlers."""
executor = AgentFrameworkExecutor(EntityDiscovery(None), MessageMapper())
from agent_framework_devui._utils import select_primary_input_type
placeholder_type = type("Placeholder", (), {})
chosen = executor._select_primary_input_type([placeholder_type, str])
chosen = select_primary_input_type([placeholder_type, str])
assert chosen is str
@@ -201,6 +208,57 @@ def test_executor_parse_raw_falls_back_to_string():
assert parsed == "hi there"
async def test_executor_handles_non_streaming_agent():
"""Test executor can handle agents with only run() method (no run_stream)."""
from agent_framework import AgentRunResponse, AgentThread, ChatMessage, Role, TextContent
class NonStreamingAgent:
"""Agent with only run() method - does NOT satisfy full AgentProtocol."""
id = "non_streaming_test"
name = "Non-Streaming Test Agent"
description = "Test agent without run_stream()"
@property
def display_name(self):
return self.name
async def run(self, messages=None, *, thread=None, **kwargs):
return AgentRunResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=f"Processed: {messages}")])],
response_id="test_123",
)
def get_new_thread(self, **kwargs):
return AgentThread()
# Create executor and register agent
discovery = EntityDiscovery(None)
mapper = MessageMapper()
executor = AgentFrameworkExecutor(discovery, mapper)
agent = NonStreamingAgent()
entity_info = await discovery.create_entity_info_from_object(agent, source="test")
discovery.register_entity(entity_info.id, entity_info, agent)
# Execute non-streaming agent (use simplified routing)
request = AgentFrameworkRequest(
model=entity_info.id, # Model IS the entity_id
input="hello",
stream=True, # DevUI always streams
)
events = []
async for event in executor.execute_streaming(request):
events.append(event)
# Should get events even though agent doesn't stream
assert len(events) > 0
text_events = [e for e in events if hasattr(e, "type") and e.type == "response.output_text.delta"]
assert len(text_events) > 0
assert "Processed: hello" in text_events[0].delta
if __name__ == "__main__":
# Simple test runner
async def run_tests():
@@ -227,12 +285,11 @@ class StreamingAgent:
entities = await executor.discover_entities()
if entities:
# Test sync execution
# Test sync execution (use simplified routing)
request = AgentFrameworkRequest(
model="agent-framework",
model=entities[0].id, # Model IS the entity_id
input="test input",
stream=False,
extra_body=AgentFrameworkExtraBody(entity_id=entities[0].id),
)
await executor.execute_sync(request)