mirror of https://github.com/microsoft/autogen.git
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 typing import Awaitable, Callable, DefaultDict, List, Set, Sequence
|
||||
from typing import Awaitable, Callable, DefaultDict, List, Sequence, Set
|
||||
|
||||
from ._agent import Agent
|
||||
from ._agent_id import AgentId
|
||||
|
|
|
@ -183,6 +183,7 @@ def create_default_gallery() -> Gallery:
|
|||
model_client=base_model,
|
||||
tools=[tools.calculator_tool],
|
||||
)
|
||||
|
||||
builder.add_agent(
|
||||
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)
|
||||
builder.add_team(
|
||||
calc_team.dump_component(),
|
||||
label="Default Team",
|
||||
label="RoundRobin 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
|
||||
websurfer_agent = MultimodalWebSurfer(
|
||||
name="websurfer_agent",
|
||||
|
|
|
@ -13,7 +13,7 @@ from ..version import VERSION
|
|||
from .config import settings
|
||||
from .deps import cleanup_managers, init_managers
|
||||
from .initialization import AppInitializer
|
||||
from .routes import runs, sessions, teams, ws
|
||||
from .routes import runs, sessions, teams, validation, ws
|
||||
|
||||
# Initialize application
|
||||
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
@ -107,6 +107,12 @@ api.include_router(
|
|||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
api.include_router(
|
||||
validation.router,
|
||||
prefix="/validate",
|
||||
tags=["validation"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
# 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(
|
||||
({
|
||||
content,
|
||||
content = "",
|
||||
isJson = false,
|
||||
className = "",
|
||||
jsonThreshold = 1000,
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
"url": null,
|
||||
"metadata": {
|
||||
"author": "AutoGen Team",
|
||||
"created_at": "2025-02-09T09:43:30.164372",
|
||||
"updated_at": "2025-02-09T09:43:30.486369",
|
||||
"created_at": "2025-02-11T18:37:53.922275",
|
||||
"updated_at": "2025-02-11T18:37:54.268540",
|
||||
"version": "1.0.0",
|
||||
"description": "A default gallery containing basic components for human-in-loop conversations",
|
||||
"tags": ["human-in-loop", "assistant", "web agents"],
|
||||
|
@ -22,7 +22,7 @@
|
|||
"version": 1,
|
||||
"component_version": 1,
|
||||
"description": "A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
|
||||
"label": "Default Team",
|
||||
"label": "RoundRobin Team",
|
||||
"config": {
|
||||
"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",
|
||||
"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
|
||||
</div>
|
||||
<TruncatableText
|
||||
content={JSON.stringify(JSON.parse(call.arguments), null, 2)}
|
||||
content={JSON.stringify(call.arguments, null, 2)}
|
||||
isJson={true}
|
||||
className="text-sm mt-1 bg-secondary p-2 rounded"
|
||||
/>
|
||||
|
|
|
@ -60,7 +60,6 @@ const RunView: React.FC<RunViewProps> = ({
|
|||
const showLLMEvents = useSettingsStore(
|
||||
(state) => state.playground.showLLMEvents
|
||||
);
|
||||
console.log("showLLMEvents", showLLMEvents);
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
if (showLLMEvents) {
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
import { Team, AgentConfig } from "../../types/datamodel";
|
||||
import { Team, Component, ComponentConfig } from "../../types/datamodel";
|
||||
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 {
|
||||
private getBaseUrl(): string {
|
||||
return getServerUrl();
|
||||
|
@ -77,51 +89,41 @@ export class TeamAPI {
|
|||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to link agent to team");
|
||||
}
|
||||
}
|
||||
|
||||
async linkAgentWithSequence(
|
||||
teamId: number,
|
||||
agentId: number,
|
||||
sequenceId: number
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}/${sequenceId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(
|
||||
data.message || "Failed to link agent to team with sequence"
|
||||
);
|
||||
// move validationapi to its own class
|
||||
|
||||
export class ValidationAPI {
|
||||
private getBaseUrl(): string {
|
||||
return getServerUrl();
|
||||
}
|
||||
|
||||
async unlinkAgent(teamId: number, agentId: number): Promise<void> {
|
||||
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");
|
||||
private getHeaders(): HeadersInit {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
async getTeamAgents(teamId: number): Promise<AgentConfig[]> {
|
||||
const response = await fetch(
|
||||
`${this.getBaseUrl()}/teams/${teamId}/agents`,
|
||||
{
|
||||
headers: this.getHeaders(),
|
||||
}
|
||||
);
|
||||
async validateComponent(
|
||||
component: Component<ComponentConfig>
|
||||
): Promise<ValidationResponse> {
|
||||
const response = await fetch(`${this.getBaseUrl()}/validate`, {
|
||||
method: "POST",
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
component: component,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.status)
|
||||
throw new Error(data.message || "Failed to fetch team agents");
|
||||
return data.data;
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "Failed to validate component");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export const validationAPI = new ValidationAPI();
|
||||
|
||||
export const teamAPI = new TeamAPI();
|
||||
|
|
|
@ -27,7 +27,16 @@ import {
|
|||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
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 { ComponentLibrary } from "./library";
|
||||
import { ComponentTypes, Team, Session } from "../../../types/datamodel";
|
||||
|
@ -43,6 +52,9 @@ import debounce from "lodash.debounce";
|
|||
import { appContext } from "../../../../hooks/provider";
|
||||
import { sessionAPI } from "../../playground/api";
|
||||
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;
|
||||
interface DragItemData {
|
||||
|
@ -76,6 +88,10 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
const [activeDragItem, setActiveDragItem] = useState<DragItemData | null>(
|
||||
null
|
||||
);
|
||||
const [validationResults, setValidationResults] =
|
||||
useState<ValidationResponse | null>(null);
|
||||
|
||||
const [validationLoading, setValidationLoading] = useState(false);
|
||||
|
||||
const [testDrawerVisible, setTestDrawerVisible] = useState(false);
|
||||
|
||||
|
@ -145,6 +161,12 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
setNodes(initialNodes);
|
||||
setEdges(initialEdges);
|
||||
}
|
||||
handleValidate();
|
||||
|
||||
return () => {
|
||||
console.log("cleanup component");
|
||||
setValidationResults(null);
|
||||
};
|
||||
}, [team, setNodes, setEdges]);
|
||||
|
||||
// Handle JSON changes
|
||||
|
@ -167,9 +189,32 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
useEffect(() => {
|
||||
return () => {
|
||||
handleJsonChange.cancel();
|
||||
setValidationResults(null);
|
||||
};
|
||||
}, [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
|
||||
const handleSave = useCallback(async () => {
|
||||
try {
|
||||
|
@ -291,6 +336,8 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
setTestDrawerVisible(false);
|
||||
};
|
||||
|
||||
const teamValidated = validationResults && validationResults.is_valid;
|
||||
|
||||
const onDragStart = (item: DragItem) => {
|
||||
// We can add any drag start logic here if needed
|
||||
};
|
||||
|
@ -303,6 +350,7 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
|
||||
<div className="flex gap-2 text-xs rounded border-dashed border p-2 mb-2 items-center">
|
||||
<div className="flex-1">
|
||||
<Switch
|
||||
|
@ -319,36 +367,16 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
<Code2 className="w-3 h-3 mt-1 inline-block mr-1" />
|
||||
</div>
|
||||
/>
|
||||
{isJsonMode ? (
|
||||
"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>
|
||||
{isJsonMode ? "View JSON" : <>Visual Builder</>}{" "}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Tooltip title="Test Team">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircle size={18} />}
|
||||
className="p-1.5 mr-2 px-2.5 hover:bg-primary/10 rounded-md text-primary/75 hover:text-primary"
|
||||
onClick={() => {
|
||||
setTestDrawerVisible(true);
|
||||
}}
|
||||
>
|
||||
Test Team
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{validationResults && !validationResults.is_valid && (
|
||||
<div className="inline-block mr-2">
|
||||
{" "}
|
||||
<ValidationErrors validation={validationResults} />
|
||||
</div>
|
||||
)}
|
||||
<Tooltip title="Download Team">
|
||||
<Button
|
||||
type="text"
|
||||
|
@ -383,6 +411,59 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||
// disabled={!isDirty}
|
||||
/>
|
||||
</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>
|
||||
<DndContext
|
||||
|
|
|
@ -392,9 +392,9 @@ export const AgentNode = memo<NodeProps<CustomNode>>((props) => {
|
|||
/> */}
|
||||
|
||||
<div className="relative">
|
||||
{component.config.model_client && (
|
||||
{component.config?.model_client && (
|
||||
<div className="text-sm">
|
||||
{component.config.model_client.config.model}
|
||||
{component.config?.model_client.config?.model}
|
||||
</div>
|
||||
)}
|
||||
<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 asyncio import Event
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
@ -16,8 +17,6 @@ from autogen_core import (
|
|||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
from asyncio import Event
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageType: ...
|
||||
|
|
Loading…
Reference in New Issue