Skip to content

Commit f49f159

Browse files
authored
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.
1 parent 07fdc4e commit f49f159

File tree

14 files changed

+640
-82
lines changed

14 files changed

+640
-82
lines changed

python/packages/autogen-core/src/autogen_core/_runtime_impl_helpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections import defaultdict
2-
from typing import Awaitable, Callable, DefaultDict, List, Set, Sequence
2+
from typing import Awaitable, Callable, DefaultDict, List, Sequence, Set
33

44
from ._agent import Agent
55
from ._agent_id import AgentId

python/packages/autogen-studio/autogenstudio/gallery/builder.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def create_default_gallery() -> Gallery:
183183
model_client=base_model,
184184
tools=[tools.calculator_tool],
185185
)
186+
186187
builder.add_agent(
187188
calc_assistant.dump_component(), description="An agent that provides assistance with ability to use tools."
188189
)
@@ -200,10 +201,25 @@ def create_default_gallery() -> Gallery:
200201
calc_team = RoundRobinGroupChat(participants=[calc_assistant], termination_condition=calc_or_term)
201202
builder.add_team(
202203
calc_team.dump_component(),
203-
label="Default Team",
204+
label="RoundRobin Team",
204205
description="A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
205206
)
206207

208+
critic_agent = AssistantAgent(
209+
name="critic_agent",
210+
system_message="You are a helpful assistant. Critique the assistant's output and suggest improvements.",
211+
description="an agent that critiques and improves the assistant's output",
212+
model_client=base_model,
213+
)
214+
selector_default_team = SelectorGroupChat(
215+
participants=[calc_assistant, critic_agent], termination_condition=calc_or_term, model_client=base_model
216+
)
217+
builder.add_team(
218+
selector_default_team.dump_component(),
219+
label="Selector Team",
220+
description="A team with 2 agents - an AssistantAgent (with a calculator tool) and a CriticAgent in a SelectorGroupChat team.",
221+
)
222+
207223
# Create web surfer agent
208224
websurfer_agent = MultimodalWebSurfer(
209225
name="websurfer_agent",

python/packages/autogen-studio/autogenstudio/web/app.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .config import settings
1414
from .deps import cleanup_managers, init_managers
1515
from .initialization import AppInitializer
16-
from .routes import runs, sessions, teams, ws
16+
from .routes import runs, sessions, teams, validation, ws
1717

1818
# Initialize application
1919
app_file_path = os.path.dirname(os.path.abspath(__file__))
@@ -107,6 +107,12 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
107107
responses={404: {"description": "Not found"}},
108108
)
109109

110+
api.include_router(
111+
validation.router,
112+
prefix="/validate",
113+
tags=["validation"],
114+
responses={404: {"description": "Not found"}},
115+
)
110116

