diff --git a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md index 6c5a0251d82f..e185a9128566 100644 --- a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md +++ b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md @@ -14,43 +14,89 @@ After defining a team, users can test it in the Playground view to accomplish va ## Declarative Specification of Componenents -AutoGen Studio uses a declarative specification system to build its GUI components. At runtime, the AGS API loads these specifications into AutoGen AgentChat objects to address tasks. +AutoGen Studio is built on the declarative specification behaviors of AutoGen AgentChat. This allows users to define teams, agents, models, tools, and termination conditions in python and then dump them into a JSON file for use in AutoGen Studio. -Here's an example of a declarative team specification: +Here's an example of an agent team and how it is converted to a JSON file: + +```python +from autogen_agentchat.agents import AssistantAgent +from autogen_agentchat.teams import RoundRobinGroupChat +from autogen_ext.models.openai import OpenAIChatCompletionClient +from autogen_agentchat.conditions import TextMentionTermination + +agent = AssistantAgent( + name="weather_agent", + model_client=OpenAIChatCompletionClient( + model="gpt-4o-mini", + ), + ) + +agent_team = RoundRobinGroupChat([agent], termination_condition=TextMentionTermination("TERMINATE")) +config = agent_team.dump_component() +print(config.model_dump_json()) +``` ```json { - "version": "1.0.0", + "provider": "autogen_agentchat.teams.RoundRobinGroupChat", "component_type": "team", - "name": "sample_team", - "participants": [ - { - "component_type": "agent", - "name": "assistant_agent", - "agent_type": "AssistantAgent", - "system_message": "You are a helpful assistant. Solve tasks carefully. When done respond with TERMINATE", - "model_client": { - "component_type": "model", - "model": "gpt-4o-2024-08-06", - "model_type": "OpenAIChatCompletionClient" - }, - "tools": [] + "version": 1, + "component_version": 1, + "description": "A team that runs a group chat with participants taking turns in a round-robin fashion\n to publish a message to all.", + "label": "RoundRobinGroupChat", + "config": { + "participants": [ + { + "provider": "autogen_agentchat.agents.AssistantAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that provides assistance with tool use.", + "label": "AssistantAgent", + "config": { + "name": "weather_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { "model": "gpt-4o-mini" } + }, + "tools": [], + "handoffs": [], + "model_context": { + "provider": "autogen_core.model_context.UnboundedChatCompletionContext", + "component_type": "chat_completion_context", + "version": 1, + "component_version": 1, + "description": "An unbounded chat completion context that keeps a view of the all the messages.", + "label": "UnboundedChatCompletionContext", + "config": {} + }, + "description": "An agent that provides assistance with ability to use tools.", + "system_message": "You are a helpful AI assistant. Solve tasks using your tools. Reply with TERMINATE when the task has been completed.", + "model_client_stream": false, + "reflect_on_tool_use": false, + "tool_call_summary_format": "{result}" + } + } + ], + "termination_condition": { + "provider": "autogen_agentchat.conditions.TextMentionTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation if a specific text is mentioned.", + "label": "TextMentionTermination", + "config": { "text": "TERMINATE" } } - ], - "team_type": "RoundRobinGroupChat", - "termination_condition": { - "component_type": "termination", - "termination_type": "MaxMessageTermination", - "max_messages": 3 } } ``` -This example shows a team with a single agent, using the `RoundRobinGroupChat` type and a `MaxMessageTermination` condition limited to 3 messages. - -```{note} -Work is currently in progress to make the entire AgentChat API declarative. This will allow all agentchat components to be `dumped` into the same declarative specification format used by AGS. -``` +This example shows a team with a single agent, using the `RoundRobinGroupChat` type and a `TextMentionTermination` condition. ## Building an Agent Team diff --git a/python/packages/autogen-studio/autogenstudio/__init__.py b/python/packages/autogen-studio/autogenstudio/__init__.py index 137cbad5a834..f67d1562da19 100644 --- a/python/packages/autogen-studio/autogenstudio/__init__.py +++ b/python/packages/autogen-studio/autogenstudio/__init__.py @@ -1,18 +1,6 @@ from .database.db_manager import DatabaseManager -from .datamodel import Agent, AgentConfig, Model, ModelConfig, Team, TeamConfig, Tool, ToolConfig +from .datamodel import Team from .teammanager import TeamManager from .version import __version__ -__all__ = [ - "Tool", - "Model", - "DatabaseManager", - "Team", - "Agent", - "ToolConfig", - "ModelConfig", - "TeamConfig", - "AgentConfig", - "TeamManager", - "__version__", -] +__all__ = ["DatabaseManager", "Team", "TeamManager", "__version__"] diff --git a/python/packages/autogen-studio/autogenstudio/database/__init__.py b/python/packages/autogen-studio/autogenstudio/database/__init__.py index acdf583557c3..be859be0a2cb 100644 --- a/python/packages/autogen-studio/autogenstudio/database/__init__.py +++ b/python/packages/autogen-studio/autogenstudio/database/__init__.py @@ -1,3 +1,6 @@ -from .component_factory import Component, ComponentFactory -from .config_manager import ConfigurationManager from .db_manager import DatabaseManager +from .gallery_builder import GalleryBuilder, create_default_gallery + +__all__ = [ + "DatabaseManager", +] diff --git a/python/packages/autogen-studio/autogenstudio/database/component_factory.py b/python/packages/autogen-studio/autogenstudio/database/component_factory.py deleted file mode 100644 index b954b39c0f4b..000000000000 --- a/python/packages/autogen-studio/autogenstudio/database/component_factory.py +++ /dev/null @@ -1,503 +0,0 @@ -import json -import logging -from datetime import datetime -from pathlib import Path -from typing import Callable, Dict, List, Literal, Optional, Union - -import aiofiles -import yaml -from autogen_agentchat.agents import AssistantAgent, UserProxyAgent -from autogen_agentchat.conditions import ( - ExternalTermination, - HandoffTermination, - MaxMessageTermination, - SourceMatchTermination, - StopMessageTermination, - TextMentionTermination, - TimeoutTermination, - TokenUsageTermination, -) -from autogen_agentchat.teams import MagenticOneGroupChat, RoundRobinGroupChat, SelectorGroupChat -from autogen_core.tools import FunctionTool -from autogen_ext.agents.file_surfer import FileSurfer -from autogen_ext.agents.magentic_one import MagenticOneCoderAgent -from autogen_ext.agents.web_surfer import MultimodalWebSurfer -from autogen_ext.models.openai import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient - -from ..datamodel.types import ( - AgentConfig, - AgentTypes, - AssistantAgentConfig, - AzureOpenAIModelConfig, - CombinationTerminationConfig, - ComponentConfig, - ComponentConfigInput, - ComponentTypes, - MagenticOneTeamConfig, - MaxMessageTerminationConfig, - ModelConfig, - ModelTypes, - MultimodalWebSurferAgentConfig, - OpenAIModelConfig, - RoundRobinTeamConfig, - SelectorTeamConfig, - TeamConfig, - TeamTypes, - TerminationConfig, - TerminationTypes, - TextMentionTerminationConfig, - ToolConfig, - ToolTypes, - UserProxyAgentConfig, -) -from ..utils.utils import Version - -logger = logging.getLogger(__name__) - -TeamComponent = Union[RoundRobinGroupChat, SelectorGroupChat, MagenticOneGroupChat] -AgentComponent = Union[AssistantAgent, MultimodalWebSurfer, UserProxyAgent, FileSurfer, MagenticOneCoderAgent] -ModelComponent = Union[OpenAIChatCompletionClient, AzureOpenAIChatCompletionClient] -ToolComponent = Union[FunctionTool] # Will grow with more tool types -TerminationComponent = Union[ - MaxMessageTermination, - StopMessageTermination, - TextMentionTermination, - TimeoutTermination, - ExternalTermination, - TokenUsageTermination, - HandoffTermination, - SourceMatchTermination, - StopMessageTermination, -] - -Component = Union[TeamComponent, AgentComponent, ModelComponent, ToolComponent, TerminationComponent] - -ReturnType = Literal["object", "dict", "config"] - -DEFAULT_SELECTOR_PROMPT = """You are in a role play game. The following roles are available: -{roles}. -Read the following conversation. Then select the next role from {participants} to play. Only return the role. - -{history} - -Read the above conversation. Then select the next role from {participants} to play. Only return the role. -""" - -CONFIG_RETURN_TYPES = Literal["object", "dict", "config"] - - -class ComponentFactory: - """Creates and manages agent components with versioned configuration loading""" - - SUPPORTED_VERSIONS = { - ComponentTypes.TEAM: ["1.0.0"], - ComponentTypes.AGENT: ["1.0.0"], - ComponentTypes.MODEL: ["1.0.0"], - ComponentTypes.TOOL: ["1.0.0"], - ComponentTypes.TERMINATION: ["1.0.0"], - } - - def __init__(self): - self._model_cache: Dict[str, ModelComponent] = {} - self._tool_cache: Dict[str, FunctionTool] = {} - self._last_cache_clear = datetime.now() - - async def load( - self, component: ComponentConfigInput, input_func: Optional[Callable] = None, return_type: ReturnType = "object" - ) -> Union[Component, dict, ComponentConfig]: - """ - Universal loader for any component type - - Args: - component: Component configuration (file path, dict, or ComponentConfig) - input_func: Optional callable for user input handling - return_type: Type of return value ('object', 'dict', or 'config') - - Returns: - Component instance, config dict, or ComponentConfig based on return_type - """ - try: - # Load and validate config - if isinstance(component, (str, Path)): - component_dict = await self._load_from_file(component) - config = self._dict_to_config(component_dict) - elif isinstance(component, dict): - config = self._dict_to_config(component) - else: - config = component - - # Validate version - if not self._is_version_supported(config.component_type, config.version): - raise ValueError( - f"Unsupported version {config.version} for " - f"component type {config.component_type}. " - f"Supported versions: {self.SUPPORTED_VERSIONS[config.component_type]}" - ) - - # Return early if dict or config requested - if return_type == "dict": - return config.model_dump() - elif return_type == "config": - return config - - # Otherwise create and return component instance - handlers = { - ComponentTypes.TEAM: lambda c: self.load_team(c, input_func), - ComponentTypes.AGENT: lambda c: self.load_agent(c, input_func), - ComponentTypes.MODEL: self.load_model, - ComponentTypes.TOOL: self.load_tool, - ComponentTypes.TERMINATION: self.load_termination, - } - - handler = handlers.get(config.component_type) - if not handler: - raise ValueError(f"Unknown component type: {config.component_type}") - - return await handler(config) - - except Exception as e: - logger.error(f"Failed to load component: {str(e)}") - raise - - async def load_directory( - self, directory: Union[str, Path], return_type: ReturnType = "object" - ) -> List[Union[Component, dict, ComponentConfig]]: - """ - Import all component configurations from a directory. - """ - components = [] - try: - directory = Path(directory) - # Using Path.iterdir() instead of os.listdir - for path in list(directory.glob("*")): - if path.suffix.lower().endswith((".json", ".yaml", ".yml")): - try: - component = await self.load(path, return_type=return_type) - components.append(component) - except Exception as e: - logger.info(f"Failed to load component: {str(e)}, {path}") - - return components - except Exception as e: - logger.info(f"Failed to load directory: {str(e)}") - return components - - def _dict_to_config(self, config_dict: dict) -> ComponentConfig: - """Convert dictionary to appropriate config type based on component_type and type discriminator""" - if "component_type" not in config_dict: - raise ValueError("component_type is required in configuration") - - component_type = ComponentTypes(config_dict["component_type"]) - - # Define mapping structure - type_mappings = { - ComponentTypes.MODEL: { - "discriminator": "model_type", - ModelTypes.OPENAI.value: OpenAIModelConfig, - ModelTypes.AZUREOPENAI.value: AzureOpenAIModelConfig, - }, - ComponentTypes.AGENT: { - "discriminator": "agent_type", - AgentTypes.ASSISTANT.value: AssistantAgentConfig, - AgentTypes.USERPROXY.value: UserProxyAgentConfig, - AgentTypes.MULTIMODAL_WEBSURFER.value: MultimodalWebSurferAgentConfig, - }, - ComponentTypes.TEAM: { - "discriminator": "team_type", - TeamTypes.ROUND_ROBIN.value: RoundRobinTeamConfig, - TeamTypes.SELECTOR.value: SelectorTeamConfig, - TeamTypes.MAGENTIC_ONE.value: MagenticOneTeamConfig, - }, - ComponentTypes.TOOL: ToolConfig, - ComponentTypes.TERMINATION: { - "discriminator": "termination_type", - TerminationTypes.MAX_MESSAGES.value: MaxMessageTerminationConfig, - TerminationTypes.TEXT_MENTION.value: TextMentionTerminationConfig, - TerminationTypes.COMBINATION.value: CombinationTerminationConfig, - }, - } - - mapping = type_mappings.get(component_type) - if not mapping: - raise ValueError(f"Unknown component type: {component_type}") - - # Handle simple cases (no discriminator) - if isinstance(mapping, type): - return mapping(**config_dict) - - # Get discriminator field value - discriminator = mapping["discriminator"] - if discriminator not in config_dict: - raise ValueError(f"Missing {discriminator} in configuration") - - type_value = config_dict[discriminator] - config_class = mapping.get(type_value) - - if not config_class: - raise ValueError(f"Unknown {discriminator}: {type_value}") - - return config_class(**config_dict) - - async def load_termination(self, config: TerminationConfig) -> TerminationComponent: - """Create termination condition instance from configuration.""" - try: - if config.termination_type == TerminationTypes.COMBINATION: - if not config.conditions or len(config.conditions) < 2: - raise ValueError("Combination termination requires at least 2 conditions") - if not config.operator: - raise ValueError("Combination termination requires an operator (and/or)") - - # Load first two conditions - conditions = [await self.load_termination(cond) for cond in config.conditions[:2]] - result = conditions[0] & conditions[1] if config.operator == "and" else conditions[0] | conditions[1] - - # Process remaining conditions if any - for condition in config.conditions[2:]: - next_condition = await self.load_termination(condition) - result = result & next_condition if config.operator == "and" else result | next_condition - - return result - - elif config.termination_type == TerminationTypes.MAX_MESSAGES: - if config.max_messages is None: - raise ValueError("max_messages parameter required for MaxMessageTermination") - return MaxMessageTermination(max_messages=config.max_messages) - - elif config.termination_type == TerminationTypes.STOP_MESSAGE: - return StopMessageTermination() - - elif config.termination_type == TerminationTypes.TEXT_MENTION: - if not config.text: - raise ValueError("text parameter required for TextMentionTermination") - return TextMentionTermination(text=config.text) - - else: - raise ValueError(f"Unsupported termination type: {config.termination_type}") - - except Exception as e: - logger.error(f"Failed to create termination condition: {str(e)}") - raise ValueError(f"Termination condition creation failed: {str(e)}") from e - - async def load_team(self, config: TeamConfig, input_func: Optional[Callable] = None) -> TeamComponent: - """Create team instance from configuration.""" - try: - # Load participants (agents) with input_func - participants = [] - for participant in config.participants: - agent = await self.load(participant, input_func=input_func) - participants.append(agent) - - # Load termination condition if specified - termination = None - if config.termination_condition: - termination = await self.load(config.termination_condition) - - # Create team based on type - if config.team_type == TeamTypes.ROUND_ROBIN: - return RoundRobinGroupChat(participants=participants, termination_condition=termination) - elif config.team_type == TeamTypes.SELECTOR: - model_client = await self.load(config.model_client) - if not model_client: - raise ValueError("SelectorGroupChat requires a model_client") - selector_prompt = config.selector_prompt if config.selector_prompt else DEFAULT_SELECTOR_PROMPT - return SelectorGroupChat( - participants=participants, - model_client=model_client, - termination_condition=termination, - selector_prompt=selector_prompt, - ) - elif config.team_type == TeamTypes.MAGENTIC_ONE: - model_client = await self.load(config.model_client) - if not model_client: - raise ValueError("MagenticOneGroupChat requires a model_client") - return MagenticOneGroupChat( - participants=participants, - model_client=model_client, - termination_condition=termination if termination is not None else None, - max_turns=config.max_turns if config.max_turns is not None else 20, - ) - else: - raise ValueError(f"Unsupported team type: {config.team_type}") - - except Exception as e: - logger.error(f"Failed to create team {config.name}: {str(e)}") - raise ValueError(f"Team creation failed: {str(e)}") from e - - async def load_agent(self, config: AgentConfig, input_func: Optional[Callable] = None) -> AgentComponent: - """Create agent instance from configuration.""" - - model_client = None - system_message = None - tools = [] - if hasattr(config, "system_message") and config.system_message: - system_message = config.system_message - if hasattr(config, "model_client") and config.model_client: - model_client = await self.load(config.model_client) - if hasattr(config, "tools") and config.tools: - for tool_config in config.tools: - tool = await self.load(tool_config) - tools.append(tool) - - try: - if config.agent_type == AgentTypes.USERPROXY: - return UserProxyAgent( - name=config.name, - description=config.description or "A human user", - input_func=input_func, # Pass through to UserProxyAgent - ) - elif config.agent_type == AgentTypes.ASSISTANT: - system_message = config.system_message if config.system_message else "You are a helpful assistant" - - return AssistantAgent( - name=config.name, - description=config.description or "A helpful assistant", - model_client=model_client, - tools=tools, - system_message=system_message, - ) - elif config.agent_type == AgentTypes.MULTIMODAL_WEBSURFER: - return MultimodalWebSurfer( - name=config.name, - model_client=model_client, - headless=config.headless if config.headless is not None else True, - debug_dir=config.logs_dir if config.logs_dir is not None else None, - downloads_folder=config.logs_dir if config.logs_dir is not None else None, - to_save_screenshots=config.to_save_screenshots if config.to_save_screenshots is not None else False, - use_ocr=config.use_ocr if config.use_ocr is not None else False, - animate_actions=config.animate_actions if config.animate_actions is not None else False, - ) - elif config.agent_type == AgentTypes.FILE_SURFER: - return FileSurfer( - name=config.name, - model_client=model_client, - ) - elif config.agent_type == AgentTypes.MAGENTIC_ONE_CODER: - return MagenticOneCoderAgent( - name=config.name, - model_client=model_client, - ) - else: - raise ValueError(f"Unsupported agent type: {config.agent_type}") - - except Exception as e: - logger.error(f"Failed to create agent {config.name}: {str(e)}") - raise ValueError(f"Agent creation failed: {str(e)}") from e - - async def load_model(self, config: ModelConfig) -> ModelComponent: - """Create model instance from configuration.""" - try: - # Check cache first - cache_key = str(config.model_dump()) - if cache_key in self._model_cache: - logger.debug(f"Using cached model for {config.model}") - return self._model_cache[cache_key] - - if config.model_type == ModelTypes.OPENAI: - args = { - "model": config.model, - "api_key": config.api_key, - "base_url": config.base_url, - } - - if hasattr(config, "model_capabilities") and config.model_capabilities is not None: - args["model_capabilities"] = config.model_capabilities - - model = OpenAIChatCompletionClient(**args) - self._model_cache[cache_key] = model - return model - elif config.model_type == ModelTypes.AZUREOPENAI: - model = AzureOpenAIChatCompletionClient( - azure_deployment=config.azure_deployment, - model=config.model, - api_version=config.api_version, - azure_endpoint=config.azure_endpoint, - api_key=config.api_key, - ) - self._model_cache[cache_key] = model - return model - else: - raise ValueError(f"Unsupported model type: {config.model_type}") - - except Exception as e: - logger.error(f"Failed to create model {config.model}: {str(e)}") - raise ValueError(f"Model creation failed: {str(e)}") from e - - async def load_tool(self, config: ToolConfig) -> ToolComponent: - """Create tool instance from configuration.""" - try: - # Validate required fields - if not all([config.name, config.description, config.content, config.tool_type]): - raise ValueError("Tool configuration missing required fields") - - # Check cache first - cache_key = str(config.model_dump()) - if cache_key in self._tool_cache: - logger.debug(f"Using cached tool '{config.name}'") - return self._tool_cache[cache_key] - - if config.tool_type == ToolTypes.PYTHON_FUNCTION: - tool = FunctionTool( - name=config.name, description=config.description, func=self._func_from_string(config.content) - ) - self._tool_cache[cache_key] = tool - return tool - else: - raise ValueError(f"Unsupported tool type: {config.tool_type}") - - except Exception as e: - logger.error(f"Failed to create tool '{config.name}': {str(e)}") - raise - - async def _load_from_file(self, path: Union[str, Path]) -> dict: - """Load configuration from JSON or YAML file.""" - path = Path(path) - if not path.exists(): - raise FileNotFoundError(f"Config file not found: {path}") - - try: - async with aiofiles.open(path) as f: - content = await f.read() - if path.suffix == ".json": - return json.loads(content) - elif path.suffix in (".yml", ".yaml"): - return yaml.safe_load(content) - else: - raise ValueError(f"Unsupported file format: {path.suffix}") - except Exception as e: - raise ValueError(f"Failed to load file {path}: {str(e)}") from e - - def _func_from_string(self, content: str) -> callable: - """Convert function string to callable.""" - try: - namespace = {} - exec(content, namespace) - for item in namespace.values(): - if callable(item) and not isinstance(item, type): - return item - raise ValueError("No function found in provided code") - except Exception as e: - raise ValueError(f"Failed to create function: {str(e)}") from e - - def _is_version_supported(self, component_type: ComponentTypes, ver: str) -> bool: - """Check if version is supported for component type.""" - try: - version = Version(ver) - supported = [Version(v) for v in self.SUPPORTED_VERSIONS[component_type]] - return any(version == v for v in supported) - except ValueError: - return False - - async def cleanup(self) -> None: - """Cleanup resources and clear caches.""" - for model in self._model_cache.values(): - if hasattr(model, "cleanup"): - await model.cleanup() - - for tool in self._tool_cache.values(): - if hasattr(tool, "cleanup"): - await tool.cleanup() - - self._model_cache.clear() - self._tool_cache.clear() - self._last_cache_clear = datetime.now() - logger.info("Cleared all component caches") diff --git a/python/packages/autogen-studio/autogenstudio/database/config_manager.py b/python/packages/autogen-studio/autogenstudio/database/config_manager.py deleted file mode 100644 index 3cd1b43d81ab..000000000000 --- a/python/packages/autogen-studio/autogenstudio/database/config_manager.py +++ /dev/null @@ -1,268 +0,0 @@ -import logging -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - -from loguru import logger - -from ..datamodel.db import Agent, LinkTypes, Model, Team, Tool -from ..datamodel.types import ComponentConfigInput, ComponentTypes, Response -from .component_factory import ComponentFactory -from .db_manager import DatabaseManager - - -class ConfigurationManager: - """Manages persistence and relationships of components using ComponentFactory for validation""" - - DEFAULT_UNIQUENESS_FIELDS = { - ComponentTypes.MODEL: ["model_type", "model"], - ComponentTypes.TOOL: ["name"], - ComponentTypes.AGENT: ["agent_type", "name"], - ComponentTypes.TEAM: ["team_type", "name"], - } - - def __init__(self, db_manager: DatabaseManager, uniqueness_fields: Dict[ComponentTypes, List[str]] = None): - self.db_manager = db_manager - self.component_factory = ComponentFactory() - self.uniqueness_fields = uniqueness_fields or self.DEFAULT_UNIQUENESS_FIELDS - - async def import_component( - self, component_config: ComponentConfigInput, user_id: str, check_exists: bool = False - ) -> Response: - """ - Import a component configuration, validate it, and store the resulting component. - - Args: - component_config: Configuration for the component (file path, dict, or ComponentConfig) - user_id: User ID to associate with imported component - check_exists: Whether to check for existing components before storing (default: False) - - Returns: - Response containing import results or error - """ - try: - # Get validated config as dict - config = await self.component_factory.load(component_config, return_type="dict") - - # Get component type - component_type = self._determine_component_type(config) - if not component_type: - raise ValueError("Unable to determine component type from config") - - # Check existence if requested - if check_exists: - existing = self._check_exists(component_type, config, user_id) - if existing: - return Response( - message=self._format_exists_message(component_type, config), - status=True, - data={"id": existing.id}, - ) - - # Route to appropriate storage method - if component_type == ComponentTypes.TEAM: - return await self._store_team(config, user_id, check_exists) - elif component_type == ComponentTypes.AGENT: - return await self._store_agent(config, user_id, check_exists) - elif component_type == ComponentTypes.MODEL: - return await self._store_model(config, user_id) - elif component_type == ComponentTypes.TOOL: - return await self._store_tool(config, user_id) - else: - raise ValueError(f"Unsupported component type: {component_type}") - - except Exception as e: - logger.error(f"Failed to import component: {str(e)}") - return Response(message=str(e), status=False) - - async def import_directory(self, directory: Union[str, Path], user_id: str, check_exists: bool = False) -> Response: - """ - Import all component configurations from a directory. - - Args: - directory: Path to directory containing configuration files - user_id: User ID to associate with imported components - check_exists: Whether to check for existing components before storing (default: False) - - Returns: - Response containing import results for all files - """ - try: - configs = await self.component_factory.load_directory(directory, return_type="dict") - - results = [] - for config in configs: - result = await self.import_component(config, user_id, check_exists) - results.append( - { - "component": self._get_component_type(config), - "status": result.status, - "message": result.message, - "id": result.data.get("id") if result.status else None, - } - ) - - return Response(message="Directory import complete", status=True, data=results) - - except Exception as e: - logger.error(f"Failed to import directory: {str(e)}") - return Response(message=str(e), status=False) - - async def _store_team(self, config: dict, user_id: str, check_exists: bool = False) -> Response: - """Store team component and manage its relationships with agents""" - try: - # Store the team - team_db = Team(user_id=user_id, config=config) - team_result = self.db_manager.upsert(team_db) - if not team_result.status: - return team_result - - team_id = team_result.data["id"] - - # Handle participants (agents) - for participant in config.get("participants", []): - if check_exists: - # Check for existing agent - agent_type = self._determine_component_type(participant) - existing_agent = self._check_exists(agent_type, participant, user_id) - if existing_agent: - # Link existing agent - self.db_manager.link(LinkTypes.TEAM_AGENT, team_id, existing_agent.id) - logger.info(f"Linked existing agent to team: {existing_agent}") - continue - - # Store and link new agent - agent_result = await self._store_agent(participant, user_id, check_exists) - if agent_result.status: - self.db_manager.link(LinkTypes.TEAM_AGENT, team_id, agent_result.data["id"]) - - return team_result - - except Exception as e: - logger.error(f"Failed to store team: {str(e)}") - return Response(message=str(e), status=False) - - async def _store_agent(self, config: dict, user_id: str, check_exists: bool = False) -> Response: - """Store agent component and manage its relationships with tools and model""" - try: - # Store the agent - agent_db = Agent(user_id=user_id, config=config) - agent_result = self.db_manager.upsert(agent_db) - if not agent_result.status: - return agent_result - - agent_id = agent_result.data["id"] - - # Handle model client - if "model_client" in config: - if check_exists: - # Check for existing model - model_type = self._determine_component_type(config["model_client"]) - existing_model = self._check_exists(model_type, config["model_client"], user_id) - if existing_model: - # Link existing model - self.db_manager.link(LinkTypes.AGENT_MODEL, agent_id, existing_model.id) - logger.info(f"Linked existing model to agent: {existing_model.config.model_type}") - else: - # Store and link new model - model_result = await self._store_model(config["model_client"], user_id) - if model_result.status: - self.db_manager.link(LinkTypes.AGENT_MODEL, agent_id, model_result.data["id"]) - else: - # Store and link new model without checking - model_result = await self._store_model(config["model_client"], user_id) - if model_result.status: - self.db_manager.link(LinkTypes.AGENT_MODEL, agent_id, model_result.data["id"]) - - # Handle tools - for tool_config in config.get("tools", []): - if check_exists: - # Check for existing tool - tool_type = self._determine_component_type(tool_config) - existing_tool = self._check_exists(tool_type, tool_config, user_id) - if existing_tool: - # Link existing tool - self.db_manager.link(LinkTypes.AGENT_TOOL, agent_id, existing_tool.id) - logger.info(f"Linked existing tool to agent: {existing_tool.config.name}") - continue - - # Store and link new tool - tool_result = await self._store_tool(tool_config, user_id) - if tool_result.status: - self.db_manager.link(LinkTypes.AGENT_TOOL, agent_id, tool_result.data["id"]) - - return agent_result - - except Exception as e: - logger.error(f"Failed to store agent: {str(e)}") - return Response(message=str(e), status=False) - - async def _store_model(self, config: dict, user_id: str) -> Response: - """Store model component (leaf node - no relationships)""" - try: - model_db = Model(user_id=user_id, config=config) - return self.db_manager.upsert(model_db) - - except Exception as e: - logger.error(f"Failed to store model: {str(e)}") - return Response(message=str(e), status=False) - - async def _store_tool(self, config: dict, user_id: str) -> Response: - """Store tool component (leaf node - no relationships)""" - try: - tool_db = Tool(user_id=user_id, config=config) - return self.db_manager.upsert(tool_db) - - except Exception as e: - logger.error(f"Failed to store tool: {str(e)}") - return Response(message=str(e), status=False) - - def _check_exists( - self, component_type: ComponentTypes, config: dict, user_id: str - ) -> Optional[Union[Model, Tool, Agent, Team]]: - """Check if component exists based on configured uniqueness fields.""" - fields = self.uniqueness_fields.get(component_type, []) - if not fields: - return None - - component_class = { - ComponentTypes.MODEL: Model, - ComponentTypes.TOOL: Tool, - ComponentTypes.AGENT: Agent, - ComponentTypes.TEAM: Team, - }.get(component_type) - - components = self.db_manager.get(component_class, {"user_id": user_id}).data - - for component in components: - matches = all(component.config.get(field) == config.get(field) for field in fields) - if matches: - return component - - return None - - def _format_exists_message(self, component_type: ComponentTypes, config: dict) -> str: - """Format existence message with identifying fields.""" - fields = self.uniqueness_fields.get(component_type, []) - field_values = [f"{field}='{config.get(field)}'" for field in fields] - return f"{component_type.value} with {' and '.join(field_values)} already exists" - - def _determine_component_type(self, config: dict) -> Optional[ComponentTypes]: - """Determine component type from configuration dictionary""" - if "team_type" in config: - return ComponentTypes.TEAM - elif "agent_type" in config: - return ComponentTypes.AGENT - elif "model_type" in config: - return ComponentTypes.MODEL - elif "tool_type" in config: - return ComponentTypes.TOOL - return None - - def _get_component_type(self, config: dict) -> str: - """Helper to get component type string from config""" - component_type = self._determine_component_type(config) - return component_type.value if component_type else "unknown" - - async def cleanup(self): - """Cleanup resources""" - await self.component_factory.cleanup() diff --git a/python/packages/autogen-studio/autogenstudio/database/db_manager.py b/python/packages/autogen-studio/autogenstudio/database/db_manager.py index 2b9ec43e3bd5..1df428016a48 100644 --- a/python/packages/autogen-studio/autogenstudio/database/db_manager.py +++ b/python/packages/autogen-studio/autogenstudio/database/db_manager.py @@ -1,17 +1,16 @@ import threading from datetime import datetime from pathlib import Path -from typing import Optional +from typing import Optional, Union from loguru import logger -from sqlalchemy import exc, func, inspect, text +from sqlalchemy import exc, inspect, text from sqlmodel import Session, SQLModel, and_, create_engine, select -from ..datamodel import LinkTypes, Response +from ..datamodel import Response, Team +from ..teammanager import TeamManager from .schema_manager import SchemaManager -# from .dbutils import init_db_samples - class DatabaseManager: _init_lock = threading.Lock() @@ -247,152 +246,85 @@ def delete(self, model_class: SQLModel, filters: dict = None): return Response(message=status_message, status=status, data=None) - def link( - self, - link_type: LinkTypes, - primary_id: int, - secondary_id: int, - sequence: Optional[int] = None, - ): - """Link two entities with automatic sequence handling.""" - with Session(self.engine) as session: - try: - # Get classes from LinkTypes - primary_class = link_type.primary_class - secondary_class = link_type.secondary_class - link_table = link_type.link_table - - # Get entities - primary_entity = session.get(primary_class, primary_id) - secondary_entity = session.get(secondary_class, secondary_id) - - if not primary_entity or not secondary_entity: - return Response(message="One or both entities do not exist", status=False) - - # Get field names - primary_id_field = f"{primary_class.__name__.lower()}_id" - secondary_id_field = f"{secondary_class.__name__.lower()}_id" - - # Check for existing link - existing_link = session.exec( - select(link_table).where( - and_( - getattr(link_table, primary_id_field) == primary_id, - getattr(link_table, secondary_id_field) == secondary_id, - ) - ) - ).first() - - if existing_link: - return Response(message="Link already exists", status=False) - - # Get the next sequence number if not provided - if sequence is None: - max_seq_result = session.exec( - select(func.max(link_table.sequence)).where(getattr(link_table, primary_id_field) == primary_id) - ).first() - sequence = 0 if max_seq_result is None else max_seq_result + 1 - - # Create new link - new_link = link_table( - **{primary_id_field: primary_id, secondary_id_field: secondary_id, "sequence": sequence} - ) - session.add(new_link) - session.commit() - - return Response(message=f"Entities linked successfully with sequence {sequence}", status=True) - - except Exception as e: - session.rollback() - return Response(message=f"Error linking entities: {str(e)}", status=False) - - def unlink(self, link_type: LinkTypes, primary_id: int, secondary_id: int, sequence: Optional[int] = None): - """Unlink two entities and reorder sequences if needed.""" - with Session(self.engine) as session: - try: - # Get classes from LinkTypes - primary_class = link_type.primary_class - secondary_class = link_type.secondary_class - link_table = link_type.link_table - - # Get field names - primary_id_field = f"{primary_class.__name__.lower()}_id" - secondary_id_field = f"{secondary_class.__name__.lower()}_id" - - # Find existing link - statement = select(link_table).where( - and_( - getattr(link_table, primary_id_field) == primary_id, - getattr(link_table, secondary_id_field) == secondary_id, + async def import_team( + self, team_config: Union[str, Path, dict], user_id: str, check_exists: bool = False + ) -> Response: + try: + # Load config if path provided + if isinstance(team_config, (str, Path)): + config = await TeamManager.load_from_file(team_config) + else: + config = team_config + + # Check existence if requested + if check_exists: + existing = await self._check_team_exists(config, user_id) + if existing: + return Response( + message="Identical team configuration already exists", status=True, data={"id": existing.id} ) - ) - if sequence is not None: - statement = statement.where(link_table.sequence == sequence) + # Store in database + team_db = Team(user_id=user_id, config=config) - existing_link = session.exec(statement).first() + result = self.upsert(team_db) + return result - if not existing_link: - return Response(message="Link does not exist", status=False) - - deleted_sequence = existing_link.sequence - session.delete(existing_link) - - # Reorder sequences for remaining links - remaining_links = session.exec( - select(link_table) - .where(getattr(link_table, primary_id_field) == primary_id) - .where(link_table.sequence > deleted_sequence) - .order_by(link_table.sequence) - ).all() - - # Decrease sequence numbers to fill the gap - for link in remaining_links: - link.sequence -= 1 + except Exception as e: + logger.error(f"Failed to import team: {str(e)}") + return Response(message=str(e), status=False) - session.commit() + async def import_teams_from_directory( + self, directory: Union[str, Path], user_id: str, check_exists: bool = False + ) -> Response: + """ + Import all team configurations from a directory. - return Response(message="Entities unlinked successfully and sequences reordered", status=True) + Args: + directory: Path to directory containing team configs + user_id: User ID to associate with imported teams + check_exists: Whether to check for existing teams - except Exception as e: - session.rollback() - return Response(message=f"Error unlinking entities: {str(e)}", status=False) + Returns: + Response containing import results for all files + """ + try: + # Load all configs from directory + configs = await TeamManager.load_from_directory(directory) - def get_linked_entities( - self, - link_type: LinkTypes, - primary_id: int, - return_json: bool = False, - ): - """Get linked entities based on link type and primary ID, ordered by sequence.""" - with Session(self.engine) as session: - try: - # Get classes from LinkTypes - primary_class = link_type.primary_class - secondary_class = link_type.secondary_class - link_table = link_type.link_table + results = [] + for config in configs: + try: + result = await self.import_team(team_config=config, user_id=user_id, check_exists=check_exists) + + # Add result info + results.append( + { + "status": result.status, + "message": result.message, + "id": result.data.get("id") if result.status else None, + } + ) - # Get field names - primary_id_field = f"{primary_class.__name__.lower()}_id" - secondary_id_field = f"{secondary_class.__name__.lower()}_id" + except Exception as e: + logger.error(f"Failed to import team config: {str(e)}") + results.append({"status": False, "message": str(e), "id": None}) - # Query both link and entity, ordered by sequence - items = session.exec( - select(secondary_class) - .join(link_table, getattr(link_table, secondary_id_field) == secondary_class.id) - .where(getattr(link_table, primary_id_field) == primary_id) - .order_by(link_table.sequence) - ).all() + return Response(message="Directory import complete", status=True, data=results) - result = [item.model_dump() if return_json else item for item in items] + except Exception as e: + logger.error(f"Failed to import directory: {str(e)}") + return Response(message=str(e), status=False) - return Response(message="Linked entities retrieved successfully", status=True, data=result) + async def _check_team_exists(self, config: dict, user_id: str) -> Optional[Team]: + """Check if identical team config already exists""" + teams = self.get(Team, {"user_id": user_id}).data - except Exception as e: - logger.error(f"Error getting linked entities: {str(e)}") - return Response(message=f"Error getting linked entities: {str(e)}", status=False, data=[]) + for team in teams: + if team.config == config: + return team - # Add new close method + return None async def close(self): """Close database connections and cleanup resources""" diff --git a/python/packages/autogen-studio/autogenstudio/database/gallery_builder.py b/python/packages/autogen-studio/autogenstudio/database/gallery_builder.py new file mode 100644 index 000000000000..7b4d69ab48c0 --- /dev/null +++ b/python/packages/autogen-studio/autogenstudio/database/gallery_builder.py @@ -0,0 +1,243 @@ +from datetime import datetime +from typing import List, Optional + +from autogen_agentchat.agents import AssistantAgent, UserProxyAgent +from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination +from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat +from autogen_core import ComponentModel +from autogen_core.models import ModelInfo +from autogen_core.tools import FunctionTool +from autogen_ext.agents.web_surfer import MultimodalWebSurfer +from autogen_ext.models.openai import OpenAIChatCompletionClient + +from autogenstudio.datamodel import Gallery, GalleryComponents, GalleryItems, GalleryMetadata + + +class GalleryBuilder: + """Builder class for creating AutoGen component galleries.""" + + def __init__(self, id: str, name: str): + self.id = id + self.name = name + self.url: Optional[str] = None + self.teams: List[ComponentModel] = [] + self.agents: List[ComponentModel] = [] + self.models: List[ComponentModel] = [] + self.tools: List[ComponentModel] = [] + self.terminations: List[ComponentModel] = [] + + # Default metadata + self.metadata = GalleryMetadata( + author="AutoGen Team", + created_at=datetime.now(), + updated_at=datetime.now(), + version="1.0.0", + description="", + tags=[], + license="MIT", + category="conversation", + ) + + def set_metadata( + self, + author: Optional[str] = None, + version: Optional[str] = None, + description: Optional[str] = None, + tags: Optional[List[str]] = None, + license: Optional[str] = None, + category: Optional[str] = None, + ) -> "GalleryBuilder": + """Update gallery metadata.""" + if author: + self.metadata.author = author + if version: + self.metadata.version = version + if description: + self.metadata.description = description + if tags: + self.metadata.tags = tags + if license: + self.metadata.license = license + if category: + self.metadata.category = category + return self + + def add_team(self, team: ComponentModel) -> "GalleryBuilder": + """Add a team component to the gallery.""" + self.teams.append(team) + return self + + def add_agent(self, agent: ComponentModel) -> "GalleryBuilder": + """Add an agent component to the gallery.""" + self.agents.append(agent) + return self + + def add_model(self, model: ComponentModel) -> "GalleryBuilder": + """Add a model component to the gallery.""" + self.models.append(model) + return self + + def add_tool(self, tool: ComponentModel) -> "GalleryBuilder": + """Add a tool component to the gallery.""" + self.tools.append(tool) + return self + + def add_termination(self, termination: ComponentModel) -> "GalleryBuilder": + """Add a termination condition component to the gallery.""" + self.terminations.append(termination) + return self + + def build(self) -> Gallery: + """Build and return the complete gallery.""" + # Update timestamps + self.metadata.updated_at = datetime.now() + + return Gallery( + id=self.id, + name=self.name, + url=self.url, + metadata=self.metadata, + items=GalleryItems( + teams=self.teams, + components=GalleryComponents( + agents=self.agents, models=self.models, tools=self.tools, terminations=self.terminations + ), + ), + ) + + +def create_default_gallery() -> Gallery: + """Create a default gallery with all components including calculator and web surfer teams.""" + builder = GalleryBuilder(id="gallery_default", name="Default Component Gallery") + + # Set metadata + builder.set_metadata( + description="A default gallery containing basic components for human-in-loop conversations", + tags=["human-in-loop", "assistant"], + category="conversation", + ) + + # Create base model client + base_model = OpenAIChatCompletionClient(model="gpt-4o-mini") + builder.add_model(base_model.dump_component()) + + 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), + ) + builder.add_model(mistral_vllm_model.dump_component()) + + # Create websurfer model client + websurfer_model = OpenAIChatCompletionClient(model="gpt-4o-mini") + builder.add_model(websurfer_model.dump_component()) + + def calculator(a: float, b: float, operator: str) -> str: + try: + if operator == "+": + return str(a + b) + elif operator == "-": + return str(a - b) + elif operator == "*": + return str(a * b) + elif operator == "/": + if b == 0: + return "Error: Division by zero" + return str(a / b) + else: + return "Error: Invalid operator. Please use +, -, *, or /" + except Exception as e: + return f"Error: {str(e)}" + + # Create calculator tool + calculator_tool = FunctionTool( + name="calculator", + description="A simple calculator that performs basic arithmetic operations", + func=calculator, + global_imports=[], + ) + builder.add_tool(calculator_tool.dump_component()) + + # Create termination conditions for calculator team + calc_text_term = TextMentionTermination(text="TERMINATE") + calc_max_term = MaxMessageTermination(max_messages=10) + calc_or_term = calc_text_term | calc_max_term + + builder.add_termination(calc_text_term.dump_component()) + builder.add_termination(calc_max_term.dump_component()) + builder.add_termination(calc_or_term.dump_component()) + + # Create calculator assistant agent + calc_assistant = AssistantAgent( + name="assistant_agent", + system_message="You are a helpful assistant. Solve tasks carefully. When done, say TERMINATE.", + model_client=base_model, + tools=[calculator_tool], + ) + builder.add_agent(calc_assistant.dump_component()) + + # Create calculator team + calc_team = RoundRobinGroupChat(participants=[calc_assistant], termination_condition=calc_or_term) + builder.add_team(calc_team.dump_component()) + + # Create web surfer agent + websurfer_agent = MultimodalWebSurfer( + name="websurfer_agent", + description="an agent that solves tasks by browsing the web", + model_client=websurfer_model, + headless=True, + ) + builder.add_agent(websurfer_agent.dump_component()) + + # Create web surfer verification assistant + verification_assistant = AssistantAgent( + name="assistant_agent", + description="an agent that verifies and summarizes information", + system_message="You are a task verification assistant who is working with a web surfer agent to solve tasks. At each point, check if the task has been completed as requested by the user. If the websurfer_agent responds and the task has not yet been completed, respond with what is left to do and then say 'keep going'. If and only when the task has been completed, summarize and present a final answer that directly addresses the user task in detail and then respond with TERMINATE.", + model_client=websurfer_model, + ) + builder.add_agent(verification_assistant.dump_component()) + + # Create web surfer user proxy + web_user_proxy = UserProxyAgent( + name="user_proxy", + description="a human user that should be consulted only when the assistant_agent is unable to verify the information provided by the websurfer_agent", + ) + builder.add_agent(web_user_proxy.dump_component()) + + # Create web surfer team termination conditions + web_max_term = MaxMessageTermination(max_messages=20) + web_text_term = TextMentionTermination(text="TERMINATE") + web_termination = web_max_term | web_text_term + builder.add_termination(web_termination.dump_component()) + + # Create web surfer team + selector_prompt = """You are the cordinator of role play game. The following roles are available: +{roles}. Given a task, the websurfer_agent will be tasked to address it by browsing the web and providing information. The assistant_agent will be tasked with verifying the information provided by the websurfer_agent and summarizing the information to present a final answer to the user. If the task needs assistance from a human user (e.g., providing feedback, preferences, or the task is stalled), you should select the user_proxy role to provide the necessary information. +Read the following conversation. Then select the next role from {participants} to play. Only return the role. + +{history} + +Read the above conversation. Then select the next role from {participants} to play. Only return the role.""" + + websurfer_team = SelectorGroupChat( + participants=[websurfer_agent, verification_assistant, web_user_proxy], + selector_prompt=selector_prompt, + model_client=base_model, + termination_condition=web_termination, + ) + builder.add_team(websurfer_team.dump_component()) + + return builder.build() + + +# if __name__ == "__main__": +# # Create and save the gallery +# gallery = create_default_gallery() + +# # Print as JSON +# print(gallery.model_dump_json(indent=2)) + +# # Save to file +# with open("gallery_default.json", "w") as f: +# f.write(gallery.model_dump_json(indent=2)) diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py b/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py index 0d46fb26334e..b47788ed7273 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py @@ -1,11 +1,25 @@ -from .db import Agent, LinkTypes, Message, Model, Run, RunStatus, Session, Team, Tool +from .db import Message, Run, RunStatus, Session, Team from .types import ( - AgentConfig, - ComponentConfigInput, + Gallery, + GalleryComponents, + GalleryItems, + GalleryMetadata, MessageConfig, - ModelConfig, + MessageMeta, Response, - TeamConfig, + SocketMessage, TeamResult, - ToolConfig, ) + +__all__ = [ + "Team", + "Run", + "RunStatus", + "Session", + "Team", + "MessageConfig", + "MessageMeta", + "TeamResult", + "Response", + "SocketMessage", +] diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/db.py b/python/packages/autogen-studio/autogenstudio/datamodel/db.py index 45f439d33910..a568d5b0caa6 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/db.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/db.py @@ -2,141 +2,14 @@ from datetime import datetime from enum import Enum -from typing import List, Optional, Tuple, Type, Union +from typing import List, Optional, Union from uuid import UUID, uuid4 -from loguru import logger -from pydantic import BaseModel -from sqlalchemy import ForeignKey, Integer, UniqueConstraint -from sqlmodel import JSON, Column, DateTime, Field, Relationship, SQLModel, func +from autogen_core import ComponentModel +from sqlalchemy import ForeignKey, Integer +from sqlmodel import JSON, Column, DateTime, Field, SQLModel, func -from .types import AgentConfig, MessageConfig, MessageMeta, ModelConfig, TeamConfig, TeamResult, ToolConfig - -# added for python3.11 and sqlmodel 0.0.22 incompatibility -if hasattr(SQLModel, "model_config"): - SQLModel.model_config["protected_namespaces"] = () -elif hasattr(SQLModel, "Config"): - - class CustomSQLModel(SQLModel): - class Config: - protected_namespaces = () - - SQLModel = CustomSQLModel -else: - logger.warning("Unable to set protected_namespaces.") - -# pylint: disable=protected-access - - -class ComponentTypes(Enum): - TEAM = "team" - AGENT = "agent" - MODEL = "model" - TOOL = "tool" - - @property - def model_class(self) -> Type[SQLModel]: - return { - ComponentTypes.TEAM: Team, - ComponentTypes.AGENT: Agent, - ComponentTypes.MODEL: Model, - ComponentTypes.TOOL: Tool, - }[self] - - -class LinkTypes(Enum): - AGENT_MODEL = "agent_model" - AGENT_TOOL = "agent_tool" - TEAM_AGENT = "team_agent" - - @property - # type: ignore - def link_config(self) -> Tuple[Type[SQLModel], Type[SQLModel], Type[SQLModel]]: - return { - LinkTypes.AGENT_MODEL: (Agent, Model, AgentModelLink), - LinkTypes.AGENT_TOOL: (Agent, Tool, AgentToolLink), - LinkTypes.TEAM_AGENT: (Team, Agent, TeamAgentLink), - }[self] - - @property - def primary_class(self) -> Type[SQLModel]: # type: ignore - return self.link_config[0] - - @property - def secondary_class(self) -> Type[SQLModel]: # type: ignore - return self.link_config[1] - - @property - def link_table(self) -> Type[SQLModel]: # type: ignore - return self.link_config[2] - - -# link models -class AgentToolLink(SQLModel, table=True): - __table_args__ = ( - UniqueConstraint("agent_id", "sequence", name="unique_agent_tool_sequence"), - {"sqlite_autoincrement": True}, - ) - agent_id: int = Field(default=None, primary_key=True, foreign_key="agent.id") - tool_id: int = Field(default=None, primary_key=True, foreign_key="tool.id") - sequence: Optional[int] = Field(default=0, primary_key=True) - - -class AgentModelLink(SQLModel, table=True): - __table_args__ = ( - UniqueConstraint("agent_id", "sequence", name="unique_agent_tool_sequence"), - {"sqlite_autoincrement": True}, - ) - agent_id: int = Field(default=None, primary_key=True, foreign_key="agent.id") - model_id: int = Field(default=None, primary_key=True, foreign_key="model.id") - sequence: Optional[int] = Field(default=0, primary_key=True) - - -class TeamAgentLink(SQLModel, table=True): - __table_args__ = ( - UniqueConstraint("agent_id", "sequence", name="unique_agent_tool_sequence"), - {"sqlite_autoincrement": True}, - ) - team_id: int = Field(default=None, primary_key=True, foreign_key="team.id") - agent_id: int = Field(default=None, primary_key=True, foreign_key="agent.id") - sequence: Optional[int] = Field(default=0, primary_key=True) - - -# database models - - -class Tool(SQLModel, table=True): - __table_args__ = {"sqlite_autoincrement": True} - id: Optional[int] = Field(default=None, primary_key=True) - created_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), server_default=func.now()), - ) # pylint: disable=not-callable - updated_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), onupdate=func.now()), - ) # pylint: disable=not-callable - user_id: Optional[str] = None - version: Optional[str] = "0.0.1" - config: Union[ToolConfig, dict] = Field(sa_column=Column(JSON)) - agents: List["Agent"] = Relationship(back_populates="tools", link_model=AgentToolLink) - - -class Model(SQLModel, table=True): - __table_args__ = {"sqlite_autoincrement": True} - id: Optional[int] = Field(default=None, primary_key=True) - created_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), server_default=func.now()), - ) # pylint: disable=not-callable - updated_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), onupdate=func.now()), - ) # pylint: disable=not-callable - user_id: Optional[str] = None - version: Optional[str] = "0.0.1" - config: Union[ModelConfig, dict] = Field(sa_column=Column(JSON)) - agents: List["Agent"] = Relationship(back_populates="models", link_model=AgentModelLink) +from .types import MessageConfig, MessageMeta, TeamResult class Team(SQLModel, table=True): @@ -152,27 +25,7 @@ class Team(SQLModel, table=True): ) # pylint: disable=not-callable user_id: Optional[str] = None version: Optional[str] = "0.0.1" - config: Union[TeamConfig, dict] = Field(sa_column=Column(JSON)) - agents: List["Agent"] = Relationship(back_populates="teams", link_model=TeamAgentLink) - - -class Agent(SQLModel, table=True): - __table_args__ = {"sqlite_autoincrement": True} - id: Optional[int] = Field(default=None, primary_key=True) - created_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), server_default=func.now()), - ) # pylint: disable=not-callable - updated_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), onupdate=func.now()), - ) # pylint: disable=not-callable - user_id: Optional[str] = None - version: Optional[str] = "0.0.1" - config: Union[AgentConfig, dict] = Field(sa_column=Column(JSON)) - tools: List[Tool] = Relationship(back_populates="agents", link_model=AgentToolLink) - models: List[Model] = Relationship(back_populates="agents", link_model=AgentModelLink) - teams: List[Team] = Relationship(back_populates="agents", link_model=TeamAgentLink) + component: Union[ComponentModel, dict] = Field(sa_column=Column(JSON)) class Message(SQLModel, table=True): @@ -250,32 +103,3 @@ class Run(SQLModel, table=True): class Config: json_encoders = {UUID: str, datetime: lambda v: v.isoformat()} - - -class GalleryConfig(SQLModel, table=False): - id: UUID = Field(default_factory=uuid4, primary_key=True, index=True) - title: Optional[str] = None - description: Optional[str] = None - run: Run - team: TeamConfig = None - tags: Optional[List[str]] = None - visibility: str = "public" # public, private, shared - - class Config: - json_encoders = {UUID: str, datetime: lambda v: v.isoformat()} - - -class Gallery(SQLModel, table=True): - __table_args__ = {"sqlite_autoincrement": True} - id: Optional[int] = Field(default=None, primary_key=True) - created_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), server_default=func.now()), - ) - updated_at: datetime = Field( - default_factory=datetime.now, - sa_column=Column(DateTime(timezone=True), onupdate=func.now()), - ) - user_id: Optional[str] = None - version: Optional[str] = "0.0.1" - config: Union[GalleryConfig, dict] = Field(default_factory=GalleryConfig, sa_column=Column(JSON)) diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/types.py b/python/packages/autogen-studio/autogenstudio/datamodel/types.py index eb02fb121ebe..b35b7a054e67 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/types.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/types.py @@ -1,55 +1,9 @@ from datetime import datetime -from enum import Enum -from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from autogen_agentchat.base import TaskResult -from autogen_core.models import ModelCapabilities -from pydantic import BaseModel, Field - - -class ModelTypes(str, Enum): - OPENAI = "OpenAIChatCompletionClient" - AZUREOPENAI = "AzureOpenAIChatCompletionClient" - - -class ToolTypes(str, Enum): - PYTHON_FUNCTION = "PythonFunction" - - -class AgentTypes(str, Enum): - ASSISTANT = "AssistantAgent" - USERPROXY = "UserProxyAgent" - MULTIMODAL_WEBSURFER = "MultimodalWebSurfer" - FILE_SURFER = "FileSurfer" - MAGENTIC_ONE_CODER = "MagenticOneCoderAgent" - - -class TeamTypes(str, Enum): - ROUND_ROBIN = "RoundRobinGroupChat" - SELECTOR = "SelectorGroupChat" - MAGENTIC_ONE = "MagenticOneGroupChat" - - -class TerminationTypes(str, Enum): - MAX_MESSAGES = "MaxMessageTermination" - STOP_MESSAGE = "StopMessageTermination" - TEXT_MENTION = "TextMentionTermination" - COMBINATION = "CombinationTermination" - - -class ComponentTypes(str, Enum): - TEAM = "team" - AGENT = "agent" - MODEL = "model" - TOOL = "tool" - TERMINATION = "termination" - - -class BaseConfig(BaseModel): - model_config = {"protected_namespaces": ()} - version: str = "1.0.0" - component_type: ComponentTypes +from autogen_core import ComponentModel +from pydantic import BaseModel class MessageConfig(BaseModel): @@ -58,134 +12,6 @@ class MessageConfig(BaseModel): message_type: Optional[str] = "text" -class BaseModelConfig(BaseConfig): - model: str - model_type: ModelTypes - api_key: Optional[str] = None - base_url: Optional[str] = None - component_type: ComponentTypes = ComponentTypes.MODEL - model_capabilities: Optional[ModelCapabilities] = None - - -class OpenAIModelConfig(BaseModelConfig): - model_type: ModelTypes = ModelTypes.OPENAI - - -class AzureOpenAIModelConfig(BaseModelConfig): - azure_deployment: str - model: str - api_version: str - azure_endpoint: str - azure_ad_token_provider: Optional[str] = None - api_key: Optional[str] = None - model_type: ModelTypes = ModelTypes.AZUREOPENAI - - -ModelConfig = OpenAIModelConfig | AzureOpenAIModelConfig - - -class ToolConfig(BaseConfig): - name: str - description: str - content: str - tool_type: ToolTypes - component_type: ComponentTypes = ComponentTypes.TOOL - - -class BaseAgentConfig(BaseConfig): - name: str - agent_type: AgentTypes - description: Optional[str] = None - component_type: ComponentTypes = ComponentTypes.AGENT - - -class AssistantAgentConfig(BaseAgentConfig): - agent_type: AgentTypes = AgentTypes.ASSISTANT - model_client: ModelConfig - tools: Optional[List[ToolConfig]] = None - system_message: Optional[str] = None - - -class UserProxyAgentConfig(BaseAgentConfig): - agent_type: AgentTypes = AgentTypes.USERPROXY - - -class MultimodalWebSurferAgentConfig(BaseAgentConfig): - agent_type: AgentTypes = AgentTypes.MULTIMODAL_WEBSURFER - model_client: ModelConfig - headless: bool = True - logs_dir: str = None - to_save_screenshots: bool = False - use_ocr: bool = False - animate_actions: bool = False - tools: Optional[List[ToolConfig]] = None - - -AgentConfig = AssistantAgentConfig | UserProxyAgentConfig | MultimodalWebSurferAgentConfig - - -class BaseTerminationConfig(BaseConfig): - termination_type: TerminationTypes - component_type: ComponentTypes = ComponentTypes.TERMINATION - - -class MaxMessageTerminationConfig(BaseTerminationConfig): - termination_type: TerminationTypes = TerminationTypes.MAX_MESSAGES - max_messages: int - - -class TextMentionTerminationConfig(BaseTerminationConfig): - termination_type: TerminationTypes = TerminationTypes.TEXT_MENTION - text: str - - -class StopMessageTerminationConfig(BaseTerminationConfig): - termination_type: TerminationTypes = TerminationTypes.STOP_MESSAGE - - -class CombinationTerminationConfig(BaseTerminationConfig): - termination_type: TerminationTypes = TerminationTypes.COMBINATION - operator: str - conditions: List["TerminationConfig"] - - -TerminationConfig = ( - MaxMessageTerminationConfig - | TextMentionTerminationConfig - | CombinationTerminationConfig - | StopMessageTerminationConfig -) - - -class BaseTeamConfig(BaseConfig): - name: str - participants: List[AgentConfig] - team_type: TeamTypes - termination_condition: Optional[TerminationConfig] = None - component_type: ComponentTypes = ComponentTypes.TEAM - max_turns: Optional[int] = None - - -class RoundRobinTeamConfig(BaseTeamConfig): - team_type: TeamTypes = TeamTypes.ROUND_ROBIN - - -class SelectorTeamConfig(BaseTeamConfig): - team_type: TeamTypes = TeamTypes.SELECTOR - selector_prompt: Optional[str] = None - model_client: ModelConfig - - -class MagenticOneTeamConfig(BaseTeamConfig): - team_type: TeamTypes = TeamTypes.MAGENTIC_ONE - model_client: ModelConfig - max_stalls: int = 3 - final_answer_prompt: Optional[str] = None - - -TeamConfig = RoundRobinTeamConfig | SelectorTeamConfig | MagenticOneTeamConfig - - class TeamResult(BaseModel): task_result: TaskResult usage: str @@ -202,6 +28,39 @@ class MessageMeta(BaseModel): usage: Optional[List[dict]] = None +class GalleryMetadata(BaseModel): + author: str + created_at: datetime + updated_at: datetime + version: str + description: Optional[str] = None + tags: Optional[List[str]] = None + license: Optional[str] = None + homepage: Optional[str] = None + category: Optional[str] = None + last_synced: Optional[datetime] = None + + +class GalleryComponents(BaseModel): + agents: List[ComponentModel] + models: List[ComponentModel] + tools: List[ComponentModel] + terminations: List[ComponentModel] + + +class GalleryItems(BaseModel): + teams: List[ComponentModel] + components: GalleryComponents + + +class Gallery(BaseModel): + id: str + name: str + url: Optional[str] = None + metadata: GalleryMetadata + items: GalleryItems + + # web request/response data models @@ -215,8 +74,3 @@ class SocketMessage(BaseModel): connection_id: str data: Dict[str, Any] type: str - - -ComponentConfig = Union[TeamConfig, AgentConfig, ModelConfig, ToolConfig, TerminationConfig] - -ComponentConfigInput = Union[str, Path, dict, ComponentConfig] diff --git a/python/packages/autogen-studio/autogenstudio/teammanager.py b/python/packages/autogen-studio/autogenstudio/teammanager.py deleted file mode 100644 index b4ba92460ef3..000000000000 --- a/python/packages/autogen-studio/autogenstudio/teammanager.py +++ /dev/null @@ -1,73 +0,0 @@ -import time -from typing import AsyncGenerator, Callable, Optional, Union - -from autogen_agentchat.base import TaskResult -from autogen_agentchat.messages import AgentEvent, ChatMessage -from autogen_core import CancellationToken - -from .database import Component, ComponentFactory -from .datamodel import ComponentConfigInput, TeamResult - - -class TeamManager: - def __init__(self) -> None: - self.component_factory = ComponentFactory() - - async def _create_team(self, team_config: ComponentConfigInput, input_func: Optional[Callable] = None) -> Component: - """Create team instance with common setup logic""" - return await self.component_factory.load(team_config, input_func=input_func) - - def _create_result(self, task_result: TaskResult, start_time: float) -> TeamResult: - """Create TeamResult with timing info""" - return TeamResult(task_result=task_result, usage="", duration=time.time() - start_time) - - async def run_stream( - self, - task: str, - team_config: ComponentConfigInput, - input_func: Optional[Callable] = None, - cancellation_token: Optional[CancellationToken] = None, - ) -> AsyncGenerator[Union[AgentEvent | ChatMessage, ChatMessage, TaskResult], None]: - """Stream the team's execution results""" - start_time = time.time() - - try: - team = await self._create_team(team_config, input_func) - stream = team.run_stream(task=task, cancellation_token=cancellation_token) - - async for message in stream: - if cancellation_token and cancellation_token.is_cancelled(): - break - - if isinstance(message, TaskResult): - yield self._create_result(message, start_time) - else: - yield message - - # close agent resources - for agent in team._participants: - if hasattr(agent, "close"): - await agent.close() - - except Exception as e: - raise e - - async def run( - self, - task: str, - team_config: ComponentConfigInput, - input_func: Optional[Callable] = None, - cancellation_token: Optional[CancellationToken] = None, - ) -> TeamResult: - """Original non-streaming run method with optional cancellation""" - start_time = time.time() - - team = await self._create_team(team_config, input_func) - result = await team.run(task=task, cancellation_token=cancellation_token) - - # close agent resources - for agent in team._participants: - if hasattr(agent, "close"): - await agent.close() - - return self._create_result(result, start_time) diff --git a/python/packages/autogen-studio/autogenstudio/teammanager/__init__.py b/python/packages/autogen-studio/autogenstudio/teammanager/__init__.py new file mode 100644 index 000000000000..7f202c73942b --- /dev/null +++ b/python/packages/autogen-studio/autogenstudio/teammanager/__init__.py @@ -0,0 +1,3 @@ +from .teammanager import TeamManager + +__all__ = ["TeamManager"] diff --git a/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py b/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py new file mode 100644 index 000000000000..886c228fab10 --- /dev/null +++ b/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py @@ -0,0 +1,134 @@ +import json +import logging +import time +from pathlib import Path +from typing import AsyncGenerator, Callable, List, Optional, Union + +import aiofiles +import yaml +from autogen_agentchat.base import TaskResult, Team +from autogen_agentchat.messages import AgentEvent, ChatMessage +from autogen_core import CancellationToken, Component, ComponentModel + +from ..datamodel.types import TeamResult + +logger = logging.getLogger(__name__) + + +class TeamManager: + """Manages team operations including loading configs and running teams""" + + @staticmethod + async def load_from_file(path: Union[str, Path]) -> dict: + """Load team configuration from JSON/YAML file""" + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"Config file not found: {path}") + + async with aiofiles.open(path) as f: + content = await f.read() + if path.suffix == ".json": + return json.loads(content) + elif path.suffix in (".yml", ".yaml"): + return yaml.safe_load(content) + raise ValueError(f"Unsupported file format: {path.suffix}") + + @staticmethod + async def load_from_directory(directory: Union[str, Path]) -> List[dict]: + """Load all team configurations from a directory + + Args: + directory (Union[str, Path]): Path to directory containing config files + + Returns: + List[dict]: List of loaded team configurations + """ + directory = Path(directory) + configs = [] + valid_extensions = {".json", ".yaml", ".yml"} + + for path in directory.iterdir(): + if path.is_file() and path.suffix.lower() in valid_extensions: + try: + config = await TeamManager.load_from_file(path) + configs.append(config) + except Exception as e: + logger.error(f"Failed to load {path}: {e}") + + return configs + + async def _create_team( + self, team_config: Union[str, Path, dict, ComponentModel], input_func: Optional[Callable] = None + ) -> Component: + """Create team instance from config""" + # Handle different input types + if isinstance(team_config, (str, Path)): + config = await self.load_from_file(team_config) + elif isinstance(team_config, dict): + config = team_config + else: + config = team_config.model_dump() + + # Use Component.load_component directly + team = Team.load_component(config) + + for agent in team._participants: + if hasattr(agent, "input_func"): + agent.input_func = input_func + + # TBD - set input function + return team + + async def run_stream( + self, + task: str, + team_config: Union[str, Path, dict, ComponentModel], + input_func: Optional[Callable] = None, + cancellation_token: Optional[CancellationToken] = None, + ) -> AsyncGenerator[Union[AgentEvent | ChatMessage, ChatMessage, TaskResult], None]: + """Stream team execution results""" + start_time = time.time() + team = None + + try: + team = await self._create_team(team_config, input_func) + + async for message in team.run_stream(task=task, cancellation_token=cancellation_token): + if cancellation_token and cancellation_token.is_cancelled(): + break + + if isinstance(message, TaskResult): + yield TeamResult(task_result=message, usage="", duration=time.time() - start_time) + else: + yield message + + finally: + # Ensure cleanup happens + if team and hasattr(team, "_participants"): + for agent in team._participants: + if hasattr(agent, "close"): + await agent.close() + + async def run( + self, + task: str, + team_config: Union[str, Path, dict, ComponentModel], + input_func: Optional[Callable] = None, + cancellation_token: Optional[CancellationToken] = None, + ) -> TeamResult: + """Run team synchronously""" + start_time = time.time() + team = None + + try: + team = await self._create_team(team_config, input_func) + result = await team.run(task=task, cancellation_token=cancellation_token) + + return TeamResult(task_result=result, usage="", duration=time.time() - start_time) + + finally: + # Ensure cleanup happens + if team and hasattr(team, "_participants"): + for agent in team._participants: + if hasattr(agent, "close"): + await agent.close() diff --git a/python/packages/autogen-studio/autogenstudio/web/app.py b/python/packages/autogen-studio/autogenstudio/web/app.py index 2e2ad3337248..82caccd98f4b 100644 --- a/python/packages/autogen-studio/autogenstudio/web/app.py +++ b/python/packages/autogen-studio/autogenstudio/web/app.py @@ -13,7 +13,7 @@ from .config import settings from .deps import cleanup_managers, init_managers from .initialization import AppInitializer -from .routes import agents, models, runs, sessions, teams, tools, ws +from .routes import runs, sessions, teams, ws # Configure logging # logger = logging.getLogger(__name__) @@ -104,26 +104,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: responses={404: {"description": "Not found"}}, ) -api.include_router( - agents.router, - prefix="/agents", - tags=["agents"], - responses={404: {"description": "Not found"}}, -) - -api.include_router( - models.router, - prefix="/models", - tags=["models"], - responses={404: {"description": "Not found"}}, -) - -api.include_router( - tools.router, - prefix="/tools", - tags=["tools"], - responses={404: {"description": "Not found"}}, -) api.include_router( ws.router, diff --git a/python/packages/autogen-studio/autogenstudio/web/config.py b/python/packages/autogen-studio/autogenstudio/web/config.py index 128edada9bf3..28b88b025563 100644 --- a/python/packages/autogen-studio/autogenstudio/web/config.py +++ b/python/packages/autogen-studio/autogenstudio/web/config.py @@ -3,7 +3,7 @@ class Settings(BaseSettings): - DATABASE_URI: str = "sqlite:///./autogen.db" + DATABASE_URI: str = "sqlite:///./autogen0404.db" API_DOCS: bool = False CLEANUP_INTERVAL: int = 300 # 5 minutes SESSION_TIMEOUT: int = 3600 # 1 hour diff --git a/python/packages/autogen-studio/autogenstudio/web/deps.py b/python/packages/autogen-studio/autogenstudio/web/deps.py index d2e5b6fabb4a..5e7cdd0d2cf4 100644 --- a/python/packages/autogen-studio/autogenstudio/web/deps.py +++ b/python/packages/autogen-studio/autogenstudio/web/deps.py @@ -5,7 +5,7 @@ from fastapi import Depends, HTTPException, status -from ..database import ConfigurationManager, DatabaseManager +from ..database import DatabaseManager from ..teammanager import TeamManager from .config import settings from .managers.connection import WebSocketManager @@ -94,9 +94,7 @@ async def init_managers(database_uri: str, config_dir: str, app_root: str) -> No _db_manager.initialize_database(auto_upgrade=settings.UPGRADE_DATABASE) # init default team config - - _team_config_manager = ConfigurationManager(db_manager=_db_manager) - await _team_config_manager.import_directory(config_dir, settings.DEFAULT_USER_ID, check_exists=True) + await _db_manager.import_teams_from_directory(config_dir, settings.DEFAULT_USER_ID, check_exists=True) # Initialize connection manager _websocket_manager = WebSocketManager(db_manager=_db_manager) diff --git a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py index 271b53d87675..67855e997404 100644 --- a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py +++ b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py @@ -1,5 +1,6 @@ import asyncio import logging +import traceback from datetime import datetime, timezone from typing import Any, Callable, Dict, Optional, Union from uuid import UUID @@ -137,12 +138,14 @@ async def start_stream(self, run_id: UUID, task: str, team_config: dict) -> None except Exception as e: logger.error(f"Stream error for run {run_id}: {e}") + traceback.print_exc() await self._handle_stream_error(run_id, e) finally: self._cancellation_tokens.pop(run_id, None) async def _save_message(self, run_id: UUID, message: Union[AgentEvent | ChatMessage, ChatMessage]) -> None: """Save a message to the database""" + run = await self._get_run(run_id) if run: db_message = Message( diff --git a/python/packages/autogen-studio/autogenstudio/web/routes/models.py b/python/packages/autogen-studio/autogenstudio/web/routes/models.py deleted file mode 100644 index f041e52cb93b..000000000000 --- a/python/packages/autogen-studio/autogenstudio/web/routes/models.py +++ /dev/null @@ -1,42 +0,0 @@ -# api/routes/models.py -from typing import Dict - -from fastapi import APIRouter, Depends, HTTPException -from openai import OpenAIError - -from ...datamodel import Model -from ..deps import get_db - -router = APIRouter() - - -@router.get("/") -async def list_models(user_id: str, db=Depends(get_db)) -> Dict: - """List all models for a user""" - response = db.get(Model, filters={"user_id": user_id}) - return {"status": True, "data": response.data} - - -@router.get("/{model_id}") -async def get_model(model_id: int, user_id: str, db=Depends(get_db)) -> Dict: - """Get a specific model""" - response = db.get(Model, filters={"id": model_id, "user_id": user_id}) - if not response.status or not response.data: - raise HTTPException(status_code=404, detail="Model not found") - return {"status": True, "data": response.data[0]} - - -@router.post("/") -async def create_model(model: Model, db=Depends(get_db)) -> Dict: - """Create a new model""" - response = db.upsert(model) - if not response.status: - raise HTTPException(status_code=400, detail=response.message) - return {"status": True, "data": response.data} - - -@router.delete("/{model_id}") -async def delete_model(model_id: int, user_id: str, db=Depends(get_db)) -> Dict: - """Delete a model""" - db.delete(filters={"id": model_id, "user_id": user_id}, model_class=Model) - return {"status": True, "message": "Model deleted successfully"} diff --git a/python/packages/autogen-studio/autogenstudio/web/routes/tools.py b/python/packages/autogen-studio/autogenstudio/web/routes/tools.py deleted file mode 100644 index da2ae7733b2b..000000000000 --- a/python/packages/autogen-studio/autogenstudio/web/routes/tools.py +++ /dev/null @@ -1,41 +0,0 @@ -# api/routes/tools.py -from typing import Dict - -from fastapi import APIRouter, Depends, HTTPException - -from ...datamodel import Tool -from ..deps import get_db - -router = APIRouter() - - -@router.get("/") -async def list_tools(user_id: str, db=Depends(get_db)) -> Dict: - """List all tools for a user""" - response = db.get(Tool, filters={"user_id": user_id}) - return {"status": True, "data": response.data} - - -@router.get("/{tool_id}") -async def get_tool(tool_id: int, user_id: str, db=Depends(get_db)) -> Dict: - """Get a specific tool""" - response = db.get(Tool, filters={"id": tool_id, "user_id": user_id}) - if not response.status or not response.data: - raise HTTPException(status_code=404, detail="Tool not found") - return {"status": True, "data": response.data[0]} - - -@router.post("/") -async def create_tool(tool: Tool, db=Depends(get_db)) -> Dict: - """Create a new tool""" - response = db.upsert(tool) - if not response.status: - raise HTTPException(status_code=400, detail=response.message) - return {"status": True, "data": response.data} - - -@router.delete("/{tool_id}") -async def delete_tool(tool_id: int, user_id: str, db=Depends(get_db)) -> Dict: - """Delete a tool""" - db.delete(filters={"id": tool_id, "user_id": user_id}, model_class=Tool) - return {"status": True, "message": "Tool deleted successfully"} diff --git a/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts b/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts index b502f04c892c..195950f08100 100644 --- a/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts +++ b/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts @@ -1,3 +1,22 @@ +// Base Component System + +export type ComponentTypes = + | "team" + | "agent" + | "model" + | "tool" + | "termination"; +export interface Component { + provider: string; + component_type: ComponentTypes; + version?: number; + component_version?: number; + description?: string | null; + config: T; + label?: string; +} + +// Message Types export interface RequestUsage { prompt_tokens: number; completion_tokens: number; @@ -11,7 +30,7 @@ export interface ImageContent { export interface FunctionCall { id: string; - arguments: string; // JSON string + arguments: string; name: string; } @@ -20,13 +39,11 @@ export interface FunctionExecutionResult { content: string; } -// Base message configuration (maps to Python BaseMessage) export interface BaseMessageConfig { source: string; models_usage?: RequestUsage; } -// Message configurations (mapping directly to Python classes) export interface TextMessageConfig extends BaseMessageConfig { content: string; } @@ -52,17 +69,6 @@ export interface ToolCallResultMessageConfig extends BaseMessageConfig { content: FunctionExecutionResult[]; } -// Message type unions (matching Python type aliases) -export type InnerMessageConfig = - | ToolCallMessageConfig - | ToolCallResultMessageConfig; - -export type ChatMessageConfig = - | TextMessageConfig - | MultiModalMessageConfig - | StopMessageConfig - | HandoffMessageConfig; - export type AgentMessageConfig = | TextMessageConfig | MultiModalMessageConfig @@ -71,210 +77,191 @@ export type AgentMessageConfig = | ToolCallMessageConfig | ToolCallResultMessageConfig; -// Database model -export interface DBModel { - id?: number; - user_id?: string; - created_at?: string; - updated_at?: string; - version?: number; -} - -export interface Message extends DBModel { - config: AgentMessageConfig; - session_id: number; - run_id: string; +// Tool Configs +export interface FunctionToolConfig { + source_code: string; + name: string; + description: string; + global_imports: any[]; // Sequence[Import] equivalent + has_cancellation_support: boolean; } -export interface Session extends DBModel { - name: string; - team_id?: number; +// Provider-based Configs +export interface SelectorGroupChatConfig { + participants: Component[]; + model_client: Component; + termination_condition?: Component; + max_turns?: number; + selector_prompt: string; + allow_repeated_speaker: boolean; } -export interface SessionRuns { - runs: Run[]; +export interface RoundRobinGroupChatConfig { + participants: Component[]; + termination_condition?: Component; + max_turns?: number; } -export interface BaseConfig { - component_type: string; - version?: string; +export interface MultimodalWebSurferConfig { + name: string; + model_client: Component; + downloads_folder?: string; description?: string; + debug_dir?: string; + headless?: boolean; + start_page?: string; + animate_actions?: boolean; + to_save_screenshots?: boolean; + use_ocr?: boolean; + browser_channel?: string; + browser_data_dir?: string; + to_resize_viewport?: boolean; +} + +export interface AssistantAgentConfig { + name: string; + model_client: Component; + tools?: Component[]; + handoffs?: any[]; // HandoffBase | str equivalent + model_context?: Component; + description: string; + system_message?: string; + reflect_on_tool_use: boolean; + tool_call_summary_format: string; } -export interface WebSocketMessage { - type: "message" | "result" | "completion" | "input_request" | "error"; - data?: AgentMessageConfig | TaskResult; - status?: RunStatus; - error?: string; - timestamp?: string; +export interface UserProxyAgentConfig { + name: string; + description: string; } -export interface TaskResult { - messages: AgentMessageConfig[]; - stop_reason?: string; +// Model Configs +export interface ModelInfo { + vision: boolean; + function_calling: boolean; + json_output: boolean; + family: string; } -export type ModelTypes = - | "OpenAIChatCompletionClient" - | "AzureOpenAIChatCompletionClient"; - -export type AgentTypes = - | "AssistantAgent" - | "UserProxyAgent" - | "MultimodalWebSurfer" - | "FileSurfer" - | "MagenticOneCoderAgent"; - -export type ToolTypes = "PythonFunction"; - -export type TeamTypes = - | "RoundRobinGroupChat" - | "SelectorGroupChat" - | "MagenticOneGroupChat"; - -export type TerminationTypes = - | "MaxMessageTermination" - | "StopMessageTermination" - | "TextMentionTermination" - | "TimeoutTermination" - | "CombinationTermination"; - -export type ComponentTypes = - | "team" - | "agent" - | "model" - | "tool" - | "termination"; - -export type ComponentConfigTypes = - | TeamConfig - | AgentConfig - | ModelConfig - | ToolConfig - | TerminationConfig; +export interface CreateArgumentsConfig { + frequency_penalty?: number; + logit_bias?: Record; + max_tokens?: number; + n?: number; + presence_penalty?: number; + response_format?: any; // ResponseFormat equivalent + seed?: number; + stop?: string | string[]; + temperature?: number; + top_p?: number; + user?: string; +} -export interface BaseModelConfig extends BaseConfig { +export interface BaseOpenAIClientConfig extends CreateArgumentsConfig { model: string; - model_type: ModelTypes; api_key?: string; + timeout?: number; + max_retries?: number; + model_capabilities?: any; // ModelCapabilities equivalent + model_info?: ModelInfo; +} + +export interface OpenAIClientConfig extends BaseOpenAIClientConfig { + organization?: string; base_url?: string; } -export interface AzureOpenAIModelConfig extends BaseModelConfig { - model_type: "AzureOpenAIChatCompletionClient"; - azure_deployment: string; - api_version: string; +export interface AzureOpenAIClientConfig extends BaseOpenAIClientConfig { azure_endpoint: string; - azure_ad_token_provider: string; + azure_deployment?: string; + api_version: string; + azure_ad_token?: string; + azure_ad_token_provider?: Component; } -export interface OpenAIModelConfig extends BaseModelConfig { - model_type: "OpenAIChatCompletionClient"; +export interface UnboundedChatCompletionContextConfig { + // Empty in example but could have props } -export type ModelConfig = AzureOpenAIModelConfig | OpenAIModelConfig; - -export interface BaseToolConfig extends BaseConfig { - name: string; - description: string; - content: string; - tool_type: ToolTypes; -} - -export interface PythonFunctionToolConfig extends BaseToolConfig { - tool_type: "PythonFunction"; +export interface OrTerminationConfig { + conditions: Component[]; } -export type ToolConfig = PythonFunctionToolConfig; - -export interface BaseAgentConfig extends BaseConfig { - name: string; - agent_type: AgentTypes; - system_message?: string; - model_client?: ModelConfig; - tools?: ToolConfig[]; - description?: string; +export interface MaxMessageTerminationConfig { + max_messages: number; } -export interface AssistantAgentConfig extends BaseAgentConfig { - agent_type: "AssistantAgent"; +export interface TextMentionTerminationConfig { + text: string; } -export interface UserProxyAgentConfig extends BaseAgentConfig { - agent_type: "UserProxyAgent"; -} +// Config type unions based on provider +export type TeamConfig = SelectorGroupChatConfig | RoundRobinGroupChatConfig; -export interface MultimodalWebSurferAgentConfig extends BaseAgentConfig { - agent_type: "MultimodalWebSurfer"; -} +export type AgentConfig = + | MultimodalWebSurferConfig + | AssistantAgentConfig + | UserProxyAgentConfig; -export interface FileSurferAgentConfig extends BaseAgentConfig { - agent_type: "FileSurfer"; -} +export type ModelConfig = OpenAIClientConfig | AzureOpenAIClientConfig; -export interface MagenticOneCoderAgentConfig extends BaseAgentConfig { - agent_type: "MagenticOneCoderAgent"; -} +export type ToolConfig = FunctionToolConfig; -export type AgentConfig = - | AssistantAgentConfig - | UserProxyAgentConfig - | MultimodalWebSurferAgentConfig - | FileSurferAgentConfig - | MagenticOneCoderAgentConfig; +export type ChatCompletionContextConfig = UnboundedChatCompletionContextConfig; -// export interface TerminationConfig extends BaseConfig { -// termination_type: TerminationTypes; -// max_messages?: number; -// text?: string; -// } +export type TerminationConfig = + | OrTerminationConfig + | MaxMessageTerminationConfig + | TextMentionTerminationConfig; -export interface BaseTerminationConfig extends BaseConfig { - termination_type: TerminationTypes; -} +export type ComponentConfig = + | TeamConfig + | AgentConfig + | ModelConfig + | ToolConfig + | TerminationConfig + | ChatCompletionContextConfig; -export interface MaxMessageTerminationConfig extends BaseTerminationConfig { - termination_type: "MaxMessageTermination"; - max_messages: number; +// DB Models +export interface DBModel { + id?: number; + user_id?: string; + created_at?: string; + updated_at?: string; + version?: number; } -export interface TextMentionTerminationConfig extends BaseTerminationConfig { - termination_type: "TextMentionTermination"; - text: string; +export interface Message extends DBModel { + config: AgentMessageConfig; + session_id: number; + run_id: string; } -export interface CombinationTerminationConfig extends BaseTerminationConfig { - termination_type: "CombinationTermination"; - operator: "and" | "or"; - conditions: TerminationConfig[]; +export interface Team extends DBModel { + component: Component; } -export type TerminationConfig = - | MaxMessageTerminationConfig - | TextMentionTerminationConfig - | CombinationTerminationConfig; - -export interface BaseTeamConfig extends BaseConfig { +export interface Session extends DBModel { name: string; - participants: AgentConfig[]; - team_type: TeamTypes; - termination_condition?: TerminationConfig; + team_id?: number; } -export interface RoundRobinGroupChatConfig extends BaseTeamConfig { - team_type: "RoundRobinGroupChat"; +// Runtime Types +export interface SessionRuns { + runs: Run[]; } -export interface SelectorGroupChatConfig extends BaseTeamConfig { - team_type: "SelectorGroupChat"; - selector_prompt: string; - model_client: ModelConfig; +export interface WebSocketMessage { + type: "message" | "result" | "completion" | "input_request" | "error"; + data?: AgentMessageConfig | TaskResult; + status?: RunStatus; + error?: string; + timestamp?: string; } -export type TeamConfig = RoundRobinGroupChatConfig | SelectorGroupChatConfig; - -export interface Team extends DBModel { - config: TeamConfig; +export interface TaskResult { + messages: AgentMessageConfig[]; + stop_reason?: string; } export interface TeamResult { @@ -290,13 +277,13 @@ export interface Run { status: RunStatus; task: AgentMessageConfig; team_result: TeamResult | null; - messages: Message[]; // Change to Message[] + messages: Message[]; error_message?: string; } export type RunStatus = | "created" - | "active" // covers 'streaming' + | "active" | "awaiting_input" | "timeout" | "complete" diff --git a/python/packages/autogen-studio/frontend/src/components/types/guards.ts b/python/packages/autogen-studio/frontend/src/components/types/guards.ts new file mode 100644 index 000000000000..63e6a5a1c706 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/types/guards.ts @@ -0,0 +1,231 @@ +import type { + Component, + ComponentConfig, + TeamConfig, + AgentConfig, + ModelConfig, + ToolConfig, + TerminationConfig, + ChatCompletionContextConfig, + SelectorGroupChatConfig, + RoundRobinGroupChatConfig, + MultimodalWebSurferConfig, + AssistantAgentConfig, + UserProxyAgentConfig, + OpenAIClientConfig, + AzureOpenAIClientConfig, + FunctionToolConfig, + OrTerminationConfig, + MaxMessageTerminationConfig, + TextMentionTerminationConfig, + UnboundedChatCompletionContextConfig, +} from "./datamodel"; + +// Provider constants +const PROVIDERS = { + // Teams + ROUND_ROBIN_TEAM: "autogen_agentchat.teams.RoundRobinGroupChat", + SELECTOR_TEAM: "autogen_agentchat.teams.SelectorGroupChat", + + // Agents + ASSISTANT_AGENT: "autogen_agentchat.agents.AssistantAgent", + USER_PROXY: "autogen_agentchat.agents.UserProxyAgent", + WEB_SURFER: "autogen_ext.agents.web_surfer.MultimodalWebSurfer", + + // Models + OPENAI: "autogen_ext.models.openai.OpenAIChatCompletionClient", + AZURE_OPENAI: + "autogen_ext.models.azure_openai.AzureOpenAIChatCompletionClient", + + // Tools + FUNCTION_TOOL: "autogen_core.tools.FunctionTool", + + // Termination + OR_TERMINATION: "autogen_agentchat.base.OrTerminationCondition", + MAX_MESSAGE: "autogen_agentchat.conditions.MaxMessageTermination", + TEXT_MENTION: "autogen_agentchat.conditions.TextMentionTermination", + + // Contexts + UNBOUNDED_CONTEXT: + "autogen_core.model_context.UnboundedChatCompletionContext", +} as const; + +// Provider type and mapping +export type Provider = (typeof PROVIDERS)[keyof typeof PROVIDERS]; + +type ProviderToConfig = { + // Teams + [PROVIDERS.SELECTOR_TEAM]: SelectorGroupChatConfig; + [PROVIDERS.ROUND_ROBIN_TEAM]: RoundRobinGroupChatConfig; + + // Agents + [PROVIDERS.ASSISTANT_AGENT]: AssistantAgentConfig; + [PROVIDERS.USER_PROXY]: UserProxyAgentConfig; + [PROVIDERS.WEB_SURFER]: MultimodalWebSurferConfig; + + // Models + [PROVIDERS.OPENAI]: OpenAIClientConfig; + [PROVIDERS.AZURE_OPENAI]: AzureOpenAIClientConfig; + + // Tools + [PROVIDERS.FUNCTION_TOOL]: FunctionToolConfig; + + // Termination + [PROVIDERS.OR_TERMINATION]: OrTerminationConfig; + [PROVIDERS.MAX_MESSAGE]: MaxMessageTerminationConfig; + [PROVIDERS.TEXT_MENTION]: TextMentionTerminationConfig; + + // Contexts + [PROVIDERS.UNBOUNDED_CONTEXT]: UnboundedChatCompletionContextConfig; +}; + +// Helper type to get config type from provider +type ConfigForProvider

