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:
Victor Dibia 2025-02-12 09:29:43 -08:00 committed by GitHub
parent 07fdc4e2da
commit f49f159a43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 640 additions and 82 deletions

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -59,7 +59,7 @@ export const LoadingDots = ({ size = 8 }) => {
export const TruncatableText = memo(
({
content,
content = "",
isJson = false,
className = "",
jsonThreshold = 1000,

View File

@ -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",

View File

@ -151,7 +151,7 @@ export const useGalleryStore = create<GalleryStore>()(
},
}),
{
name: "gallery-storage-v6",
name: "gallery-storage-v7",
}
)
);

View File

@ -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"
/>

View File

@ -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) {

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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)}
/>
)}
</>
);
};

View File

@ -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: ...