111117
# Version endpoint
112118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# api/routes/validation.py
2+
import importlib
3+
from typing import Any, Dict, List, Optional
4+
5+
from autogen_core import ComponentModel, is_component_class
6+
from fastapi import APIRouter, HTTPException
7+
from pydantic import BaseModel
8+
9+
router = APIRouter()
10+
11+
12+
class ValidationRequest(BaseModel):
13+
component: Dict[str, Any]
14+
15+
16+
class ValidationError(BaseModel):
17+
field: str
18+
error: str
19+
suggestion: Optional[str] = None
20+
21+
22+
class ValidationResponse(BaseModel):
23+
is_valid: bool
24+
errors: List[ValidationError] = []
25+
warnings: List[ValidationError] = []
26+
27+
28+
class ValidationService:
29+
@staticmethod
30+
def validate_provider(provider: str) -> Optional[ValidationError]:
31+
"""Validate that the provider exists and can be imported"""
32+
try:
33+
if provider in ["azure_openai_chat_completion_client", "AzureOpenAIChatCompletionClient"]:
34+
provider = "autogen_ext.models.openai.AzureOpenAIChatCompletionClient"
35+
elif provider in ["openai_chat_completion_client", "OpenAIChatCompletionClient"]:
36+
provider = "autogen_ext.models.openai.OpenAIChatCompletionClient"
37+
38+
module_path, class_name = provider.rsplit(".", maxsplit=1)
39+
module = importlib.import_module(module_path)
40+
component_class = getattr(module, class_name)
41+
42+
if not is_component_class(component_class):
43+
return ValidationError(
44+
field="provider",
45+
error=f"Class {provider} is not a valid component class",
46+
suggestion="Ensure the class inherits from Component and implements required methods",
47+
)
48+
return None
49+
except ImportError:
50+
return ValidationError(
51+
field="provider",
52+
error=f"Could not import provider {provider}",
53+
suggestion="Check that the provider module is installed and the path is correct",
54+
)
55+
except Exception as e:
56+
return ValidationError(
57+
field="provider",
58+
error=f"Error validating provider: {str(e)}",
59+
suggestion="Check the provider string format and class implementation",
60+
)
61+
62+
@staticmethod
63+
def validate_component_type(component: Dict[str, Any]) -> Optional[ValidationError]:
64+
"""Validate the component type"""
65+
if "component_type" not in component:
66+
return ValidationError(
67+
field="component_type",
68+
error="Component type is missing",
69+
suggestion="Add a component_type field to the component configuration",
70+
)
71+
return None
72+
73+
@staticmethod
74+
def validate_config_schema(component: Dict[str, Any]) -> List[ValidationError]:
75+
"""Validate the component configuration against its schema"""
76+
errors = []
77+
try:
78+
# Convert to ComponentModel for initial validation
79+
model = ComponentModel(**component)
80+
81+
# Get the component class
82+
provider = model.provider
83+
module_path, class_name = provider.rsplit(".", maxsplit=1)
84+
module = importlib.import_module(module_path)
85+
component_class = getattr(module, class_name)
86+
87+
# Validate against component's schema
88+
if hasattr(component_class, "component_config_schema"):
89+
try:
90+
component_class.component_config_schema.model_validate(model.config)
91+
except Exception as e:
92+
errors.append(
93+
ValidationError(
94+
field="config",
95+
error=f"Config validation failed: {str(e)}",
96+
suggestion="Check that the config matches the component's schema",
97+
)
98+
)
99+
else:
100+
errors.append(
101+
ValidationError(
102+
field="config",
103+
error="Component class missing config schema",
104+
suggestion="Implement component_config_schema in the component class",
105+
)
106+
)
107+
except Exception as e:
108+
errors.append(
109+
ValidationError(
110+
field="config",
111+
error=f"Schema validation error: {str(e)}",
112+
suggestion="Check the component configuration format",
113+
)
114+
)
115+
return errors
116+
117+
@staticmethod
118+
def validate_instantiation(component: Dict[str, Any]) -> Optional[ValidationError]:
119+
"""Validate that the component can be instantiated"""
120+
try:
121+
model = ComponentModel(**component)
122+
# Attempt to load the component
123+
module_path, class_name = model.provider.rsplit(".", maxsplit=1)
124+
module = importlib.import_module(module_path)
125+
component_class = getattr(module, class_name)
126+
component_class.load_component(model)
127+
return None
128+
except Exception as e:
129+
return ValidationError(
130+
field="instantiation",
131+
error=f"Failed to instantiate component: {str(e)}",
132+
suggestion="Check that the component can be properly instantiated with the given config",
133+
)
134+
135+
@classmethod
136+
def validate(cls, component: Dict[str, Any]) -> ValidationResponse:
137+
"""Validate a component configuration"""
138+
errors = []
139+
warnings = []
140+
141+
# Check provider
142+
if provider_error := cls.validate_provider(component.get("provider", "")):
143+
errors.append(provider_error)
144+
145+
# Check component type
146+
if type_error := cls.validate_component_type(component):
147+
errors.append(type_error)
148+
149+
# Validate schema
150+
schema_errors = cls.validate_config_schema(component)
151+
errors.extend(schema_errors)
152+
153+
# Only attempt instantiation if no errors so far
154+
if not errors:
155+
if inst_error := cls.validate_instantiation(component):
156+
errors.append(inst_error)
157+
158+
# Check for version warnings
159+
if "version" not in component:
160+
warnings.append(
161+
ValidationError(
162+
field="version",
163+
error="Component version not specified",
164+
suggestion="Consider adding a version to ensure compatibility",
165+
)
166+
)
167+
168+
return ValidationResponse(is_valid=len(errors) == 0, errors=errors, warnings=warnings)
169+
170+
171+
@router.post("/")
172+
async def validate_component(request: ValidationRequest) -> ValidationResponse:
173+
"""Validate a component configuration"""
174+
return ValidationService.validate(request.component)

python/packages/autogen-studio/frontend/src/components/views/atoms.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const LoadingDots = ({ size = 8 }) => {
5959

6060
export const TruncatableText = memo(
6161
({
62-
content,
62+
content = "",
6363
isJson = false,
6464
className = "",
6565
jsonThreshold = 1000,

0 commit comments

Comments
 (0)