= P extends keyof ProviderToConfig + ? ProviderToConfig[P] + : never; + +export function isComponent(value: any): value is Component { + return ( + value && + typeof value === "object" && + "provider" in value && + "component_type" in value && + "config" in value + ); +} +// Generic component type guard +function isComponentOfType

( + component: Component, + provider: P +): component is Component> { + return component.provider === provider; +} + +// Base component type guards +export function isTeamComponent( + component: Component +): component is Component { + return component.component_type === "team"; +} + +export function isAgentComponent( + component: Component +): component is Component { + return component.component_type === "agent"; +} + +export function isModelComponent( + component: Component +): component is Component { + return component.component_type === "model"; +} + +export function isToolComponent( + component: Component +): component is Component { + return component.component_type === "tool"; +} + +export function isTerminationComponent( + component: Component +): component is Component { + return component.component_type === "termination"; +} + +// export function isChatCompletionContextComponent( +// component: Component +// ): component is Component { +// return component.component_type === "chat_completion_context"; +// } + +// Team provider guards with proper type narrowing +export function isRoundRobinTeam( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.ROUND_ROBIN_TEAM); +} + +export function isSelectorTeam( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.SELECTOR_TEAM); +} + +// Agent provider guards with proper type narrowing +export function isAssistantAgent( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.ASSISTANT_AGENT); +} + +export function isUserProxyAgent( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.USER_PROXY); +} + +export function isWebSurferAgent( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.WEB_SURFER); +} + +// Model provider guards with proper type narrowing +export function isOpenAIModel( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.OPENAI); +} + +export function isAzureOpenAIModel( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.AZURE_OPENAI); +} + +// Tool provider guards with proper type narrowing +export function isFunctionTool( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.FUNCTION_TOOL); +} + +// Termination provider guards with proper type narrowing +export function isOrTermination( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.OR_TERMINATION); +} + +export function isMaxMessageTermination( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.MAX_MESSAGE); +} + +export function isTextMentionTermination( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.TEXT_MENTION); +} + +// Context provider guards with proper type narrowing +export function isUnboundedContext( + component: Component +): component is Component { + return isComponentOfType(component, PROVIDERS.UNBOUNDED_CONTEXT); +} + +// Runtime assertions +export function assertComponentType

