AGS - Test Model Component in UI, Compare Sessions (#5963)

<!-- 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?

- Adds ability to test model clients in UI after configrations, before
they are used in agents or teams
- Adds UI side by side comparison of sessions
<img width="1878" alt="image"
src="https://github.com/user-attachments/assets/f792d8d6-3f5d-4d8c-a365-5a9e9c00f49e"
/>
<img width="1877" alt="image"
src="https://github.com/user-attachments/assets/5a115a5a-95e1-4956-a733-5f0065711fe3"
/>


<!-- Please give a short summary of the change and the problem this
solves. -->

## Related issue number

<!-- For example: "Closes #1234" -->

Closes #4273 
Closes #5728

## Checks

- [ ] I've included any doc changes needed for
<https://microsoft.github.io/autogen/>. See
<https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> 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-03-17 11:00:46 -07:00 committed by GitHub
parent 685142cf51
commit 8f8ee0478a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1001 additions and 1538 deletions

View File

@ -1,3 +1,4 @@
import json
import threading
from datetime import datetime
from pathlib import Path
@ -12,6 +13,13 @@ from ..teammanager import TeamManager
from .schema_manager import SchemaManager
class CustomJSONEncoder(json.JSONEncoder):
def default(self, obj):
if hasattr(obj, "get_secret_value") and callable(obj.get_secret_value):
return obj.get_secret_value()
return super().default(obj)
class DatabaseManager:
_init_lock = threading.Lock()
@ -29,7 +37,9 @@ class DatabaseManager:
if base_dir is not None and isinstance(base_dir, str):
base_dir = Path(base_dir)
self.engine = create_engine(engine_uri, connect_args=connection_args)
self.engine = create_engine(
engine_uri, connect_args=connection_args, json_serializer=lambda obj: json.dumps(obj, cls=CustomJSONEncoder)
)
self.schema_manager = SchemaManager(
engine=self.engine,
base_dir=base_dir,
@ -163,7 +173,7 @@ class DatabaseManager:
model.updated_at = datetime.now()
for key, value in model.model_dump().items():
setattr(existing_model, key, value)
model = existing_model # Use the updated existing model
model = existing_model
session.add(model)
else:
session.add(model)

View File

@ -5,7 +5,7 @@ from enum import Enum
from typing import List, Optional, Union
from autogen_core import ComponentModel
from pydantic import ConfigDict
from pydantic import ConfigDict, SecretStr
from sqlalchemy import ForeignKey, Integer, String
from sqlmodel import JSON, Column, DateTime, Field, SQLModel, func
@ -141,7 +141,12 @@ class Gallery(SQLModel, table=True):
sa_column=Column(JSON),
)
model_config = ConfigDict(json_encoders={datetime: lambda v: v.isoformat()}) # type: ignore[call-arg]
model_config = ConfigDict(
json_encoders={
datetime: lambda v: v.isoformat(),
SecretStr: lambda v: v.get_secret_value(), # Add this line
}
) # type: ignore[call-arg]
class Settings(SQLModel, table=True):

View File

@ -6,7 +6,7 @@ from autogen_agentchat.base import TaskResult
from autogen_agentchat.messages import BaseChatMessage
from autogen_core import ComponentModel
from autogen_ext.models.openai import OpenAIChatCompletionClient
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, SecretStr
class MessageConfig(BaseModel):
@ -71,9 +71,7 @@ class GalleryConfig(BaseModel):
components: GalleryComponents
model_config = ConfigDict(
json_encoders={
datetime: lambda v: v.isoformat(),
}
json_encoders={datetime: lambda v: v.isoformat(), SecretStr: lambda v: v.get_secret_value()}
)

View File

@ -1,6 +1,8 @@
import os
from datetime import datetime
from typing import List, Optional
import anthropic
from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat
@ -8,6 +10,7 @@ from autogen_core import ComponentModel
from autogen_core.models import ModelInfo
from autogen_ext.agents.web_surfer import MultimodalWebSurfer
from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor
from autogen_ext.models.anthropic import AnthropicChatCompletionClient
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.models.openai._openai_client import AzureOpenAIChatCompletionClient
from autogen_ext.tools.code_execution import PythonCodeExecutionTool
@ -132,6 +135,12 @@ class GalleryBuilder:
def create_default_gallery() -> GalleryConfig:
"""Create a default gallery with all components including calculator and web surfer teams."""
# model clients require API keys to be set in the environment or passed in
# as arguments. For testing purposes, we set them to "test" if not already set.
for key in ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY", "ANTHROPIC_API_KEY"]:
if not os.environ.get(key):
os.environ[key] = "test"
# url = "https://raw.githubusercontent.com/microsoft/autogen/refs/heads/main/python/packages/autogen-studio/autogenstudio/gallery/default.json"
builder = GalleryBuilder(id="gallery_default", name="Default Component Gallery")
@ -149,7 +158,9 @@ def create_default_gallery() -> GalleryConfig:
mistral_vllm_model = OpenAIChatCompletionClient(
model="TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
base_url="http://localhost:1234/v1",
model_info=ModelInfo(vision=False, function_calling=True, json_output=False, family="unknown"),
model_info=ModelInfo(
vision=False, function_calling=True, json_output=False, family="unknown", structured_output=False
),
)
builder.add_model(
mistral_vllm_model.dump_component(),
@ -157,13 +168,20 @@ def create_default_gallery() -> GalleryConfig:
description="Local Mistral-7B model client for instruction-based generation (Ollama, LMStudio).",
)
anthropic_model = AnthropicChatCompletionClient(model="claude-3-7-sonnet-20250219")
builder.add_model(
anthropic_model.dump_component(),
label="Anthropic Claude-3-7",
description="Anthropic Claude-3 model client.",
)
# create an azure mode
az_model_client = AzureOpenAIChatCompletionClient(
azure_deployment="{your-azure-deployment}",
model="gpt-4o-mini",
api_version="2024-06-01",
azure_endpoint="https://{your-custom-endpoint}.openai.azure.com/",
api_key="sk-...", # For key-based authentication.
api_key="test",
)
builder.add_model(
az_model_client.dump_component(),

View File

@ -0,0 +1,188 @@
# api/validator/test_service.py
import asyncio
from typing import Any, Dict, List, Optional
from autogen_core import ComponentModel
from autogen_core.models import ChatCompletionClient, UserMessage
from pydantic import BaseModel
class ComponentTestResult(BaseModel):
status: bool
message: str
data: Optional[Any] = None
logs: List[str] = []
class ComponentTestRequest(BaseModel):
component: ComponentModel
model_client: Optional[Dict[str, Any]] = None
timeout: Optional[int] = 30
class ComponentTestService:
@staticmethod
async def test_agent(
component: ComponentModel, model_client: Optional[ChatCompletionClient] = None
) -> ComponentTestResult:
"""Test an agent component with a simple message"""
try:
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.messages import TextMessage
from autogen_core import CancellationToken
# If model_client is provided, use it; otherwise, use the component's model (if applicable)
agent_config = component.config or {}
# Try to load the agent
try:
# Construct the agent with the model client if provided
if model_client:
agent_config["model_client"] = model_client
agent = AssistantAgent(name=agent_config.get("name", "assistant"), **agent_config)
logs = ["Agent component loaded successfully"]
except Exception as e:
return ComponentTestResult(
status=False,
message=f"Failed to initialize agent: {str(e)}",
logs=[f"Agent initialization error: {str(e)}"],
)
# Test the agent with a simple message
test_question = "What is 2+2? Keep it brief."
try:
response = await agent.on_messages(
[TextMessage(content=test_question, source="user")],
cancellation_token=CancellationToken(),
)
# Check if we got a valid response
status = response and response.chat_message is not None
if status:
logs.append(
f"Agent responded with: {response.chat_message.content} to the question : {test_question}"
)
else:
logs.append("Agent did not return a valid response")
return ComponentTestResult(
status=status,
message="Agent test completed successfully" if status else "Agent test failed - no valid response",
data=response.chat_message.model_dump() if status else None,
logs=logs,
)
except Exception as e:
return ComponentTestResult(
status=False,
message=f"Error during agent response: {str(e)}",
logs=logs + [f"Agent response error: {str(e)}"],
)
except Exception as e:
return ComponentTestResult(
status=False, message=f"Error testing agent component: {str(e)}", logs=[f"Exception: {str(e)}"]
)
@staticmethod
async def test_model(
component: ComponentModel, model_client: Optional[ChatCompletionClient] = None
) -> ComponentTestResult:
"""Test a model component with a simple prompt"""
try:
# Use the component itself as a model client
model = ChatCompletionClient.load_component(component)
# Prepare a simple test message
test_question = "What is 2+2? Give me only the answer."
messages = [UserMessage(content=test_question, source="user")]
# Try to get a response
response = await model.create(messages=messages)
# Test passes if we got a response with content
status = response and response.content is not None
logs = ["Model component loaded successfully"]
if status:
logs.append(f"Model responded with: {response.content} (Query:{test_question})")
else:
logs.append("Model did not return a valid response")
return ComponentTestResult(
status=status,
message="Model test completed successfully" if status else "Model test failed - no valid response",
data=response.model_dump() if status else None,
logs=logs,
)
except Exception as e:
return ComponentTestResult(
status=False, message=f"Error testing model component: {str(e)}", logs=[f"Exception: {str(e)}"]
)
@staticmethod
async def test_tool(component: ComponentModel) -> ComponentTestResult:
"""Test a tool component with sample inputs"""
# Placeholder for tool test logic
return ComponentTestResult(
status=True, message="Tool test not yet implemented", logs=["Tool component loaded successfully"]
)
@staticmethod
async def test_team(
component: ComponentModel, model_client: Optional[ChatCompletionClient] = None
) -> ComponentTestResult:
"""Test a team component with a simple task"""
# Placeholder for team test logic
return ComponentTestResult(
status=True, message="Team test not yet implemented", logs=["Team component loaded successfully"]
)
@staticmethod
async def test_termination(component: ComponentModel) -> ComponentTestResult:
"""Test a termination component with sample message history"""
# Placeholder for termination test logic
return ComponentTestResult(
status=True,
message="Termination test not yet implemented",
logs=["Termination component loaded successfully"],
)
@classmethod
async def test_component(
cls, component: ComponentModel, timeout: int = 60, model_client: Optional[ChatCompletionClient] = None
) -> ComponentTestResult:
"""Test a component based on its type with appropriate test inputs"""
try:
# Get component type
component_type = component.component_type
# Select test method based on component type
test_method = {
"agent": cls.test_agent,
"model": cls.test_model,
"tool": cls.test_tool,
"team": cls.test_team,
"termination": cls.test_termination,
}.get(component_type or "unknown")
if not test_method:
return ComponentTestResult(status=False, message=f"Unknown component type: {component_type}")
# Determine if the test method accepts a model_client parameter
accepts_model_client = component_type in ["agent", "model", "team"]
# Run test with timeout
try:
if accepts_model_client:
result = await asyncio.wait_for(test_method(component, model_client), timeout=timeout)
else:
result = await asyncio.wait_for(test_method(component), timeout=timeout)
return result
except asyncio.TimeoutError:
return ComponentTestResult(status=False, message=f"Component test exceeded the {timeout}s timeout")
except Exception as e:
return ComponentTestResult(status=False, message=f"Error testing component: {str(e)}")

View File

@ -0,0 +1,165 @@
# validation/validation_service.py
import importlib
from calendar import c
from typing import Any, Dict, List, Optional
from autogen_core import ComponentModel, is_component_class
from pydantic import BaseModel
class ValidationRequest(BaseModel):
component: ComponentModel
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: ComponentModel) -> Optional[ValidationError]:
"""Validate the component type"""
if not component.component_type:
return ValidationError(
field="component_type",
error="Component type is missing",
suggestion="Add a component_type field to the component configuration",
)
@staticmethod
def validate_config_schema(component: ComponentModel) -> List[ValidationError]:
"""Validate the component configuration against its schema"""
errors = []
try:
# Convert to ComponentModel for initial validation
model = component.model_copy(deep=True)
# 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: ComponentModel) -> Optional[ValidationError]:
"""Validate that the component can be instantiated"""
try:
model = component.model_copy(deep=True)
# 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: ComponentModel) -> ValidationResponse:
"""Validate a component configuration"""
errors = []
warnings = []
# Check provider
if provider_error := cls.validate_provider(component.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 not component.version:
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)

View File

@ -4,7 +4,7 @@ from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URI: str = "sqlite:///./autogen04201.db"
DATABASE_URI: str = "sqlite:///./autogen04202.db"
API_DOCS: bool = False
CLEANUP_INTERVAL: int = 300 # 5 minutes
SESSION_TIMEOUT: int = 3600 # 1 hour

View File

@ -37,15 +37,17 @@ async def create_gallery_entry(gallery_data: Gallery, db: DatabaseManager = Depe
@router.get("/")
async def list_gallery_entries(user_id: str, db: DatabaseManager = Depends(get_db)) -> Response:
result = db.get(Gallery, filters={"user_id": user_id})
if not result.data or len(result.data) == 0:
# create a default gallery entry
gallery_config = create_default_gallery()
default_gallery = Gallery(user_id=user_id, config=gallery_config.model_dump())
db.upsert(default_gallery)
try:
result = db.get(Gallery, filters={"user_id": user_id})
return result
if not result.data or len(result.data) == 0:
# create a default gallery entry
gallery_config = create_default_gallery()
default_gallery = Gallery(user_id=user_id, config=gallery_config.model_dump())
db.upsert(default_gallery)
result = db.get(Gallery, filters={"user_id": user_id})
return result
except Exception as e:
return Response(status=False, data=[], message=f"Error retrieving gallery entries: {str(e)}")
@router.get("/{gallery_id}")

View File

@ -1,11 +1,11 @@
# /api/runs routes
from typing import Dict
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from ...datamodel import Message, MessageConfig, Run, RunStatus, Session, Team
from ..deps import get_db, get_team_manager, get_websocket_manager
from ...datamodel import Message, Run, RunStatus, Session
from ..deps import get_db
router = APIRouter()

View File

@ -1,174 +1,37 @@
# 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
from fastapi import APIRouter
from ...validation.component_test_service import ComponentTestRequest, ComponentTestResult, ComponentTestService
from ...validation.validation_service import ValidationError, ValidationRequest, ValidationResponse, ValidationService
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)
try:
return ValidationService.validate(request.component)
except Exception as e:
return ValidationResponse(
is_valid=False, errors=[ValidationError(field="validation", error=str(e))], warnings=[]
)
@router.post("/test")
async def test_component(request: ComponentTestRequest) -> ComponentTestResult:
"""Test a component functionality with appropriate inputs based on type"""
# First validate the component configuration
validation_result = ValidationService.validate(request.component)
# Only proceed with testing if the component is valid
if not validation_result.is_valid:
return ComponentTestResult(
status=False, message="Component validation failed", logs=[e.error for e in validation_result.errors]
)
# If validation passed, run the functional test
return await ComponentTestService.test_component(
component=request.component, timeout=request.timeout if request.timeout else 60
)

View File

@ -93,6 +93,49 @@ export interface FromModuleImport {
// Import can be either a string (direct import) or a FromModuleImport
export type Import = string | FromModuleImport;
// Code Executor Base Config
export interface CodeExecutorBaseConfig {
timeout?: number;
work_dir?: string;
}
// Local Command Line Code Executor Config
export interface LocalCommandLineCodeExecutorConfig
extends CodeExecutorBaseConfig {
functions_module?: string;
}
// Docker Command Line Code Executor Config
export interface DockerCommandLineCodeExecutorConfig
extends CodeExecutorBaseConfig {
image?: string;
container_name?: string;
bind_dir?: string;
auto_remove?: boolean;
stop_container?: boolean;
functions_module?: string;
extra_volumes?: Record<string, Record<string, string>>;
extra_hosts?: Record<string, string>;
init_command?: string;
}
// Jupyter Code Executor Config
export interface JupyterCodeExecutorConfig extends CodeExecutorBaseConfig {
kernel_name?: string;
output_dir?: string;
}
// Python Code Execution Tool Config
export interface PythonCodeExecutionToolConfig {
executor: Component<
| LocalCommandLineCodeExecutorConfig
| DockerCommandLineCodeExecutorConfig
| JupyterCodeExecutorConfig
>;
description?: string;
name?: string;
}
// The complete FunctionToolConfig interface
export interface FunctionToolConfig {
source_code: string;
@ -227,6 +270,10 @@ export interface OrTerminationConfig {
conditions: Component<TerminationConfig>[];
}
export interface AndTerminationConfig {
conditions: Component<TerminationConfig>[];
}
export interface MaxMessageTerminationConfig {
max_messages: number;
}
@ -248,12 +295,13 @@ export type ModelConfig =
| AzureOpenAIClientConfig
| AnthropicClientConfig;
export type ToolConfig = FunctionToolConfig;
export type ToolConfig = FunctionToolConfig | PythonCodeExecutionToolConfig;
export type ChatCompletionContextConfig = UnboundedChatCompletionContextConfig;
export type TerminationConfig =
| OrTerminationConfig
| AndTerminationConfig
| MaxMessageTerminationConfig
| TextMentionTerminationConfig;

View File

@ -20,6 +20,7 @@ import type {
TextMentionTerminationConfig,
UnboundedChatCompletionContextConfig,
AnthropicClientConfig,
AndTerminationConfig,
} from "./datamodel";
// Provider constants
@ -43,6 +44,7 @@ const PROVIDERS = {
// Termination
OR_TERMINATION: "autogen_agentchat.base.OrTerminationCondition",
AND_TERMINATION: "autogen_agentchat.base.AndTerminationCondition",
MAX_MESSAGE: "autogen_agentchat.conditions.MaxMessageTermination",
TEXT_MENTION: "autogen_agentchat.conditions.TextMentionTermination",
@ -74,6 +76,7 @@ type ProviderToConfig = {
// Termination
[PROVIDERS.OR_TERMINATION]: OrTerminationConfig;
[PROVIDERS.AND_TERMINATION]: AndTerminationConfig;
[PROVIDERS.MAX_MESSAGE]: MaxMessageTerminationConfig;
[PROVIDERS.TEXT_MENTION]: TextMentionTerminationConfig;
@ -204,6 +207,22 @@ export function isOrTermination(
return isComponentOfType(component, PROVIDERS.OR_TERMINATION);
}
// is Or or And termination
export function isCombinationTermination(
component: Component<ComponentConfig>
): component is Component<OrTerminationConfig | AndTerminationConfig> {
return (
isComponentOfType(component, PROVIDERS.OR_TERMINATION) ||
isComponentOfType(component, PROVIDERS.AND_TERMINATION)
);
}
export function isAndTermination(
component: Component<ComponentConfig>
): component is Component<AndTerminationConfig> {
return isComponentOfType(component, PROVIDERS.AND_TERMINATION);
}
export function isMaxMessageTermination(
component: Component<ComponentConfig>
): component is Component<MaxMessageTerminationConfig> {

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Tabs, Button, Tooltip, Drawer } from "antd";
import { Tabs, Button, Tooltip, Drawer, Input } from "antd";
import {
Package,
Users,
@ -21,6 +21,7 @@ import {
ComponentTypes,
Gallery,
} from "../../types/datamodel";
import TextArea from "antd/es/input/TextArea";
type CategoryKey = `${ComponentTypes}s`;
@ -153,6 +154,11 @@ export const GalleryDetail: React.FC<{
index: number;
} | null>(null);
const [activeTab, setActiveTab] = useState<ComponentTypes>("team");
const [isEditingDetails, setIsEditingDetails] = useState(false);
const [tempName, setTempName] = useState(gallery.config.name);
const [tempDescription, setTempDescription] = useState(
gallery.config.metadata.description
);
const updateGallery = (
category: CategoryKey,
@ -263,6 +269,23 @@ export const GalleryDetail: React.FC<{
setEditingComponent(null);
};
const handleDetailsSave = () => {
const updatedGallery = {
...gallery,
config: {
...gallery.config,
name: tempName,
metadata: {
...gallery.config.metadata,
description: tempDescription,
},
},
};
onSave(updatedGallery);
onDirtyStateChange(true);
setIsEditingDetails(false);
};
const tabItems = Object.entries(iconMap).map(([key, Icon]) => ({
key,
label: (
@ -313,19 +336,57 @@ export const GalleryDetail: React.FC<{
/>
<div className="relative z-10 p-6 h-full flex flex-col justify-between">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-medium text-primary">
{gallery.config.name}
</h1>
{gallery.config.url && (
<Tooltip title="Remote Gallery">
<Globe className="w-5 h-5 text-secondary" />
</Tooltip>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isEditingDetails ? (
<Input
value={tempName}
onChange={(e) => setTempName(e.target.value)}
className="text-2xl font-medium bg-background/50 backdrop-blur px-2 py-1 rounded w-[400px]"
/>
) : (
<h1 className="text-2xl font-medium text-primary">
{gallery.config.name}
</h1>
)}
{gallery.config.url && (
<Tooltip title="Remote Gallery">
<Globe className="w-5 h-5 text-secondary" />
</Tooltip>
)}
</div>
{!isEditingDetails ? (
<Button
icon={<Edit className="w-4 h-4" />}
onClick={() => setIsEditingDetails(true)}
type="text"
className="text-white hover:text-white/80"
>
Edit
</Button>
) : (
<div className="flex gap-2">
<Button onClick={() => setIsEditingDetails(false)}>
Cancel
</Button>
<Button type="primary" onClick={handleDetailsSave}>
Save
</Button>
</div>
)}
</div>
<p className="text-secondary w-1/2 mt-2 line-clamp-2">
{gallery.config.metadata.description}
</p>
{isEditingDetails ? (
<TextArea
value={tempDescription}
onChange={(e) => setTempDescription(e.target.value)}
className="w-1/2 bg-background/50 backdrop-blur px-2 py-1 rounded mt-2"
rows={2}
/>
) : (
<p className="text-secondary w-1/2 mt-2 line-clamp-2">
{gallery.config.metadata.description}
</p>
)}
</div>
<div className="flex gap-2">
<div className="bg-tertiary backdrop-blur rounded p-2 flex items-center gap-2">

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { message } from "antd";
import { Button, message, Tooltip } from "antd";
import { getServerUrl } from "../../../utils/utils";
import { IStatus } from "../../../types/app";
import {
@ -20,14 +20,36 @@ import { teamAPI } from "../../teambuilder/api";
import { sessionAPI } from "../api";
import RunView from "./runview";
import { TIMEOUT_CONFIG } from "./types";
import { ChevronRight, MessagesSquare } from "lucide-react";
import {
ChevronRight,
MessagesSquare,
SplitSquareHorizontal,
X,
} from "lucide-react";
import SessionDropdown from "./sessiondropdown";
const logo = require("../../../../images/landing/welcome.svg").default;
interface ChatViewProps {
session: Session | null;
isCompareMode?: boolean;
isSecondaryView?: boolean; // To know if this is the right panel
onCompareClick?: () => void;
onExitCompare?: () => void;
onSessionChange?: (session: Session) => void;
availableSessions?: Session[];
showCompareButton?: boolean;
}
export default function ChatView({ session }: ChatViewProps) {
export default function ChatView({
session,
isCompareMode = false,
isSecondaryView = false,
onCompareClick,
onExitCompare,
onSessionChange,
availableSessions = [],
showCompareButton = true,
}: ChatViewProps) {
const serverUrl = getServerUrl();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
@ -506,14 +528,54 @@ export default function ChatView({ session }: ChatViewProps) {
return (
<div className="text-primary h-[calc(100vh-165px)] bg-primary relative rounded flex-1 scroll">
{contextHolder}
<div className="flex pt-2 items-center gap-2 text-sm">
<span className="text-primary font-medium"> Sessions</span>
{session && (
<>
<ChevronRight className="w-4 h-4 text-secondary" />
<span className="text-secondary">{session.name}</span>
</>
)}
<div className="flex pt-2 items-center justify-between text-sm h-10">
<div className="flex items-center gap-2 min-w-0 overflow-hidden flex-1 pr-4">
{isCompareMode ? (
<SessionDropdown
session={session}
availableSessions={availableSessions}
onSessionChange={onSessionChange || (() => {})}
className="w-full"
/>
) : (
<>
<span className="text-primary font-medium whitespace-nowrap flex-shrink-0">
Sessions
</span>
{session && (
<>
<ChevronRight className="w-4 h-4 text-secondary flex-shrink-0" />
<Tooltip title={session.name}>
<span className="text-secondary truncate overflow-hidden">
{session.name}
</span>
</Tooltip>
</>
)}
</>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0 whitespace-nowrap">
{!isCompareMode && !isSecondaryView && showCompareButton && (
<Button
type="text"
onClick={onCompareClick}
icon={<SplitSquareHorizontal className="w-4 h-4" />}
>
Compare
</Button>
)}
{isCompareMode && isSecondaryView && (
<Button
type="text"
onClick={onExitCompare}
icon={<X className="w-4 h-4" />}
>
Exit Compare
</Button>
)}
</div>
</div>
<div className="flex flex-col h-full">
<div

View File

@ -0,0 +1,83 @@
import * as React from "react";
import { Dropdown, Input, MenuProps, Tooltip } from "antd";
import { ChevronDown, TextSearch } from "lucide-react";
import { Session } from "../../../types/datamodel";
import { getRelativeTimeString } from "../../atoms";
interface SessionDropdownProps {
session: Session | null;
availableSessions: Session[];
onSessionChange: (session: Session) => void;
className?: string;
}
const SessionDropdown: React.FC<SessionDropdownProps> = ({
session,
availableSessions,
onSessionChange,
className = "",
}) => {
const [search, setSearch] = React.useState<string>("");
// Filter sessions based on search input
const filteredSessions = availableSessions.filter((s) =>
s.name.toLowerCase().includes(search.toLowerCase())
);
// This needs to follow the exact MenuProps structure from antd
const items: MenuProps["items"] = [
{
type: "group",
key: "search-sessions",
label: (
<div>
<div className="text-xs text-secondary mb-1">Search sessions</div>
<Input
prefix={<TextSearch className="w-4 h-4" />}
placeholder="Search sessions"
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()} // Prevent dropdown from closing
/>
</div>
),
},
{
type: "divider",
},
...filteredSessions.map((s) => ({
key: (s.id || "").toString(),
label: (
<div className="py-1">
<div className="font-medium">{s.name}</div>
<div className="text-xs text-secondary">
{getRelativeTimeString(s.updated_at || "")}
</div>
</div>
),
})),
];
const handleMenuClick: MenuProps["onClick"] = ({ key }) => {
const selectedSession = availableSessions.find((s) => s.id === Number(key));
if (selectedSession) {
onSessionChange(selectedSession);
}
};
return (
<Dropdown menu={{ items, onClick: handleMenuClick }} trigger={["click"]}>
<div
className={`cursor-pointer flex items-center gap-2 min-w-0 ${className}`}
>
<Tooltip title={session?.name}>
<span className="text-primary font-medium truncate overflow-hidden">
{session?.name || "Select Session"}
</span>
</Tooltip>
<ChevronDown className="w-4 h-4 text-secondary flex-shrink-0" />
</div>
</Dropdown>
);
};
export default SessionDropdown;

View File

@ -26,9 +26,30 @@ export const SessionManager: React.FC = () => {
const { user } = useContext(appContext);
const { session, setSession, sessions, setSessions } = useConfigStore();
const [isCompareMode, setIsCompareMode] = useState(false);
const [comparisonSession, setComparisonSession] = useState<Session | null>(
null
);
const galleryStore = useGalleryStore();
const handleCompareClick = () => {
if (sessions.length > 1) {
// Find the first session that isn't the current one
const otherSession = sessions.find((s) => s.id !== session?.id);
setComparisonSession(otherSession || session);
} else {
// If only one session, show it in both panels
setComparisonSession(session);
}
setIsCompareMode(true);
};
const handleExitCompare = () => {
setIsCompareMode(false);
setComparisonSession(null);
};
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("sessionSidebar", JSON.stringify(isSidebarOpen));
@ -149,6 +170,7 @@ export const SessionManager: React.FC = () => {
}
};
// Modify the existing session selection handler
const handleSelectSession = async (selectedSession: Session) => {
if (!user?.id || !selectedSession.id) return;
@ -156,11 +178,10 @@ export const SessionManager: React.FC = () => {
setIsLoading(true);
const data = await sessionAPI.getSession(selectedSession.id, user.id);
if (!data) {
// Session not found
messageApi.error("Session not found");
window.history.pushState({}, "", window.location.pathname); // Clear URL
window.history.pushState({}, "", window.location.pathname);
if (sessions.length > 0) {
setSession(sessions[0]); // Fall back to first session
setSession(sessions[0]);
} else {
setSession(null);
}
@ -171,12 +192,6 @@ export const SessionManager: React.FC = () => {
} catch (error) {
console.error("Error loading session:", error);
messageApi.error("Error loading session");
window.history.pushState({}, "", window.location.pathname); // Clear invalid URL
if (sessions.length > 0) {
setSession(sessions[0]); // Fall back to first session
} else {
setSession(null);
}
} finally {
setIsLoading(false);
}
@ -253,13 +268,36 @@ export const SessionManager: React.FC = () => {
</div>
<div
className={`flex-1 transition-all -mr-4 duration-200 ${
className={`flex-1 transition-all duration-200 ${
isSidebarOpen ? "ml-64" : "ml-12"
}`}
>
{session && sessions.length > 0 ? (
<div className="pl-4">
{session && <ChatView session={session} />}
<div className="pl-4 flex gap-4">
{/* Primary ChatView */}
<div className={`flex-1 ${isCompareMode ? "w-1/2" : "w-full"}`}>
<ChatView
session={session}
isCompareMode={isCompareMode}
onCompareClick={handleCompareClick}
onSessionChange={handleSelectSession}
availableSessions={sessions}
/>
</div>
{/* Comparison ChatView */}
{isCompareMode && (
<div className="flex-1 w-1/2 border-l border-secondary/20 pl-4">
<ChatView
session={comparisonSession}
isCompareMode={true}
isSecondaryView={true}
onExitCompare={handleExitCompare}
onSessionChange={setComparisonSession}
availableSessions={sessions}
/>
</div>
)}
</div>
) : (
<div className="flex items-center justify-center h-full text-secondary">

View File

@ -14,6 +14,13 @@ export interface ValidationResponse {
warnings: ValidationError[];
}
export interface ComponentTestResult {
status: boolean;
message: string;
data?: any;
logs: string[];
}
export class TeamAPI extends BaseAPI {
async listTeams(userId: string): Promise<Team[]> {
const response = await fetch(
@ -66,20 +73,6 @@ export class TeamAPI extends BaseAPI {
const data = await response.json();
if (!data.status) throw new Error(data.message || "Failed to delete team");
}
// Team-Agent Link Management
async linkAgent(teamId: number, agentId: number): Promise<void> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}`,
{
method: "POST",
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to link agent to team");
}
}
// move validationapi to its own class
@ -103,6 +96,27 @@ export class ValidationAPI extends BaseAPI {
return data;
}
async testComponent(
component: Component<ComponentConfig>,
timeout: number = 60
): Promise<ComponentTestResult> {
const response = await fetch(`${this.getBaseUrl()}/validate/test`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify({
component: component,
timeout: timeout,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to test component");
}
return data;
}
}
export const validationAPI = new ValidationAPI();

View File

@ -1,6 +1,6 @@
import React, { useState, useCallback, useRef } from "react";
import { Button, Breadcrumb } from "antd";
import { ChevronLeft, Code, FormInput } from "lucide-react";
import { Button, Breadcrumb, message, Tooltip } from "antd";
import { ChevronLeft, Code, FormInput, PlayCircle } from "lucide-react";
import { Component, ComponentConfig } from "../../../../types/datamodel";
import {
isTeamComponent,
@ -16,7 +16,8 @@ import { ToolFields } from "./fields/tool-fields";
import { TerminationFields } from "./fields/termination-fields";
import debounce from "lodash.debounce";
import { MonacoEditor } from "../../../monaco";
import { ComponentTestResult, validationAPI } from "../../api";
import TestDetails from "./testresults";
export interface EditPath {
componentType: string;
id: string;
@ -42,6 +43,12 @@ export const ComponentEditor: React.FC<ComponentEditorProps> = ({
Object.assign({}, component)
);
const [isJsonEditing, setIsJsonEditing] = useState(false);
const [testLoading, setTestLoading] = useState(false);
const [testResult, setTestResult] = useState<ComponentTestResult | null>(
null
);
const [messageApi, contextHolder] = message.useMessage();
const editorRef = useRef(null);
@ -49,6 +56,7 @@ export const ComponentEditor: React.FC<ComponentEditorProps> = ({
React.useEffect(() => {
setWorkingCopy(component);
setEditPath([]);
setTestResult(null);
}, [component]);
const getCurrentComponent = useCallback(
@ -214,6 +222,32 @@ export const ComponentEditor: React.FC<ComponentEditorProps> = ({
const currentComponent = getCurrentComponent(workingCopy) || workingCopy;
const handleTestComponent = async () => {
setTestLoading(true);
setTestResult(null);
try {
const result = await validationAPI.testComponent(currentComponent);
setTestResult(result);
if (result.status) {
messageApi.success("Component test passed!");
} else {
messageApi.error("Component test failed!");
}
} catch (error) {
console.error("Test component error:", error);
setTestResult({
status: false,
message: error instanceof Error ? error.message : "Test failed",
logs: [],
});
messageApi.error("Failed to test component");
} finally {
setTestLoading(false);
}
};
const renderFields = useCallback(() => {
const commonProps = {
component: currentComponent,
@ -278,8 +312,13 @@ export const ComponentEditor: React.FC<ComponentEditorProps> = ({
onClose?.();
}, [workingCopy, onChange, onClose]);
// show test button only for model component
const showTestButton = isModelComponent(currentComponent);
return (
<div className="flex flex-col h-full">
{contextHolder}
<div className="flex items-center gap-4 mb-6">
{navigationDepth && editPath.length > 0 && (
<Button
@ -291,24 +330,54 @@ export const ComponentEditor: React.FC<ComponentEditorProps> = ({
<div className="flex-1">
<Breadcrumb items={breadcrumbItems} />
</div>
{/* Test Component Button */}
{showTestButton && (
<Tooltip title="Test Component">
<Button
onClick={handleTestComponent}
loading={testLoading}
type="default"
className="flex items-center gap-2 text-xs mr-0"
icon={
<div className="relative">
<PlayCircle className="w-4 h-4 text-accent" />
{testResult && (
<div
className={`absolute top-0 right-0 w-2 h-2 ${
testResult.status ? "bg-green-500" : "bg-red-500"
} rounded-full`}
></div>
)}
</div>
}
>
Test
</Button>
</Tooltip>
)}
<Button
onClick={() => setIsJsonEditing((prev) => !prev)}
type="default"
className="flex text-accent items-center gap-2 text-xs "
className="flex text-accent items-center gap-2 text-xs"
>
{isJsonEditing ? (
<>
<FormInput className="w-4 text-accent h-4 mr-1 inline-block" />
Switch to Form Editor
Form Editor
</>
) : (
<>
<Code className="w-4 text-accent h-4 mr-1 inline-block" />
Switch to JSON Editor
JSON Editor
</>
)}
</Button>
</div>
{testResult && (
<TestDetails result={testResult} onClose={() => setTestResult(null)} />
)}
{isJsonEditing ? (
<div className="flex-1 overflow-y-auto">
<MonacoEditor

View File

@ -10,6 +10,7 @@ import {
isOrTermination,
isMaxMessageTermination,
isTextMentionTermination,
isCombinationTermination,
} from "../../../../../types/guards";
import { PROVIDERS } from "../../../../../types/guards";
import DetailGroup from "../detailgroup";
@ -93,7 +94,7 @@ export const TerminationFields: React.FC<TerminationFieldsProps> = ({
};
const handleAddCondition = () => {
if (!selectedConditionType || !isOrTermination(component)) return;
if (!selectedConditionType || !isCombinationTermination(component)) return;
const newCondition = createNewCondition(selectedConditionType);
const currentConditions = component.config.conditions || [];
@ -109,7 +110,7 @@ export const TerminationFields: React.FC<TerminationFieldsProps> = ({
};
const handleRemoveCondition = (index: number) => {
if (!isOrTermination(component)) return;
if (!isCombinationTermination(component)) return;
const currentConditions = [...component.config.conditions];
currentConditions.splice(index, 1);
@ -121,7 +122,7 @@ export const TerminationFields: React.FC<TerminationFieldsProps> = ({
});
};
if (isOrTermination(component)) {
if (isCombinationTermination(component)) {
return (
<DetailGroup title="Termination Conditions">
<div className="space-y-4">

View File

@ -0,0 +1,85 @@
import React from "react";
import {
AlertCircle,
ChevronDown,
ChevronUp,
Terminal,
XCircle,
CheckCircle,
} from "lucide-react";
import { ComponentTestResult } from "../../api";
interface TestDetailsProps {
result: ComponentTestResult;
onClose: () => void;
}
const TestDetails: React.FC<TestDetailsProps> = ({ result, onClose }) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const statusColor = result.status ? " border-green-200" : " border-red-200";
const iconColor = result.status ? "text-green-500" : "text-red-500";
return (
<div
className={`mb-6 rounded-lg border text-primary ${statusColor} overflow-hidden`}
>
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
{result.status ? (
<CheckCircle className={`w-5 h-5 ${iconColor}`} />
) : (
<AlertCircle className={`w-5 h-5 ${iconColor}`} />
)}
<span className="font-medium text-primary">{result.message}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="p-1 hover:bg-black/5 rounded-md"
>
{isExpanded ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</button>
<button
onClick={onClose}
className="p-1 hover:bg-black/5 rounded-md"
>
<XCircle className="w-4 h-4" />
</button>
</div>
</div>
{isExpanded && result.logs && result.logs.length > 0 && (
<div className="mt-4">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-4 h-4" />
<span className="text-sm font-medium">Execution Logs</span>
</div>
<pre className="bg-secondary text-primary p-4 rounded-md text-sm font-mono overflow-x-auto">
{result.logs.join("\n")}
</pre>
</div>
)}
{isExpanded && result.data && (
<div className="mt-4">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-4 h-4" />
<span className="text-sm font-medium">Additional Data</span>
</div>
<pre className="bg-secondary text-primary p-4 rounded-md text-sm font-mono overflow-x-auto">
{JSON.stringify(result.data, null, 2)}
</pre>
</div>
)}
</div>
</div>
);
};
export default TestDetails;

View File

@ -103,7 +103,7 @@ export const ComponentLibrary: React.FC<LibraryProps> = ({
title: "Tools",
type: "tool" as ComponentTypes,
items: defaultGallery.config.components.tools.map((tool) => ({
label: tool.config.name,
label: tool.config?.name || tool.label,
config: tool,
})),
icon: <Wrench className="w-4 h-4" />,

View File

@ -1,185 +0,0 @@
// fields/agent-fields.tsx
import React from "react";
import { Form, Input, Switch } from "antd";
import {
isAssistantAgent,
isUserProxyAgent,
isWebSurferAgent,
} from "../../../../../types/guards";
import { NestedComponentButton } from "./fields";
import { NodeEditorFieldsProps } from "./fields";
const { TextArea } = Input;
export const AgentFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (!component) return null;
if (isAssistantAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="System Message" name={["config", "system_message"]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item
label="Reflect on Tool Use"
name={["config", "reflect_on_tool_use"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Tool Call Summary Format"
name={["config", "tool_call_summary_format"]}
>
<Input />
</Form.Item>
{component.config.model_client && (
<NestedComponentButton
label="Model Client"
component={component.config.model_client}
parentField="model_client"
onNavigate={onNavigate}
/>
)}
{component.config.tools && component.config.tools.length > 0 && (
<NestedComponentButton
label="Tools"
component={component.config.tools}
parentField="tools"
onNavigate={onNavigate}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
)}
</>
);
}
if (isUserProxyAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
</>
);
}
if (isWebSurferAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Downloads Folder"
name={["config", "downloads_folder"]}
>
<Input />
</Form.Item>
<Form.Item label="Description" name={["config", "description"]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Start Page" name={["config", "start_page"]}>
<Input />
</Form.Item>
<Form.Item
label="Headless"
name={["config", "headless"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Animate Actions"
name={["config", "animate_actions"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Save Screenshots"
name={["config", "to_save_screenshots"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Use OCR"
name={["config", "use_ocr"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label="Browser Channel" name={["config", "browser_channel"]}>
<Input />
</Form.Item>
<Form.Item
label="Browser Data Directory"
name={["config", "browser_data_dir"]}
>
<Input />
</Form.Item>
<Form.Item
label="Resize Viewport"
name={["config", "to_resize_viewport"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
{component.config.model_client && (
<NestedComponentButton
label="Model Client"
component={component.config.model_client}
parentField="model_client"
onNavigate={onNavigate}
/>
)}
</>
);
}
return null;
};
export default AgentFields;

View File

@ -1,282 +0,0 @@
import React from "react";
import { Button, Form, Input } from "antd";
import {
Component,
ComponentConfig,
FunctionToolConfig,
} from "../../../../../types/datamodel";
import {
isTeamComponent,
isAgentComponent,
isModelComponent,
isToolComponent,
isTerminationComponent,
} from "../../../../../types/guards";
import DetailGroup from "../detailgroup";
import { TeamFields } from "./team-fields";
import { AgentFields } from "./agent-fields";
import { ModelFields } from "./model-fields";
import { ToolFields } from "./tool-fields";
import { TerminationFields } from "./termination-fields";
import { Edit, MinusCircle, PlusCircle } from "lucide-react";
import { EditPath } from "../node-editor";
const { TextArea } = Input;
export interface NodeEditorFieldsProps {
component: Component<ComponentConfig>;
onNavigate: (componentType: string, id: string, parentField: string) => void;
workingCopy: Component<ComponentConfig> | null;
setWorkingCopy: (component: Component<ComponentConfig> | null) => void;
editPath: EditPath[];
updateComponentAtPath: (
root: Component<ComponentConfig>,
path: EditPath[],
updates: Partial<Component<ComponentConfig>>
) => Component<ComponentConfig>;
getCurrentComponent: (
root: Component<ComponentConfig>
) => Component<ComponentConfig> | null;
}
const NodeEditorFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
let specificFields = null;
if (isTeamComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<TeamFields
component={component}
onNavigate={onNavigate}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
</DetailGroup>
);
} else if (isAgentComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<AgentFields
component={component}
onNavigate={onNavigate}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
</DetailGroup>
);
} else if (isModelComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<ModelFields component={component} />
</DetailGroup>
);
} else if (isToolComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<ToolFields
component={component}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
</DetailGroup>
);
} else if (isTerminationComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<TerminationFields component={component} onNavigate={onNavigate} />
</DetailGroup>
);
}
return (
<>
<DetailGroup title="Component Details">
<CommonFields />
</DetailGroup>
{specificFields}
</>
);
};
export default NodeEditorFields;
// // fields/common-fields.tsx
export const CommonFields: React.FC = () => {
return (
<>
<Form.Item label="Label" name="label">
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<TextArea rows={4} />
</Form.Item>
</>
);
};
interface NestedComponentButtonProps {
label: string;
component: Component<ComponentConfig> | Component<ComponentConfig>[];
parentField: string;
onNavigate: (componentType: string, id: string, parentField: string) => void;
workingCopy?: Component<ComponentConfig> | null;
setWorkingCopy?: (component: Component<ComponentConfig> | null) => void;
editPath?: any[];
updateComponentAtPath?: any;
getCurrentComponent?: any;
}
export const NestedComponentButton: React.FC<NestedComponentButtonProps> = ({
label,
component,
parentField,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (Array.isArray(component)) {
return (
<div className="space-y-2 mb-4">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">{label}</span>
{parentField === "tools" && (
<Button
type="dashed"
size="small"
onClick={() => {
const blankTool: Component<FunctionToolConfig> = {
provider: "autogen_core.tools.FunctionTool",
component_type: "tool",
version: 1,
component_version: 1,
description:
"Create custom tools by wrapping standard Python functions.",
label: "New Tool",
config: {
source_code: "def new_function():\n pass",
name: "new_function",
description: "Description of the new function",
global_imports: [],
has_cancellation_support: false,
},
};
if (
workingCopy &&
setWorkingCopy &&
updateComponentAtPath &&
getCurrentComponent &&
editPath
) {
const currentTools =
component as Component<ComponentConfig>[];
const updatedTools = [...currentTools, blankTool];
const updatedCopy = updateComponentAtPath(
workingCopy,
editPath,
{
config: {
...getCurrentComponent(workingCopy)?.config,
tools: updatedTools,
},
}
);
setWorkingCopy(updatedCopy);
}
}}
icon={<PlusCircle className="w-4 h-4" />}
>
Add Tool
</Button>
)}
</div>
{component.map((item, index) => (
<div key={item.label} className="flex items-center gap-2">
<Button
onClick={() =>
onNavigate(item.component_type, item.label || "", parentField)
}
className="w-full flex justify-between items-center"
>
<span>{item.label}</span>
<Edit className="w-4 h-4" />
</Button>
{parentField === "tools" && (
<Button
type="text"
danger
icon={<MinusCircle className="w-4 h-4" />}
onClick={() => {
if (
workingCopy &&
setWorkingCopy &&
updateComponentAtPath &&
getCurrentComponent &&
editPath
) {
const currentTools =
component as Component<ComponentConfig>[];
const updatedTools = currentTools.filter(
(_, idx) => idx !== index
);
const updatedCopy = updateComponentAtPath(
workingCopy,
editPath,
{
config: {
...getCurrentComponent(workingCopy)?.config,
tools: updatedTools,
},
}
);
setWorkingCopy(updatedCopy);
}
}}
/>
)}
</div>
))}
</div>
);
}
return component ? (
<div className="mb-4">
<Button
onClick={() =>
onNavigate(
component.component_type,
component.label || "",
parentField
)
}
className="w-full flex justify-between items-center"
>
<span>{label}</span>
<Edit className="w-4 h-4" />
</Button>
</div>
) : null;
};

View File

@ -1,182 +0,0 @@
// fields/model-fields.tsx
import React from "react";
import { Form, Input, InputNumber, Select } from "antd";
import { Component, ComponentConfig } from "../../../../../types/datamodel";
import { isOpenAIModel, isAzureOpenAIModel } from "../../../../../types/guards";
interface ModelFieldsProps {
component: Component<ComponentConfig>;
}
export const ModelFields: React.FC<ModelFieldsProps> = ({ component }) => {
if (!component) return null;
// Common arguments fields shared between OpenAI and Azure OpenAI models
const ArgumentsFields = () => (
<>
<Form.Item
label="Temperature"
name={["config", "temperature"]}
tooltip="Controls randomness in the model's output. Higher values (e.g., 0.8) make output more random, lower values (e.g., 0.2) make it more focused."
>
<InputNumber min={0} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Max Tokens"
name={["config", "max_tokens"]}
tooltip="Maximum length of the model's output in tokens"
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Top P"
name={["config", "top_p"]}
tooltip="Controls diversity via nucleus sampling. Lower values (e.g., 0.1) make output more focused, higher values (e.g., 0.9) make it more diverse."
>
<InputNumber min={0} max={1} step={0.1} />
</Form.Item>
<Form.Item
label="Frequency Penalty"
name={["config", "frequency_penalty"]}
tooltip="Decreases the model's likelihood to repeat the same information. Values range from -2.0 to 2.0."
>
<InputNumber min={-2} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Presence Penalty"
name={["config", "presence_penalty"]}
tooltip="Increases the model's likelihood to talk about new topics. Values range from -2.0 to 2.0."
>
<InputNumber min={-2} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Stop Sequences"
name={["config", "stop"]}
tooltip="Sequences where the model will stop generating further tokens"
>
<Select
mode="tags"
placeholder="Enter stop sequences"
style={{ width: "100%" }}
/>
</Form.Item>
</>
);
if (isOpenAIModel(component)) {
return (
<>
<Form.Item
label="Model"
name={["config", "model"]}
rules={[{ required: true }]}
tooltip="The name of the OpenAI model to use (e.g., gpt-4, gpt-3.5-turbo)"
>
<Input />
</Form.Item>
<Form.Item
label="API Key"
name={["config", "api_key"]}
tooltip="Your OpenAI API key"
>
<Input.Password />
</Form.Item>
<Form.Item
label="Organization"
name={["config", "organization"]}
tooltip="Optional: Your OpenAI organization ID"
>
<Input />
</Form.Item>
<Form.Item
label="Base URL"
name={["config", "base_url"]}
tooltip="Optional: Custom base URL for API requests"
>
<Input />
</Form.Item>
<Form.Item
label="Timeout"
name={["config", "timeout"]}
tooltip="Request timeout in seconds"
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Max Retries"
name={["config", "max_retries"]}
tooltip="Maximum number of retry attempts for failed requests"
>
<InputNumber min={0} />
</Form.Item>
<ArgumentsFields />
</>
);
}
if (isAzureOpenAIModel(component)) {
return (
<>
<Form.Item
label="Model"
name={["config", "model"]}
rules={[{ required: true }]}
tooltip="The name of the Azure OpenAI model deployment"
>
<Input />
</Form.Item>
<Form.Item
label="Azure Endpoint"
name={["config", "azure_endpoint"]}
rules={[{ required: true }]}
tooltip="Your Azure OpenAI service endpoint URL"
>
<Input />
</Form.Item>
<Form.Item
label="Azure Deployment"
name={["config", "azure_deployment"]}
tooltip="The name of your Azure OpenAI model deployment"
>
<Input />
</Form.Item>
<Form.Item
label="API Version"
name={["config", "api_version"]}
rules={[{ required: true }]}
tooltip="Azure OpenAI API version (e.g., 2023-05-15)"
>
<Input />
</Form.Item>
<Form.Item
label="Azure AD Token"
name={["config", "azure_ad_token"]}
tooltip="Optional: Azure Active Directory token for authentication"
>
<Input.Password />
</Form.Item>
<ArgumentsFields />
</>
);
}
return null;
};
export default ModelFields;

View File

@ -1,77 +0,0 @@
import React from "react";
import { Form, Input, InputNumber, Switch } from "antd";
import { isSelectorTeam, isRoundRobinTeam } from "../../../../../types/guards";
import { NestedComponentButton, NodeEditorFieldsProps } from "./fields";
const { TextArea } = Input;
export const TeamFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (!component) return null;
if (isSelectorTeam(component)) {
return (
<>
<Form.Item
label="Selector Prompt"
name={["config", "selector_prompt"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Max Turns" name={["config", "max_turns"]}>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Allow Repeated Speaker"
name={["config", "allow_repeated_speaker"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
{component.config.model_client && (
<NestedComponentButton
label="Model Client"
component={component.config.model_client}
parentField="model_client"
onNavigate={onNavigate}
/>
)}
{component.config.termination_condition && (
<NestedComponentButton
label="Termination Condition"
component={component.config.termination_condition}
parentField="termination_condition"
onNavigate={onNavigate}
/>
)}
</>
);
}
if (isRoundRobinTeam(component)) {
return (
<>
<Form.Item label="Max Turns" name={["config", "max_turns"]}>
<InputNumber min={1} />
</Form.Item>
{component.config.termination_condition && (
<NestedComponentButton
label="Termination Condition"
component={component.config.termination_condition}
parentField="termination_condition"
onNavigate={onNavigate}
/>
)}
</>
);
}
return null;
};

View File

@ -1,275 +0,0 @@
import React, { useCallback, useState } from "react";
import { Input, InputNumber, Button, Select, Tooltip } from "antd";
import { PlusCircle, MinusCircle, Edit, HelpCircle } from "lucide-react";
import {
Component,
ComponentConfig,
TerminationConfig,
} from "../../../../../types/datamodel";
import {
isOrTermination,
isMaxMessageTermination,
isTextMentionTermination,
} from "../../../../../types/guards";
import { PROVIDERS } from "../../../../../types/guards";
import DetailGroup from "../../component-editor/detailgroup";
interface TerminationFieldsProps {
component: Component<TerminationConfig>;
onChange: (updates: Partial<Component<ComponentConfig>>) => void;
onNavigate?: (
componentType: string,
id: string,
parentField: string,
index?: number
) => void;
workingCopy?: Component<ComponentConfig> | null;
setWorkingCopy?: (component: Component<ComponentConfig> | null) => void;
editPath?: any[];
updateComponentAtPath?: any;
getCurrentComponent?: any;
}
const TERMINATION_TYPES = {
MAX_MESSAGE: {
label: "Max Messages",
provider: PROVIDERS.MAX_MESSAGE,
defaultConfig: {
max_messages: 10,
include_agent_event: false,
},
},
TEXT_MENTION: {
label: "Text Mention",
provider: PROVIDERS.TEXT_MENTION,
defaultConfig: {
text: "TERMINATE",
},
},
};
const InputWithTooltip: React.FC<{
label: string;
tooltip: string;
children: React.ReactNode;
}> = ({ label, tooltip, children }) => (
<label className="block">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-primary">{label}</span>
<Tooltip title={tooltip}>
<HelpCircle className="w-4 h-4 text-secondary" />
</Tooltip>
</div>
{children}
</label>
);
export const TerminationFields: React.FC<TerminationFieldsProps> = ({
component,
onChange,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
const [showAddCondition, setShowAddCondition] = useState(false);
const [selectedConditionType, setSelectedConditionType] =
useState<string>("");
if (!component) return null;
const handleComponentUpdate = useCallback(
(updates: Partial<Component<ComponentConfig>>) => {
onChange({
...component,
...updates,
config: {
...component.config,
...(updates.config || {}),
},
});
},
[component, onChange]
);
const createNewCondition = (type: string) => {
const template = TERMINATION_TYPES[type as keyof typeof TERMINATION_TYPES];
return {
provider: template.provider,
component_type: "termination",
version: 1,
component_version: 1,
description: `${template.label} termination condition`,
label: template.label,
config: template.defaultConfig,
};
};
const handleAddCondition = () => {
if (!selectedConditionType || !isOrTermination(component)) return;
const newCondition = createNewCondition(selectedConditionType);
const currentConditions = component.config.conditions || [];
handleComponentUpdate({
config: {
conditions: [...currentConditions, newCondition],
},
});
setShowAddCondition(false);
setSelectedConditionType("");
};
const handleRemoveCondition = (index: number) => {
if (!isOrTermination(component)) return;
const currentConditions = [...component.config.conditions];
currentConditions.splice(index, 1);
handleComponentUpdate({
config: {
conditions: currentConditions,
},
});
};
const handleNavigateToCondition = (
condition: Component<TerminationConfig>,
index: number
) => {
if (!onNavigate) return;
// Use both the condition ID and the array index
const conditionId = condition.label || `condition-${index}`;
// The critical change: pass the index to onNavigate
onNavigate("termination", conditionId, "conditions", index);
};
if (isOrTermination(component)) {
return (
<DetailGroup title="Termination Conditions">
<div className="space-y-4">
<div className="flex justify-between items-center">
<Button
type="dashed"
onClick={() => setShowAddCondition(true)}
icon={<PlusCircle className="w-4 h-4" />}
className="w-full"
>
Add Condition
</Button>
</div>
{showAddCondition && (
<div className="border rounded p-4 space-y-4">
<InputWithTooltip
label="Condition Type"
tooltip="Select the type of termination condition to add"
>
<Select
value={selectedConditionType}
onChange={setSelectedConditionType}
className="w-full"
>
{Object.entries(TERMINATION_TYPES).map(([key, value]) => (
<Select.Option key={key} value={key}>
{value.label}
</Select.Option>
))}
</Select>
</InputWithTooltip>
<Button
onClick={handleAddCondition}
disabled={!selectedConditionType}
className="w-full"
>
Add
</Button>
</div>
)}
<div className="space-y-2">
{component.config.conditions?.map((condition, index) => (
<div key={index} className="flex items-center gap-2">
<Button
onClick={() => handleNavigateToCondition(condition, index)}
className="w-full flex justify-between items-center"
>
<span>
{condition.label || `Condition ${index + 1}`}
{isMaxMessageTermination(condition) &&
` (Max: ${condition.config.max_messages})`}
{isTextMentionTermination(condition) &&
` (Text: "${condition.config.text}")`}
</span>
<Edit className="w-4 h-4" />
</Button>
<Button
type="text"
danger
icon={<MinusCircle className="w-4 h-4" />}
onClick={() => handleRemoveCondition(index)}
/>
</div>
))}
{(!component.config.conditions ||
component.config.conditions.length === 0) && (
<div className="text-center p-4 text-secondary bg-secondary/20 rounded">
No conditions added yet
</div>
)}
</div>
</div>
</DetailGroup>
);
}
if (isMaxMessageTermination(component)) {
return (
<DetailGroup title="Max Messages Configuration">
<InputWithTooltip
label="Max Messages"
tooltip="Maximum number of messages before termination"
>
<InputNumber
min={1}
value={component.config.max_messages}
onChange={(value) =>
handleComponentUpdate({
config: { max_messages: value },
})
}
className="w-full"
/>
</InputWithTooltip>
</DetailGroup>
);
}
if (isTextMentionTermination(component)) {
return (
<DetailGroup title="Text Mention Configuration">
<InputWithTooltip
label="Termination Text"
tooltip="Text that triggers termination when mentioned"
>
<Input
value={component.config.text}
onChange={(e) =>
handleComponentUpdate({
config: { text: e.target.value },
})
}
/>
</InputWithTooltip>
</DetailGroup>
);
}
return null;
};
export default React.memo(TerminationFields);

View File

@ -1,265 +0,0 @@
import React, { useRef, useState } from "react";
import { Form, Input, Switch, Select, Button, Space } from "antd";
import { PlusCircle, MinusCircle } from "lucide-react";
import { Import } from "../../../../../types/datamodel";
import { isFunctionTool } from "../../../../../types/guards";
import { MonacoEditor } from "../../../../monaco";
import { NodeEditorFieldsProps } from "./fields";
const { TextArea } = Input;
const { Option } = Select;
interface ImportState {
module: string;
imports: string;
}
export const ToolFields: React.FC<
Omit<NodeEditorFieldsProps, "onNavigate">
> = ({
component,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (!component || !isFunctionTool(component)) return null;
const editorRef = useRef(null);
const [showAddImport, setShowAddImport] = useState(false);
const [importType, setImportType] = useState<"direct" | "fromModule">(
"direct"
);
const [directImport, setDirectImport] = useState("");
const [moduleImport, setModuleImport] = useState<ImportState>({
module: "",
imports: "",
});
const formatImport = (imp: Import): string => {
if (!imp) return "";
if (typeof imp === "string") {
return imp;
}
return `from ${imp.module} import ${imp.imports.join(", ")}`;
};
const handleAddImport = (form: { add: (value: string | Import) => void }) => {
if (importType === "direct" && directImport) {
form.add(directImport);
setDirectImport("");
} else if (
importType === "fromModule" &&
moduleImport.module &&
moduleImport.imports
) {
form.add({
module: moduleImport.module,
imports: moduleImport.imports
.split(",")
.map((i) => i.trim())
.filter((i) => i),
});
setModuleImport({ module: "", imports: "" });
}
setShowAddImport(false);
};
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Global Imports">
<div className="space-y-2">
<Form.List name={["config", "global_imports"]}>
{(fields, { add, remove }) => (
<div className="space-y-2">
{/* Existing Imports */}
<div className="flex flex-wrap gap-2">
{fields.map((field) => (
<div
key={field.key}
className="flex items-center gap-2 bg-tertiary rounded px-2 py-1"
>
<Form.Item {...field} noStyle>
<Input type="hidden" />
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) => {
const prevImport =
prevValues.config?.global_imports?.[field.name];
const curImport =
curValues.config?.global_imports?.[field.name];
return (
JSON.stringify(prevImport) !==
JSON.stringify(curImport)
);
}}
noStyle
>
{({ getFieldValue }) => {
const imp = getFieldValue([
"config",
"global_imports",
field.name,
]);
return (
<span className="text-sm">{formatImport(imp)}</span>
);
}}
</Form.Item>
<Button
type="text"
size="small"
className="flex items-center justify-center h-6 w-6 p-0"
onClick={() => remove(field.name)}
icon={<MinusCircle className="h-4 w-4" />}
/>
</div>
))}
</div>
{/* Add Import UI */}
{showAddImport ? (
<div className="border rounded p-3 space-y-3">
<Form.Item className="mb-2">
<Select
value={importType}
onChange={setImportType}
style={{ width: 200 }}
>
<Option value="direct">Direct Import</Option>
<Option value="fromModule">From Module Import</Option>
</Select>
</Form.Item>
{importType === "direct" ? (
<Space>
<Input
placeholder="Package name (e.g., os)"
className="w-64"
value={directImport}
onChange={(e) => setDirectImport(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && directImport) {
handleAddImport({ add });
}
}}
/>
<Button
onClick={() => handleAddImport({ add })}
disabled={!directImport}
>
Add
</Button>
</Space>
) : (
<Space direction="vertical" className="w-full">
<Input
placeholder="Module name (e.g., typing)"
className="w-64"
value={moduleImport.module}
onChange={(e) =>
setModuleImport((prev) => ({
...prev,
module: e.target.value,
}))
}
/>
<Space className="w-full">
<Input
placeholder="Import names (comma-separated)"
className="w-64"
value={moduleImport.imports}
onChange={(e) =>
setModuleImport((prev) => ({
...prev,
imports: e.target.value,
}))
}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
moduleImport.module &&
moduleImport.imports
) {
handleAddImport({ add });
}
}}
/>
<Button
onClick={() => handleAddImport({ add })}
disabled={
!moduleImport.module || !moduleImport.imports
}
>
Add
</Button>
</Space>
</Space>
)}
</div>
) : (
<Button
type="dashed"
onClick={() => setShowAddImport(true)}
className="w-full"
>
<PlusCircle className="h-4 w-4 mr-2" />
Add Import
</Button>
)}
</div>
)}
</Form.List>
</div>
</Form.Item>
<Form.Item
label="Source Code"
name={["config", "source_code"]}
rules={[{ required: true }]}
>
<div className="h-96">
<Form.Item noStyle shouldUpdate>
{({ getFieldValue, setFieldValue }) => (
<MonacoEditor
value={getFieldValue(["config", "source_code"]) || ""}
editorRef={editorRef}
language="python"
onChange={(value) =>
setFieldValue(["config", "source_code"], value)
}
/>
)}
</Form.Item>
</div>
</Form.Item>
<Form.Item
label="Has Cancellation Support"
name={["config", "has_cancellation_support"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
</>
);
};
export default ToolFields;

View File

@ -11,20 +11,16 @@ import {
LucideIcon,
Users,
Wrench,
Settings,
Brain,
Timer,
Trash2Icon,
Edit,
Bot,
} from "lucide-react";
import { NodeData, CustomNode } from "./types";
import { CustomNode } from "./types";
import {
AgentConfig,
TeamConfig,
ModelConfig,
ToolConfig,
TerminationConfig,
ComponentTypes,
Component,
ComponentConfig,
@ -34,13 +30,7 @@ import { TruncatableText } from "../../atoms";
import { useTeamBuilderStore } from "./store";
import {
isAssistantAgent,
isAzureOpenAIModel,
isFunctionTool,
isMaxMessageTermination,
isOpenAIModel,
isOrTermination,
isSelectorTeam,
isTextMentionTermination,
isWebSurferAgent,
} from "../../../types/guards";

View File

@ -1,5 +1,5 @@
import { create } from "zustand";
import { isEqual } from "lodash";
import { clone, isEqual } from "lodash";
import {
CustomNode,
CustomEdge,
@ -161,8 +161,10 @@ export const useTeamBuilderStore = create<TeamBuilderState>((set, get) => ({
targetNode.data.component.config.tools = [];
}
const toolName = getUniqueName(
clonedComponent.config.name,
targetNode.data.component.config.tools.map((t) => t.config.name)
clonedComponent.config.name || clonedComponent.label || "tool",
targetNode.data.component.config.tools.map(
(t) => t.config.name || t.label || "tool"
)
);
clonedComponent.config.name = toolName;
targetNode.data.component.config.tools.push(clonedComponent);

View File

@ -90,7 +90,7 @@ const TestDrawer = ({ isVisble, onClose, team }: TestDrawerProps) => {
}
>
{loading && <p>Creating a test session...</p>}
{session && <ChatView session={session} />}
{session && <ChatView session={session} showCompareButton={false} />}
</Drawer>
</div>
);

View File

@ -137,18 +137,26 @@ export const TeamSidebar: React.FC<TeamSidebarProps> = ({
{/* Tab Navigation */}
<div className="flex border-b border-secondary">
<button
className={`flex items-center px-2 py-1 text-sm font-medium ${
style={{ width: "110px" }}
className={`flex items-center px-2 py-1 text-sm font-medium ${
activeTab === "recent"
? "text-accent border-b-2 border-accent"
: "text-secondary hover:text-primary"
}`}
onClick={() => setActiveTab("recent")}
>
<History className="w-4 h-4 mr-1.5" />
Recents
<span className="ml-1 text-xs">({teams.length})</span>
{!isLoading && (
<>
{" "}
<History className="w-4 h-4 mr-1.5" /> Recents{" "}
<span className="ml-1 text-xs">({teams.length})</span>
</>
)}
{isLoading && activeTab === "recent" && (
<RefreshCcw className="w-4 h-4 ml-2 animate-spin" />
<>
Loading <RefreshCcw className="w-4 h-4 ml-2 animate-spin" />
</>
)}
</button>
<button

View File

@ -32,10 +32,10 @@ dependencies = [
"loguru",
"pyyaml",
"html2text",
"autogen-core>=0.4.5,<0.5",
"autogen-agentchat>=0.4.5,<0.5",
"autogen-core>=0.4.9.2,<0.5",
"autogen-agentchat>=0.4.9.2,<0.5",
"autogen-ext[magentic-one, openai, azure]>=0.4.2,<0.5",
"azure-identity"
"anthropic",
]
optional-dependencies = {web = ["fastapi", "uvicorn"], database = ["psycopg"]}

View File

@ -807,10 +807,10 @@ source = { editable = "packages/autogen-studio" }
dependencies = [
{ name = "aiofiles" },
{ name = "alembic" },
{ name = "anthropic" },
{ name = "autogen-agentchat" },
{ name = "autogen-core" },
{ name = "autogen-ext", extra = ["azure", "magentic-one", "openai"] },
{ name = "azure-identity" },
{ name = "fastapi", extra = ["standard"] },
{ name = "html2text" },
{ name = "loguru" },
@ -837,10 +837,10 @@ web = [
requires-dist = [
{ name = "aiofiles" },
{ name = "alembic" },
{ name = "anthropic" },
{ name = "autogen-agentchat", editable = "packages/autogen-agentchat" },
{ name = "autogen-core", editable = "packages/autogen-core" },
{ name = "autogen-ext", extras = ["azure", "magentic-one", "openai"], editable = "packages/autogen-ext" },
{ name = "azure-identity" },
{ name = "fastapi", marker = "extra == 'web'" },
{ name = "fastapi", extras = ["standard"] },
{ name = "html2text" },