diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index f1ca1c6a62..3ce0bbe41e 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -781,6 +781,27 @@ def _extract_workflow_hil_responses(self, input_data: Any) -> dict[str, Any] | N Returns: Dict of {request_id: response_value} if found, None otherwise """ + # Handle case where input_data might be a JSON string (from streamWorkflowExecutionOpenAI) + # The input field type is: str | list[Any] | dict[str, Any] + if isinstance(input_data, str): + try: + parsed = json.loads(input_data) + # Only use parsed value if it's a list (ResponseInputParam format expected for HIL) + if isinstance(parsed, list): + input_data = parsed + else: + # Parsed to dict, string, or primitive - not HIL response format + return None + except (json.JSONDecodeError, TypeError): + # Plain text string, not valid JSON - not HIL format + return None + + # At this point, input_data should be a list or dict + # HIL responses are always in list format (ResponseInputParam) + if isinstance(input_data, dict): + # This is structured workflow input (dict), not HIL responses + return None + if not isinstance(input_data, list): return None diff --git a/python/packages/devui/tests/test_execution.py b/python/packages/devui/tests/test_execution.py index 83ff966b0e..bdcf2288ae 100644 --- a/python/packages/devui/tests/test_execution.py +++ b/python/packages/devui/tests/test_execution.py @@ -261,6 +261,27 @@ class WorkflowInput(BaseModel): assert parsed.metadata == {"key": "value"} +def test_extract_workflow_hil_responses_handles_stringified_json(): + """Test HIL response extraction handles both stringified and parsed JSON (regression test).""" + from agent_framework_devui._discovery import EntityDiscovery + from agent_framework_devui._executor import AgentFrameworkExecutor + from agent_framework_devui._mapper import MessageMapper + + executor = AgentFrameworkExecutor(EntityDiscovery(None), MessageMapper()) + + # Regression test: Frontend sends stringified JSON via streamWorkflowExecutionOpenAI + stringified = '[{"type":"message","content":[{"type":"workflow_hil_response","responses":{"req_1":"spam"}}]}]' + assert executor._extract_workflow_hil_responses(stringified) == {"req_1": "spam"} + + # Ensure parsed format still works + parsed = [{"type": "message", "content": [{"type": "workflow_hil_response", "responses": {"req_2": "ham"}}]}] + assert executor._extract_workflow_hil_responses(parsed) == {"req_2": "ham"} + + # Non-HIL inputs should return None + assert executor._extract_workflow_hil_responses("plain text") is None + assert executor._extract_workflow_hil_responses({"email": "test"}) is None + + 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 diff --git a/python/packages/devui/tests/test_server.py b/python/packages/devui/tests/test_server.py index d245601f41..8a3dbd3579 100644 --- a/python/packages/devui/tests/test_server.py +++ b/python/packages/devui/tests/test_server.py @@ -280,18 +280,14 @@ async def test_api_restrictions_in_user_mode(): assert dev_client.get("/v1/entities").status_code == 200 assert user_client.get("/v1/entities").status_code == 200 - # Test 4: Entity info should be restricted in user mode + # Test 4: Entity info should be accessible in both modes (UI needs this) dev_response = dev_client.get("/v1/entities/test_agent/info") assert dev_response.status_code in [200, 404, 500] # Not 403 user_response = user_client.get("/v1/entities/test_agent/info") - assert user_response.status_code == 403 - error_data = user_response.json() - # FastAPI wraps HTTPException detail in 'detail' field - error = error_data.get("detail", {}).get("error") or error_data.get("error") - assert error is not None - assert "developer mode" in error["message"].lower() - assert error["code"] == "developer_mode_required" + # Should return 404 (entity doesn't exist) or 500 (other error), but NOT 403 (forbidden) + # User mode needs entity info to display workflows/agents in the UI + assert user_response.status_code in [200, 404, 500] # Not 403 # Test 5: Hot reload should be restricted in user mode dev_response = dev_client.post("/v1/entities/test_agent/reload") @@ -329,10 +325,11 @@ async def test_api_restrictions_in_user_mode(): # Test 8: Chat endpoint should work in both modes chat_payload = {"model": "test_agent", "input": "Hello"} dev_response = dev_client.post("/v1/responses", json=chat_payload) - assert dev_response.status_code in [200, 404] # 404 if agent doesn't exist + # 200=success, 400=missing entity_id in metadata, 404=entity not found + assert dev_response.status_code in [200, 400, 404] user_response = user_client.post("/v1/responses", json=chat_payload) - assert user_response.status_code in [200, 404] + assert user_response.status_code in [200, 400, 404] if __name__ == "__main__":