Python: DevUI improvements. (#1091)

* enable deeplinking in ui, add agent details to entity info, add usage data, add middleware example in samples and foundry agent.

* update ui build

* Update python/packages/devui/frontend/src/components/workflow/workflow-input-form.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/devui/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/devui/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* imporove mapping for agent nodes and serialiation for agent run events

* lint fixes

* update pyproj toml and ui updates

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Victor Dibia
2025-10-03 15:22:03 -07:00
committed by GitHub
Unverified
parent 61ac6d43b2
commit 01f438d710
33 changed files with 2733 additions and 1164 deletions
@@ -134,6 +134,52 @@ class EntityDiscovery:
# Extract tools/executors using Agent Framework specific logic
tools_list = await self._extract_tools_from_object(entity_object, entity_type)
# Extract agent-specific fields (for agents only)
instructions = None
model = None
chat_client_type = None
context_providers_list = None
middleware_list = None
if entity_type == "agent":
# Try to get instructions
if hasattr(entity_object, "chat_options") and hasattr(entity_object.chat_options, "instructions"):
instructions = entity_object.chat_options.instructions
# Try to get model - check both chat_options and chat_client
if (
hasattr(entity_object, "chat_options")
and hasattr(entity_object.chat_options, "model_id")
and entity_object.chat_options.model_id
):
model = entity_object.chat_options.model_id
elif hasattr(entity_object, "chat_client") and hasattr(entity_object.chat_client, "model_id"):
model = entity_object.chat_client.model_id
# Try to get chat client type
if hasattr(entity_object, "chat_client"):
chat_client_type = entity_object.chat_client.__class__.__name__
# Try to get context providers
if (
hasattr(entity_object, "context_provider")
and entity_object.context_provider
and hasattr(entity_object.context_provider, "__class__")
):
context_providers_list = [entity_object.context_provider.__class__.__name__]
# Try to get middleware
if hasattr(entity_object, "middleware") and entity_object.middleware:
middleware_list = []
for m in entity_object.middleware:
# Try multiple ways to get a good name for middleware
if hasattr(m, "__name__"): # Function or callable
middleware_list.append(m.__name__)
elif hasattr(m, "__class__"): # Class instance
middleware_list.append(m.__class__.__name__)
else:
middleware_list.append(str(m))
# Create EntityInfo with Agent Framework specifics
return EntityInfo(
id=entity_id,
@@ -142,6 +188,11 @@ class EntityDiscovery:
type=entity_type,
framework="agent_framework",
tools=[str(tool) for tool in (tools_list or [])],
instructions=instructions,
model=model,
chat_client_type=chat_client_type,
context_providers=context_providers_list,
middleware=middleware_list,
executors=tools_list if entity_type == "workflow" else [],
input_schema={"type": "string"}, # Default schema
start_executor_id=tools_list[0] if tools_list and entity_type == "workflow" else None,
@@ -446,6 +497,48 @@ class EntityDiscovery:
if tools:
tools_union = [tool for tool in tools]
# Extract agent-specific fields (for agents only)
instructions = None
model = None
chat_client_type = None
context_providers_list = None
middleware_list = None
if obj_type == "agent":
# Try to get instructions
if hasattr(obj, "chat_options") and hasattr(obj.chat_options, "instructions"):
instructions = obj.chat_options.instructions
# Try to get model - check both chat_options and chat_client
if hasattr(obj, "chat_options") and hasattr(obj.chat_options, "model_id") and obj.chat_options.model_id:
model = obj.chat_options.model_id
elif hasattr(obj, "chat_client") and hasattr(obj.chat_client, "model_id"):
model = obj.chat_client.model_id
# Try to get chat client type
if hasattr(obj, "chat_client"):
chat_client_type = obj.chat_client.__class__.__name__
# Try to get context providers
if (
hasattr(obj, "context_provider")
and obj.context_provider
and hasattr(obj.context_provider, "__class__")
):
context_providers_list = [obj.context_provider.__class__.__name__]
# Try to get middleware
if hasattr(obj, "middleware") and obj.middleware:
middleware_list = []
for m in obj.middleware:
# Try multiple ways to get a good name for middleware
if hasattr(m, "__name__"): # Function or callable
middleware_list.append(m.__name__)
elif hasattr(m, "__class__"): # Class instance
middleware_list.append(m.__class__.__name__)
else:
middleware_list.append(str(m))
entity_info = EntityInfo(
id=entity_id,
type=obj_type,
@@ -453,6 +546,11 @@ class EntityDiscovery:
framework="agent_framework",
description=description,
tools=tools_union,
instructions=instructions,
model=model,
chat_client_type=chat_client_type,
context_providers=context_providers_list,
middleware=middleware_list,
metadata={
"module_path": module_path,
"entity_type": obj_type,
@@ -151,6 +151,20 @@ class AgentFrameworkExecutor:
if not display_contents:
continue
# Extract usage information if present
usage_data = None
for content in af_msg.contents:
content_type = getattr(content, "type", None)
if content_type == "usage":
details = getattr(content, "details", None)
if details:
usage_data = {
"total_tokens": getattr(details, "total_token_count", 0) or 0,
"prompt_tokens": getattr(details, "input_token_count", 0) or 0,
"completion_tokens": getattr(details, "output_token_count", 0) or 0,
}
break
ui_message = {
"id": af_msg.message_id or f"restored-{i}",
"role": role,
@@ -160,6 +174,10 @@ class AgentFrameworkExecutor:
"message_id": af_msg.message_id,
}
# Add usage data if available
if usage_data:
ui_message["usage"] = usage_data
ui_messages.append(ui_message)
logger.info(f"Restored {len(ui_messages)} display messages for thread {thread_id}")
@@ -697,6 +715,8 @@ class AgentFrameworkExecutor:
Parsed input for workflow
"""
try:
from ._utils import parse_input_for_type
# Get the start executor and its input type
start_executor, message_types = self._get_start_executor_message_types(workflow)
if not start_executor:
@@ -713,45 +733,12 @@ class AgentFrameworkExecutor:
logger.debug("Could not select primary input type for workflow - using raw dict")
return input_data
# If input type is dict, return as-is
if input_type is dict:
return input_data
# Handle primitive types
if input_type in (str, int, float, bool):
try:
if isinstance(input_data, input_type):
return input_data
if "input" in input_data:
return input_type(input_data["input"])
if len(input_data) == 1:
value = next(iter(input_data.values()))
return input_type(value)
return input_data
except (ValueError, TypeError) as e:
logger.warning(f"Failed to convert input to {input_type}: {e}")
return input_data
# If it's a Pydantic model, validate and create instance
if hasattr(input_type, "model_validate"):
try:
return input_type.model_validate(input_data)
except Exception as e:
logger.warning(f"Failed to validate input as {input_type}: {e}")
return input_data
# If it's a dataclass or other type with annotations
elif hasattr(input_type, "__annotations__"):
try:
return input_type(**input_data)
except Exception as e:
logger.warning(f"Failed to create {input_type} from input data: {e}")
return input_data
# Use consolidated parsing logic from _utils
return parse_input_for_type(input_data, input_type)
except Exception as e:
logger.warning(f"Error parsing structured workflow input: {e}")
return input_data
return input_data
def _parse_raw_workflow_input(self, workflow: Any, raw_input: str) -> Any:
"""Parse raw input string based on workflow's expected input type.
@@ -764,6 +751,8 @@ class AgentFrameworkExecutor:
Parsed input for workflow
"""
try:
from ._utils import parse_input_for_type
# Get the start executor and its input type
start_executor, message_types = self._get_start_executor_message_types(workflow)
if not start_executor:
@@ -780,43 +769,9 @@ class AgentFrameworkExecutor:
logger.debug("Could not select primary input type for workflow - using raw string")
return raw_input
# If input type is str, return as-is
if input_type is str:
return raw_input
# If it's a Pydantic model, try to parse JSON
if hasattr(input_type, "model_validate_json"):
try:
# First try to parse as JSON
if raw_input.strip().startswith("{"):
return input_type.model_validate_json(raw_input)
# Try common field names
common_fields = ["message", "text", "input", "data", "content"]
for field in common_fields:
try:
return input_type(**{field: raw_input})
except Exception as e:
logger.debug(f"Failed to parse input using field '{field}': {e}")
continue
# Last resort: try default constructor
return input_type()
except Exception as e:
logger.debug(f"Failed to parse input as {input_type}: {e}")
# If it's a dataclass, try JSON parsing
elif hasattr(input_type, "__annotations__"):
try:
if raw_input.strip().startswith("{"):
parsed = json.loads(raw_input)
return input_type(**parsed)
except Exception as e:
logger.debug(f"Failed to parse input as {input_type}: {e}")
# Use consolidated parsing logic from _utils
return parse_input_for_type(raw_input, input_type)
except Exception as e:
logger.debug(f"Error determining workflow input type: {e}")
# Fallback: return raw string
return raw_input
logger.debug(f"Error parsing workflow input: {e}")
return raw_input
@@ -6,7 +6,6 @@ import json
import logging
import uuid
from collections.abc import Sequence
from dataclasses import asdict, is_dataclass
from datetime import datetime
from typing import Any, Union
@@ -97,8 +96,18 @@ class MessageMapper:
# Import Agent Framework types for proper isinstance checks
try:
from agent_framework import AgentRunResponseUpdate, WorkflowEvent
from agent_framework._workflows._events import AgentRunUpdateEvent
# Handle agent updates (AgentRunResponseUpdate)
# Handle AgentRunUpdateEvent - workflow event wrapping AgentRunResponseUpdate
# This must be checked BEFORE generic WorkflowEvent check
if isinstance(raw_event, AgentRunUpdateEvent):
# Extract the AgentRunResponseUpdate from the event's data attribute
if raw_event.data and isinstance(raw_event.data, AgentRunResponseUpdate):
return await self._convert_agent_update(raw_event.data, context)
# If no data, treat as generic workflow event
return await self._convert_workflow_event(raw_event, context)
# Handle agent updates (AgentRunResponseUpdate) - for direct agent execution
if isinstance(raw_event, AgentRunResponseUpdate):
return await self._convert_agent_update(raw_event, context)
@@ -258,13 +267,22 @@ class MessageMapper:
List of OpenAI response stream events
"""
try:
serialized_payload = self._serialize_payload(getattr(event, "data", None))
# Get event data and serialize if it's a SerializationMixin
event_data = getattr(event, "data", None)
if event_data is not None and hasattr(event_data, "to_dict"):
# SerializationMixin objects - convert to dict for JSON serialization
try:
event_data = event_data.to_dict()
except Exception as e:
logger.debug(f"Failed to serialize event data with to_dict(): {e}")
event_data = str(event_data)
# Create structured workflow event
workflow_event = ResponseWorkflowEventComplete(
type="response.workflow_event.complete",
data={
"event_type": event.__class__.__name__,
"data": serialized_payload,
"data": event_data,
"executor_id": getattr(event, "executor_id", None),
"timestamp": datetime.now().isoformat(),
},
@@ -280,59 +298,6 @@ class MessageMapper:
logger.warning(f"Error converting workflow event: {e}")
return [await self._create_error_event(str(e), context)]
def _serialize_payload(self, value: Any) -> Any:
"""Best-effort JSON serialization for workflow payloads."""
if value is None:
return None
if isinstance(value, (str, int, float, bool)):
return value
if isinstance(value, (list, tuple, set)):
return [self._serialize_payload(item) for item in value]
if isinstance(value, dict):
return {str(k): self._serialize_payload(v) for k, v in value.items()}
if is_dataclass(value) and not isinstance(value, type):
try:
return self._serialize_payload(asdict(value))
except Exception as exc:
logger.debug("Failed to serialize dataclass payload: %s", exc)
model_dump_method = getattr(value, "model_dump", None)
if model_dump_method is not None and callable(model_dump_method):
try:
dumped = model_dump_method()
return self._serialize_payload(dumped)
except Exception as exc:
logger.debug("Failed to serialize payload via model_dump: %s", exc)
dict_method = getattr(value, "dict", None)
if dict_method is not None and callable(dict_method):
try:
dict_result = dict_method()
return self._serialize_payload(dict_result)
except Exception as exc:
logger.debug("Failed to serialize payload via dict(): %s", exc)
to_dict_method = getattr(value, "to_dict", None)
if to_dict_method is not None and callable(to_dict_method):
try:
to_dict_result = to_dict_method()
return self._serialize_payload(to_dict_result)
except Exception as exc:
logger.debug("Failed to serialize payload via to_dict(): %s", exc)
model_dump_json_method = getattr(value, "model_dump_json", None)
if model_dump_json_method is not None and callable(model_dump_json_method):
try:
json_str = model_dump_json_method()
if isinstance(json_str, (str, bytes, bytearray)):
return json.loads(json_str)
except Exception as exc:
logger.debug("Failed to serialize payload via model_dump_json: %s", exc)
if hasattr(value, "__dict__"):
try:
return self._serialize_payload({
key: self._serialize_payload(val) for key, val in value.__dict__.items() if not key.startswith("_")
})
except Exception as exc:
logger.debug("Failed to serialize payload via __dict__: %s", exc)
return str(value)
# Content type mappers - implementing our comprehensive mapping plan
async def _map_text_content(self, content: Any, context: dict[str, Any]) -> ResponseTextDeltaEvent:
@@ -409,13 +374,24 @@ class MessageMapper:
context["usage_data"] = []
context["usage_data"].append(content)
# Extract usage from UsageContent.details (UsageDetails object)
details = getattr(content, "details", None)
total_tokens = 0
prompt_tokens = 0
completion_tokens = 0
if details:
total_tokens = getattr(details, "total_token_count", 0) or 0
prompt_tokens = getattr(details, "input_token_count", 0) or 0
completion_tokens = getattr(details, "output_token_count", 0) or 0
return ResponseUsageEventComplete(
type="response.usage.complete",
data={
"usage_data": getattr(content, "usage_data", {}),
"total_tokens": getattr(content, "total_tokens", 0),
"completion_tokens": getattr(content, "completion_tokens", 0),
"prompt_tokens": getattr(content, "prompt_tokens", 0),
"usage_data": details.to_dict() if details and hasattr(details, "to_dict") else {},
"total_tokens": total_tokens,
"completion_tokens": completion_tokens,
"prompt_tokens": prompt_tokens,
"timestamp": datetime.now().isoformat(),
},
item_id=context["item_id"],
@@ -263,6 +263,8 @@ class DevServer:
start_executor_id = ""
try:
from ._utils import generate_input_schema
start_executor = entity_obj.get_start_executor()
except Exception as e:
logger.debug(f"Could not extract input info for workflow {entity_id}: {e}")
@@ -278,17 +280,8 @@ class DevServer:
if input_type:
input_type_name = getattr(input_type, "__name__", str(input_type))
if input_type is str:
input_schema = {"type": "string"}
elif input_type is dict:
input_schema = {"type": "object"}
elif hasattr(input_type, "model_json_schema"):
try:
input_schema = input_type.model_json_schema()
except Exception as exc: # pragma: no cover - defensive path
logger.debug(f"model_json_schema() failed for workflow {entity_id}: {exc}")
elif hasattr(input_type, "__annotations__"):
input_schema = {"type": "object"}
# Generate schema using comprehensive schema generation
input_schema = generate_input_schema(input_type)
if not input_schema:
input_schema = {"type": "string"}
@@ -0,0 +1,421 @@
# Copyright (c) Microsoft. All rights reserved.
"""Utility functions for DevUI."""
import inspect
import json
import logging
from dataclasses import fields, is_dataclass
from typing import Any, get_args, get_origin
logger = logging.getLogger(__name__)
# ============================================================================
# Type System Utilities
# ============================================================================
def is_serialization_mixin(cls: type) -> bool:
"""Check if class is a SerializationMixin subclass.
Args:
cls: Class to check
Returns:
True if class is a SerializationMixin subclass
"""
try:
from agent_framework._serialization import SerializationMixin
return isinstance(cls, type) and issubclass(cls, SerializationMixin)
except ImportError:
return False
def _type_to_schema(type_hint: Any, field_name: str) -> dict[str, Any]:
"""Convert a type hint to JSON schema.
Args:
type_hint: Type hint to convert
field_name: Name of the field (for documentation)
Returns:
JSON schema dict
"""
type_str = str(type_hint)
# Handle None/Optional
if type_hint is type(None):
return {"type": "null"}
# Handle basic types
if type_hint is str or "str" in type_str:
return {"type": "string"}
if type_hint is int or "int" in type_str:
return {"type": "integer"}
if type_hint is float or "float" in type_str:
return {"type": "number"}
if type_hint is bool or "bool" in type_str:
return {"type": "boolean"}
# Handle Literal types (for enum-like values)
if "Literal" in type_str:
origin = get_origin(type_hint)
if origin is not None:
args = get_args(type_hint)
if args:
return {"type": "string", "enum": list(args)}
# Handle Union/Optional
if "Union" in type_str or "Optional" in type_str:
origin = get_origin(type_hint)
if origin is not None:
args = get_args(type_hint)
# Filter out None type
non_none_args = [arg for arg in args if arg is not type(None)]
if len(non_none_args) == 1:
return _type_to_schema(non_none_args[0], field_name)
# Multiple types - pick first non-None
if non_none_args:
return _type_to_schema(non_none_args[0], field_name)
# Handle collections
if "list" in type_str or "List" in type_str or "Sequence" in type_str:
origin = get_origin(type_hint)
if origin is not None:
args = get_args(type_hint)
if args:
items_schema = _type_to_schema(args[0], field_name)
return {"type": "array", "items": items_schema}
return {"type": "array"}
if "dict" in type_str or "Dict" in type_str or "Mapping" in type_str:
return {"type": "object"}
# Default fallback
return {"type": "string", "description": f"Type: {type_hint}"}
def generate_schema_from_serialization_mixin(cls: type[Any]) -> dict[str, Any]:
"""Generate JSON schema from SerializationMixin class.
Introspects the __init__ signature to extract parameter types and defaults.
Args:
cls: SerializationMixin subclass
Returns:
JSON schema dict
"""
sig = inspect.signature(cls)
# Get type hints
try:
from typing import get_type_hints
type_hints = get_type_hints(cls)
except Exception:
type_hints = {}
properties: dict[str, Any] = {}
required: list[str] = []
for param_name, param in sig.parameters.items():
if param_name in ("self", "kwargs"):
continue
# Get type annotation
param_type = type_hints.get(param_name, str)
# Generate schema for this parameter
param_schema = _type_to_schema(param_type, param_name)
properties[param_name] = param_schema
# Check if required (no default value, not VAR_KEYWORD)
if param.default == inspect.Parameter.empty and param.kind != inspect.Parameter.VAR_KEYWORD:
required.append(param_name)
schema: dict[str, Any] = {"type": "object", "properties": properties}
if required:
schema["required"] = required
return schema
def generate_schema_from_dataclass(cls: type[Any]) -> dict[str, Any]:
"""Generate JSON schema from dataclass.
Args:
cls: Dataclass type
Returns:
JSON schema dict
"""
if not is_dataclass(cls):
return {"type": "object"}
properties: dict[str, Any] = {}
required: list[str] = []
for field in fields(cls):
# Generate schema for field type
field_schema = _type_to_schema(field.type, field.name)
properties[field.name] = field_schema
# Check if required (no default value)
if field.default == field.default_factory: # No default
required.append(field.name)
schema: dict[str, Any] = {"type": "object", "properties": properties}
if required:
schema["required"] = required
return schema
def generate_input_schema(input_type: type) -> dict[str, Any]:
"""Generate JSON schema for workflow input type.
Supports multiple input types in priority order:
1. Built-in types (str, dict, int, etc.)
2. Pydantic models (via model_json_schema)
3. SerializationMixin classes (via __init__ introspection)
4. Dataclasses (via fields introspection)
5. Fallback to string
Args:
input_type: Input type to generate schema for
Returns:
JSON schema dict
"""
# 1. Built-in types
if input_type is str:
return {"type": "string"}
if input_type is dict:
return {"type": "object"}
if input_type is int:
return {"type": "integer"}
if input_type is float:
return {"type": "number"}
if input_type is bool:
return {"type": "boolean"}
# 2. Pydantic models (legacy support)
if hasattr(input_type, "model_json_schema"):
return input_type.model_json_schema() # type: ignore
# 3. SerializationMixin classes (ChatMessage, etc.)
if is_serialization_mixin(input_type):
return generate_schema_from_serialization_mixin(input_type)
# 4. Dataclasses
if is_dataclass(input_type):
return generate_schema_from_dataclass(input_type)
# 5. Fallback to string
type_name = getattr(input_type, "__name__", str(input_type))
return {"type": "string", "description": f"Input type: {type_name}"}
# ============================================================================
# Input Parsing Utilities
# ============================================================================
def parse_input_for_type(input_data: Any, target_type: type) -> Any:
"""Parse input data to match the target type.
Handles conversion from raw input (string, dict) to the expected type:
- Built-in types: direct conversion
- Pydantic models: use model_validate or model_validate_json
- SerializationMixin: use from_dict or construct from string
- Dataclasses: construct from dict
Args:
input_data: Raw input data (string, dict, or already correct type)
target_type: Expected type for the input
Returns:
Parsed input matching target_type, or original input if parsing fails
"""
# If already correct type, return as-is
if isinstance(input_data, target_type):
return input_data
# Handle string input
if isinstance(input_data, str):
return _parse_string_input(input_data, target_type)
# Handle dict input
if isinstance(input_data, dict):
return _parse_dict_input(input_data, target_type)
# Fallback: return original
return input_data
def _parse_string_input(input_str: str, target_type: type) -> Any:
"""Parse string input to target type.
Args:
input_str: Input string
target_type: Target type
Returns:
Parsed input or original string
"""
# Built-in types
if target_type is str:
return input_str
if target_type is int:
try:
return int(input_str)
except ValueError:
return input_str
elif target_type is float:
try:
return float(input_str)
except ValueError:
return input_str
elif target_type is bool:
return input_str.lower() in ("true", "1", "yes")
# Pydantic models
if hasattr(target_type, "model_validate_json"):
try:
# Try parsing as JSON first
if input_str.strip().startswith("{"):
return target_type.model_validate_json(input_str) # type: ignore
# Try common field names with the string value
common_fields = ["text", "message", "content", "input", "data"]
for field in common_fields:
try:
return target_type(**{field: input_str}) # type: ignore
except Exception as e:
logger.debug(f"Failed to parse string input with field '{field}': {e}")
continue
except Exception as e:
logger.debug(f"Failed to parse string as Pydantic model: {e}")
# SerializationMixin (like ChatMessage)
if is_serialization_mixin(target_type):
try:
# Try parsing as JSON dict first
if input_str.strip().startswith("{"):
data = json.loads(input_str)
if hasattr(target_type, "from_dict"):
return target_type.from_dict(data) # type: ignore
return target_type(**data) # type: ignore
# For ChatMessage specifically: create from text
# Try common field patterns
common_fields = ["text", "message", "content"]
sig = inspect.signature(target_type)
params = list(sig.parameters.keys())
# If it has 'text' param, use it
if "text" in params:
try:
return target_type(role="user", text=input_str) # type: ignore
except Exception as e:
logger.debug(f"Failed to create SerializationMixin with text field: {e}")
# Try other common fields
for field in common_fields:
if field in params:
try:
return target_type(**{field: input_str}) # type: ignore
except Exception as e:
logger.debug(f"Failed to create SerializationMixin with field '{field}': {e}")
continue
except Exception as e:
logger.debug(f"Failed to parse string as SerializationMixin: {e}")
# Dataclasses
if is_dataclass(target_type):
try:
# Try parsing as JSON
if input_str.strip().startswith("{"):
data = json.loads(input_str)
return target_type(**data) # type: ignore
# Try common field names
common_fields = ["text", "message", "content", "input", "data"]
for field in common_fields:
try:
return target_type(**{field: input_str}) # type: ignore
except Exception as e:
logger.debug(f"Failed to create dataclass with field '{field}': {e}")
continue
except Exception as e:
logger.debug(f"Failed to parse string as dataclass: {e}")
# Fallback: return original string
return input_str
def _parse_dict_input(input_dict: dict[str, Any], target_type: type) -> Any:
"""Parse dict input to target type.
Args:
input_dict: Input dictionary
target_type: Target type
Returns:
Parsed input or original dict
"""
# Handle primitive types - extract from common field names
if target_type in (str, int, float, bool):
try:
# If it's already the right type, return as-is
if isinstance(input_dict, target_type):
return input_dict
# Try "input" field first (common for workflow inputs)
if "input" in input_dict:
return target_type(input_dict["input"]) # type: ignore
# If single-key dict, extract the value
if len(input_dict) == 1:
value = next(iter(input_dict.values()))
return target_type(value) # type: ignore
# Otherwise, return as-is
return input_dict
except (ValueError, TypeError) as e:
logger.debug(f"Failed to convert dict to {target_type}: {e}")
return input_dict
# If target is dict, return as-is
if target_type is dict:
return input_dict
# Pydantic models
if hasattr(target_type, "model_validate"):
try:
return target_type.model_validate(input_dict) # type: ignore
except Exception as e:
logger.debug(f"Failed to validate dict as Pydantic model: {e}")
# SerializationMixin
if is_serialization_mixin(target_type):
try:
if hasattr(target_type, "from_dict"):
return target_type.from_dict(input_dict) # type: ignore
return target_type(**input_dict) # type: ignore
except Exception as e:
logger.debug(f"Failed to parse dict as SerializationMixin: {e}")
# Dataclasses
if is_dataclass(target_type):
try:
return target_type(**input_dict) # type: ignore
except Exception as e:
logger.debug(f"Failed to parse dict as dataclass: {e}")
# Fallback: return original dict
return input_dict
@@ -37,6 +37,13 @@ class EntityInfo(BaseModel):
# Environment variable requirements
required_env_vars: list[EnvVarRequirement] | None = None
# Agent-specific fields (optional, populated when available)
instructions: str | None = None
model: str | None = None
chat_client_type: str | None = None
context_providers: list[str] | None = None
middleware: list[str] | None = None
# Workflow-specific fields (populated only for detailed info requests)
executors: list[str] | None = None
workflow_dump: dict[str, Any] | None = None
@@ -0,0 +1,33 @@
<svg width="805" height="805" viewBox="0 0 805 805" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_iii_510_1294)">
<path d="M402.488 119.713C439.197 119.713 468.955 149.472 468.955 186.18C468.955 192.086 471.708 197.849 476.915 200.635L546.702 237.977C555.862 242.879 566.95 240.96 576.092 236.023C585.476 230.955 596.218 228.078 607.632 228.078C644.341 228.078 674.098 257.836 674.099 294.545C674.099 316.95 663.013 336.765 646.028 348.806C637.861 354.595 631.412 363.24 631.412 373.251V430.818C631.412 440.83 637.861 449.475 646.028 455.264C663.013 467.305 674.099 487.121 674.099 509.526C674.099 546.235 644.341 575.994 607.632 575.994C598.598 575.994 589.985 574.191 582.133 570.926C573.644 567.397 563.91 566.393 555.804 570.731L469.581 616.867C469.193 617.074 468.955 617.479 468.955 617.919C468.955 654.628 439.197 684.386 402.488 684.386C365.779 684.386 336.021 654.628 336.021 617.919C336.021 616.802 335.423 615.765 334.439 615.238L249.895 570C241.61 565.567 231.646 566.713 223.034 570.472C214.898 574.024 205.914 575.994 196.47 575.994C159.761 575.994 130.002 546.235 130.002 509.526C130.002 486.66 141.549 466.49 159.13 454.531C167.604 448.766 174.349 439.975 174.349 429.726V372.538C174.349 362.289 167.604 353.498 159.13 347.734C141.549 335.774 130.002 315.604 130.002 292.738C130.002 256.029 159.761 226.271 196.47 226.271C208.223 226.271 219.263 229.322 228.843 234.674C238.065 239.827 249.351 241.894 258.666 236.91L328.655 199.459C333.448 196.895 336.021 191.616 336.021 186.18C336.021 149.471 365.779 119.713 402.488 119.713ZM475.716 394.444C471.337 396.787 468.955 401.586 468.955 406.552C468.955 429.68 457.142 450.048 439.221 461.954C430.571 467.7 423.653 476.574 423.653 486.959V537.511C423.653 547.896 430.746 556.851 439.379 562.622C449 569.053 461.434 572.052 471.637 566.592L527.264 536.826C536.887 531.677 541.164 520.44 541.164 509.526C541.164 485.968 553.42 465.272 571.904 453.468C580.846 447.757 588.054 438.749 588.054 428.139V371.427C588.054 363.494 582.671 356.676 575.716 352.862C569.342 349.366 561.663 348.454 555.253 351.884L475.716 394.444ZM247.992 349.841C241.997 346.633 234.806 347.465 228.873 350.785C222.524 354.337 217.706 360.639 217.706 367.915V429.162C217.706 439.537 224.611 448.404 233.248 454.152C251.144 466.062 262.937 486.417 262.937 509.526C262.937 519.654 267.026 529.991 275.955 534.769L334.852 566.284C344.582 571.49 356.362 568.81 365.528 562.667C373.735 557.166 380.296 548.643 380.296 538.764V486.305C380.296 476.067 373.564 467.282 365.103 461.516C347.548 449.552 336.021 429.398 336.021 406.552C336.021 400.967 333.389 395.536 328.465 392.902L247.992 349.841ZM270.019 280.008C265.421 282.469 262.936 287.522 262.937 292.738C262.937 293.308 262.929 293.876 262.915 294.443C262.615 306.354 266.961 318.871 277.466 324.492L334.017 354.751C344.13 360.163 356.442 357.269 366.027 350.969C376.495 344.088 389.024 340.085 402.488 340.085C416.203 340.085 428.947 344.239 439.532 351.357C449.163 357.834 461.63 360.861 471.864 355.385L526.625 326.083C537.106 320.474 541.458 307.999 541.182 296.115C541.17 295.593 541.164 295.069 541.164 294.545C541.164 288.551 538.376 282.696 533.091 279.868L463.562 242.664C454.384 237.753 443.274 239.688 434.123 244.65C424.716 249.75 413.941 252.647 402.488 252.647C390.83 252.647 379.873 249.646 370.348 244.373C361.148 239.281 349.917 237.256 340.646 242.217L270.019 280.008Z" fill="url(#paint0_linear_510_1294)"/>
</g>
<defs>
<filter id="filter0_iii_510_1294" x="103.759" y="93.4694" width="578.735" height="599.314" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="8.39647" dy="8.39647"/>
<feGaussianBlur stdDeviation="20.9912"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.835294 0 0 0 0 0.623529 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_510_1294"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-26.2432" dy="-26.2432"/>
<feGaussianBlur stdDeviation="20.9912"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.368627 0 0 0 0 0.262745 0 0 0 0 0.564706 0 0 0 0.3 0"/>
<feBlend mode="plus-darker" in2="effect1_innerShadow_510_1294" result="effect2_innerShadow_510_1294"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-26.2432" dy="-26.2432"/>
<feGaussianBlur stdDeviation="50"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.368627 0 0 0 0 0.262745 0 0 0 0 0.564706 0 0 0 0.1 0"/>
<feBlend mode="plus-darker" in2="effect2_innerShadow_510_1294" result="effect3_innerShadow_510_1294"/>
</filter>
<linearGradient id="paint0_linear_510_1294" x1="255.628" y1="-34.3245" x2="618.483" y2="632.032" gradientUnits="userSpaceOnUse">
<stop stop-color="#D59FFF"/>
<stop offset="1" stop-color="#8562C5"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/agentframework.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Framework Dev UI</title>
<script type="module" crossorigin src="/assets/index-DPEaaIdK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D1AmQWga.css">
<script type="module" crossorigin src="/assets/index-D0SfShuZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-WsCIE0bH.css">
</head>
<body>
<div id="root"></div>