mirror of https://github.com/microsoft/autogen.git
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:
parent
685142cf51
commit
8f8ee0478a
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)}")
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
|
@ -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" />,
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]}
|
||||
|
||||
|
|
|
@ -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" },
|
||||
|
|
Loading…
Reference in New Issue