( + component: Component, + provider: P +): asserts component is Component> { + if (!isComponentOfType(component, provider)) { + throw new Error( + `Expected component with provider ${provider}, got ${component.provider}` + ); + } +} + +export { PROVIDERS }; diff --git a/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx b/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx index b3db483b4a29..8fac8ecdc2af 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx @@ -1,5 +1,6 @@ import React, { memo, useState } from "react"; import { Loader2, Maximize2, Minimize2, X } from "lucide-react"; +import ReactMarkdown from "react-markdown"; export const LoadingIndicator = ({ size = 16 }: { size: number }) => (

@@ -80,7 +81,8 @@ export const TruncatableText = memo( ${className} `} > - {displayContent} + {/* {displayContent} */} + {displayContent} {shouldTruncate && !isExpanded && (
)} diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx index 4da98faf77c2..bb561719e53f 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx @@ -3,14 +3,8 @@ import { Copy } from "lucide-react"; import { Guide } from "../types"; import PythonGuide from "./python"; import DockerGuide from "./docker"; -import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; -import js from "react-syntax-highlighter/dist/esm/languages/prism/javascript"; -import python from "react-syntax-highlighter/dist/esm/languages/prism/python"; -import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; - -SyntaxHighlighter.registerLanguage("javascript", js); -SyntaxHighlighter.registerLanguage("python", python); +import { MonacoEditor } from "../../monaco"; interface GuideContentProps { guide: Guide; @@ -48,6 +42,8 @@ interface CodeSectionProps { language?: string; } +const editorRef = React.createRef(); + export const CodeSection: React.FC = ({ title, description, @@ -57,19 +53,16 @@ export const CodeSection: React.FC = ({ }) => (

{title}

- {description &&

{description}

} + {description &&
{description}
} {code && ( -
+
+ - {/* overflow scroll custom style */} - - {code} -
)}
diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx index 8ad34818a55a..fa0c79e4f47a 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx @@ -29,13 +29,37 @@ const PythonGuide: React.FC = () => { {" "}
+ {/* Basic Usage */} + + {/* Installation Steps */}
{/* Basic Usage */} AutoGen Studio offers a convenience CLI command to serve a team as a REST API endpoint.{" "} diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json b/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json new file mode 100644 index 000000000000..7c876e8949c6 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json @@ -0,0 +1,539 @@ +{ + "id": "gallery_default", + "name": "Default Component Gallery", + "url": null, + "metadata": { + "author": "AutoGen Team", + "created_at": "2025-01-29T22:13:58.687387", + "updated_at": "2025-01-29T22:13:58.715730", + "version": "1.0.0", + "description": "A default gallery containing basic components for human-in-loop conversations", + "tags": ["human-in-loop", "assistant"], + "license": "MIT", + "homepage": null, + "category": "conversation", + "last_synced": null + }, + "items": { + "teams": [ + { + "provider": "autogen_agentchat.teams.RoundRobinGroupChat", + "component_type": "team", + "version": 1, + "component_version": 1, + "description": "A team that runs a group chat with participants taking turns in a round-robin fashion\n to publish a message to all.", + "label": "RoundRobinGroupChat", + "config": { + "participants": [ + { + "provider": "autogen_agentchat.agents.AssistantAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that provides assistance with tool use.", + "label": "AssistantAgent", + "config": { + "name": "assistant_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "tools": [ + { + "provider": "autogen_core.tools.FunctionTool", + "component_type": "tool", + "version": 1, + "component_version": 1, + "description": "Create custom tools by wrapping standard Python functions.", + "label": "FunctionTool", + "config": { + "source_code": "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'\n", + "name": "calculator", + "description": "A simple calculator that performs basic arithmetic operations", + "global_imports": [], + "has_cancellation_support": false + } + } + ], + "handoffs": [], + "model_context": { + "provider": "autogen_core.model_context.UnboundedChatCompletionContext", + "component_type": "chat_completion_context", + "version": 1, + "component_version": 1, + "description": "An unbounded chat completion context that keeps a view of the all the messages.", + "label": "UnboundedChatCompletionContext", + "config": {} + }, + "description": "An agent that provides assistance with ability to use tools.", + "system_message": "You are a helpful assistant. Solve tasks carefully. When done, say TERMINATE.", + "model_client_stream": false, + "reflect_on_tool_use": false, + "tool_call_summary_format": "{result}" + } + } + ], + "termination_condition": { + "provider": "autogen_agentchat.base.OrTerminationCondition", + "component_type": "termination", + "version": 1, + "component_version": 1, + "label": "OrTerminationCondition", + "config": { + "conditions": [ + { + "provider": "autogen_agentchat.conditions.TextMentionTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation if a specific text is mentioned.", + "label": "TextMentionTermination", + "config": { + "text": "TERMINATE" + } + }, + { + "provider": "autogen_agentchat.conditions.MaxMessageTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation after a maximum number of messages have been exchanged.", + "label": "MaxMessageTermination", + "config": { + "max_messages": 10 + } + } + ] + } + } + } + }, + { + "provider": "autogen_agentchat.teams.SelectorGroupChat", + "component_type": "team", + "version": 1, + "component_version": 1, + "description": "A group chat team that have participants takes turn to publish a message\n to all, using a ChatCompletion model to select the next speaker after each message.", + "label": "Web Agents (Operator)", + "config": { + "participants": [ + { + "provider": "autogen_ext.agents.web_surfer.MultimodalWebSurfer", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "MultimodalWebSurfer is a multimodal agent that acts as a web surfer that can search the web and visit web pages.", + "label": "MultimodalWebSurfer", + "config": { + "name": "websurfer_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "description": "an agent that solves tasks by browsing the web", + "headless": true, + "start_page": "https://www.bing.com/", + "animate_actions": false, + "to_save_screenshots": false, + "use_ocr": false, + "to_resize_viewport": true + } + }, + { + "provider": "autogen_agentchat.agents.AssistantAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that provides assistance with tool use.", + "label": "AssistantAgent", + "config": { + "name": "assistant_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "tools": [], + "handoffs": [], + "model_context": { + "provider": "autogen_core.model_context.UnboundedChatCompletionContext", + "component_type": "chat_completion_context", + "version": 1, + "component_version": 1, + "description": "An unbounded chat completion context that keeps a view of the all the messages.", + "label": "UnboundedChatCompletionContext", + "config": {} + }, + "description": "an agent that verifies and summarizes information", + "system_message": "You are a task verification assistant who is working with a web surfer agent to solve tasks. At each point, check if the task has been completed as requested by the user. If the websurfer_agent responds and the task has not yet been completed, respond with what is left to do and then say 'keep going'. If and only when the task has been completed, summarize and present a final answer that directly addresses the user task in detail and then respond with TERMINATE.", + "model_client_stream": false, + "reflect_on_tool_use": false, + "tool_call_summary_format": "{result}" + } + }, + { + "provider": "autogen_agentchat.agents.UserProxyAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that can represent a human user through an input function.", + "label": "UserProxyAgent", + "config": { + "name": "user_proxy", + "description": "a human user that should be consulted only when the assistant_agent is unable to verify the information provided by the websurfer_agent" + } + } + ], + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "termination_condition": { + "provider": "autogen_agentchat.base.OrTerminationCondition", + "component_type": "termination", + "version": 1, + "component_version": 1, + "label": "OrTerminationCondition", + "config": { + "conditions": [ + { + "provider": "autogen_agentchat.conditions.MaxMessageTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation after a maximum number of messages have been exchanged.", + "label": "MaxMessageTermination", + "config": { + "max_messages": 20 + } + }, + { + "provider": "autogen_agentchat.conditions.TextMentionTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation if a specific text is mentioned.", + "label": "TextMentionTermination", + "config": { + "text": "TERMINATE" + } + } + ] + } + }, + "selector_prompt": "You are the cordinator of role play game. The following roles are available:\n{roles}. Given a task, the websurfer_agent will be tasked to address it by browsing the web and providing information. The assistant_agent will be tasked with verifying the information provided by the websurfer_agent and summarizing the information to present a final answer to the user. \nIf the task needs assistance from a human user (e.g., providing feedback, preferences, or the task is stalled), you should select the user_proxy role to provide the necessary information.\n\nRead the following conversation. Then select the next role from {participants} to play. Only return the role.\n\n{history}\n\nRead the above conversation. Then select the next role from {participants} to play. Only return the role.", + "allow_repeated_speaker": false + } + } + ], + "components": { + "agents": [ + { + "provider": "autogen_agentchat.agents.AssistantAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that provides assistance with tool use.", + "label": "AssistantAgent", + "config": { + "name": "assistant_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "tools": [ + { + "provider": "autogen_core.tools.FunctionTool", + "component_type": "tool", + "version": 1, + "component_version": 1, + "description": "Create custom tools by wrapping standard Python functions.", + "label": "FunctionTool", + "config": { + "source_code": "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'\n", + "name": "calculator", + "description": "A simple calculator that performs basic arithmetic operations", + "global_imports": [], + "has_cancellation_support": false + } + } + ], + "handoffs": [], + "model_context": { + "provider": "autogen_core.model_context.UnboundedChatCompletionContext", + "component_type": "chat_completion_context", + "version": 1, + "component_version": 1, + "description": "An unbounded chat completion context that keeps a view of the all the messages.", + "label": "UnboundedChatCompletionContext", + "config": {} + }, + "description": "An agent that provides assistance with ability to use tools.", + "system_message": "You are a helpful assistant. Solve tasks carefully. When done, say TERMINATE.", + "model_client_stream": false, + "reflect_on_tool_use": false, + "tool_call_summary_format": "{result}" + } + }, + { + "provider": "autogen_ext.agents.web_surfer.MultimodalWebSurfer", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "MultimodalWebSurfer is a multimodal agent that acts as a web surfer that can search the web and visit web pages.", + "label": "MultimodalWebSurfer", + "config": { + "name": "websurfer_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "description": "an agent that solves tasks by browsing the web", + "headless": true, + "start_page": "https://www.bing.com/", + "animate_actions": false, + "to_save_screenshots": false, + "use_ocr": false, + "to_resize_viewport": true + } + }, + { + "provider": "autogen_agentchat.agents.AssistantAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that provides assistance with tool use.", + "label": "AssistantAgent", + "config": { + "name": "assistant_agent", + "model_client": { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + "tools": [], + "handoffs": [], + "model_context": { + "provider": "autogen_core.model_context.UnboundedChatCompletionContext", + "component_type": "chat_completion_context", + "version": 1, + "component_version": 1, + "description": "An unbounded chat completion context that keeps a view of the all the messages.", + "label": "UnboundedChatCompletionContext", + "config": {} + }, + "description": "an agent that verifies and summarizes information", + "system_message": "You are a task verification assistant who is working with a web surfer agent to solve tasks. At each point, check if the task has been completed as requested by the user. If the websurfer_agent responds and the task has not yet been completed, respond with what is left to do and then say 'keep going'. If and only when the task has been completed, summarize and present a final answer that directly addresses the user task in detail and then respond with TERMINATE.", + "model_client_stream": false, + "reflect_on_tool_use": false, + "tool_call_summary_format": "{result}" + } + }, + { + "provider": "autogen_agentchat.agents.UserProxyAgent", + "component_type": "agent", + "version": 1, + "component_version": 1, + "description": "An agent that can represent a human user through an input function.", + "label": "UserProxyAgent", + "config": { + "name": "user_proxy", + "description": "a human user that should be consulted only when the assistant_agent is unable to verify the information provided by the websurfer_agent" + } + } + ], + "models": [ + { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + }, + { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Example on how to use the OpenAIChatCopletionClient with local models (Ollama, vllm etc).", + "label": "Mistral-7B vllm", + "config": { + "model": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF", + "base_url": "http://localhost:1234/v1" + } + }, + { + "provider": "autogen_ext.models.openai.OpenAIChatCompletionClient", + "component_type": "model", + "version": 1, + "component_version": 1, + "description": "Chat completion client for OpenAI hosted models.", + "label": "OpenAIChatCompletionClient", + "config": { + "model": "gpt-4o-mini" + } + } + ], + "tools": [ + { + "provider": "autogen_core.tools.FunctionTool", + "component_type": "tool", + "version": 1, + "component_version": 1, + "description": "Create custom tools by wrapping standard Python functions.", + "label": "FunctionTool", + "config": { + "source_code": "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'\n", + "name": "calculator", + "description": "A simple calculator that performs basic arithmetic operations", + "global_imports": [], + "has_cancellation_support": false + } + } + ], + "terminations": [ + { + "provider": "autogen_agentchat.conditions.TextMentionTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation if a specific text is mentioned.", + "label": "TextMentionTermination", + "config": { + "text": "TERMINATE" + } + }, + { + "provider": "autogen_agentchat.conditions.MaxMessageTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation after a maximum number of messages have been exchanged.", + "label": "MaxMessageTermination", + "config": { + "max_messages": 10 + } + }, + { + "provider": "autogen_agentchat.base.OrTerminationCondition", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": null, + "label": "OrTerminationCondition", + "config": { + "conditions": [ + { + "provider": "autogen_agentchat.conditions.TextMentionTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation if a specific text is mentioned.", + "label": "TextMentionTermination", + "config": { + "text": "TERMINATE" + } + }, + { + "provider": "autogen_agentchat.conditions.MaxMessageTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation after a maximum number of messages have been exchanged.", + "label": "MaxMessageTermination", + "config": { + "max_messages": 10 + } + } + ] + } + }, + { + "provider": "autogen_agentchat.base.OrTerminationCondition", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": null, + "label": "OrTerminationCondition", + "config": { + "conditions": [ + { + "provider": "autogen_agentchat.conditions.MaxMessageTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation after a maximum number of messages have been exchanged.", + "label": "MaxMessageTermination", + "config": { + "max_messages": 20 + } + }, + { + "provider": "autogen_agentchat.conditions.TextMentionTermination", + "component_type": "termination", + "version": 1, + "component_version": 1, + "description": "Terminate the conversation if a specific text is mentioned.", + "label": "TextMentionTermination", + "config": { + "text": "TERMINATE" + } + } + ] + } + } + ] + } + } +} diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx index 00e9fe331bf7..0eb23391fed2 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx @@ -19,13 +19,13 @@ import { import type { Gallery } from "./types"; import { useGalleryStore } from "./store"; import { MonacoEditor } from "../monaco"; -import { ComponentConfigTypes } from "../../types/datamodel"; import { getRelativeTimeString, TruncatableText } from "../atoms"; +import { Component, ComponentConfig } from "../../types/datamodel"; const ComponentGrid: React.FC<{ title: string; icon: React.ReactNode; - items: ComponentConfigTypes[]; + items: Component[]; }> = ({ title, icon, items }) => { const [isExpanded, setIsExpanded] = useState(true); @@ -58,16 +58,17 @@ const ComponentGrid: React.FC<{ key={idx} className="bg-secondary rounded p-3 hover:bg-tertiary transition-colors" > -
- {item.component_type} +
+ {item.provider}
+
{item.label}
{item.description && ( -

+

-

+
)}
))} diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx index c09096f73c2d..5cd2ffdfb00b 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx @@ -3,6 +3,7 @@ import { persist } from "zustand/middleware"; import { Gallery } from "./types"; import { AgentConfig, + Component, ModelConfig, TeamConfig, TerminationConfig, @@ -25,12 +26,12 @@ interface GalleryStore { syncGallery: (id: string) => Promise; getLastSyncTime: (id: string) => string | null; getGalleryComponents: () => { - teams: TeamConfig[]; + teams: Component[]; components: { - agents: AgentConfig[]; - models: ModelConfig[]; - tools: ToolConfig[]; - terminations: TerminationConfig[]; + agents: Component[]; + models: Component[]; + tools: Component[]; + terminations: Component[]; }; }; } @@ -150,7 +151,7 @@ export const useGalleryStore = create()( }, }), { - name: "gallery-storage", + name: "gallery-storage-v1", } ) ); diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts index 015eb961c926..7db306b904d9 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts @@ -1,5 +1,6 @@ import { AgentConfig, + Component, ModelConfig, TeamConfig, TerminationConfig, @@ -25,12 +26,12 @@ export interface Gallery { url?: string; metadata: GalleryMetadata; items: { - teams: TeamConfig[]; + teams: Component[]; components: { - agents: AgentConfig[]; - models: ModelConfig[]; - tools: ToolConfig[]; - terminations: TerminationConfig[]; + agents: Component[]; + models: Component[]; + tools: Component[]; + terminations: Component[]; }; }; } diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts index 2b0e4ea83e8d..d8dc75bc13fb 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts @@ -1,194 +1,15 @@ -import { - AssistantAgentConfig, - CombinationTerminationConfig, - MaxMessageTerminationConfig, - OpenAIModelConfig, - PythonFunctionToolConfig, - RoundRobinGroupChatConfig, - TextMentionTerminationConfig, - UserProxyAgentConfig, -} from "../../types/datamodel"; +import { Gallery } from "./types"; -export const defaultGallery = { - id: "gallery_default", - name: "Default Component Gallery", - metadata: { - author: "AutoGen Team", - created_at: "2024-12-12T00:00:00Z", - updated_at: "2024-12-12T00:00:00Z", - version: "1.0.0", - description: - "A default gallery containing basic components for human-in-loop conversations", - tags: ["human-in-loop", "assistant"], - license: "MIT", - category: "conversation", - }, - items: { - teams: [ - { - component_type: "team", - description: - "A team with an assistant agent and a user agent to enable human-in-loop task completion in a round-robin fashion", - name: "huma_in_loop_team", - participants: [ - { - component_type: "agent", - description: - "An assistant agent that can help users complete tasks", - name: "assistant_agent", - agent_type: "AssistantAgent", - system_message: - "You are a helpful assistant. Solve tasks carefully. You also have a calculator tool which you can use if needed. When the task is done respond with TERMINATE.", - model_client: { - component_type: "model", - description: "A GPT-4o mini model", - model: "gpt-4o-mini", - model_type: "OpenAIChatCompletionClient", - }, - tools: [ - { - component_type: "tool", - name: "calculator", - description: - "A simple calculator that performs basic arithmetic operations between two numbers", - content: - "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'", - tool_type: "PythonFunction", - }, - ], - }, - { - component_type: "agent", - description: "A user agent that is driven by a human user", - name: "user_agent", - agent_type: "UserProxyAgent", - tools: [], - }, - ], - team_type: "RoundRobinGroupChat", - termination_condition: { - description: - "Terminate the conversation when the user mentions 'TERMINATE' or after 10 messages", - component_type: "termination", - termination_type: "CombinationTermination", - operator: "or", - conditions: [ - { - component_type: "termination", - description: - "Terminate the conversation when the user mentions 'TERMINATE'", - termination_type: "TextMentionTermination", - text: "TERMINATE", - }, - { - component_type: "termination", - description: "Terminate the conversation after 10 messages", - termination_type: "MaxMessageTermination", - max_messages: 10, - }, - ], - }, - } as RoundRobinGroupChatConfig, - ], - components: { - agents: [ - { - component_type: "agent", - description: "An assistant agent that can help users complete tasks", - name: "assistant_agent", - agent_type: "AssistantAgent", - system_message: - "You are a helpful assistant. Solve tasks carefully. You also have a calculator tool which you can use if needed. When the task is done respond with TERMINATE.", - model_client: { - component_type: "model", - description: "A GPT-4o mini model", - model: "gpt-4o-mini", - model_type: "OpenAIChatCompletionClient", - }, - tools: [ - { - component_type: "tool", - name: "calculator", - description: - "A simple calculator that performs basic arithmetic operations between two numbers", - content: - "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'", - tool_type: "PythonFunction", - }, - ], - } as AssistantAgentConfig, - { - component_type: "agent", - description: "A user agent that is driven by a human user", - name: "user_agent", - agent_type: "UserProxyAgent", - tools: [], - } as UserProxyAgentConfig, - ], - models: [ - { - component_type: "model", - description: "A GPT-4o mini model", - model: "gpt-4o-mini", - model_type: "OpenAIChatCompletionClient", - } as OpenAIModelConfig, - ], - tools: [ - { - component_type: "tool", - name: "calculator", - description: - "A simple calculator that performs basic arithmetic operations between two numbers", - content: - "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'", - tool_type: "PythonFunction", - } as PythonFunctionToolConfig, - { - component_type: "tool", - name: "fetch_website", - description: "Fetch and return the content of a website URL", - content: - "async def fetch_website(url: str) -> str:\n try:\n import requests\n from urllib.parse import urlparse\n \n # Validate URL format\n parsed = urlparse(url)\n if not parsed.scheme or not parsed.netloc:\n return \"Error: Invalid URL format. Please include http:// or https://\"\n \n # Add scheme if not present\n if not url.startswith(('http://', 'https://')): \n url = 'https://' + url\n \n # Set headers to mimic a browser request\n headers = {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'\n }\n \n # Make the request with a timeout\n response = requests.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n \n # Return the text content\n return response.text\n \n except requests.exceptions.Timeout:\n return \"Error: Request timed out\"\n except requests.exceptions.ConnectionError:\n return \"Error: Failed to connect to the website\"\n except requests.exceptions.HTTPError as e:\n return f\"Error: HTTP {e.response.status_code} - {e.response.reason}\"\n except Exception as e:\n return f\"Error: {str(e)}\"", - tool_type: "PythonFunction", - } as PythonFunctionToolConfig, - ], - terminations: [ - { - component_type: "termination", - description: - "Terminate the conversation when the user mentions 'TERMINATE'", - termination_type: "TextMentionTermination", - text: "TERMINATE", - } as TextMentionTerminationConfig, - { - component_type: "termination", - description: "Terminate the conversation after 10 messages", - termination_type: "MaxMessageTermination", - max_messages: 10, - } as MaxMessageTerminationConfig, - { - component_type: "termination", - description: - "Terminate the conversation when the user mentions 'TERMINATE' or after 10 messages", - termination_type: "CombinationTermination", - operator: "or", - conditions: [ - { - component_type: "termination", - description: - "Terminate the conversation when the user mentions 'TERMINATE'", - termination_type: "TextMentionTermination", - text: "TERMINATE", - }, - { - component_type: "termination", - description: "Terminate the conversation after 10 messages", - termination_type: "MaxMessageTermination", - max_messages: 10, - }, - ], - } as CombinationTerminationConfig, - ], - }, - }, +// Load and parse the gallery JSON file +const loadGalleryFromJson = (): Gallery => { + try { + // You can adjust the path to your JSON file as needed + const galleryJson = require("./default_gallery.json"); + return galleryJson as Gallery; + } catch (error) { + console.error("Error loading gallery JSON:", error); + throw error; + } }; + +export const defaultGallery: Gallery = loadGalleryFromJson(); diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx index 9b18038c6102..e569b517a23d 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx @@ -25,6 +25,7 @@ import { AgentConfig, TeamConfig, Run, + Component, } from "../../../../types/datamodel"; import { CustomEdge, CustomEdgeData } from "./edge"; import { useConfigStore } from "../../../../../hooks/store"; @@ -32,7 +33,7 @@ import { AgentFlowToolbar } from "./toolbar"; import { EdgeMessageModal } from "./edgemessagemodal"; interface AgentFlowProps { - teamConfig: TeamConfig; + teamConfig: Component; run: Run; } @@ -151,7 +152,7 @@ const getLayoutedElements = ( const createNode = ( id: string, type: "user" | "agent" | "end", - agentConfig?: AgentConfig, + agentConfig?: Component, isActive: boolean = false, run?: Run ): Node => { @@ -218,7 +219,7 @@ const createNode = ( data: { type: "agent", label: id, - agentType: agentConfig?.agent_type || "", + agentType: agentConfig?.label || "", description: agentConfig?.description || "", isActive, status: "", @@ -281,8 +282,8 @@ const AgentFlow: React.FC = ({ teamConfig, run }) => { // Add first message node if it exists if (messages.length > 0) { - const firstAgentConfig = teamConfig.participants.find( - (p) => p.name === messages[0].source + const firstAgentConfig = teamConfig.config.participants.find( + (p) => p.config.name === messages[0].source ); nodeMap.set( messages[0].source, @@ -322,8 +323,8 @@ const AgentFlow: React.FC = ({ teamConfig, run }) => { } if (!nodeMap.has(nextMsg.source)) { - const agentConfig = teamConfig.participants.find( - (p) => p.name === nextMsg.source + const agentConfig = teamConfig.config.participants.find( + (p) => p.config.name === nextMsg.source ); nodeMap.set( nextMsg.source, @@ -479,7 +480,7 @@ const AgentFlow: React.FC = ({ teamConfig, run }) => { return { nodes: Array.from(nodeMap.values()), edges: newEdges }; }, - [teamConfig.participants, run, settings] + [teamConfig.config.participants, run, settings] ); const handleToggleFullscreen = useCallback(() => { diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx index 65d9452bbf49..8f5ea8bcd39a 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx @@ -11,6 +11,7 @@ import { RunStatus, TeamResult, Session, + Component, } from "../../../types/datamodel"; import { appContext } from "../../../../hooks/provider"; import ChatInput from "./chatinput"; @@ -46,7 +47,8 @@ export default function ChatView({ session }: ChatViewProps) { const [activeSocket, setActiveSocket] = React.useState( null ); - const [teamConfig, setTeamConfig] = React.useState(null); + const [teamConfig, setTeamConfig] = + React.useState | null>(null); const inputTimeoutRef = React.useRef(null); const activeSocketRef = React.useRef(null); @@ -94,7 +96,7 @@ export default function ChatView({ session }: ChatViewProps) { teamAPI .getTeam(session.team_id, user.email) .then((team) => { - setTeamConfig(team.config); + setTeamConfig(team.component); }) .catch((error) => { console.error("Error loading team config:", error); diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx index 57557ec1527a..73afef90e415 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx @@ -11,7 +11,7 @@ import { ChevronUp, Bot, } from "lucide-react"; -import { Run, Message, TeamConfig } from "../../../types/datamodel"; +import { Run, Message, TeamConfig, Component } from "../../../types/datamodel"; import AgentFlow from "./agentflow/agentflow"; import { RenderMessage } from "./rendermessage"; import InputRequestView from "./inputrequest"; @@ -24,7 +24,7 @@ import { interface RunViewProps { run: Run; - teamConfig?: TeamConfig; + teamConfig?: Component; onInputResponse?: (response: string) => void; onCancel?: () => void; isFirstRun?: boolean; @@ -54,6 +54,7 @@ const RunView: React.FC = ({ }, [run.messages]); // Only depend on messages changing const calculateThreadTokens = (messages: Message[]) => { + // console.log("messages", messages); return messages.reduce((total, msg) => { if (!msg.config.models_usage) return total; return ( diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/editor.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/editor.tsx index c8358695521c..c89ee39a70f6 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/editor.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/editor.tsx @@ -132,7 +132,7 @@ export const SessionEditor: React.FC = ({ } options={teams.map((team) => ({ value: team.id, - label: `${team.config.name} (${team.config.team_type})`, + label: `${team.component.label} (${team.component.component_type})`, }))} notFoundContent={loading ? : null} /> diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx index 6abcb4cfcf69..d0e5f3dd37c9 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useRef, useState } from "react"; +//team/builder/builder.tsx +import React, { useCallback, useEffect, useRef, useState } from "react"; import { DndContext, useSensor, @@ -29,7 +30,8 @@ import { edgeTypes, nodeTypes } from "./nodes"; import "./builder.css"; import TeamBuilderToolbar from "./toolbar"; import { MonacoEditor } from "../../monaco"; -import { NodeEditor } from "./node-editor"; +import { NodeEditor } from "./node-editor/node-editor"; +import debounce from "lodash.debounce"; const { Sider, Content } = Layout; @@ -66,6 +68,7 @@ export const TeamBuilder: React.FC = ({ history, updateNode, selectedNodeId, + setSelectedNode, } = useTeamBuilderStore(); const currentHistoryIndex = useTeamBuilderStore( @@ -113,9 +116,9 @@ export const TeamBuilder: React.FC = ({ // Load initial config React.useEffect(() => { - if (team?.config) { + if (team?.component) { const { nodes: initialNodes, edges: initialEdges } = loadFromJson( - team.config + team.component ); setNodes(initialNodes); setEdges(initialEdges); @@ -124,36 +127,45 @@ export const TeamBuilder: React.FC = ({ // Handle JSON changes const handleJsonChange = useCallback( - (value: string) => { + debounce((value: string) => { try { const config = JSON.parse(value); - loadFromJson(config); - // dirty ? + // Always consider JSON edits as changes that should affect isDirty state + loadFromJson(config, false); + // Force history update even if nodes/edges appear same + useTeamBuilderStore.getState().addToHistory(); } catch (error) { console.error("Invalid JSON:", error); } - }, + }, 1000), [loadFromJson] ); + // Cleanup debounced function + useEffect(() => { + return () => { + handleJsonChange.cancel(); + }; + }, [handleJsonChange]); + // Handle save const handleSave = useCallback(async () => { try { - const config = syncToJson(); - if (!config) { + const component = syncToJson(); + if (!component) { throw new Error("Unable to generate valid configuration"); } if (onChange) { - console.log("Saving team configuration", config); + console.log("Saving team configuration", component); const teamData: Partial = team ? { ...team, - config, + component, created_at: undefined, updated_at: undefined, } - : { config }; + : { component }; await onChange(teamData); resetHistory(); } @@ -212,7 +224,10 @@ export const TeamBuilder: React.FC = ({ const targetNode = nodes.find((node) => node.id === over.id); if (!targetNode) return; - const isValid = validateDropTarget(draggedType, targetNode.data.type); + const isValid = validateDropTarget( + draggedType, + targetNode.data.component.component_type + ); // Add visual feedback class to target node if (isValid) { targetNode.className = "drop-target-valid"; @@ -228,14 +243,16 @@ export const TeamBuilder: React.FC = ({ const draggedItem = active.data.current.current; const dropZoneId = over.id as string; - const [nodeId, zoneType] = dropZoneId.split("-zone")[0].split("-"); - + const [nodeId] = dropZoneId.split("@@@"); // Find target node const targetNode = nodes.find((node) => node.id === nodeId); if (!targetNode) return; // Validate drop - const isValid = validateDropTarget(draggedItem.type, targetNode.data.type); + const isValid = validateDropTarget( + draggedItem.type, + targetNode.data.component.component_type + ); if (!isValid) return; const position = { @@ -244,12 +261,7 @@ export const TeamBuilder: React.FC = ({ }; // Pass both new node data AND target node id - addNode( - draggedItem.type as ComponentTypes, - position, - draggedItem.config, - nodeId - ); + addNode(position, draggedItem.config, nodeId); }; const onDragStart = (item: DragItem) => { @@ -408,6 +420,7 @@ export const TeamBuilder: React.FC = ({ handleSave(); } }} + onClose={() => setSelectedNode(null)} /> diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx index c5bdfceb1269..34118174e464 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx @@ -10,22 +10,16 @@ import { Timer, Maximize2, Minimize2, + GripVertical, } from "lucide-react"; -import type { - AgentConfig, - ModelConfig, - TerminationConfig, - ToolConfig, -} from "../../../types/datamodel"; import Sider from "antd/es/layout/Sider"; import { useGalleryStore } from "../../gallery/store"; +import { ComponentTypes } from "../../../types/datamodel"; interface ComponentConfigTypes { [key: string]: any; } -type ComponentTypes = "agent" | "model" | "tool" | "termination"; - interface LibraryProps {} interface PresetItemProps { @@ -66,11 +60,12 @@ const PresetItem: React.FC = ({ style={style} {...attributes} {...listeners} - className="p-2 text-primary mb-2 border border-secondary rounded cursor-move hover:bg-secondary transition-colors" + className="p-2 text-primary mb-2 border border-secondary rounded cursor-move hover:bg-secondary transition-colors " >
+ {icon} - {label} + {label}
); @@ -91,7 +86,7 @@ export const ComponentLibrary: React.FC = () => { title: "Agents", type: "agent" as ComponentTypes, items: defaultGallery.items.components.agents.map((agent) => ({ - label: agent.name, + label: agent.label, config: agent, })), icon: , @@ -100,7 +95,7 @@ export const ComponentLibrary: React.FC = () => { title: "Models", type: "model" as ComponentTypes, items: defaultGallery.items.components.models.map((model) => ({ - label: `${model.model_type} - ${model.model}`, + label: `${model.component_type} - ${model.config.model}`, config: model, })), icon: , @@ -109,7 +104,7 @@ export const ComponentLibrary: React.FC = () => { title: "Tools", type: "tool" as ComponentTypes, items: defaultGallery.items.components.tools.map((tool) => ({ - label: tool.name, + label: tool.config.name, config: tool, })), icon: , @@ -119,7 +114,7 @@ export const ComponentLibrary: React.FC = () => { type: "termination" as ComponentTypes, items: defaultGallery.items.components.terminations.map( (termination) => ({ - label: `${termination.termination_type}`, + label: `${termination.label}`, config: termination, }) ), @@ -131,7 +126,7 @@ export const ComponentLibrary: React.FC = () => { const items: CollapseProps["items"] = sections.map((section) => { const filteredItems = section.items.filter((item) => - item.label.toLowerCase().includes(searchTerm.toLowerCase()) + item.label?.toLowerCase().includes(searchTerm.toLowerCase()) ); return { @@ -153,7 +148,7 @@ export const ComponentLibrary: React.FC = () => { id={`${section.title.toLowerCase()}-${itemIndex}`} type={section.type} config={item.config} - label={item.label} + label={item.label || ""} icon={section.icon} /> ))} diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/node-editor.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/node-editor.tsx deleted file mode 100644 index 07520e47a2d8..000000000000 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/node-editor.tsx +++ /dev/null @@ -1,665 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Drawer, Button, Space, message, Select, Input } from "antd"; -import { NodeEditorProps } from "./types"; -import { useTeamBuilderStore } from "./store"; -import { - TeamConfig, - ComponentTypes, - TeamTypes, - ModelTypes, - SelectorGroupChatConfig, - RoundRobinGroupChatConfig, - ModelConfig, - AzureOpenAIModelConfig, - OpenAIModelConfig, - ComponentConfigTypes, - AgentConfig, - ToolConfig, - AgentTypes, - ToolTypes, - TerminationConfig, - TerminationTypes, - MaxMessageTerminationConfig, - TextMentionTerminationConfig, - CombinationTerminationConfig, -} from "../../../types/datamodel"; - -const { TextArea } = Input; - -interface EditorProps { - value: T; - onChange: (value: T) => void; - disabled?: boolean; -} - -const TeamEditor: React.FC> = ({ - value, - onChange, - disabled, -}) => { - const handleTypeChange = (teamType: TeamTypes) => { - if (teamType === "SelectorGroupChat") { - onChange({ - ...value, - team_type: teamType, - selector_prompt: "", - model_client: { - component_type: "model", - model: "", - model_type: "OpenAIChatCompletionClient", - }, - } as SelectorGroupChatConfig); - } else { - const { selector_prompt, model_client, ...rest } = - value as SelectorGroupChatConfig; - onChange({ - ...rest, - team_type: teamType, - } as RoundRobinGroupChatConfig); - } - }; - - return ( - -
- - onChange({ ...value, name: e.target.value })} - disabled={disabled} - /> -
- - {value.team_type === "SelectorGroupChat" && ( - <> -
- -