Python: Add Function Approval UI to DevUI (#1401)

* ensure function aproval is parsed correctly

* udpate ui, add deployment guide button, other debug panel fixes

* feat(devui): Implement lazy loading architecture with enhanced security and state management

Major architectural improvements to DevUI for better performance, security, and developer experience:

Performance & Architecture:
- Implement lazy loading for entity discovery - entities loaded on-demand instead of at startup
- Add hot reload capability for development workflow via new reload endpoint
- Reduce startup time and memory footprint by deferring module imports

Security Enhancements:
- Remove remote entity loading capabilities (POST /v1/entities/add, DELETE endpoints)
- DevUI now strictly local development tool - no remote code execution
- Add explicit security documentation and best practices in README

Frontend Improvements:
- Migrate to Zustand for centralized state management (replacing prop drilling)
- Add lightweight zero-dependency markdown renderer with code block copy support
- Improve gallery UX with setup instructions modal instead of direct URL loading
- Enhanced message UI with copy functionality and better token usage display

Testing & Quality:
- Expand test coverage for lazy loading, type detection, and cache invalidation
- Add comprehensive tests for new behaviors (+231 lines of test code)
- Improve type safety and documentation throughout

Breaking Changes:
- Remote entity loading via URLs is no longer supported
- Entities must be loaded from local filesystem only

* update ui issues, uupdate test descripion
This commit is contained in:
Victor Dibia
2025-10-15 14:36:29 -07:00
committed by GitHub
Unverified
parent 331c750515
commit b64358df7e
38 changed files with 3726 additions and 1814 deletions
+223 -8
View File
@@ -68,7 +68,11 @@ async def test_empty_directory():
async def test_discovery_accepts_agents_with_only_run():
"""Test that discovery accepts agents with only run() method."""
"""Test that discovery accepts agents with only run() method.
With lazy loading, entities with only __init__.py are discovered
but marked as "unknown" type until loaded.
"""
import tempfile
from pathlib import Path
@@ -110,13 +114,224 @@ agent = NonStreamingAgent()
discovery = EntityDiscovery(str(temp_path))
entities = await discovery.discover_entities()
# Should discover the non-streaming agent
agents = [e for e in entities if e.type == "agent"]
assert len(agents) == 1
# ID is auto-generated, just check it exists and starts with agent_
assert agents[0].id.startswith("agent_")
assert agents[0].name == "Non-Streaming Agent"
assert not agents[0].metadata.get("has_run_stream")
# With lazy loading, entity is discovered but type is "unknown"
# (no agent.py or workflow.py to detect type from)
assert len(entities) == 1
entity = entities[0]
assert entity.id == "non_streaming_agent"
assert entity.type == "unknown" # Type not yet determined
assert entity.tools == [] # Sparse metadata
# Trigger lazy loading to get full metadata
agent_obj = await discovery.load_entity(entity.id)
assert agent_obj is not None
# Now check enriched metadata after loading
enriched = discovery.get_entity_info(entity.id)
assert enriched.type == "agent" # Now correctly identified
assert enriched.name == "Non-Streaming Agent"
assert not enriched.metadata.get("has_run_stream")
async def test_lazy_loading():
"""Test that entities are loaded on-demand, not at discovery time."""
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create test workflow
workflow_dir = temp_path / "test_workflow"
workflow_dir.mkdir()
(workflow_dir / "workflow.py").write_text("""
from agent_framework import WorkflowBuilder, FunctionExecutor
# Create a simple workflow with a start executor
def test_func(input: str) -> str:
return f"Processed: {input}"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
""")
discovery = EntityDiscovery(str(temp_path))
# Discovery should NOT import module
entities = await discovery.discover_entities()
assert len(entities) == 1
assert entities[0].id == "test_workflow"
assert entities[0].type == "workflow" # Type detected from filename
assert entities[0].tools == [] # Sparse metadata (not loaded yet)
# Entity should NOT be in loaded_objects yet
assert discovery.get_entity_object("test_workflow") is None
# Trigger lazy load
workflow_obj = await discovery.load_entity("test_workflow")
assert workflow_obj is not None
# Now in cache
assert discovery.get_entity_object("test_workflow") is workflow_obj
# Second load is instant (from cache)
workflow_obj2 = await discovery.load_entity("test_workflow")
assert workflow_obj2 is workflow_obj # Same object
async def test_type_detection():
"""Test that entity types are detected from filenames."""
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create workflow with workflow.py
workflow_dir = temp_path / "my_workflow"
workflow_dir.mkdir()
(workflow_dir / "workflow.py").write_text("""
from agent_framework import WorkflowBuilder, FunctionExecutor
def test_func(input: str) -> str:
return f"Processed: {input}"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
""")
# Create agent with agent.py
agent_dir = temp_path / "my_agent"
agent_dir.mkdir()
(agent_dir / "agent.py").write_text("""
from agent_framework import AgentRunResponse, AgentThread, ChatMessage, Role, TextContent
class TestAgent:
name = "Test Agent"
async def run(self, messages=None, *, thread=None, **kwargs):
return AgentRunResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="test")])],
response_id="test"
)
def get_new_thread(self, **kwargs):
return AgentThread()
agent = TestAgent()
""")
# Create ambiguous entity with __init__.py only
unknown_dir = temp_path / "my_thing"
unknown_dir.mkdir()
(unknown_dir / "__init__.py").write_text("# thing")
discovery = EntityDiscovery(str(temp_path))
entities = await discovery.discover_entities()
# Check types detected correctly
by_id = {e.id: e for e in entities}
assert by_id["my_workflow"].type == "workflow"
assert by_id["my_agent"].type == "agent"
assert by_id["my_thing"].type == "unknown"
async def test_hot_reload():
"""Test that invalidate_entity() enables hot reload."""
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create workflow
workflow_dir = temp_path / "test_workflow"
workflow_dir.mkdir()
workflow_file = workflow_dir / "workflow.py"
workflow_file.write_text("""
from agent_framework import WorkflowBuilder, FunctionExecutor
def test_func(input: str) -> str:
return "v1"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
""")
discovery = EntityDiscovery(str(temp_path))
await discovery.discover_entities()
# Load entity
workflow1 = await discovery.load_entity("test_workflow")
assert workflow1 is not None
# Modify file to create a different workflow
workflow_file.write_text("""
from agent_framework import WorkflowBuilder, FunctionExecutor
def test_func(input: str) -> str:
return "v2"
def test_func2(input: str) -> str:
return "v2_extra"
builder = WorkflowBuilder()
executor1 = FunctionExecutor(id="test_executor", func=test_func)
executor2 = FunctionExecutor(id="test_executor2", func=test_func2)
builder.set_start_executor(executor1)
builder.add_edge(executor1, executor2)
workflow = builder.build()
""")
# Without invalidation, gets cached version
workflow2 = await discovery.load_entity("test_workflow")
assert workflow2 is workflow1 # Same object (cached)
# Old workflow has 1 executor
assert len(workflow2.get_executors_list()) == 1
# Invalidate cache
discovery.invalidate_entity("test_workflow")
# Now reloads from disk
workflow3 = await discovery.load_entity("test_workflow")
assert workflow3 is not workflow1 # Different object
# New workflow has 2 executors
assert len(workflow3.get_executors_list()) == 2
async def test_in_memory_entities_bypass_lazy_loading():
"""Test that in-memory entities work as before (no lazy loading needed)."""
from agent_framework import FunctionExecutor, WorkflowBuilder
# Create in-memory workflow
def test_func(input: str) -> str:
return f"Processed: {input}"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
discovery = EntityDiscovery()
# Register in-memory entity
entity_info = await discovery.create_entity_info_from_object(workflow, entity_type="workflow", source="in_memory")
discovery.register_entity(entity_info.id, entity_info, workflow)
# Should be immediately available (no lazy loading)
loaded = discovery.get_entity_object(entity_info.id)
assert loaded is workflow
# load_entity() should return immediately from cache
loaded2 = await discovery.load_entity(entity_info.id)
assert loaded2 is workflow # Same object (cache hit)
if __name__ == "__main__":
@@ -73,7 +73,11 @@ async def test_executor_entity_discovery(executor):
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"
# Entities with only an `__init__.py` file cannot have their type determined
# until the module is imported during lazy loading. This is why 'unknown' type exists.
assert entity.type in ["agent", "workflow", "unknown"], (
"Entity should have valid type (unknown allowed during discovery phase)"
)
async def test_executor_get_entity_info(executor):
@@ -84,7 +88,7 @@ async def test_executor_get_entity_info(executor):
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"]
assert entity_info.type in ["agent", "workflow", "unknown"]
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="requires OpenAI API key")