mirror of
https://github.com/microsoft/autogen.git
synced 2025-08-15 12:11:30 +00:00
Support Component Validation API in AGS (#5503)
<!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? It is useful to rapidly validate any changes to a team structure as teams are built either via drag and drop or by modifying the underlying spec You can now “validate” your team. The key ideas are as follows - Each team is based on some Component Config specification which is a pedantic model underneath. - Validation is 3 pronged based on a ValidatorService class - Data model validation (validate component schema) - Instantiation validation (validate component can be instantiated) - Provider validation, component_type validation (validate that provider exists and can be imported) - UX: each time a component is **loaded or saved**, it is automatically validated and any errors shown (via a server endpoint). This way, the developer immediately knows if updates to the configuration is wrong or has errors. > Note: this is different from actually running the component against a task. Currently you can run the entire team. In a separate PR we will implement ability to run/test other components. <img width="1360" alt="image" src="https://github.com/user-attachments/assets/d61095b7-0b07-463a-b4b2-5c50ded750f6" /> <img width="1368" alt="image" src="https://github.com/user-attachments/assets/09a1677e-76e8-44a4-9749-15c27457efbb" /> <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number Closes #4616 <!-- For example: "Closes #1234" --> ## Checks - [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
This commit is contained in:
parent
07fdc4e2da
commit
f49f159a43
@ -1,5 +1,5 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Awaitable, Callable, DefaultDict, List, Set, Sequence
|
from typing import Awaitable, Callable, DefaultDict, List, Sequence, Set
|
||||||
|
|
||||||
from ._agent import Agent
|
from ._agent import Agent
|
||||||
from ._agent_id import AgentId
|
from ._agent_id import AgentId
|
||||||
|
@ -183,6 +183,7 @@ def create_default_gallery() -> Gallery:
|
|||||||
model_client=base_model,
|
model_client=base_model,
|
||||||
tools=[tools.calculator_tool],
|
tools=[tools.calculator_tool],
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.add_agent(
|
builder.add_agent(
|
||||||
calc_assistant.dump_component(), description="An agent that provides assistance with ability to use tools."
|
calc_assistant.dump_component(), description="An agent that provides assistance with ability to use tools."
|
||||||
)
|
)
|
||||||
@ -200,10 +201,25 @@ def create_default_gallery() -> Gallery:
|
|||||||
calc_team = RoundRobinGroupChat(participants=[calc_assistant], termination_condition=calc_or_term)
|
calc_team = RoundRobinGroupChat(participants=[calc_assistant], termination_condition=calc_or_term)
|
||||||
builder.add_team(
|
builder.add_team(
|
||||||
calc_team.dump_component(),
|
calc_team.dump_component(),
|
||||||
label="Default Team",
|
label="RoundRobin Team",
|
||||||
description="A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
|
description="A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
critic_agent = AssistantAgent(
|
||||||
|
name="critic_agent",
|
||||||
|
system_message="You are a helpful assistant. Critique the assistant's output and suggest improvements.",
|
||||||
|
description="an agent that critiques and improves the assistant's output",
|
||||||
|
model_client=base_model,
|
||||||
|
)
|
||||||
|
selector_default_team = SelectorGroupChat(
|
||||||
|
participants=[calc_assistant, critic_agent], termination_condition=calc_or_term, model_client=base_model
|
||||||
|
)
|
||||||
|
builder.add_team(
|
||||||
|
selector_default_team.dump_component(),
|
||||||
|
label="Selector Team",
|
||||||
|
description="A team with 2 agents - an AssistantAgent (with a calculator tool) and a CriticAgent in a SelectorGroupChat team.",
|
||||||
|
)
|
||||||
|
|
||||||
# Create web surfer agent
|
# Create web surfer agent
|
||||||
websurfer_agent = MultimodalWebSurfer(
|
websurfer_agent = MultimodalWebSurfer(
|
||||||
name="websurfer_agent",
|
name="websurfer_agent",
|
||||||
|
@ -13,7 +13,7 @@ from ..version import VERSION
|
|||||||
from .config import settings
|
from .config import settings
|
||||||
from .deps import cleanup_managers, init_managers
|
from .deps import cleanup_managers, init_managers
|
||||||
from .initialization import AppInitializer
|
from .initialization import AppInitializer
|
||||||
from .routes import runs, sessions, teams, ws
|
from .routes import runs, sessions, teams, validation, ws
|
||||||
|
|
||||||
# Initialize application
|
# Initialize application
|
||||||
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
@ -107,6 +107,12 @@ api.include_router(
|
|||||||
responses={404: {"description": "Not found"}},
|
responses={404: {"description": "Not found"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
api.include_router(
|
||||||
|
validation.router,
|
||||||
|
prefix="/validate",
|
||||||
|
tags=["validation"],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
# Version endpoint
|
# Version endpoint
|
||||||
|
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
# api/routes/validation.py
|
||||||
|
import importlib
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from autogen_core import ComponentModel, is_component_class
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationRequest(BaseModel):
|
||||||
|
component: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(BaseModel):
|
||||||
|
field: str
|
||||||
|
error: str
|
||||||
|
suggestion: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationResponse(BaseModel):
|
||||||
|
is_valid: bool
|
||||||
|
errors: List[ValidationError] = []
|
||||||
|
warnings: List[ValidationError] = []
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationService:
|
||||||
|
@staticmethod
|
||||||
|
def validate_provider(provider: str) -> Optional[ValidationError]:
|
||||||
|
"""Validate that the provider exists and can be imported"""
|
||||||
|
try:
|
||||||
|
if provider in ["azure_openai_chat_completion_client", "AzureOpenAIChatCompletionClient"]:
|
||||||
|
provider = "autogen_ext.models.openai.AzureOpenAIChatCompletionClient"
|
||||||
|
elif provider in ["openai_chat_completion_client", "OpenAIChatCompletionClient"]:
|
||||||
|
provider = "autogen_ext.models.openai.OpenAIChatCompletionClient"
|
||||||
|
|
||||||
|
module_path, class_name = provider.rsplit(".", maxsplit=1)
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
component_class = getattr(module, class_name)
|
||||||
|
|
||||||
|
if not is_component_class(component_class):
|
||||||
|
return ValidationError(
|
||||||
|
field="provider",
|
||||||
|
error=f"Class {provider} is not a valid component class",
|
||||||
|
suggestion="Ensure the class inherits from Component and implements required methods",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
except ImportError:
|
||||||
|
return ValidationError(
|
||||||
|
field="provider",
|
||||||
|
error=f"Could not import provider {provider}",
|
||||||
|
suggestion="Check that the provider module is installed and the path is correct",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ValidationError(
|
||||||
|
field="provider",
|
||||||
|
error=f"Error validating provider: {str(e)}",
|
||||||
|
suggestion="Check the provider string format and class implementation",
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_component_type(component: Dict[str, Any]) -> Optional[ValidationError]:
|
||||||
|
"""Validate the component type"""
|
||||||
|
if "component_type" not in component:
|
||||||
|
return ValidationError(
|
||||||
|
field="component_type",
|
||||||
|
error="Component type is missing",
|
||||||
|
suggestion="Add a component_type field to the component configuration",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_config_schema(component: Dict[str, Any]) -> List[ValidationError]:
|
||||||
|
"""Validate the component configuration against its schema"""
|
||||||
|
errors = []
|
||||||
|
try:
|
||||||
|
# Convert to ComponentModel for initial validation
|
||||||
|
model = ComponentModel(**component)
|
||||||
|
|
||||||
|
# Get the component class
|
||||||
|
provider = model.provider
|
||||||
|
module_path, class_name = provider.rsplit(".", maxsplit=1)
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
component_class = getattr(module, class_name)
|
||||||
|
|
||||||
|
# Validate against component's schema
|
||||||
|
if hasattr(component_class, "component_config_schema"):
|
||||||
|
try:
|
||||||
|
component_class.component_config_schema.model_validate(model.config)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(
|
||||||
|
ValidationError(
|
||||||
|
field="config",
|
||||||
|
error=f"Config validation failed: {str(e)}",
|
||||||
|
suggestion="Check that the config matches the component's schema",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
ValidationError(
|
||||||
|
field="config",
|
||||||
|
error="Component class missing config schema",
|
||||||
|
suggestion="Implement component_config_schema in the component class",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(
|
||||||
|
ValidationError(
|
||||||
|
field="config",
|
||||||
|
error=f"Schema validation error: {str(e)}",
|
||||||
|
suggestion="Check the component configuration format",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return errors
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_instantiation(component: Dict[str, Any]) -> Optional[ValidationError]:
|
||||||
|
"""Validate that the component can be instantiated"""
|
||||||
|
try:
|
||||||
|
model = ComponentModel(**component)
|
||||||
|
# Attempt to load the component
|
||||||
|
module_path, class_name = model.provider.rsplit(".", maxsplit=1)
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
component_class = getattr(module, class_name)
|
||||||
|
component_class.load_component(model)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return ValidationError(
|
||||||
|
field="instantiation",
|
||||||
|
error=f"Failed to instantiate component: {str(e)}",
|
||||||
|
suggestion="Check that the component can be properly instantiated with the given config",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, component: Dict[str, Any]) -> ValidationResponse:
|
||||||
|
"""Validate a component configuration"""
|
||||||
|
errors = []
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Check provider
|
||||||
|
if provider_error := cls.validate_provider(component.get("provider", "")):
|
||||||
|
errors.append(provider_error)
|
||||||
|
|
||||||
|
# Check component type
|
||||||
|
if type_error := cls.validate_component_type(component):
|
||||||
|
errors.append(type_error)
|
||||||
|
|
||||||
|
# Validate schema
|
||||||
|
schema_errors = cls.validate_config_schema(component)
|
||||||
|
errors.extend(schema_errors)
|
||||||
|
|
||||||
|
# Only attempt instantiation if no errors so far
|
||||||
|
if not errors:
|
||||||
|
if inst_error := cls.validate_instantiation(component):
|
||||||
|
errors.append(inst_error)
|
||||||
|
|
||||||
|
# Check for version warnings
|
||||||
|
if "version" not in component:
|
||||||
|
warnings.append(
|
||||||
|
ValidationError(
|
||||||
|
field="version",
|
||||||
|
error="Component version not specified",
|
||||||
|
suggestion="Consider adding a version to ensure compatibility",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ValidationResponse(is_valid=len(errors) == 0, errors=errors, warnings=warnings)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def validate_component(request: ValidationRequest) -> ValidationResponse:
|
||||||
|
"""Validate a component configuration"""
|
||||||
|
return ValidationService.validate(request.component)
|
@ -59,7 +59,7 @@ export const LoadingDots = ({ size = 8 }) => {
|
|||||||
|
|
||||||
export const TruncatableText = memo(
|
export const TruncatableText = memo(
|
||||||
({
|
({
|
||||||
content,
|
content = "",
|
||||||
isJson = false,
|
isJson = false,
|
||||||
className = "",
|
className = "",
|
||||||
jsonThreshold = 1000,
|
jsonThreshold = 1000,
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
"url": null,
|
"url": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "AutoGen Team",
|
"author": "AutoGen Team",
|
||||||
"created_at": "2025-02-09T09:43:30.164372",
|
"created_at": "2025-02-11T18:37:53.922275",
|
||||||
"updated_at": "2025-02-09T09:43:30.486369",
|
"updated_at": "2025-02-11T18:37:54.268540",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A default gallery containing basic components for human-in-loop conversations",
|
"description": "A default gallery containing basic components for human-in-loop conversations",
|
||||||
"tags": ["human-in-loop", "assistant", "web agents"],
|
"tags": ["human-in-loop", "assistant", "web agents"],
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"component_version": 1,
|
"component_version": 1,
|
||||||
"description": "A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
|
"description": "A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
|
||||||
"label": "Default Team",
|
"label": "RoundRobin Team",
|
||||||
"config": {
|
"config": {
|
||||||
"participants": [
|
"participants": [
|
||||||
{
|
{
|
||||||
@ -116,6 +116,158 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"provider": "autogen_agentchat.teams.SelectorGroupChat",
|
||||||
|
"component_type": "team",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "A team with 2 agents - an AssistantAgent (with a calculator tool) and a CriticAgent in a SelectorGroupChat team.",
|
||||||
|
"label": "Selector Team",
|
||||||
|
"config": {
|
||||||
|
"participants": [
|
||||||
|
{
|
||||||
|
"provider": "autogen_agentchat.agents.AssistantAgent",
|
||||||
|
"component_type": "agent",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "An agent that provides assistance with tool use.",
|
||||||
|
"label": "AssistantAgent",
|
||||||
|
"config": {
|
||||||
|
"name": "assistant_agent",
|
||||||
|
"model_client": {
|
||||||
|
"provider": "autogen_ext.models.openai.OpenAIChatCompletionClient",
|
||||||
|
"component_type": "model",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "Chat completion client for OpenAI hosted models.",
|
||||||
|
"label": "OpenAIChatCompletionClient",
|
||||||
|
"config": {
|
||||||
|
"model": "gpt-4o-mini"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"provider": "autogen_core.tools.FunctionTool",
|
||||||
|
"component_type": "tool",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "Create custom tools by wrapping standard Python functions.",
|
||||||
|
"label": "FunctionTool",
|
||||||
|
"config": {
|
||||||
|
"source_code": "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == \"+\":\n return str(a + b)\n elif operator == \"-\":\n return str(a - b)\n elif operator == \"*\":\n return str(a * b)\n elif operator == \"/\":\n if b == 0:\n return \"Error: Division by zero\"\n return str(a / b)\n else:\n return \"Error: Invalid operator. Please use +, -, *, or /\"\n except Exception as e:\n return f\"Error: {str(e)}\"\n",
|
||||||
|
"name": "calculator",
|
||||||
|
"description": "A simple calculator that performs basic arithmetic operations",
|
||||||
|
"global_imports": [],
|
||||||
|
"has_cancellation_support": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handoffs": [],
|
||||||
|
"model_context": {
|
||||||
|
"provider": "autogen_core.model_context.UnboundedChatCompletionContext",
|
||||||
|
"component_type": "chat_completion_context",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "An unbounded chat completion context that keeps a view of the all the messages.",
|
||||||
|
"label": "UnboundedChatCompletionContext",
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
"description": "An agent that provides assistance with ability to use tools.",
|
||||||
|
"system_message": "You are a helpful assistant. Solve tasks carefully. When done, say TERMINATE.",
|
||||||
|
"model_client_stream": false,
|
||||||
|
"reflect_on_tool_use": false,
|
||||||
|
"tool_call_summary_format": "{result}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "autogen_agentchat.agents.AssistantAgent",
|
||||||
|
"component_type": "agent",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "An agent that provides assistance with tool use.",
|
||||||
|
"label": "AssistantAgent",
|
||||||
|
"config": {
|
||||||
|
"name": "critic_agent",
|
||||||
|
"model_client": {
|
||||||
|
"provider": "autogen_ext.models.openai.OpenAIChatCompletionClient",
|
||||||
|
"component_type": "model",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "Chat completion client for OpenAI hosted models.",
|
||||||
|
"label": "OpenAIChatCompletionClient",
|
||||||
|
"config": {
|
||||||
|
"model": "gpt-4o-mini"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": [],
|
||||||
|
"handoffs": [],
|
||||||
|
"model_context": {
|
||||||
|
"provider": "autogen_core.model_context.UnboundedChatCompletionContext",
|
||||||
|
"component_type": "chat_completion_context",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "An unbounded chat completion context that keeps a view of the all the messages.",
|
||||||
|
"label": "UnboundedChatCompletionContext",
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
"description": "an agent that critiques and improves the assistant's output",
|
||||||
|
"system_message": "You are a helpful assistant. Critique the assistant's output and suggest improvements.",
|
||||||
|
"model_client_stream": false,
|
||||||
|
"reflect_on_tool_use": false,
|
||||||
|
"tool_call_summary_format": "{result}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model_client": {
|
||||||
|
"provider": "autogen_ext.models.openai.OpenAIChatCompletionClient",
|
||||||
|
"component_type": "model",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "Chat completion client for OpenAI hosted models.",
|
||||||
|
"label": "OpenAIChatCompletionClient",
|
||||||
|
"config": {
|
||||||
|
"model": "gpt-4o-mini"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"termination_condition": {
|
||||||
|
"provider": "autogen_agentchat.base.OrTerminationCondition",
|
||||||
|
"component_type": "termination",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"label": "OrTerminationCondition",
|
||||||
|
"config": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"provider": "autogen_agentchat.conditions.TextMentionTermination",
|
||||||
|
"component_type": "termination",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "Terminate the conversation if a specific text is mentioned.",
|
||||||
|
"label": "TextMentionTermination",
|
||||||
|
"config": {
|
||||||
|
"text": "TERMINATE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "autogen_agentchat.conditions.MaxMessageTermination",
|
||||||
|
"component_type": "termination",
|
||||||
|
"version": 1,
|
||||||
|
"component_version": 1,
|
||||||
|
"description": "Terminate the conversation after a maximum number of messages have been exchanged.",
|
||||||
|
"label": "MaxMessageTermination",
|
||||||
|
"config": {
|
||||||
|
"max_messages": 10,
|
||||||
|
"include_agent_event": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selector_prompt": "You are in a role play game. The following roles are available:\n{roles}.\nRead the following conversation. Then select the next role from {participants} to play. Only return the role.\n\n{history}\n\nRead the above conversation. Then select the next role from {participants} to play. Only return the role.\n",
|
||||||
|
"allow_repeated_speaker": false,
|
||||||
|
"max_selector_attempts": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"provider": "autogen_agentchat.teams.SelectorGroupChat",
|
"provider": "autogen_agentchat.teams.SelectorGroupChat",
|
||||||
"component_type": "team",
|
"component_type": "team",
|
||||||
|
@ -151,7 +151,7 @@ export const useGalleryStore = create<GalleryStore>()(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "gallery-storage-v6",
|
name: "gallery-storage-v7",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -63,7 +63,7 @@ const RenderToolCall: React.FC<{ content: FunctionCall[] }> = ({ content }) => (
|
|||||||
Calling {call.name} tool with arguments
|
Calling {call.name} tool with arguments
|
||||||
</div>
|
</div>
|
||||||
<TruncatableText
|
<TruncatableText
|
||||||
content={JSON.stringify(JSON.parse(call.arguments), null, 2)}
|
content={JSON.stringify(call.arguments, null, 2)}
|
||||||
isJson={true}
|
isJson={true}
|
||||||
className="text-sm mt-1 bg-secondary p-2 rounded"
|
className="text-sm mt-1 bg-secondary p-2 rounded"
|
||||||
/>
|
/>
|
||||||
|
@ -60,7 +60,6 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
const showLLMEvents = useSettingsStore(
|
const showLLMEvents = useSettingsStore(
|
||||||
(state) => state.playground.showLLMEvents
|
(state) => state.playground.showLLMEvents
|
||||||
);
|
);
|
||||||
console.log("showLLMEvents", showLLMEvents);
|
|
||||||
|
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
if (showLLMEvents) {
|
if (showLLMEvents) {
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
import { Team, AgentConfig } from "../../types/datamodel";
|
import { Team, Component, ComponentConfig } from "../../types/datamodel";
|
||||||
import { getServerUrl } from "../../utils";
|
import { getServerUrl } from "../../utils";
|
||||||
|
|
||||||
|
interface ValidationError {
|
||||||
|
field: string;
|
||||||
|
error: string;
|
||||||
|
suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResponse {
|
||||||
|
is_valid: boolean;
|
||||||
|
errors: ValidationError[];
|
||||||
|
warnings: ValidationError[];
|
||||||
|
}
|
||||||
|
|
||||||
export class TeamAPI {
|
export class TeamAPI {
|
||||||
private getBaseUrl(): string {
|
private getBaseUrl(): string {
|
||||||
return getServerUrl();
|
return getServerUrl();
|
||||||
@ -77,51 +89,41 @@ export class TeamAPI {
|
|||||||
if (!data.status)
|
if (!data.status)
|
||||||
throw new Error(data.message || "Failed to link agent to team");
|
throw new Error(data.message || "Failed to link agent to team");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async linkAgentWithSequence(
|
// move validationapi to its own class
|
||||||
teamId: number,
|
|
||||||
agentId: number,
|
export class ValidationAPI {
|
||||||
sequenceId: number
|
private getBaseUrl(): string {
|
||||||
): Promise<void> {
|
return getServerUrl();
|
||||||
const response = await fetch(
|
}
|
||||||
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}/${sequenceId}`,
|
|
||||||
{
|
private getHeaders(): HeadersInit {
|
||||||
|
return {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateComponent(
|
||||||
|
component: Component<ComponentConfig>
|
||||||
|
): Promise<ValidationResponse> {
|
||||||
|
const response = await fetch(`${this.getBaseUrl()}/validate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
}
|
body: JSON.stringify({
|
||||||
);
|
component: component,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!data.status)
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(data.message || "Failed to validate component");
|
||||||
data.message || "Failed to link agent to team with sequence"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlinkAgent(teamId: number, agentId: number): Promise<void> {
|
return data;
|
||||||
const response = await fetch(
|
|
||||||
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}`,
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
headers: this.getHeaders(),
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
if (!data.status)
|
|
||||||
throw new Error(data.message || "Failed to unlink agent from team");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTeamAgents(teamId: number): Promise<AgentConfig[]> {
|
export const validationAPI = new ValidationAPI();
|
||||||
const response = await fetch(
|
|
||||||
`${this.getBaseUrl()}/teams/${teamId}/agents`,
|
|
||||||
{
|
|
||||||
headers: this.getHeaders(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
if (!data.status)
|
|
||||||
throw new Error(data.message || "Failed to fetch team agents");
|
|
||||||
return data.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const teamAPI = new TeamAPI();
|
export const teamAPI = new TeamAPI();
|
||||||
|
@ -27,7 +27,16 @@ import {
|
|||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { Button, Layout, message, Modal, Switch, Tooltip } from "antd";
|
import { Button, Layout, message, Modal, Switch, Tooltip } from "antd";
|
||||||
import { Cable, Code2, Download, PlayCircle, Save } from "lucide-react";
|
import {
|
||||||
|
Cable,
|
||||||
|
CheckCircle,
|
||||||
|
CircleX,
|
||||||
|
Code2,
|
||||||
|
Download,
|
||||||
|
ListCheck,
|
||||||
|
PlayCircle,
|
||||||
|
Save,
|
||||||
|
} from "lucide-react";
|
||||||
import { useTeamBuilderStore } from "./store";
|
import { useTeamBuilderStore } from "./store";
|
||||||
import { ComponentLibrary } from "./library";
|
import { ComponentLibrary } from "./library";
|
||||||
import { ComponentTypes, Team, Session } from "../../../types/datamodel";
|
import { ComponentTypes, Team, Session } from "../../../types/datamodel";
|
||||||
@ -43,6 +52,9 @@ import debounce from "lodash.debounce";
|
|||||||
import { appContext } from "../../../../hooks/provider";
|
import { appContext } from "../../../../hooks/provider";
|
||||||
import { sessionAPI } from "../../playground/api";
|
import { sessionAPI } from "../../playground/api";
|
||||||
import TestDrawer from "./testdrawer";
|
import TestDrawer from "./testdrawer";
|
||||||
|
import { teamAPI, validationAPI, ValidationResponse } from "../api";
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { ValidationErrors } from "./validationerrors";
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
const { Sider, Content } = Layout;
|
||||||
interface DragItemData {
|
interface DragItemData {
|
||||||
@ -76,6 +88,10 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
const [activeDragItem, setActiveDragItem] = useState<DragItemData | null>(
|
const [activeDragItem, setActiveDragItem] = useState<DragItemData | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [validationResults, setValidationResults] =
|
||||||
|
useState<ValidationResponse | null>(null);
|
||||||
|
|
||||||
|
const [validationLoading, setValidationLoading] = useState(false);
|
||||||
|
|
||||||
const [testDrawerVisible, setTestDrawerVisible] = useState(false);
|
const [testDrawerVisible, setTestDrawerVisible] = useState(false);
|
||||||
|
|
||||||
@ -145,6 +161,12 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
setNodes(initialNodes);
|
setNodes(initialNodes);
|
||||||
setEdges(initialEdges);
|
setEdges(initialEdges);
|
||||||
}
|
}
|
||||||
|
handleValidate();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log("cleanup component");
|
||||||
|
setValidationResults(null);
|
||||||
|
};
|
||||||
}, [team, setNodes, setEdges]);
|
}, [team, setNodes, setEdges]);
|
||||||
|
|
||||||
// Handle JSON changes
|
// Handle JSON changes
|
||||||
@ -167,9 +189,32 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
handleJsonChange.cancel();
|
handleJsonChange.cancel();
|
||||||
|
setValidationResults(null);
|
||||||
};
|
};
|
||||||
}, [handleJsonChange]);
|
}, [handleJsonChange]);
|
||||||
|
|
||||||
|
const handleValidate = useCallback(async () => {
|
||||||
|
const component = syncToJson();
|
||||||
|
if (!component) {
|
||||||
|
throw new Error("Unable to generate valid configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setValidationLoading(true);
|
||||||
|
const validationResult = await validationAPI.validateComponent(component);
|
||||||
|
|
||||||
|
setValidationResults(validationResult);
|
||||||
|
// if (validationResult.is_valid) {
|
||||||
|
// messageApi.success("Validation successful");
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Validation error:", error);
|
||||||
|
messageApi.error("Validation failed");
|
||||||
|
} finally {
|
||||||
|
setValidationLoading(false);
|
||||||
|
}
|
||||||
|
}, [syncToJson]);
|
||||||
|
|
||||||
// Handle save
|
// Handle save
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -291,6 +336,8 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
setTestDrawerVisible(false);
|
setTestDrawerVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const teamValidated = validationResults && validationResults.is_valid;
|
||||||
|
|
||||||
const onDragStart = (item: DragItem) => {
|
const onDragStart = (item: DragItem) => {
|
||||||
// We can add any drag start logic here if needed
|
// We can add any drag start logic here if needed
|
||||||
};
|
};
|
||||||
@ -303,6 +350,7 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
|
|
||||||
<div className="flex gap-2 text-xs rounded border-dashed border p-2 mb-2 items-center">
|
<div className="flex gap-2 text-xs rounded border-dashed border p-2 mb-2 items-center">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Switch
|
<Switch
|
||||||
@ -319,36 +367,16 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
<Code2 className="w-3 h-3 mt-1 inline-block mr-1" />
|
<Code2 className="w-3 h-3 mt-1 inline-block mr-1" />
|
||||||
</div>
|
</div>
|
||||||
/>
|
/>
|
||||||
{isJsonMode ? (
|
{isJsonMode ? "View JSON" : <>Visual Builder</>}{" "}
|
||||||
"JSON "
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Visual builder{" "}
|
|
||||||
{/* <span className="text-xs text-orange-500 border border-orange-400 rounded-lg px-2 mx-1">
|
|
||||||
{" "}
|
|
||||||
experimental{" "}
|
|
||||||
</span> */}
|
|
||||||
</>
|
|
||||||
)}{" "}
|
|
||||||
mode{" "}
|
|
||||||
<span className="text-xs text-orange-500 ml-1 underline">
|
|
||||||
{" "}
|
|
||||||
(experimental)
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Tooltip title="Test Team">
|
{validationResults && !validationResults.is_valid && (
|
||||||
<Button
|
<div className="inline-block mr-2">
|
||||||
type="primary"
|
{" "}
|
||||||
icon={<PlayCircle size={18} />}
|
<ValidationErrors validation={validationResults} />
|
||||||
className="p-1.5 mr-2 px-2.5 hover:bg-primary/10 rounded-md text-primary/75 hover:text-primary"
|
</div>
|
||||||
onClick={() => {
|
)}
|
||||||
setTestDrawerVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Test Team
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Download Team">
|
<Tooltip title="Download Team">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@ -383,6 +411,59 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
// disabled={!isDirty}
|
// disabled={!isDirty}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title=<div>
|
||||||
|
Validate Team
|
||||||
|
{validationResults && (
|
||||||
|
<div className="text-xs text-center my-1">
|
||||||
|
{teamValidated ? (
|
||||||
|
<span>
|
||||||
|
<CheckCircle className="w-3 h-3 text-green-500 inline-block mr-1" />
|
||||||
|
success
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className="">
|
||||||
|
<CircleX className="w-3 h-3 text-red-500 inline-block mr-1" />
|
||||||
|
errors
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
loading={validationLoading}
|
||||||
|
icon={
|
||||||
|
<div className="relative">
|
||||||
|
<ListCheck size={18} />
|
||||||
|
{validationResults && (
|
||||||
|
<div
|
||||||
|
className={` ${
|
||||||
|
teamValidated ? "bg-green-500" : "bg-red-500"
|
||||||
|
} absolute top-0 right-0 w-2 h-2 rounded-full`}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className="p-1.5 hover:bg-primary/10 rounded-md text-primary/75 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleValidate}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Run Team">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlayCircle size={18} />}
|
||||||
|
className="p-1.5 ml-2 px-2.5 hover:bg-primary/10 rounded-md text-primary/75 hover:text-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setTestDrawerVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DndContext
|
<DndContext
|
||||||
|
@ -392,9 +392,9 @@ export const AgentNode = memo<NodeProps<CustomNode>>((props) => {
|
|||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{component.config.model_client && (
|
{component.config?.model_client && (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{component.config.model_client.config.model}
|
{component.config?.model_client.config?.model}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<DroppableZone
|
<DroppableZone
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AlertTriangle, XCircle, X } from "lucide-react";
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
import { ValidationResponse } from "../api";
|
||||||
|
|
||||||
|
interface ValidationErrorViewProps {
|
||||||
|
validation: ValidationResponse;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ValidationErrorView: React.FC<ValidationErrorViewProps> = ({
|
||||||
|
validation,
|
||||||
|
onClose,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
style={{ zIndex: 1000 }}
|
||||||
|
className="fixed inset-0 bg-black/80 flex items-center justify-center transition-opacity duration-300"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative bg-primary w-full h-full md:w-4/5 md:h-4/5 md:rounded-lg p-8 overflow-auto"
|
||||||
|
style={{ opacity: 0.95 }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Tooltip title="Close">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 p-2 rounded-full bg-tertiary hover:bg-secondary text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<XCircle size={20} className="text-red-500" />
|
||||||
|
<h3 className="text-lg font-medium">Validation Issues</h3>
|
||||||
|
<h4 className="text-sm text-secondary">
|
||||||
|
{validation.errors.length} errors • {validation.warnings.length}{" "}
|
||||||
|
warnings
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Errors Section */}
|
||||||
|
{validation.errors.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Errors</h4>
|
||||||
|
{validation.errors.map((error, idx) => (
|
||||||
|
<div key={idx} className="p-4 bg-tertiary rounded-lg">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<XCircle className="h-4 w-4 text-red-500 shrink-0 mt-1" />
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium uppercase text-secondary mb-1">
|
||||||
|
{error.field}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">{error.error}</div>
|
||||||
|
{error.suggestion && (
|
||||||
|
<div className="text-sm mt-2 text-secondary">
|
||||||
|
Suggestion: {error.suggestion}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Warnings Section */}
|
||||||
|
{validation.warnings.length > 0 && (
|
||||||
|
<div className="space-y-2 mt-6">
|
||||||
|
<h4 className="text-sm font-medium">Warnings</h4>
|
||||||
|
{validation.warnings.map((warning, idx) => (
|
||||||
|
<div key={idx} className="p-4 bg-tertiary rounded-lg">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-500 shrink-0 mt-1" />
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium uppercase text-secondary mb-1">
|
||||||
|
{warning.field}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">{warning.error}</div>
|
||||||
|
{warning.suggestion && (
|
||||||
|
<div className="text-sm mt-2 text-secondary">
|
||||||
|
Suggestion: {warning.suggestion}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ValidationErrorsProps {
|
||||||
|
validation: ValidationResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidationErrors: React.FC<ValidationErrorsProps> = ({
|
||||||
|
validation,
|
||||||
|
}) => {
|
||||||
|
const [showFullView, setShowFullView] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 py-2 px-3 bg-secondary rounded text-sm text-secondary hover:text-primary transition-colors group cursor-pointer"
|
||||||
|
onClick={() => setShowFullView(true)}
|
||||||
|
>
|
||||||
|
<XCircle size={14} className="text-red-500" />
|
||||||
|
<span className="flex-1">
|
||||||
|
{validation.errors.length} errors • {validation.warnings.length}{" "}
|
||||||
|
warnings
|
||||||
|
</span>
|
||||||
|
<AlertTriangle size={14} className="group-hover:text-accent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showFullView && (
|
||||||
|
<ValidationErrorView
|
||||||
|
validation={validation}
|
||||||
|
onClose={() => setShowFullView(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from asyncio import Event
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -16,8 +17,6 @@ from autogen_core import (
|
|||||||
)
|
)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from asyncio import Event
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MessageType: ...
|
class MessageType: ...
|
||||||
|
Loading…
x
Reference in New Issue
Block a user