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:
Victor Dibia
2025-09-22 16:30:08 -07:00
committed by GitHub
Unverified
parent adb6dcd2af
commit 1ef24d3e91
98 changed files with 18045 additions and 4 deletions
@@ -0,0 +1,72 @@
# Copyright (c) Microsoft. All rights reserved.
"""Agent Framework DevUI Models - OpenAI-compatible types and custom extensions."""
# Import discovery models
# Import all OpenAI types directly from the openai package
from openai.types.responses import (
Response,
ResponseErrorEvent,
ResponseFunctionCallArgumentsDeltaEvent,
ResponseInputParam,
ResponseOutputMessage,
ResponseOutputText,
ResponseReasoningTextDeltaEvent,
ResponseStreamEvent,
ResponseTextDeltaEvent,
ResponseUsage,
ToolParam,
)
from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
from openai.types.shared import Metadata, ResponsesModel
from ._discovery_models import DiscoveryResponse, EntityInfo
from ._openai_custom import (
AgentFrameworkRequest,
OpenAIError,
ResponseFunctionResultComplete,
ResponseFunctionResultDelta,
ResponseTraceEvent,
ResponseTraceEventComplete,
ResponseTraceEventDelta,
ResponseUsageEventComplete,
ResponseUsageEventDelta,
ResponseWorkflowEventComplete,
ResponseWorkflowEventDelta,
)
# Type alias for compatibility
OpenAIResponse = Response
# Export all types for easy importing
__all__ = [
"AgentFrameworkRequest",
"DiscoveryResponse",
"EntityInfo",
"InputTokensDetails",
"Metadata",
"OpenAIError",
"OpenAIResponse",
"OutputTokensDetails",
"Response",
"ResponseErrorEvent",
"ResponseFunctionCallArgumentsDeltaEvent",
"ResponseFunctionResultComplete",
"ResponseFunctionResultDelta",
"ResponseInputParam",
"ResponseOutputMessage",
"ResponseOutputText",
"ResponseReasoningTextDeltaEvent",
"ResponseStreamEvent",
"ResponseTextDeltaEvent",
"ResponseTraceEvent",
"ResponseTraceEventComplete",
"ResponseTraceEventDelta",
"ResponseUsage",
"ResponseUsageEventComplete",
"ResponseUsageEventDelta",
"ResponseWorkflowEventComplete",
"ResponseWorkflowEventDelta",
"ResponsesModel",
"ToolParam",
]
@@ -0,0 +1,33 @@
# Copyright (c) Microsoft. All rights reserved.
"""Discovery API models for entity information."""
from typing import Any
from pydantic import BaseModel, Field
class EntityInfo(BaseModel):
"""Entity information for discovery and detailed views."""
# Always present (core entity data)
id: str
type: str # "agent", "workflow"
name: str
description: str | None = None
framework: str
tools: list[str | dict[str, Any]] | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
# Workflow-specific fields (populated only for detailed info requests)
executors: list[str] | None = None
workflow_dump: dict[str, Any] | None = None
input_schema: dict[str, Any] | None = None
input_type_name: str | None = None
start_executor_id: str | None = None
class DiscoveryResponse(BaseModel):
"""Response model for entity discovery."""
entities: list[EntityInfo] = Field(default_factory=list)
@@ -0,0 +1,202 @@
# Copyright (c) Microsoft. All rights reserved.
"""Custom OpenAI-compatible event types for Agent Framework extensions.
These are custom event types that extend beyond the standard OpenAI Responses API
to support Agent Framework specific features like workflows, traces, and function results.
"""
from typing import Any, Literal
from pydantic import BaseModel
# Custom Agent Framework OpenAI event types for structured data
class ResponseWorkflowEventDelta(BaseModel):
"""Structured workflow event with completion tracking."""
type: Literal["response.workflow_event.delta"] = "response.workflow_event.delta"
delta: dict[str, Any]
executor_id: str | None = None
is_complete: bool = False # Track if this is the final part
item_id: str
output_index: int = 0
sequence_number: int
class ResponseWorkflowEventComplete(BaseModel):
"""Complete workflow event data."""
type: Literal["response.workflow_event.complete"] = "response.workflow_event.complete"
data: dict[str, Any] # Complete event data, not delta
executor_id: str | None = None
item_id: str
output_index: int = 0
sequence_number: int
class ResponseFunctionResultDelta(BaseModel):
"""Structured function result with completion tracking."""
type: Literal["response.function_result.delta"] = "response.function_result.delta"
delta: dict[str, Any]
call_id: str
is_complete: bool = False
item_id: str
output_index: int = 0
sequence_number: int
class ResponseFunctionResultComplete(BaseModel):
"""Complete function result data."""
type: Literal["response.function_result.complete"] = "response.function_result.complete"
data: dict[str, Any] # Complete function result data, not delta
call_id: str
item_id: str
output_index: int = 0
sequence_number: int
class ResponseTraceEventDelta(BaseModel):
"""Structured trace event with completion tracking."""
type: Literal["response.trace.delta"] = "response.trace.delta"
delta: dict[str, Any]
span_id: str | None = None
is_complete: bool = False
item_id: str
output_index: int = 0
sequence_number: int
class ResponseTraceEventComplete(BaseModel):
"""Complete trace event data."""
type: Literal["response.trace.complete"] = "response.trace.complete"
data: dict[str, Any] # Complete trace data, not delta
span_id: str | None = None
item_id: str
output_index: int = 0
sequence_number: int
class ResponseUsageEventDelta(BaseModel):
"""Structured usage event with completion tracking."""
type: Literal["response.usage.delta"] = "response.usage.delta"
delta: dict[str, Any]
is_complete: bool = False
item_id: str
output_index: int = 0
sequence_number: int
class ResponseUsageEventComplete(BaseModel):
"""Complete usage event data."""
type: Literal["response.usage.complete"] = "response.usage.complete"
data: dict[str, Any] # Complete usage data, not delta
item_id: str
output_index: int = 0
sequence_number: int
# Agent Framework extension fields
class AgentFrameworkExtraBody(BaseModel):
"""Agent Framework specific routing fields for OpenAI requests."""
entity_id: str
thread_id: str | None = None
input_data: dict[str, Any] | None = None
class Config:
extra = "allow" # Allow additional fields
# Agent Framework Request Model - Extending real OpenAI types
class AgentFrameworkRequest(BaseModel):
"""OpenAI ResponseCreateParams with Agent Framework extensions.
This properly extends the real OpenAI API request format while adding
our custom routing fields in extra_body.
"""
# All OpenAI fields from ResponseCreateParams
model: str
input: str | list[Any] # ResponseInputParam
stream: bool | None = False
# Common OpenAI optional fields
instructions: str | None = None
metadata: dict[str, Any] | None = None
temperature: float | None = None
max_output_tokens: int | None = None
tools: list[dict[str, Any]] | None = None
# Agent Framework extension - strongly typed
extra_body: AgentFrameworkExtraBody | None = None
class Config:
# Allow extra fields from OpenAI spec
extra = "allow"
entity_id: str | None = None # Allow entity_id as top-level field
def get_entity_id(self) -> str | None:
"""Get entity_id from either top-level field or extra_body."""
# Priority 1: Top-level entity_id field
if self.entity_id:
return self.entity_id
# Priority 2: entity_id in extra_body
if self.extra_body and hasattr(self.extra_body, "entity_id"):
return self.extra_body.entity_id
return None
def to_openai_params(self) -> dict[str, Any]:
"""Convert to dict for OpenAI client compatibility."""
data = self.model_dump(exclude={"extra_body", "entity_id"}, exclude_none=True)
if self.extra_body:
# Don't merge extra_body into main params to keep them separate
data["extra_body"] = self.extra_body
return data
# Error handling
class ResponseTraceEvent(BaseModel):
"""Trace event for execution tracing."""
type: Literal["trace_event"] = "trace_event"
data: dict[str, Any]
timestamp: str
class OpenAIError(BaseModel):
"""OpenAI standard error response model."""
error: dict[str, Any]
@classmethod
def create(cls, message: str, type: str = "invalid_request_error", code: str | None = None) -> "OpenAIError":
"""Create a standard OpenAI error response."""
error_data = {"message": message, "type": type, "code": code}
return cls(error=error_data)
# Export all custom types
__all__ = [
"AgentFrameworkRequest",
"OpenAIError",
"ResponseFunctionResultComplete",
"ResponseFunctionResultDelta",
"ResponseTraceEvent",
"ResponseTraceEventComplete",
"ResponseTraceEventDelta",
"ResponseUsageEventComplete",
"ResponseUsageEventDelta",
"ResponseWorkflowEventComplete",
"ResponseWorkflowEventDelta",
]