v1 of AutoGen Studio on AgentChat (#4097)

* add skeleton worflow manager

* add test notebook

* update test nb

* add sample team spec

* refactor requirements to agentchat and ext

* add base provider to return agentchat agents from json spec

* initial api refactor, update dbmanager

* api refactor

* refactor tests

* ags api tutorial update

* ui refactor

* general refactor

* minor refactor updates

* backend api refaactor

* ui refactor and update

* implement v1 for streaming connection with ui updates

* backend refactor

* ui refactor

* minor ui tweak

* minor refactor and tweaks

* general refactor

* update tests

* sync uv.lock with main

* uv lock update
This commit is contained in:
Victor Dibia 2024-11-09 14:32:24 -08:00 committed by GitHub
parent f40b0c2730
commit 0e985d4b40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 20736 additions and 13600 deletions

View File

@ -2,6 +2,8 @@ database.sqlite
.cache/*
autogenstudio/web/files/user/*
autogenstudio/test
autogenstudio/database/alembic.ini
autogenstudio/database/alembic/*
autogenstudio/web/files/ui/*
OAI_CONFIG_LIST
scratch/
@ -10,8 +12,10 @@ autogenstudio/web/ui/*
autogenstudio/web/skills/user/*
.release.sh
.nightly.sh
notebooks/test
notebooks/work_dir/*
notebooks/test.db
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@ -1,4 +1,3 @@
from .chatmanager import *
from .datamodel import *
from .version import __version__
from .workflowmanager import *
from .teammanager import *

View File

@ -1,179 +0,0 @@
import os
from datetime import datetime
from queue import Queue
from typing import Any, Dict, List, Optional, Tuple, Union
from loguru import logger
from .datamodel import Message
from .websocket_connection_manager import WebSocketConnectionManager
from .workflowmanager import WorkflowManager
class AutoGenChatManager:
"""
This class handles the automated generation and management of chat interactions
using an automated workflow configuration and message queue.
"""
def __init__(
self, message_queue: Queue, websocket_manager: WebSocketConnectionManager = None, human_input_timeout: int = 180
) -> None:
"""
Initializes the AutoGenChatManager with a message queue.
:param message_queue: A queue to use for sending messages asynchronously.
"""
self.message_queue = message_queue
self.websocket_manager = websocket_manager
self.a_human_input_timeout = human_input_timeout
def send(self, message: dict) -> None:
"""
Sends a message by putting it into the message queue.
:param message: The message string to be sent.
"""
if self.message_queue is not None:
self.message_queue.put_nowait(message)
async def a_send(self, message: dict) -> None:
"""
Asynchronously sends a message via the WebSocketManager class
:param message: The message string to be sent.
"""
for connection, socket_client_id in self.websocket_manager.active_connections:
if message["connection_id"] == socket_client_id:
logger.info(
f"Sending message to connection_id: {message['connection_id']}. Connection ID: {socket_client_id}"
)
await self.websocket_manager.send_message(message, connection)
else:
logger.info(
f"Skipping message for connection_id: {message['connection_id']}. Connection ID: {socket_client_id}"
)
async def a_prompt_for_input(self, prompt: dict, timeout: int = 60) -> str:
"""
Sends the user a prompt and waits for a response asynchronously via the WebSocketManager class
:param message: The message string to be sent.
"""
for connection, socket_client_id in self.websocket_manager.active_connections:
if prompt["connection_id"] == socket_client_id:
logger.info(
f"Sending message to connection_id: {prompt['connection_id']}. Connection ID: {socket_client_id}"
)
try:
result = await self.websocket_manager.get_input(prompt, connection, timeout)
return result
except Exception as e:
return f"Error: {e}\nTERMINATE"
else:
logger.info(
f"Skipping message for connection_id: {prompt['connection_id']}. Connection ID: {socket_client_id}"
)
def chat(
self,
message: Message,
history: List[Dict[str, Any]],
workflow: Any = None,
connection_id: Optional[str] = None,
user_dir: Optional[str] = None,
**kwargs,
) -> Message:
"""
Processes an incoming message according to the agent's workflow configuration
and generates a response.
:param message: An instance of `Message` representing an incoming message.
:param history: A list of dictionaries, each representing a past interaction.
:param flow_config: An instance of `AgentWorkFlowConfig`. If None, defaults to a standard configuration.
:param connection_id: An optional connection identifier.
:param kwargs: Additional keyword arguments.
:return: An instance of `Message` representing a response.
"""
# create a working director for workflow based on user_dir/session_id/time_hash
work_dir = os.path.join(
user_dir,
str(message.session_id),
datetime.now().strftime("%Y%m%d_%H-%M-%S"),
)
os.makedirs(work_dir, exist_ok=True)
# if no flow config is provided, use the default
if workflow is None:
raise ValueError("Workflow must be specified")
workflow_manager = WorkflowManager(
workflow=workflow,
history=history,
work_dir=work_dir,
send_message_function=self.send,
a_send_message_function=self.a_send,
connection_id=connection_id,
)
message_text = message.content.strip()
result_message: Message = workflow_manager.run(message=f"{message_text}", clear_history=False, history=history)
result_message.user_id = message.user_id
result_message.session_id = message.session_id
return result_message
async def a_chat(
self,
message: Message,
history: List[Dict[str, Any]],
workflow: Any = None,
connection_id: Optional[str] = None,
user_dir: Optional[str] = None,
**kwargs,
) -> Message:
"""
Processes an incoming message according to the agent's workflow configuration
and generates a response.
:param message: An instance of `Message` representing an incoming message.
:param history: A list of dictionaries, each representing a past interaction.
:param flow_config: An instance of `AgentWorkFlowConfig`. If None, defaults to a standard configuration.
:param connection_id: An optional connection identifier.
:param kwargs: Additional keyword arguments.
:return: An instance of `Message` representing a response.
"""
# create a working director for workflow based on user_dir/session_id/time_hash
work_dir = os.path.join(
user_dir,
str(message.session_id),
datetime.now().strftime("%Y%m%d_%H-%M-%S"),
)
os.makedirs(work_dir, exist_ok=True)
# if no flow config is provided, use the default
if workflow is None:
raise ValueError("Workflow must be specified")
workflow_manager = WorkflowManager(
workflow=workflow,
history=history,
work_dir=work_dir,
send_message_function=self.send,
a_send_message_function=self.a_send,
a_human_input_function=self.a_prompt_for_input,
a_human_input_timeout=self.a_human_input_timeout,
connection_id=connection_id,
)
message_text = message.content.strip()
result_message: Message = await workflow_manager.a_run(
message=f"{message_text}", clear_history=False, history=history
)
result_message.user_id = message.user_id
result_message.session_id = message.session_id
return result_message

View File

@ -15,10 +15,11 @@ def ui(
host: str = "127.0.0.1",
port: int = 8081,
workers: int = 1,
reload: Annotated[bool, typer.Option("--reload")] = False,
reload: Annotated[bool, typer.Option("--reload")] = True,
docs: bool = True,
appdir: str = None,
database_uri: Optional[str] = None,
upgrade_database: bool = False,
):
"""
Run the AutoGen Studio UI.
@ -30,7 +31,7 @@ def ui(
reload (bool, optional): Whether to reload the UI on code changes. Defaults to False.
docs (bool, optional): Whether to generate API docs. Defaults to False.
appdir (str, optional): Path to the AutoGen Studio app directory. Defaults to None.
database-uri (str, optional): Database URI to connect to. Defaults to None. Examples include sqlite:///autogenstudio.db, postgresql://user:password@localhost/autogenstudio.
database-uri (str, optional): Database URI to connect to. Defaults to None.
"""
os.environ["AUTOGENSTUDIO_API_DOCS"] = str(docs)
@ -38,6 +39,8 @@ def ui(
os.environ["AUTOGENSTUDIO_APPDIR"] = appdir
if database_uri:
os.environ["AUTOGENSTUDIO_DATABASE_URI"] = database_uri
if upgrade_database:
os.environ["AUTOGENSTUDIO_UPGRADE_DATABASE"] = "1"
uvicorn.run(
"autogenstudio.web.app:app",
@ -45,6 +48,11 @@ def ui(
port=port,
workers=workers,
reload=reload,
reload_excludes=[
"**/alembic/*",
"**/alembic.ini",
"**/versions/*"
] if reload else None
)

View File

@ -1,3 +1,3 @@
# from .dbmanager import *
from .dbmanager import *
from .utils import *
from .db_manager import DatabaseManager
from .component_factory import ComponentFactory
from .config_manager import ConfigurationManager

View File

@ -1,116 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,355 @@
import os
from pathlib import Path
from typing import List, Literal, Union, Optional, Dict, Any, Type
from datetime import datetime
import json
from autogen_agentchat.task import MaxMessageTermination, TextMentionTermination, StopMessageTermination
import yaml
import logging
from packaging import version
from ..datamodel import (
TeamConfig, AgentConfig, ModelConfig, ToolConfig,
TeamTypes, AgentTypes, ModelTypes, ToolTypes,
ComponentType, ComponentConfig, ComponentConfigInput, TerminationConfig, TerminationTypes, Response
)
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat
from autogen_ext.models import OpenAIChatCompletionClient
from autogen_core.components.tools import FunctionTool
logger = logging.getLogger(__name__)
# Type definitions for supported components
TeamComponent = Union[RoundRobinGroupChat, SelectorGroupChat]
AgentComponent = Union[AssistantAgent] # Will grow with more agent types
# Will grow with more model types
ModelComponent = Union[OpenAIChatCompletionClient]
ToolComponent = Union[FunctionTool] # Will grow with more tool types
TerminationComponent = Union[MaxMessageTermination,
StopMessageTermination, TextMentionTermination]
# Config type definitions
Component = Union[TeamComponent, AgentComponent, ModelComponent, ToolComponent]
ReturnType = Literal['object', 'dict', 'config']
Component = Union[RoundRobinGroupChat, SelectorGroupChat,
AssistantAgent, OpenAIChatCompletionClient, FunctionTool]
class ComponentFactory:
"""Creates and manages agent components with versioned configuration loading"""
SUPPORTED_VERSIONS = {
ComponentType.TEAM: ["1.0.0"],
ComponentType.AGENT: ["1.0.0"],
ComponentType.MODEL: ["1.0.0"],
ComponentType.TOOL: ["1.0.0"],
ComponentType.TERMINATION: ["1.0.0"]
}
def __init__(self):
self._model_cache: Dict[str, OpenAIChatCompletionClient] = {}
self._tool_cache: Dict[str, FunctionTool] = {}
self._last_cache_clear = datetime.now()
async def load(self, component: ComponentConfigInput, return_type: ReturnType = 'object') -> Union[Component, dict, ComponentConfig]:
"""
Universal loader for any component type
Args:
component: Component configuration (file path, dict, or ComponentConfig)
return_type: Type of return value ('object', 'dict', or 'config')
Returns:
Component instance, config dict, or ComponentConfig based on return_type
Raises:
ValueError: If component type is unknown or version unsupported
"""
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 = {
ComponentType.TEAM: self.load_team,
ComponentType.AGENT: self.load_agent,
ComponentType.MODEL: self.load_model,
ComponentType.TOOL: self.load_tool,
ComponentType.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], check_exists: bool = False, 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)
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"""
if "component_type" not in config_dict:
raise ValueError("component_type is required in configuration")
config_types = {
ComponentType.TEAM: TeamConfig,
ComponentType.AGENT: AgentConfig,
ComponentType.MODEL: ModelConfig,
ComponentType.TOOL: ToolConfig,
ComponentType.TERMINATION: TerminationConfig # Add mapping for termination
}
component_type = ComponentType(config_dict["component_type"])
config_class = config_types.get(component_type)
if not config_class:
raise ValueError(f"Unknown component type: {component_type}")
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.MAX_MESSAGES:
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)}")
async def load_team(self, config: TeamConfig) -> TeamComponent:
"""Create team instance from configuration."""
try:
# Load participants (agents)
participants = []
for participant in config.participants:
agent = await self.load(participant)
participants.append(agent)
# Load model client if specified
model_client = None
if config.model_client:
model_client = await self.load(config.model_client)
# Load termination condition if specified
termination = None
if config.termination_condition:
# Now we can use the universal load() method since termination is a proper component
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:
if not model_client:
raise ValueError(
"SelectorGroupChat requires a model_client")
return SelectorGroupChat(
participants=participants,
model_client=model_client,
termination_condition=termination
)
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)}")
async def load_agent(self, config: AgentConfig) -> AgentComponent:
"""Create agent instance from configuration."""
try:
# Load model client if specified
model_client = None
if config.model_client:
model_client = await self.load(config.model_client)
system_message = config.system_message if config.system_message else "You are a helpful assistant"
# Load tools if specified
tools = []
if config.tools:
for tool_config in config.tools:
tool = await self.load(tool_config)
tools.append(tool)
if config.agent_type == AgentTypes.ASSISTANT:
return AssistantAgent(
name=config.name,
model_client=model_client,
tools=tools,
system_message=system_message
)
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)}")
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:
model = OpenAIChatCompletionClient(
model=config.model,
api_key=config.api_key,
base_url=config.base_url
)
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)}")
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
# Helper methods remain largely the same
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:
with open(path) as f:
if path.suffix == '.json':
return json.load(f)
elif path.suffix in ('.yml', '.yaml'):
return yaml.safe_load(f)
else:
raise ValueError(f"Unsupported file format: {path.suffix}")
except Exception as e:
raise ValueError(f"Failed to load file {path}: {str(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)}")
def _is_version_supported(self, component_type: ComponentType, ver: str) -> bool:
"""Check if version is supported for component type."""
try:
v = version.parse(ver)
return ver in self.SUPPORTED_VERSIONS[component_type]
except version.InvalidVersion:
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")

View File

@ -0,0 +1,322 @@
import logging
from typing import Optional, Union, Dict, Any, List
from pathlib import Path
from loguru import logger
from ..datamodel import (
Model, Team, Agent, Tool,
Response, ComponentTypes, LinkTypes,
ComponentConfigInput
)
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(
f"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()

View File

@ -0,0 +1,424 @@
import threading
from datetime import datetime
from typing import Optional
from loguru import logger
from sqlalchemy import exc, text, func
from sqlmodel import Session, SQLModel, and_, create_engine, select
from .schema_manager import SchemaManager
from ..datamodel import (
Response,
LinkTypes
)
# from .dbutils import init_db_samples
class DatabaseManager:
"""A class to manage database operations"""
_init_lock = threading.Lock()
def __init__(self, engine_uri: str, auto_upgrade: bool = True):
connection_args = {
"check_same_thread": True} if "sqlite" in engine_uri else {}
self.engine = create_engine(engine_uri, connect_args=connection_args)
self.schema_manager = SchemaManager(
engine=self.engine,
auto_upgrade=auto_upgrade,
)
# Check and upgrade on startup
upgraded, status = self.schema_manager.check_and_upgrade()
if upgraded:
logger.info("Database schema was upgraded automatically")
else:
logger.info(f"Schema status: {status}")
def reset_db(self, recreate_tables: bool = True):
"""
Reset the database by dropping all tables and optionally recreating them.
Args:
recreate_tables (bool): If True, recreates the tables after dropping them.
Set to False if you want to call create_db_and_tables() separately.
"""
if not self._init_lock.acquire(blocking=False):
logger.warning("Database reset already in progress")
return Response(
message="Database reset already in progress",
status=False,
data=None
)
try:
# Dispose existing connections
self.engine.dispose()
with Session(self.engine) as session:
try:
# Disable foreign key checks for SQLite
if 'sqlite' in str(self.engine.url):
session.exec(text('PRAGMA foreign_keys=OFF'))
# Drop all tables
SQLModel.metadata.drop_all(self.engine)
logger.info("All tables dropped successfully")
# Re-enable foreign key checks for SQLite
if 'sqlite' in str(self.engine.url):
session.exec(text('PRAGMA foreign_keys=ON'))
session.commit()
except Exception as e:
session.rollback()
raise e
finally:
session.close()
self._init_lock.release()
if recreate_tables:
logger.info("Recreating tables...")
self.create_db_and_tables()
return Response(
message="Database reset successfully" if recreate_tables else "Database tables dropped successfully",
status=True,
data=None
)
except Exception as e:
error_msg = f"Error while resetting database: {str(e)}"
logger.error(error_msg)
return Response(
message=error_msg,
status=False,
data=None
)
finally:
if self._init_lock.locked():
self._init_lock.release()
logger.info("Database reset lock released")
def create_db_and_tables(self):
"""Create a new database and tables"""
with self._init_lock:
try:
SQLModel.metadata.create_all(self.engine)
logger.info("Database tables created successfully")
try:
# init_db_samples(self)
pass
except Exception as e:
logger.info(
"Error while initializing database samples: " + str(e))
except Exception as e:
logger.info("Error while creating database tables:" + str(e))
def upsert(self, model: SQLModel, return_json: bool = True):
"""Create or update an entity
Args:
model (SQLModel): The model instance to create or update
return_json (bool, optional): If True, returns the model as a dictionary.
If False, returns the SQLModel instance. Defaults to True.
Returns:
Response: Contains status, message and data (either dict or SQLModel based on return_json)
"""
status = True
model_class = type(model)
existing_model = None
with Session(self.engine) as session:
try:
existing_model = session.exec(
select(model_class).where(model_class.id == model.id)).first()
if existing_model:
model.updated_at = datetime.now()
for key, value in model.model_dump().items():
setattr(existing_model, key, value)
model = existing_model # Use the updated existing model
session.add(model)
else:
session.add(model)
session.commit()
session.refresh(model)
except Exception as e:
session.rollback()
logger.error("Error while updating/creating " +
str(model_class.__name__) + ": " + str(e))
status = False
return Response(
message=(
f"{model_class.__name__} Updated Successfully"
if existing_model
else f"{model_class.__name__} Created Successfully"
),
status=status,
data=model.model_dump() if return_json else model,
)
def _model_to_dict(self, model_obj):
return {col.name: getattr(model_obj, col.name) for col in model_obj.__table__.columns}
def get(
self,
model_class: SQLModel,
filters: dict = None,
return_json: bool = False,
order: str = "desc",
):
"""List entities"""
with Session(self.engine) as session:
result = []
status = True
status_message = ""
try:
statement = select(model_class)
if filters:
conditions = [getattr(model_class, col) ==
value for col, value in filters.items()]
statement = statement.where(and_(*conditions))
if hasattr(model_class, "created_at") and order:
order_by_clause = getattr(
model_class.created_at, order)() # Dynamically apply asc/desc
statement = statement.order_by(order_by_clause)
items = session.exec(statement).all()
result = [self._model_to_dict(
item) if return_json else item for item in items]
status_message = f"{model_class.__name__} Retrieved Successfully"
except Exception as e:
session.rollback()
status = False
status_message = f"Error while fetching {model_class.__name__}"
logger.error("Error while getting items: " +
str(model_class.__name__) + " " + str(e))
return Response(message=status_message, status=status, data=result)
def delete(self, model_class: SQLModel, filters: dict = None):
"""Delete an entity"""
status_message = ""
status = True
with Session(self.engine) as session:
try:
statement = select(model_class)
if filters:
conditions = [
getattr(model_class, col) == value for col, value in filters.items()]
statement = statement.where(and_(*conditions))
rows = session.exec(statement).all()
if rows:
for row in rows:
session.delete(row)
session.commit()
status_message = f"{model_class.__name__} Deleted Successfully"
else:
status_message = "Row not found"
logger.info(f"Row with filters {filters} not found")
except exc.IntegrityError as e:
session.rollback()
status = False
status_message = f"Integrity error: The {model_class.__name__} is linked to another entity and cannot be deleted. {e}"
# Log the specific integrity error
logger.error(status_message)
except Exception as e:
session.rollback()
status = False
status_message = f"Error while deleting: {e}"
logger.error(status_message)
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
)
)
if sequence is not None:
statement = statement.where(
link_table.sequence == sequence)
existing_link = session.exec(statement).first()
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
session.commit()
return Response(
message="Entities unlinked successfully and sequences reordered",
status=True
)
except Exception as e:
session.rollback()
return Response(message=f"Error unlinking entities: {str(e)}", status=False)
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
# Get field names
primary_id_field = f"{primary_class.__name__.lower()}_id"
secondary_id_field = f"{secondary_class.__name__.lower()}_id"
# 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()
result = [
item.model_dump() if return_json else item for item in items]
return Response(
message="Linked entities retrieved successfully",
status=True,
data=result
)
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=[]
)
# Add new close method
async def close(self):
"""Close database connections and cleanup resources"""
logger.info("Closing database connections...")
try:
# Dispose of the SQLAlchemy engine
self.engine.dispose()
logger.info("Database connections closed successfully")
except Exception as e:
logger.error(f"Error closing database connections: {str(e)}")
raise

View File

@ -1,491 +0,0 @@
import threading
from datetime import datetime
from typing import Optional
from loguru import logger
from sqlalchemy import exc
from sqlmodel import Session, SQLModel, and_, create_engine, select
from ..datamodel import (
Agent,
AgentLink,
AgentModelLink,
AgentSkillLink,
Model,
Response,
Skill,
Workflow,
WorkflowAgentLink,
WorkflowAgentType,
)
from .utils import init_db_samples
valid_link_types = ["agent_model", "agent_skill", "agent_agent", "workflow_agent"]
class WorkflowAgentMap(SQLModel):
agent: Agent
link: WorkflowAgentLink
class DBManager:
"""A class to manage database operations"""
_init_lock = threading.Lock() # Class-level lock
def __init__(self, engine_uri: str):
connection_args = {"check_same_thread": True} if "sqlite" in engine_uri else {}
self.engine = create_engine(engine_uri, connect_args=connection_args)
# run_migration(engine_uri=engine_uri)
def create_db_and_tables(self):
"""Create a new database and tables"""
with self._init_lock: # Use the lock
try:
SQLModel.metadata.create_all(self.engine)
try:
init_db_samples(self)
except Exception as e:
logger.info("Error while initializing database samples: " + str(e))
except Exception as e:
logger.info("Error while creating database tables:" + str(e))
def upsert(self, model: SQLModel):
"""Create a new entity"""
# check if the model exists, update else add
status = True
model_class = type(model)
existing_model = None
with Session(self.engine) as session:
try:
existing_model = session.exec(select(model_class).where(model_class.id == model.id)).first()
if existing_model:
model.updated_at = datetime.now()
for key, value in model.model_dump().items():
setattr(existing_model, key, value)
model = existing_model
session.add(model)
else:
session.add(model)
session.commit()
session.refresh(model)
except Exception as e:
session.rollback()
logger.error("Error while updating " + str(model_class.__name__) + ": " + str(e))
status = False
response = Response(
message=(
f"{model_class.__name__} Updated Successfully "
if existing_model
else f"{model_class.__name__} Created Successfully"
),
status=status,
data=model.model_dump(),
)
return response
def _model_to_dict(self, model_obj):
return {col.name: getattr(model_obj, col.name) for col in model_obj.__table__.columns}
def get_items(
self,
model_class: SQLModel,
session: Session,
filters: dict = None,
return_json: bool = False,
order: str = "desc",
):
"""List all entities"""
result = []
status = True
status_message = ""
try:
if filters:
conditions = [getattr(model_class, col) == value for col, value in filters.items()]
statement = select(model_class).where(and_(*conditions))
if hasattr(model_class, "created_at") and order:
if order == "desc":
statement = statement.order_by(model_class.created_at.desc())
else:
statement = statement.order_by(model_class.created_at.asc())
else:
statement = select(model_class)
if return_json:
result = [self._model_to_dict(row) for row in session.exec(statement).all()]
else:
result = session.exec(statement).all()
status_message = f"{model_class.__name__} Retrieved Successfully"
except Exception as e:
session.rollback()
status = False
status_message = f"Error while fetching {model_class.__name__}"
logger.error("Error while getting items: " + str(model_class.__name__) + " " + str(e))
response: Response = Response(
message=status_message,
status=status,
data=result,
)
return response
def get(
self,
model_class: SQLModel,
filters: dict = None,
return_json: bool = False,
order: str = "desc",
):
"""List all entities"""
with Session(self.engine) as session:
response = self.get_items(model_class, session, filters, return_json, order)
return response
def delete(self, model_class: SQLModel, filters: dict = None):
"""Delete an entity"""
row = None
status_message = ""
status = True
with Session(self.engine) as session:
try:
if filters:
conditions = [getattr(model_class, col) == value for col, value in filters.items()]
row = session.exec(select(model_class).where(and_(*conditions))).all()
else:
row = session.exec(select(model_class)).all()
if row:
for row in row:
session.delete(row)
session.commit()
status_message = f"{model_class.__name__} Deleted Successfully"
else:
print(f"Row with filters {filters} not found")
logger.info("Row with filters + filters + not found")
status_message = "Row not found"
except exc.IntegrityError as e:
session.rollback()
logger.error("Integrity ... Error while deleting: " + str(e))
status_message = f"The {model_class.__name__} is linked to another entity and cannot be deleted."
status = False
except Exception as e:
session.rollback()
logger.error("Error while deleting: " + str(e))
status_message = f"Error while deleting: {e}"
status = False
response = Response(
message=status_message,
status=status,
data=None,
)
return response
def get_linked_entities(
self,
link_type: str,
primary_id: int,
return_json: bool = False,
agent_type: Optional[str] = None,
sequence_id: Optional[int] = None,
):
"""
Get all entities linked to the primary entity.
Args:
link_type (str): The type of link to retrieve, e.g., "agent_model".
primary_id (int): The identifier for the primary model.
return_json (bool): Whether to return the result as a JSON object.
Returns:
List[SQLModel]: A list of linked entities.
"""
linked_entities = []
if link_type not in valid_link_types:
return []
status = True
status_message = ""
with Session(self.engine) as session:
try:
if link_type == "agent_model":
# get the agent
agent = self.get_items(Agent, filters={"id": primary_id}, session=session).data[0]
linked_entities = agent.models
elif link_type == "agent_skill":
agent = self.get_items(Agent, filters={"id": primary_id}, session=session).data[0]
linked_entities = agent.skills
elif link_type == "agent_agent":
agent = self.get_items(Agent, filters={"id": primary_id}, session=session).data[0]
linked_entities = agent.agents
elif link_type == "workflow_agent":
linked_entities = session.exec(
select(WorkflowAgentLink, Agent)
.join(Agent, WorkflowAgentLink.agent_id == Agent.id)
.where(
WorkflowAgentLink.workflow_id == primary_id,
)
).all()
linked_entities = [WorkflowAgentMap(agent=agent, link=link) for link, agent in linked_entities]
linked_entities = sorted(linked_entities, key=lambda x: x.link.sequence_id) # type: ignore
except Exception as e:
logger.error("Error while getting linked entities: " + str(e))
status_message = f"Error while getting linked entities: {e}"
status = False
if return_json:
linked_entities = [row.model_dump() for row in linked_entities]
response = Response(
message=status_message,
status=status,
data=linked_entities,
)
return response
def link(
self,
link_type: str,
primary_id: int,
secondary_id: int,
agent_type: Optional[str] = None,
sequence_id: Optional[int] = None,
) -> Response:
"""
Link two entities together.
Args:
link_type (str): The type of link to create, e.g., "agent_model".
primary_id (int): The identifier for the primary model.
secondary_id (int): The identifier for the secondary model.
agent_type (Optional[str]): The type of agent, e.g., "sender" or receiver.
Returns:
Response: The response of the linking operation, including success status and message.
"""
# TBD verify that is creator of the primary entity being linked
status = True
status_message = ""
primary_model = None
secondary_model = None
if link_type not in valid_link_types:
status = False
status_message = f"Invalid link type: {link_type}. Valid link types are: {valid_link_types}"
else:
with Session(self.engine) as session:
try:
if link_type == "agent_model":
primary_model = session.exec(select(Agent).where(Agent.id == primary_id)).first()
secondary_model = session.exec(select(Model).where(Model.id == secondary_id)).first()
if primary_model is None or secondary_model is None:
status = False
status_message = "One or both entity records do not exist."
else:
# check if the link already exists
existing_link = session.exec(
select(AgentModelLink).where(
AgentModelLink.agent_id == primary_id,
AgentModelLink.model_id == secondary_id,
)
).first()
if existing_link: # link already exists
return Response(
message=(
f"{secondary_model.__class__.__name__} already linked "
f"to {primary_model.__class__.__name__}"
),
status=False,
)
else:
primary_model.models.append(secondary_model)
elif link_type == "agent_agent":
primary_model = session.exec(select(Agent).where(Agent.id == primary_id)).first()
secondary_model = session.exec(select(Agent).where(Agent.id == secondary_id)).first()
if primary_model is None or secondary_model is None:
status = False
status_message = "One or both entity records do not exist."
else:
# check if the link already exists
existing_link = session.exec(
select(AgentLink).where(
AgentLink.parent_id == primary_id,
AgentLink.agent_id == secondary_id,
)
).first()
if existing_link:
return Response(
message=(
f"{secondary_model.__class__.__name__} already linked "
f"to {primary_model.__class__.__name__}"
),
status=False,
)
else:
primary_model.agents.append(secondary_model)
elif link_type == "agent_skill":
primary_model = session.exec(select(Agent).where(Agent.id == primary_id)).first()
secondary_model = session.exec(select(Skill).where(Skill.id == secondary_id)).first()
if primary_model is None or secondary_model is None:
status = False
status_message = "One or both entity records do not exist."
else:
# check if the link already exists
existing_link = session.exec(
select(AgentSkillLink).where(
AgentSkillLink.agent_id == primary_id,
AgentSkillLink.skill_id == secondary_id,
)
).first()
if existing_link:
return Response(
message=(
f"{secondary_model.__class__.__name__} already linked "
f"to {primary_model.__class__.__name__}"
),
status=False,
)
else:
primary_model.skills.append(secondary_model)
elif link_type == "workflow_agent":
primary_model = session.exec(select(Workflow).where(Workflow.id == primary_id)).first()
secondary_model = session.exec(select(Agent).where(Agent.id == secondary_id)).first()
if primary_model is None or secondary_model is None:
status = False
status_message = "One or both entity records do not exist."
else:
# check if the link already exists
existing_link = session.exec(
select(WorkflowAgentLink).where(
WorkflowAgentLink.workflow_id == primary_id,
WorkflowAgentLink.agent_id == secondary_id,
WorkflowAgentLink.agent_type == agent_type,
WorkflowAgentLink.sequence_id == sequence_id,
)
).first()
if existing_link:
return Response(
message=(
f"{secondary_model.__class__.__name__} already linked "
f"to {primary_model.__class__.__name__}"
),
status=False,
)
else:
# primary_model.agents.append(secondary_model)
workflow_agent_link = WorkflowAgentLink(
workflow_id=primary_id,
agent_id=secondary_id,
agent_type=agent_type,
sequence_id=sequence_id,
)
session.add(workflow_agent_link)
# add and commit the link
session.add(primary_model)
session.commit()
status_message = (
f"{secondary_model.__class__.__name__} successfully linked "
f"to {primary_model.__class__.__name__}"
)
except Exception as e:
session.rollback()
logger.error("Error while linking: " + str(e))
status = False
status_message = f"Error while linking due to an exception: {e}"
response = Response(
message=status_message,
status=status,
)
return response
def unlink(
self,
link_type: str,
primary_id: int,
secondary_id: int,
agent_type: Optional[str] = None,
sequence_id: Optional[int] = 0,
) -> Response:
"""
Unlink two entities.
Args:
link_type (str): The type of link to remove, e.g., "agent_model".
primary_id (int): The identifier for the primary model.
secondary_id (int): The identifier for the secondary model.
agent_type (Optional[str]): The type of agent, e.g., "sender" or receiver.
Returns:
Response: The response of the unlinking operation, including success status and message.
"""
status = True
status_message = ""
print("primary", primary_id, "secondary", secondary_id, "sequence", sequence_id, "agent_type", agent_type)
if link_type not in valid_link_types:
status = False
status_message = f"Invalid link type: {link_type}. Valid link types are: {valid_link_types}"
return Response(message=status_message, status=status)
with Session(self.engine) as session:
try:
if link_type == "agent_model":
existing_link = session.exec(
select(AgentModelLink).where(
AgentModelLink.agent_id == primary_id,
AgentModelLink.model_id == secondary_id,
)
).first()
elif link_type == "agent_skill":
existing_link = session.exec(
select(AgentSkillLink).where(
AgentSkillLink.agent_id == primary_id,
AgentSkillLink.skill_id == secondary_id,
)
).first()
elif link_type == "agent_agent":
existing_link = session.exec(
select(AgentLink).where(
AgentLink.parent_id == primary_id,
AgentLink.agent_id == secondary_id,
)
).first()
elif link_type == "workflow_agent":
existing_link = session.exec(
select(WorkflowAgentLink).where(
WorkflowAgentLink.workflow_id == primary_id,
WorkflowAgentLink.agent_id == secondary_id,
WorkflowAgentLink.agent_type == agent_type,
WorkflowAgentLink.sequence_id == sequence_id,
)
).first()
if existing_link:
session.delete(existing_link)
session.commit()
status_message = "Link removed successfully."
else:
status = False
status_message = "Link does not exist."
except Exception as e:
session.rollback()
logger.error("Error while unlinking: " + str(e))
status = False
status_message = f"Error while unlinking due to an exception: {e}"
return Response(message=status_message, status=status)

View File

@ -1 +0,0 @@
Generic single-database configuration.

View File

@ -1,80 +0,0 @@
import os
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from sqlmodel import SQLModel
from autogenstudio.datamodel import *
from autogenstudio.utils import get_db_uri
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
config.set_main_option("sqlalchemy.url", get_db_uri())
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -1,27 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,505 @@
import os
from pathlib import Path
import shutil
from typing import Optional, Tuple, List
from loguru import logger
from alembic import command
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from alembic.autogenerate import compare_metadata
from sqlalchemy import Engine
from sqlmodel import SQLModel
class SchemaManager:
"""
Manages database schema validation and migrations using Alembic.
Provides automatic schema validation, migrations, and safe upgrades.
Args:
engine: SQLAlchemy engine instance
auto_upgrade: Whether to automatically upgrade schema when differences found
init_mode: Controls initialization behavior:
- "none": No automatic initialization (raises error if not set up)
- "auto": Initialize if not present (default)
- "force": Always reinitialize, removing existing configuration
"""
def __init__(
self,
engine: Engine,
auto_upgrade: bool = True,
init_mode: str = "auto"
):
if init_mode not in ["none", "auto", "force"]:
raise ValueError("init_mode must be one of: none, auto, force")
self.engine = engine
self.auto_upgrade = auto_upgrade
# Set up paths relative to this file
self.base_dir = Path(__file__).parent
self.alembic_dir = self.base_dir / 'alembic'
self.alembic_ini_path = self.base_dir / 'alembic.ini'
# Handle initialization based on mode
if init_mode == "none":
self._validate_alembic_setup()
else:
self._ensure_alembic_setup(force=init_mode == "force")
def _cleanup_existing_alembic(self) -> None:
"""
Safely removes existing Alembic configuration while preserving versions directory.
"""
logger.info(
"Cleaning up existing Alembic configuration while preserving versions...")
# Create a backup of versions directory if it exists
if self.alembic_dir.exists() and (self.alembic_dir / 'versions').exists():
logger.info("Preserving existing versions directory")
# Remove alembic directory contents EXCEPT versions
if self.alembic_dir.exists():
for item in self.alembic_dir.iterdir():
if item.name != 'versions':
try:
if item.is_dir():
shutil.rmtree(item)
logger.info(f"Removed directory: {item}")
else:
item.unlink()
logger.info(f"Removed file: {item}")
except Exception as e:
logger.error(f"Failed to remove {item}: {e}")
# Remove alembic.ini if it exists
if self.alembic_ini_path.exists():
try:
self.alembic_ini_path.unlink()
logger.info(
f"Removed existing alembic.ini: {self.alembic_ini_path}")
except Exception as e:
logger.error(f"Failed to remove alembic.ini: {e}")
def _ensure_alembic_setup(self, *, force: bool = False) -> None:
"""
Ensures Alembic is properly set up, initializing if necessary.
Args:
force: If True, removes existing configuration and reinitializes
"""
try:
self._validate_alembic_setup()
if force:
logger.info(
"Force initialization requested. Cleaning up existing configuration...")
self._cleanup_existing_alembic()
self._initialize_alembic()
except FileNotFoundError:
logger.info("Alembic configuration not found. Initializing...")
if self.alembic_dir.exists():
logger.warning(
"Found existing alembic directory but missing configuration")
self._cleanup_existing_alembic()
self._initialize_alembic()
logger.info("Alembic initialization complete")
def _initialize_alembic(self) -> str:
"""Initializes Alembic configuration in the local directory."""
logger.info("Initializing Alembic configuration...")
# Check if versions exists
has_versions = (self.alembic_dir / 'versions').exists()
logger.info(f"Existing versions directory found: {has_versions}")
# Create base directories
self.alembic_dir.mkdir(exist_ok=True)
if not has_versions:
(self.alembic_dir / 'versions').mkdir(exist_ok=True)
# Write alembic.ini
ini_content = self._generate_alembic_ini_content()
with open(self.alembic_ini_path, 'w') as f:
f.write(ini_content)
logger.info("Created alembic.ini")
if not has_versions:
# Only run init if no versions directory
config = self.get_alembic_config()
command.init(config, str(self.alembic_dir))
logger.info("Initialized new Alembic directory structure")
else:
# Create minimal env.py if it doesn't exist
env_path = self.alembic_dir / 'env.py'
if not env_path.exists():
self._create_minimal_env_py(env_path)
logger.info("Created minimal env.py")
else:
# Update existing env.py
self._update_env_py(env_path)
logger.info("Updated existing env.py")
logger.info(f"Alembic setup completed at {self.base_dir}")
return str(self.alembic_ini_path)
def _create_minimal_env_py(self, env_path: Path) -> None:
"""Creates a minimal env.py file for Alembic."""
content = '''
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from sqlmodel import SQLModel
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = SQLModel.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()'''
with open(env_path, 'w') as f:
f.write(content)
def _generate_alembic_ini_content(self) -> str:
"""
Generates content for alembic.ini file.
"""
return f"""
[alembic]
script_location = {self.alembic_dir}
sqlalchemy.url = {self.engine.url}
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
""".strip()
def _update_env_py(self, env_path: Path) -> None:
"""
Updates the env.py file to use SQLModel metadata.
"""
try:
with open(env_path, 'r') as f:
content = f.read()
# Add SQLModel import
if "from sqlmodel import SQLModel" not in content:
content = "from sqlmodel import SQLModel\n" + content
# Replace target_metadata
content = content.replace(
"target_metadata = None",
"target_metadata = SQLModel.metadata"
)
# Add compare_type=True to context.configure
if "context.configure(" in content and "compare_type=True" not in content:
content = content.replace(
"context.configure(",
"context.configure(compare_type=True,"
)
with open(env_path, 'w') as f:
f.write(content)
logger.info("Updated env.py with SQLModel metadata")
except Exception as e:
logger.error(f"Failed to update env.py: {e}")
raise
# Fixed: use keyword-only argument
def _ensure_alembic_setup(self, *, force: bool = False) -> None:
"""
Ensures Alembic is properly set up, initializing if necessary.
Args:
force: If True, removes existing configuration and reinitializes
"""
try:
self._validate_alembic_setup()
if force:
logger.info(
"Force initialization requested. Cleaning up existing configuration...")
self._cleanup_existing_alembic()
self._initialize_alembic()
except FileNotFoundError:
logger.info("Alembic configuration not found. Initializing...")
if self.alembic_dir.exists():
logger.warning(
"Found existing alembic directory but missing configuration")
self._cleanup_existing_alembic()
self._initialize_alembic()
logger.info("Alembic initialization complete")
def _validate_alembic_setup(self) -> None:
"""Validates that Alembic is properly configured."""
if not self.alembic_ini_path.exists():
raise FileNotFoundError("Alembic configuration not found")
def get_alembic_config(self) -> Config:
"""
Gets Alembic configuration.
Returns:
Config: Alembic Config object
Raises:
FileNotFoundError: If alembic.ini cannot be found
"""
if not self.alembic_ini_path.exists():
raise FileNotFoundError("Could not find alembic.ini")
return Config(str(self.alembic_ini_path))
def get_current_revision(self) -> Optional[str]:
"""
Gets the current database revision.
Returns:
str: Current revision string or None if no revision
"""
with self.engine.connect() as conn:
context = MigrationContext.configure(conn)
return context.get_current_revision()
def get_head_revision(self) -> str:
"""
Gets the latest available revision.
Returns:
str: Head revision string
"""
config = self.get_alembic_config()
script = ScriptDirectory.from_config(config)
return script.get_current_head()
def get_schema_differences(self) -> List[tuple]:
"""
Detects differences between current database and models.
Returns:
List[tuple]: List of differences found
"""
with self.engine.connect() as conn:
context = MigrationContext.configure(conn)
diff = compare_metadata(context, SQLModel.metadata)
return list(diff)
def check_schema_status(self) -> Tuple[bool, str]:
"""
Checks if database schema matches current models and migrations.
Returns:
Tuple[bool, str]: (needs_upgrade, status_message)
"""
try:
current_rev = self.get_current_revision()
head_rev = self.get_head_revision()
if current_rev != head_rev:
return True, f"Database needs upgrade: {current_rev} -> {head_rev}"
differences = self.get_schema_differences()
if differences:
changes_desc = "\n".join(str(diff) for diff in differences)
return True, f"Unmigrated changes detected:\n{changes_desc}"
return False, "Database schema is up to date"
except Exception as e:
logger.error(f"Error checking schema status: {str(e)}")
return True, f"Error checking schema: {str(e)}"
def upgrade_schema(self, revision: str = "head") -> bool:
"""
Upgrades database schema to specified revision.
Args:
revision: Target revision (default: "head")
Returns:
bool: True if upgrade successful
"""
try:
config = self.get_alembic_config()
command.upgrade(config, revision)
logger.info(f"Schema upgraded successfully to {revision}")
return True
except Exception as e:
logger.error(f"Schema upgrade failed: {str(e)}")
return False
def check_and_upgrade(self) -> Tuple[bool, str]:
"""
Checks schema status and upgrades if necessary (and auto_upgrade is True).
Returns:
Tuple[bool, str]: (action_taken, status_message)
"""
needs_upgrade, status = self.check_schema_status()
if needs_upgrade:
if self.auto_upgrade:
if self.upgrade_schema():
return True, "Schema was automatically upgraded"
else:
return False, "Automatic schema upgrade failed"
else:
return False, f"Schema needs upgrade but auto_upgrade is disabled. Status: {status}"
return False, status
def generate_revision(self, message: str = "auto") -> Optional[str]:
"""
Generates new migration revision for current schema changes.
Args:
message: Revision message
Returns:
str: Revision ID if successful, None otherwise
"""
try:
config = self.get_alembic_config()
command.revision(
config,
message=message,
autogenerate=True
)
return self.get_head_revision()
except Exception as e:
logger.error(f"Failed to generate revision: {str(e)}")
return None
def get_pending_migrations(self) -> List[str]:
"""
Gets list of pending migrations that need to be applied.
Returns:
List[str]: List of pending migration revision IDs
"""
config = self.get_alembic_config()
script = ScriptDirectory.from_config(config)
current = self.get_current_revision()
head = self.get_head_revision()
if current == head:
return []
pending = []
for rev in script.iterate_revisions(current, head):
pending.append(rev.revision)
return pending
def print_status(self) -> None:
"""Prints current migration status information to logger."""
current = self.get_current_revision()
head = self.get_head_revision()
differences = self.get_schema_differences()
pending = self.get_pending_migrations()
logger.info("=== Database Schema Status ===")
logger.info(f"Current revision: {current}")
logger.info(f"Head revision: {head}")
logger.info(f"Pending migrations: {len(pending)}")
for rev in pending:
logger.info(f" - {rev}")
logger.info(f"Unmigrated changes: {len(differences)}")
for diff in differences:
logger.info(f" - {diff}")
def ensure_schema_up_to_date(self) -> bool:
"""
Ensures the database schema is up to date, generating and applying migrations if needed.
Returns:
bool: True if schema is up to date or was successfully updated
"""
try:
# Check for unmigrated changes
differences = self.get_schema_differences()
if differences:
# Generate new migration
revision = self.generate_revision("auto-generated")
if not revision:
return False
logger.info(f"Generated new migration: {revision}")
# Apply any pending migrations
upgraded, status = self.check_and_upgrade()
if not upgraded and "needs upgrade" in status.lower():
return False
return True
except Exception as e:
logger.error(f"Failed to ensure schema is up to date: {e}")
return False

File diff suppressed because one or more lines are too long

View File

@ -1,297 +0,0 @@
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Dict, List, Literal, Optional, Union
from sqlalchemy import ForeignKey, Integer, orm
from sqlmodel import (
JSON,
Column,
DateTime,
Field,
Relationship,
SQLModel,
func,
)
from sqlmodel import (
Enum as SqlEnum,
)
# 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:
print("Warning: Unable to set protected_namespaces.")
# pylint: disable=protected-access
class MessageMeta(SQLModel, table=False):
task: Optional[str] = None
messages: Optional[List[Dict[str, Any]]] = None
summary_method: Optional[str] = "last"
files: Optional[List[dict]] = None
time: Optional[datetime] = None
log: Optional[List[dict]] = None
usage: Optional[List[dict]] = None
class Message(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
role: str
content: str
session_id: Optional[int] = Field(
default=None, sa_column=Column(Integer, ForeignKey("session.id", ondelete="CASCADE"))
)
connection_id: Optional[str] = None
meta: Optional[Union[MessageMeta, dict]] = Field(default={}, sa_column=Column(JSON))
class Session(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
workflow_id: Optional[int] = Field(default=None, foreign_key="workflow.id")
name: Optional[str] = None
description: Optional[str] = None
class AgentSkillLink(SQLModel, table=True):
__table_args__ = {"sqlite_autoincrement": True}
agent_id: int = Field(default=None, primary_key=True, foreign_key="agent.id")
skill_id: int = Field(default=None, primary_key=True, foreign_key="skill.id")
class AgentModelLink(SQLModel, table=True):
__table_args__ = {"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")
class Skill(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"
name: str
content: str
description: Optional[str] = None
secrets: Optional[List[dict]] = Field(default_factory=list, sa_column=Column(JSON))
libraries: Optional[List[str]] = Field(default_factory=list, sa_column=Column(JSON))
agents: List["Agent"] = Relationship(back_populates="skills", link_model=AgentSkillLink)
class LLMConfig(SQLModel, table=False):
"""Data model for LLM Config for AutoGen"""
config_list: List[Any] = Field(default_factory=list)
temperature: float = 0
cache_seed: Optional[Union[int, None]] = None
timeout: Optional[int] = None
max_tokens: Optional[int] = 2048
extra_body: Optional[dict] = None
class ModelTypes(str, Enum):
openai = "open_ai"
cerebras = "cerebras"
google = "google"
azure = "azure"
anthropic = "anthropic"
mistral = "mistral"
together = "together"
groq = "groq"
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"
model: str
api_key: Optional[str] = None
base_url: Optional[str] = None
api_type: ModelTypes = Field(default=ModelTypes.openai, sa_column=Column(SqlEnum(ModelTypes)))
api_version: Optional[str] = None
description: Optional[str] = None
agents: List["Agent"] = Relationship(back_populates="models", link_model=AgentModelLink)
class CodeExecutionConfigTypes(str, Enum):
local = "local"
docker = "docker"
none = "none"
class AgentConfig(SQLModel, table=False):
name: Optional[str] = None
human_input_mode: str = "NEVER"
max_consecutive_auto_reply: int = 10
system_message: Optional[str] = None
is_termination_msg: Optional[Union[bool, str, Callable]] = None
code_execution_config: CodeExecutionConfigTypes = Field(
default=CodeExecutionConfigTypes.local, sa_column=Column(SqlEnum(CodeExecutionConfigTypes))
)
default_auto_reply: Optional[str] = ""
description: Optional[str] = None
llm_config: Optional[Union[LLMConfig, bool]] = Field(default=False, sa_column=Column(JSON))
admin_name: Optional[str] = "Admin"
messages: Optional[List[Dict]] = Field(default_factory=list)
max_round: Optional[int] = 100
speaker_selection_method: Optional[str] = "auto"
allow_repeat_speaker: Optional[Union[bool, List["AgentConfig"]]] = True
class AgentType(str, Enum):
assistant = "assistant"
userproxy = "userproxy"
groupchat = "groupchat"
class WorkflowAgentType(str, Enum):
sender = "sender"
receiver = "receiver"
planner = "planner"
sequential = "sequential"
class WorkflowAgentLink(SQLModel, table=True):
__table_args__ = {"sqlite_autoincrement": True}
workflow_id: int = Field(default=None, primary_key=True, foreign_key="workflow.id")
agent_id: int = Field(default=None, primary_key=True, foreign_key="agent.id")
agent_type: WorkflowAgentType = Field(
default=WorkflowAgentType.sender,
sa_column=Column(SqlEnum(WorkflowAgentType), primary_key=True),
)
sequence_id: Optional[int] = Field(default=0, primary_key=True)
class AgentLink(SQLModel, table=True):
__table_args__ = {"sqlite_autoincrement": True}
parent_id: Optional[int] = Field(default=None, foreign_key="agent.id", primary_key=True)
agent_id: Optional[int] = Field(default=None, foreign_key="agent.id", primary_key=True)
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"
type: AgentType = Field(default=AgentType.assistant, sa_column=Column(SqlEnum(AgentType)))
config: Union[AgentConfig, dict] = Field(default_factory=AgentConfig, sa_column=Column(JSON))
skills: List[Skill] = Relationship(back_populates="agents", link_model=AgentSkillLink)
models: List[Model] = Relationship(back_populates="agents", link_model=AgentModelLink)
workflows: List["Workflow"] = Relationship(link_model=WorkflowAgentLink, back_populates="agents")
parents: List["Agent"] = Relationship(
back_populates="agents",
link_model=AgentLink,
sa_relationship_kwargs=dict(
primaryjoin="Agent.id==AgentLink.agent_id",
secondaryjoin="Agent.id==AgentLink.parent_id",
),
)
agents: List["Agent"] = Relationship(
back_populates="parents",
link_model=AgentLink,
sa_relationship_kwargs=dict(
primaryjoin="Agent.id==AgentLink.parent_id",
secondaryjoin="Agent.id==AgentLink.agent_id",
),
)
task_instruction: Optional[str] = None
class WorkFlowType(str, Enum):
autonomous = "autonomous"
sequential = "sequential"
class WorkFlowSummaryMethod(str, Enum):
last = "last"
none = "none"
llm = "llm"
class Workflow(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"
name: str
description: str
agents: List[Agent] = Relationship(back_populates="workflows", link_model=WorkflowAgentLink)
type: WorkFlowType = Field(default=WorkFlowType.autonomous, sa_column=Column(SqlEnum(WorkFlowType)))
summary_method: Optional[WorkFlowSummaryMethod] = Field(
default=WorkFlowSummaryMethod.last,
sa_column=Column(SqlEnum(WorkFlowSummaryMethod)),
)
sample_tasks: Optional[List[str]] = Field(default_factory=list, sa_column=Column(JSON))
class Response(SQLModel):
message: str
status: bool
data: Optional[Any] = None
class SocketMessage(SQLModel, table=False):
connection_id: str
data: Dict[str, Any]
type: str

View File

@ -0,0 +1,2 @@
from .db import *
from .types import *

View File

@ -0,0 +1,282 @@
# defines how core data types in autogenstudio are serialized and stored in the database
from datetime import datetime
from enum import Enum
from typing import List, Optional, Union, Tuple, Type
from sqlalchemy import ForeignKey, Integer, UniqueConstraint
from sqlmodel import JSON, Column, DateTime, Field, SQLModel, func, Relationship, SQLModel
from uuid import UUID, uuid4
from .types import ToolConfig, ModelConfig, AgentConfig, TeamConfig, MessageConfig, MessageMeta
# 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:
print("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(
default_factory=ToolConfig, 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(
default_factory=ModelConfig, sa_column=Column(JSON))
agents: List["Agent"] = Relationship(
back_populates="models", link_model=AgentModelLink)
class Team(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[TeamConfig, dict] = Field(
default_factory=TeamConfig, 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(
default_factory=AgentConfig, 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)
class Message(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[MessageConfig, dict] = Field(
default_factory=MessageConfig, sa_column=Column(JSON))
session_id: Optional[int] = Field(
default=None, sa_column=Column(Integer, ForeignKey("session.id", ondelete="CASCADE"))
)
run_id: Optional[UUID] = Field(
default=None, foreign_key="run.id"
)
message_meta: Optional[Union[MessageMeta, dict]] = Field(
default={}, sa_column=Column(JSON))
class Session(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"
team_id: Optional[int] = Field(
default=None, sa_column=Column(Integer, ForeignKey("team.id", ondelete="CASCADE"))
)
name: Optional[str] = None
class RunStatus(str, Enum):
CREATED = "created"
ACTIVE = "active"
COMPLETE = "complete"
ERROR = "error"
STOPPED = "stopped"
class Run(SQLModel, table=True):
"""Represents a single execution run within a session"""
__table_args__ = {"sqlite_autoincrement": True}
# Primary key using UUID
id: UUID = Field(
default_factory=uuid4,
primary_key=True,
index=True
)
# Timestamps using the same pattern as other models
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())
)
# Foreign key to Session
session_id: Optional[int] = Field(
default=None,
sa_column=Column(
Integer,
ForeignKey("session.id", ondelete="CASCADE"),
nullable=False
)
)
# Run status and metadata
status: RunStatus = Field(default=RunStatus.CREATED)
error_message: Optional[str] = None
# Metadata storage following pattern from Message model
run_meta: dict = Field(default={}, sa_column=Column(JSON))
# Version tracking like other models
version: Optional[str] = "0.0.1"

View File

@ -0,0 +1,136 @@
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
from autogen_agentchat.base._task import TaskResult
class ModelTypes(str, Enum):
OPENAI = "OpenAIChatCompletionClient"
class ToolTypes(str, Enum):
PYTHON_FUNCTION = "PythonFunction"
class AgentTypes(str, Enum):
ASSISTANT = "AssistantAgent"
CODING = "CodingAssistantAgent"
class TeamTypes(str, Enum):
ROUND_ROBIN = "RoundRobinGroupChat"
SELECTOR = "SelectorGroupChat"
class TerminationTypes(str, Enum):
MAX_MESSAGES = "MaxMessageTermination"
STOP_MESSAGE = "StopMessageTermination"
TEXT_MENTION = "TextMentionTermination"
class ComponentType(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: ComponentType
class MessageConfig(BaseModel):
source: str
content: str
message_type: Optional[str] = "text"
class ModelConfig(BaseConfig):
model: str
model_type: ModelTypes
api_key: Optional[str] = None
base_url: Optional[str] = None
component_type: ComponentType = ComponentType.MODEL
class ToolConfig(BaseConfig):
name: str
description: str
content: str
tool_type: ToolTypes
component_type: ComponentType = ComponentType.TOOL
class AgentConfig(BaseConfig):
name: str
agent_type: AgentTypes
system_message: Optional[str] = None
model_client: Optional[ModelConfig] = None
tools: Optional[List[ToolConfig]] = None
description: Optional[str] = None
component_type: ComponentType = ComponentType.AGENT
class TerminationConfig(BaseConfig):
termination_type: TerminationTypes
max_messages: Optional[int] = None
text: Optional[str] = None
component_type: ComponentType = ComponentType.TERMINATION
class TeamConfig(BaseConfig):
name: str
participants: List[AgentConfig]
team_type: TeamTypes
model_client: Optional[ModelConfig] = None
termination_condition: Optional[TerminationConfig] = None
component_type: ComponentType = ComponentType.TEAM
class TeamResult(BaseModel):
task_result: TaskResult
usage: str
duration: float
class MessageMeta(BaseModel):
task: Optional[str] = None
task_result: Optional[TaskResult] = None
summary_method: Optional[str] = "last"
files: Optional[List[dict]] = None
time: Optional[datetime] = None
log: Optional[List[dict]] = None
usage: Optional[List[dict]] = None
# web request/response data models
class Response(BaseModel):
message: str
status: bool
data: Optional[Any] = None
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]

View File

@ -0,0 +1,67 @@
from typing import AsyncGenerator, Union, Optional
import time
from .database import ComponentFactory
from .datamodel import TeamResult, TaskResult, ComponentConfigInput
from autogen_agentchat.messages import InnerMessage, ChatMessage
from autogen_core.base import CancellationToken
class TeamManager:
def __init__(self) -> None:
self.component_factory = ComponentFactory()
async def run_stream(
self,
task: str,
team_config: ComponentConfigInput,
cancellation_token: Optional[CancellationToken] = None
) -> AsyncGenerator[Union[InnerMessage, ChatMessage, TaskResult], None]:
"""Stream the team's execution results"""
start_time = time.time()
try:
# Let factory handle all config processing
team = await self.component_factory.load(team_config)
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 TeamResult(
task_result=message,
usage="",
duration=time.time() - start_time
)
else:
yield message
except Exception as e:
raise e
async def run(
self,
task: str,
team_config: ComponentConfigInput,
cancellation_token: Optional[CancellationToken] = None
) -> TeamResult:
"""Original non-streaming run method with optional cancellation"""
start_time = time.time()
# Let factory handle all config processing
team = await self.component_factory.load(team_config)
result = await team.run(
task=task,
cancellation_token=cancellation_token
)
return TeamResult(
task_result=result,
usage="",
duration=time.time() - start_time
)

View File

@ -1,242 +0,0 @@
{
"models": [
{
"model": "gpt-4",
"api_key": "Your Azure API key here",
"base_url": "Your Azure base URL here",
"api_type": "azure",
"api_version": "Your Azure API version here",
"description": "Azure Open AI model configuration"
},
{
"model": "gpt-4-1106-preview",
"description": "OpenAI model configuration"
},
{
"model": "TheBloke/zephyr-7B-alpha-AWQ",
"api_key": "EMPTY",
"base_url": "http://localhost:8000/v1",
"description": "Local model example with vLLM server endpoint"
}
],
"agents": [
{
"type": "userproxy",
"config": {
"name": "userproxy",
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 5,
"system_message": "You are a helpful assistant.",
"default_auto_reply": "TERMINATE",
"llm_config": false,
"code_execution_config": {
"work_dir": null,
"use_docker": false
},
"description": "A user proxy agent that executes code."
}
},
{
"type": "assistant",
"skills": [
{
"title": "find_papers_arxiv",
"description": "This skill finds relevant papers on arXiv given a query.",
"content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n \"\"\"\n Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n Args:\n query (str): The search query.\n max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n Returns:\n jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n Example:\n >>> results = search_arxiv(\"attention is all you need\")\n >>> print(results)\n \"\"\"\n\n import arxiv\n\n key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n # Create the cache if it doesn't exist\n cache_dir = \".cache\"\n if not os.path.isdir(cache_dir):\n os.mkdir(cache_dir)\n\n fname = os.path.join(cache_dir, key + \".cache\")\n\n # Cache hit\n if os.path.isfile(fname):\n fh = open(fname, \"r\", encoding=\"utf-8\")\n data = json.loads(fh.read())\n fh.close()\n return data\n\n # Normalize the query, removing operator keywords\n query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n query = re.sub(r\"\\s+\", \" \", query).strip()\n\n search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n jresults = list()\n for result in search.results():\n r = dict()\n r[\"entry_id\"] = result.entry_id\n r[\"updated\"] = str(result.updated)\n r[\"published\"] = str(result.published)\n r[\"title\"] = result.title\n r[\"authors\"] = [str(a) for a in result.authors]\n r[\"summary\"] = result.summary\n r[\"comment\"] = result.comment\n r[\"journal_ref\"] = result.journal_ref\n r[\"doi\"] = result.doi\n r[\"primary_category\"] = result.primary_category\n r[\"categories\"] = result.categories\n r[\"links\"] = [str(link) for link in result.links]\n r[\"pdf_url\"] = result.pdf_url\n jresults.append(r)\n\n if len(jresults) > max_results:\n jresults = jresults[0:max_results]\n\n # Save to cache\n fh = open(fname, \"w\")\n fh.write(json.dumps(jresults))\n fh.close()\n return jresults\n",
"file_name": "find_papers_arxiv"
},
{
"title": "generate_images",
"description": "This skill generates images from a given query using OpenAI's DALL-E model and saves them to disk.",
"content": "from typing import List\nimport uuid\nimport requests # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n \"\"\"\n Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk. Use the code below anytime there is a request to create an image.\n\n :param query: A natural language description of the image to be generated.\n :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n :return: A list of filenames for the saved images.\n \"\"\"\n\n client = OpenAI() # Initialize the OpenAI client\n response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size) # Generate images\n\n # List to store the file names of saved images\n saved_files = []\n\n # Check if the response is successful\n if response.data:\n for image_data in response.data:\n # Generate a random UUID as the file name\n file_name = str(uuid.uuid4()) + \".png\" # Assuming the image is a PNG\n file_path = Path(file_name)\n\n img_url = image_data.url\n img_response = requests.get(img_url)\n if img_response.status_code == 200:\n # Write the binary content to a file\n with open(file_path, \"wb\") as img_file:\n img_file.write(img_response.content)\n print(f\"Image saved to {file_path}\")\n saved_files.append(str(file_path))\n else:\n print(f\"Failed to download the image from {img_url}\")\n else:\n print(\"No image data found in the response!\")\n\n # Return the list of saved files\n return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n"
}
],
"config": {
"name": "primary_assistant",
"description": "A primary assistant agent that writes plans and code to solve tasks.",
"llm_config": {
"config_list": [
{
"model": "gpt-4-1106-preview"
}
],
"temperature": 0.1,
"timeout": 600,
"cache_seed": null
},
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 8,
"system_message": "You are a helpful AI assistant. Solve tasks using your coding and language skills. In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the user to execute. 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. Reply 'TERMINATE' in the end when everything is done."
}
}
],
"skills": [
{
"title": "fetch_profile",
"description": "This skill fetches the text content from a personal website.",
"content": "from typing import Optional\nimport requests\nfrom bs4 import BeautifulSoup\n\n\ndef fetch_user_profile(url: str) -> Optional[str]:\n \"\"\"\n Fetches the text content from a personal website.\n\n Given a URL of a person's personal website, this function scrapes\n the content of the page and returns the text found within the <body>.\n\n Args:\n url (str): The URL of the person's personal website.\n\n Returns:\n Optional[str]: The text content of the website's body, or None if any error occurs.\n \"\"\"\n try:\n # Send a GET request to the URL\n response = requests.get(url)\n # Check for successful access to the webpage\n if response.status_code == 200:\n # Parse the HTML content of the page using BeautifulSoup\n soup = BeautifulSoup(response.text, \"html.parser\")\n # Extract the content of the <body> tag\n body_content = soup.find(\"body\")\n # Return all the text in the body tag, stripping leading/trailing whitespaces\n return \" \".join(body_content.stripped_strings) if body_content else None\n else:\n # Return None if the status code isn't 200 (success)\n return None\n except requests.RequestException:\n # Return None if any request-related exception is caught\n return None\n"
},
{
"title": "generate_images",
"description": "This skill generates images from a given query using OpenAI's DALL-E model and saves them to disk.",
"content": "from typing import List\nimport uuid\nimport requests # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n \"\"\"\n Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk. Use the code below anytime there is a request to create an image.\n\n :param query: A natural language description of the image to be generated.\n :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n :return: A list of filenames for the saved images.\n \"\"\"\n\n client = OpenAI() # Initialize the OpenAI client\n response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size) # Generate images\n\n # List to store the file names of saved images\n saved_files = []\n\n # Check if the response is successful\n if response.data:\n for image_data in response.data:\n # Generate a random UUID as the file name\n file_name = str(uuid.uuid4()) + \".png\" # Assuming the image is a PNG\n file_path = Path(file_name)\n\n img_url = image_data.url\n img_response = requests.get(img_url)\n if img_response.status_code == 200:\n # Write the binary content to a file\n with open(file_path, \"wb\") as img_file:\n img_file.write(img_response.content)\n print(f\"Image saved to {file_path}\")\n saved_files.append(str(file_path))\n else:\n print(f\"Failed to download the image from {img_url}\")\n else:\n print(\"No image data found in the response!\")\n\n # Return the list of saved files\n return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n"
}
],
"workflows": [
{
"name": "Travel Agent Group Chat Workflow",
"description": "A group chat workflow",
"type": "groupchat",
"sender": {
"type": "userproxy",
"config": {
"name": "userproxy",
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 5,
"system_message": "You are a helpful assistant.",
"code_execution_config": {
"work_dir": null,
"use_docker": false
}
}
},
"receiver": {
"type": "groupchat",
"config": {
"name": "group_chat_manager",
"llm_config": {
"config_list": [
{
"model": "gpt-4-1106-preview"
}
],
"temperature": 0.1,
"timeout": 600,
"cache_seed": 42
},
"human_input_mode": "NEVER",
"system_message": "Group chat manager"
},
"groupchat_config": {
"admin_name": "Admin",
"max_round": 10,
"speaker_selection_method": "auto",
"agents": [
{
"type": "assistant",
"config": {
"name": "travel_planner",
"llm_config": {
"config_list": [
{
"model": "gpt-4-1106-preview"
}
],
"temperature": 0.1,
"timeout": 600,
"cache_seed": 42
},
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 8,
"system_message": "You are a helpful assistant that can suggest a travel plan for a user. You are the primary cordinator who will receive suggestions or advice from other agents (local_assistant, language_assistant). You must ensure that the finally plan integrates the suggestions from other agents or team members. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE."
}
},
{
"type": "assistant",
"config": {
"name": "local_assistant",
"llm_config": {
"config_list": [
{
"model": "gpt-4-1106-preview"
}
],
"temperature": 0.1,
"timeout": 600,
"cache_seed": 42
},
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 8,
"system_message": "You are a helpful assistant that can review travel plans, providing critical feedback on how the trip can be enriched for enjoyment of the local culture. If the plan already includes local experiences, you can mention that the plan is satisfactory, with rationale."
}
},
{
"type": "assistant",
"config": {
"name": "language_assistant",
"llm_config": {
"config_list": [
{
"model": "gpt-4-1106-preview"
}
],
"temperature": 0.1,
"timeout": 600,
"cache_seed": 42
},
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 8,
"system_message": "You are a helpful assistant that can review travel plans, providing feedback on important/critical tips about how best to address language or communication challenges for the given destination. If the plan already includes language tips, you can mention that the plan is satisfactory, with rationale."
}
}
]
}
}
},
{
"name": "General Agent Workflow",
"description": "This workflow is used for general purpose tasks.",
"sender": {
"type": "userproxy",
"config": {
"name": "userproxy",
"description": "A user proxy agent that executes code.",
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 10,
"system_message": "You are a helpful assistant.",
"default_auto_reply": "TERMINATE",
"llm_config": false,
"code_execution_config": {
"work_dir": null,
"use_docker": false
}
}
},
"receiver": {
"type": "assistant",
"skills": [
{
"title": "find_papers_arxiv",
"description": "This skill finds relevant papers on arXiv given a query.",
"content": "import os\nimport re\nimport json\nimport hashlib\n\n\ndef search_arxiv(query, max_results=10):\n \"\"\"\n Searches arXiv for the given query using the arXiv API, then returns the search results. This is a helper function. In most cases, callers will want to use 'find_relevant_papers( query, max_results )' instead.\n\n Args:\n query (str): The search query.\n max_results (int, optional): The maximum number of search results to return. Defaults to 10.\n\n Returns:\n jresults (list): A list of dictionaries. Each dictionary contains fields such as 'title', 'authors', 'summary', and 'pdf_url'\n\n Example:\n >>> results = search_arxiv(\"attention is all you need\")\n >>> print(results)\n \"\"\"\n\n import arxiv\n\n key = hashlib.md5((\"search_arxiv(\" + str(max_results) + \")\" + query).encode(\"utf-8\")).hexdigest()\n # Create the cache if it doesn't exist\n cache_dir = \".cache\"\n if not os.path.isdir(cache_dir):\n os.mkdir(cache_dir)\n\n fname = os.path.join(cache_dir, key + \".cache\")\n\n # Cache hit\n if os.path.isfile(fname):\n fh = open(fname, \"r\", encoding=\"utf-8\")\n data = json.loads(fh.read())\n fh.close()\n return data\n\n # Normalize the query, removing operator keywords\n query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n query = re.sub(r\"\\s(and|or|not)\\s\", \" \", \" \" + query + \" \")\n query = re.sub(r\"[^\\s\\w]\", \" \", query.lower())\n query = re.sub(r\"\\s+\", \" \", query).strip()\n\n search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n\n jresults = list()\n for result in search.results():\n r = dict()\n r[\"entry_id\"] = result.entry_id\n r[\"updated\"] = str(result.updated)\n r[\"published\"] = str(result.published)\n r[\"title\"] = result.title\n r[\"authors\"] = [str(a) for a in result.authors]\n r[\"summary\"] = result.summary\n r[\"comment\"] = result.comment\n r[\"journal_ref\"] = result.journal_ref\n r[\"doi\"] = result.doi\n r[\"primary_category\"] = result.primary_category\n r[\"categories\"] = result.categories\n r[\"links\"] = [str(link) for link in result.links]\n r[\"pdf_url\"] = result.pdf_url\n jresults.append(r)\n\n if len(jresults) > max_results:\n jresults = jresults[0:max_results]\n\n # Save to cache\n fh = open(fname, \"w\")\n fh.write(json.dumps(jresults))\n fh.close()\n return jresults\n"
},
{
"title": "generate_images",
"description": "This skill generates images from a given query using OpenAI's DALL-E model and saves them to disk.",
"content": "from typing import List\nimport uuid\nimport requests # to perform HTTP requests\nfrom pathlib import Path\n\nfrom openai import OpenAI\n\n\ndef generate_and_save_images(query: str, image_size: str = \"1024x1024\") -> List[str]:\n \"\"\"\n Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk. Use the code below anytime there is a request to create an image.\n\n :param query: A natural language description of the image to be generated.\n :param image_size: The size of the image to be generated. (default is \"1024x1024\")\n :return: A list of filenames for the saved images.\n \"\"\"\n\n client = OpenAI() # Initialize the OpenAI client\n response = client.images.generate(model=\"dall-e-3\", prompt=query, n=1, size=image_size) # Generate images\n\n # List to store the file names of saved images\n saved_files = []\n\n # Check if the response is successful\n if response.data:\n for image_data in response.data:\n # Generate a random UUID as the file name\n file_name = str(uuid.uuid4()) + \".png\" # Assuming the image is a PNG\n file_path = Path(file_name)\n\n img_url = image_data.url\n img_response = requests.get(img_url)\n if img_response.status_code == 200:\n # Write the binary content to a file\n with open(file_path, \"wb\") as img_file:\n img_file.write(img_response.content)\n print(f\"Image saved to {file_path}\")\n saved_files.append(str(file_path))\n else:\n print(f\"Failed to download the image from {img_url}\")\n else:\n print(\"No image data found in the response!\")\n\n # Return the list of saved files\n return saved_files\n\n\n# Example usage of the function:\n# generate_and_save_images(\"A cute baby sea otter\")\n"
}
],
"config": {
"description": "Default assistant to generate plans and write code to solve tasks.",
"name": "primary_assistant",
"llm_config": {
"config_list": [
{
"model": "gpt-4-1106-preview"
}
],
"temperature": 0.1,
"timeout": 600,
"cache_seed": null
},
"human_input_mode": "NEVER",
"max_consecutive_auto_reply": 15,
"system_message": "You are a helpful AI assistant. Solve tasks using your coding and language skills. In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the user to execute. 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. Reply 'TERMINATE' in the end when everything is done."
}
},
"type": "twoagents"
}
]
}

View File

@ -10,10 +10,8 @@ from typing import Any, Dict, List, Tuple, Union
from dotenv import load_dotenv
from loguru import logger
from autogen.coding import DockerCommandLineCodeExecutor, LocalCommandLineCodeExecutor
from autogen.oai.client import ModelClient, OpenAIWrapper
from ..datamodel import CodeExecutionConfigTypes, Model, Skill
from ..datamodel import Model
from ..version import APP_NAME
@ -44,26 +42,6 @@ def str_to_datetime(dt_str: str) -> datetime:
return datetime.fromisoformat(dt_str)
def clear_folder(folder_path: str) -> None:
"""
Clear the contents of a folder.
:param folder_path: The path to the folder to clear.
"""
# exit if the folder does not exist
if not os.path.exists(folder_path):
return
# exit if the folder does not exist
if not os.path.exists(folder_path):
return
for file in os.listdir(folder_path):
file_path = os.path.join(folder_path, file)
if os.path.isfile(file_path):
os.remove(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
def get_file_type(file_path: str) -> str:
"""
@ -153,31 +131,6 @@ def get_file_type(file_path: str) -> str:
return file_type
def serialize_file(file_path: str) -> Tuple[str, str]:
"""
Reads a file from a given file path, base64 encodes its content,
and returns the base64 encoded string along with the file type.
The file type is determined by the file extension. If the file extension is not
recognized, 'unknown' will be used as the file type.
:param file_path: The path to the file to be serialized.
:return: A tuple containing the base64 encoded string of the file and the file type.
"""
file_type = get_file_type(file_path)
# Read the file and encode its contents
try:
with open(file_path, "rb") as file:
file_content = file.read()
base64_encoded_content = base64.b64encode(file_content).decode("utf-8")
except Exception as e:
raise IOError(f"An error occurred while reading the file: {e}") from e
return base64_encoded_content, file_type
def get_modified_files(start_timestamp: float, end_timestamp: float, source_dir: str) -> List[Dict[str, str]]:
"""
Identify files from source_dir that were modified within a specified timestamp range.
@ -200,7 +153,8 @@ def get_modified_files(start_timestamp: float, end_timestamp: float, source_dir:
for root, dirs, files in os.walk(source_dir):
# Update directories and files to exclude those to be ignored
dirs[:] = [d for d in dirs if d not in ignore_files]
files[:] = [f for f in files if f not in ignore_files and os.path.splitext(f)[1] not in ignore_extensions]
files[:] = [f for f in files if f not in ignore_files and os.path.splitext(f)[
1] not in ignore_extensions]
for file in files:
file_path = os.path.join(root, file)
@ -209,7 +163,9 @@ def get_modified_files(start_timestamp: float, end_timestamp: float, source_dir:
# Verify if the file was modified within the given timestamp range
if start_timestamp <= file_mtime <= end_timestamp:
file_relative_path = (
"files/user" + file_path.split("files/user", 1)[1] if "files/user" in file_path else ""
"files/user" +
file_path.split(
"files/user", 1)[1] if "files/user" in file_path else ""
)
file_type = get_file_type(file_path)
@ -289,138 +245,6 @@ def init_app_folders(app_file_path: str) -> Dict[str, str]:
return folders
def get_skills_prompt(skills: List[Skill], work_dir: str) -> str:
"""
Create a prompt with the content of all skills and write the skills to a file named skills.py in the work_dir.
:param skills: A dictionary skills
:return: A string containing the content of all skills
"""
instruction = """
While solving the task you may use functions below which will be available in a file called skills.py .
To use a function skill.py in code, IMPORT THE FUNCTION FROM skills.py and then use the function.
If you need to install python packages, write shell code to
install via pip and use --quiet option.
"""
prompt = "" # filename: skills.py
for skill in skills:
if not isinstance(skill, Skill):
skill = Skill(**skill)
if skill.secrets:
for secret in skill.secrets:
if secret.get("value") is not None:
os.environ[secret["secret"]] = secret["value"]
prompt += f"""
##### Begin of {skill.name} #####
from skills import {skill.name} # Import the function from skills.py
{skill.content}
#### End of {skill.name} ####
"""
return instruction + prompt
def save_skills_to_file(skills: List[Skill], work_dir: str) -> None:
"""
Write the skills to a file named skills.py in the work_dir.
:param skills: A dictionary skills
"""
# TBD: Double check for duplicate skills?
# check if work_dir exists
if not os.path.exists(work_dir):
os.makedirs(work_dir)
skills_content = ""
for skill in skills:
if not isinstance(skill, Skill):
skill = Skill(**skill)
skills_content += f"""
##### Begin of {skill.name} #####
{skill.content}
#### End of {skill.name} ####
"""
# overwrite skills.py in work_dir
with open(os.path.join(work_dir, "skills.py"), "w", encoding="utf-8") as f:
f.write(skills_content)
def delete_files_in_folder(folders: Union[str, List[str]]) -> None:
"""
Delete all files and directories in the specified folders.
:param folders: A list of folders or a single folder string
"""
if isinstance(folders, str):
folders = [folders]
for folder in folders:
# Check if the folder exists
if not os.path.isdir(folder):
continue
# List all the entries in the directory
for entry in os.listdir(folder):
# Get the full path
path = os.path.join(folder, entry)
try:
if os.path.isfile(path) or os.path.islink(path):
# Remove the file or link
os.remove(path)
elif os.path.isdir(path):
# Remove the directory and all its content
shutil.rmtree(path)
except Exception as e:
# Print the error message and skip
logger.info(f"Failed to delete {path}. Reason: {e}")
def extract_successful_code_blocks(messages: List[Dict[str, str]]) -> List[str]:
"""
Parses through a list of messages containing code blocks and execution statuses,
returning the array of code blocks that executed successfully and retains
the backticks for Markdown rendering.
Parameters:
messages (List[Dict[str, str]]): A list of message dictionaries containing 'content' and 'role' keys.
Returns:
List[str]: A list containing the code blocks that were successfully executed, including backticks.
"""
successful_code_blocks = []
# Regex pattern to capture code blocks enclosed in triple backticks.
code_block_regex = r"```[\s\S]*?```"
for i, row in enumerate(messages):
message = row["message"]
if message["role"] == "user" and "execution succeeded" in message["content"]:
if i > 0 and messages[i - 1]["message"]["role"] == "assistant":
prev_content = messages[i - 1]["message"]["content"]
# Find all matches for code blocks
code_blocks = re.findall(code_block_regex, prev_content)
# Add the code blocks with backticks
successful_code_blocks.extend(code_blocks)
return successful_code_blocks
def sanitize_model(model: Model):
"""
Sanitize model dictionary to remove None values and empty strings and only keep valid keys.
@ -429,7 +253,8 @@ def sanitize_model(model: Model):
model = model.model_dump()
valid_keys = ["model", "base_url", "api_key", "api_type", "api_version"]
# only add key if value is not None
sanitized_model = {k: v for k, v in model.items() if (v is not None and v != "") and k in valid_keys}
sanitized_model = {k: v for k, v in model.items() if (
v is not None and v != "") and k in valid_keys}
return sanitized_model
@ -440,134 +265,29 @@ def test_model(model: Model):
print("Testing model", model)
sanitized_model = sanitize_model(model)
client = OpenAIWrapper(config_list=[sanitized_model])
response = client.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant that can add numbers. ONLY RETURN THE RESULT.",
},
{
"role": "user",
"content": "2+2=",
},
],
cache_seed=None,
)
return response.choices[0].message.content
# def summarize_chat_history(task: str, messages: List[Dict[str, str]], client: ModelClient):
# """
# Summarize the chat history using the model endpoint and returning the response.
# """
# summarization_system_prompt = f"""
# You are a helpful assistant that is able to review the chat history between a set of agents (userproxy agents, assistants etc) as they try to address a given TASK and provide a summary. Be SUCCINCT but also comprehensive enough to allow others (who cannot see the chat history) understand and recreate the solution.
def load_code_execution_config(code_execution_type: CodeExecutionConfigTypes, work_dir: str):
"""
Load the code execution configuration based on the code execution type.
:param code_execution_type: The code execution type.
:param work_dir: The working directory to store code execution files.
:return: The code execution configuration.
"""
work_dir = Path(work_dir)
work_dir.mkdir(exist_ok=True)
executor = None
if code_execution_type == CodeExecutionConfigTypes.local:
executor = LocalCommandLineCodeExecutor(work_dir=work_dir)
elif code_execution_type == CodeExecutionConfigTypes.docker:
try:
executor = DockerCommandLineCodeExecutor(work_dir=work_dir)
except Exception as e:
logger.error(f"Error initializing Docker executor: {e}")
return False
elif code_execution_type == CodeExecutionConfigTypes.none:
return False
else:
raise ValueError(f"Invalid code execution type: {code_execution_type}")
code_execution_config = {
"executor": executor,
}
return code_execution_config
def summarize_chat_history(task: str, messages: List[Dict[str, str]], client: ModelClient):
"""
Summarize the chat history using the model endpoint and returning the response.
"""
summarization_system_prompt = f"""
You are a helpful assistant that is able to review the chat history between a set of agents (userproxy agents, assistants etc) as they try to address a given TASK and provide a summary. Be SUCCINCT but also comprehensive enough to allow others (who cannot see the chat history) understand and recreate the solution.
The task requested by the user is:
===
{task}
===
The summary should focus on extracting the actual solution to the task from the chat history (assuming the task was addressed) such that any other agent reading the summary will understand what the actual solution is. Use a neutral tone and DO NOT directly mention the agents. Instead only focus on the actions that were carried out (e.g. do not say 'assistant agent generated some code visualization code ..' instead say say 'visualization code was generated ..'. The answer should be framed as a response to the user task. E.g. if the task is "What is the height of the Eiffel tower", the summary should be "The height of the Eiffel Tower is ...").
"""
summarization_prompt = [
{
"role": "system",
"content": summarization_system_prompt,
},
{
"role": "user",
"content": f"Summarize the following chat history. {str(messages)}",
},
]
response = client.create(messages=summarization_prompt, cache_seed=None)
return response.choices[0].message.content
def get_autogen_log(db_path="logs.db"):
"""
Fetches data the autogen logs database.
Args:
dbname (str): Name of the database file. Defaults to "logs.db".
table (str): Name of the table to query. Defaults to "chat_completions".
Returns:
list: A list of dictionaries, where each dictionary represents a row from the table.
"""
import json
import sqlite3
con = sqlite3.connect(db_path)
query = """
SELECT
chat_completions.*,
agents.name AS agent_name
FROM
chat_completions
JOIN
agents ON chat_completions.wrapper_id = agents.wrapper_id
"""
cursor = con.execute(query)
rows = cursor.fetchall()
column_names = [description[0] for description in cursor.description]
data = [dict(zip(column_names, row)) for row in rows]
for row in data:
response = json.loads(row["response"])
print(response)
total_tokens = response.get("usage", {}).get("total_tokens", 0)
row["total_tokens"] = total_tokens
con.close()
return data
def find_key_value(d, target_key):
"""
Recursively search for a key in a nested dictionary and return its value.
"""
if d is None:
return None
if isinstance(d, dict):
if target_key in d:
return d[target_key]
for k in d:
item = find_key_value(d[k], target_key)
if item is not None:
return item
elif isinstance(d, list):
for i in d:
item = find_key_value(i, target_key)
if item is not None:
return item
return None
# The task requested by the user is:
# ===
# {task}
# ===
# The summary should focus on extracting the actual solution to the task from the chat history (assuming the task was addressed) such that any other agent reading the summary will understand what the actual solution is. Use a neutral tone and DO NOT directly mention the agents. Instead only focus on the actions that were carried out (e.g. do not say 'assistant agent generated some code visualization code ..' instead say say 'visualization code was generated ..'. The answer should be framed as a response to the user task. E.g. if the task is "What is the height of the Eiffel tower", the summary should be "The height of the Eiffel Tower is ...").
# """
# summarization_prompt = [
# {
# "role": "system",
# "content": summarization_system_prompt,
# },
# {
# "role": "user",
# "content": f"Summarize the following chat history. {str(messages)}",
# },
# ]
# response = client.create(messages=summarization_prompt, cache_seed=None)
# return response.choices[0].message.content

View File

@ -1,93 +1,63 @@
import asyncio
# api/app.py
import os
import queue
import threading
import traceback
from contextlib import asynccontextmanager
from typing import Any, Union
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
# import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from loguru import logger
from openai import OpenAIError
from ..chatmanager import AutoGenChatManager
from ..database import workflow_from_id
from ..database.dbmanager import DBManager
from ..datamodel import Agent, Message, Model, Response, Session, Skill, Workflow
from ..profiler import Profiler
from ..utils import check_and_cast_datetime_fields, init_app_folders, sha256_hash, test_model
from .routes import sessions, runs, teams, agents, models, tools, ws
from .deps import init_managers, cleanup_managers
from .config import settings
from .initialization import AppInitializer
from ..version import VERSION
from ..websocket_connection_manager import WebSocketConnectionManager
profiler = Profiler()
managers = {"chat": None} # manage calls to autogen
# Create thread-safe queue for messages between api thread and autogen threads
message_queue = queue.Queue()
active_connections = []
active_connections_lock = asyncio.Lock()
websocket_manager = WebSocketConnectionManager(
active_connections=active_connections,
active_connections_lock=active_connections_lock,
)
def message_handler():
while True:
message = message_queue.get()
logger.info(
"** Processing Agent Message on Queue: Active Connections: "
+ str([client_id for _, client_id in websocket_manager.active_connections])
+ " **"
)
for connection, socket_client_id in websocket_manager.active_connections:
if message["connection_id"] == socket_client_id:
logger.info(
f"Sending message to connection_id: {message['connection_id']}. Connection ID: {socket_client_id}"
)
asyncio.run(websocket_manager.send_message(message, connection))
else:
logger.info(
f"Skipping message for connection_id: {message['connection_id']}. Connection ID: {socket_client_id}"
)
message_queue.task_done()
message_handler_thread = threading.Thread(target=message_handler, daemon=True)
message_handler_thread.start()
# Configure logging
# logger = logging.getLogger(__name__)
# logging.basicConfig(level=logging.INFO)
# Initialize application
app_file_path = os.path.dirname(os.path.abspath(__file__))
folders = init_app_folders(app_file_path)
ui_folder_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui")
database_engine_uri = folders["database_engine_uri"]
dbmanager = DBManager(engine_uri=database_engine_uri)
HUMAN_INPUT_TIMEOUT_SECONDS = 180
initializer = AppInitializer(settings, app_file_path)
@asynccontextmanager
async def lifespan(app: FastAPI):
print("***** App started *****")
managers["chat"] = AutoGenChatManager(
message_queue=message_queue,
websocket_manager=websocket_manager,
human_input_timeout=HUMAN_INPUT_TIMEOUT_SECONDS,
)
dbmanager.create_db_and_tables()
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""
Lifecycle manager for the FastAPI application.
Handles initialization and cleanup of application resources.
"""
# Startup
logger.info("Initializing application...")
try:
# Initialize managers (DB, Connection, Team)
await init_managers(initializer.database_uri, initializer.config_dir)
logger.info("Managers initialized successfully")
yield
# Close all active connections
await websocket_manager.disconnect_all()
print("***** App stopped *****")
# Any other initialization code
logger.info("Application startup complete")
except Exception as e:
logger.error(f"Failed to initialize application: {str(e)}")
raise
app = FastAPI(lifespan=lifespan)
yield # Application runs here
# Shutdown
try:
logger.info("Cleaning up application resources...")
await cleanup_managers()
logger.info("Application shutdown complete")
except Exception as e:
logger.error(f"Error during shutdown: {str(e)}")
# allow cross origin requests for testing on localhost:800* ports only
# Create FastAPI application
app = FastAPI(lifespan=lifespan, debug=True)
# CORS middleware configuration
app.add_middleware(
CORSMiddleware,
allow_origins=[
@ -101,412 +71,114 @@ app.add_middleware(
allow_headers=["*"],
)
show_docs = os.environ.get("AUTOGENSTUDIO_API_DOCS", "False").lower() == "true"
docs_url = "/docs" if show_docs else None
# Create API router with version and documentation
api = FastAPI(
root_path="/api",
title="AutoGen Studio API",
version=VERSION,
docs_url=docs_url,
description="AutoGen Studio is a low-code tool for building and testing multi-agent workflows using AutoGen.",
description="AutoGen Studio is a low-code tool for building and testing multi-agent workflows.",
docs_url="/docs" if settings.API_DOCS else None,
)
# mount an api route such that the main route serves the ui and the /api
app.mount("/api", api)
app.mount("/", StaticFiles(directory=ui_folder_path, html=True), name="ui")
api.mount(
"/files",
StaticFiles(directory=folders["files_static_root"], html=True),
name="files",
# Include all routers with their prefixes
api.include_router(
sessions.router,
prefix="/sessions",
tags=["sessions"],
responses={404: {"description": "Not found"}},
)
api.include_router(
runs.router,
prefix="/runs",
tags=["runs"],
responses={404: {"description": "Not found"}},
)
api.include_router(
teams.router,
prefix="/teams",
tags=["teams"],
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,
prefix="/ws",
tags=["websocket"],
responses={404: {"description": "Not found"}},
)
# manage websocket connections
def create_entity(model: Any, model_class: Any, filters: dict = None):
"""Create a new entity"""
model = check_and_cast_datetime_fields(model)
try:
response: Response = dbmanager.upsert(model)
return response.model_dump(mode="json")
except Exception as ex_error:
print(ex_error)
return {
"status": False,
"message": f"Error occurred while creating {model_class.__name__}: " + str(ex_error),
}
def list_entity(
model_class: Any,
filters: dict = None,
return_json: bool = True,
order: str = "desc",
):
"""List all entities for a user"""
return dbmanager.get(model_class, filters=filters, return_json=return_json, order=order)
def delete_entity(model_class: Any, filters: dict = None):
"""Delete an entity"""
return dbmanager.delete(filters=filters, model_class=model_class)
@api.get("/skills")
async def list_skills(user_id: str):
"""List all skills for a user"""
filters = {"user_id": user_id}
return list_entity(Skill, filters=filters)
@api.post("/skills")
async def create_skill(skill: Skill):
"""Create a new skill"""
filters = {"user_id": skill.user_id}
return create_entity(skill, Skill, filters=filters)
@api.delete("/skills/delete")
async def delete_skill(skill_id: int, user_id: str):
"""Delete a skill"""
filters = {"id": skill_id, "user_id": user_id}
return delete_entity(Skill, filters=filters)
@api.get("/models")
async def list_models(user_id: str):
"""List all models for a user"""
filters = {"user_id": user_id}
return list_entity(Model, filters=filters)
@api.post("/models")
async def create_model(model: Model):
"""Create a new model"""
return create_entity(model, Model)
@api.post("/models/test")
async def test_model_endpoint(model: Model):
"""Test a model"""
try:
response = test_model(model)
return {
"status": True,
"message": "Model tested successfully",
"data": response,
}
except (OpenAIError, Exception) as ex_error:
return {
"status": False,
"message": "Error occurred while testing model: " + str(ex_error),
}
@api.delete("/models/delete")
async def delete_model(model_id: int, user_id: str):
"""Delete a model"""
filters = {"id": model_id, "user_id": user_id}
return delete_entity(Model, filters=filters)
@api.get("/agents")
async def list_agents(user_id: str):
"""List all agents for a user"""
filters = {"user_id": user_id}
return list_entity(Agent, filters=filters)
@api.post("/agents")
async def create_agent(agent: Agent):
"""Create a new agent"""
return create_entity(agent, Agent)
@api.delete("/agents/delete")
async def delete_agent(agent_id: int, user_id: str):
"""Delete an agent"""
filters = {"id": agent_id, "user_id": user_id}
return delete_entity(Agent, filters=filters)
@api.post("/agents/link/model/{agent_id}/{model_id}")
async def link_agent_model(agent_id: int, model_id: int):
"""Link a model to an agent"""
return dbmanager.link(link_type="agent_model", primary_id=agent_id, secondary_id=model_id)
@api.delete("/agents/link/model/{agent_id}/{model_id}")
async def unlink_agent_model(agent_id: int, model_id: int):
"""Unlink a model from an agent"""
return dbmanager.unlink(link_type="agent_model", primary_id=agent_id, secondary_id=model_id)
@api.get("/agents/link/model/{agent_id}")
async def get_agent_models(agent_id: int):
"""Get all models linked to an agent"""
return dbmanager.get_linked_entities("agent_model", agent_id, return_json=True)
@api.post("/agents/link/skill/{agent_id}/{skill_id}")
async def link_agent_skill(agent_id: int, skill_id: int):
"""Link an a skill to an agent"""
return dbmanager.link(link_type="agent_skill", primary_id=agent_id, secondary_id=skill_id)
@api.delete("/agents/link/skill/{agent_id}/{skill_id}")
async def unlink_agent_skill(agent_id: int, skill_id: int):
"""Unlink an a skill from an agent"""
return dbmanager.unlink(link_type="agent_skill", primary_id=agent_id, secondary_id=skill_id)
@api.get("/agents/link/skill/{agent_id}")
async def get_agent_skills(agent_id: int):
"""Get all skills linked to an agent"""
return dbmanager.get_linked_entities("agent_skill", agent_id, return_json=True)
@api.post("/agents/link/agent/{primary_agent_id}/{secondary_agent_id}")
async def link_agent_agent(primary_agent_id: int, secondary_agent_id: int):
"""Link an agent to another agent"""
return dbmanager.link(
link_type="agent_agent",
primary_id=primary_agent_id,
secondary_id=secondary_agent_id,
)
@api.delete("/agents/link/agent/{primary_agent_id}/{secondary_agent_id}")
async def unlink_agent_agent(primary_agent_id: int, secondary_agent_id: int):
"""Unlink an agent from another agent"""
return dbmanager.unlink(
link_type="agent_agent",
primary_id=primary_agent_id,
secondary_id=secondary_agent_id,
)
@api.get("/agents/link/agent/{agent_id}")
async def get_linked_agents(agent_id: int):
"""Get all agents linked to an agent"""
return dbmanager.get_linked_entities("agent_agent", agent_id, return_json=True)
@api.get("/workflows")
async def list_workflows(user_id: str):
"""List all workflows for a user"""
filters = {"user_id": user_id}
return list_entity(Workflow, filters=filters)
@api.get("/workflows/{workflow_id}")
async def get_workflow(workflow_id: int, user_id: str):
"""Get a workflow"""
filters = {"id": workflow_id, "user_id": user_id}
return list_entity(Workflow, filters=filters)
@api.get("/workflows/export/{workflow_id}")
async def export_workflow(workflow_id: int, user_id: str):
"""Export a user workflow"""
response = Response(message="Workflow exported successfully", status=True, data=None)
try:
workflow_details = workflow_from_id(workflow_id, dbmanager=dbmanager)
response.data = workflow_details
except Exception as ex_error:
response.message = "Error occurred while exporting workflow: " + str(ex_error)
response.status = False
return response.model_dump(mode="json")
@api.post("/workflows")
async def create_workflow(workflow: Workflow):
"""Create a new workflow"""
return create_entity(workflow, Workflow)
@api.delete("/workflows/delete")
async def delete_workflow(workflow_id: int, user_id: str):
"""Delete a workflow"""
filters = {"id": workflow_id, "user_id": user_id}
return delete_entity(Workflow, filters=filters)
@api.post("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}")
async def link_workflow_agent(workflow_id: int, agent_id: int, agent_type: str):
"""Link an agent to a workflow"""
return dbmanager.link(
link_type="workflow_agent",
primary_id=workflow_id,
secondary_id=agent_id,
agent_type=agent_type,
)
@api.post("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}/{sequence_id}")
async def link_workflow_agent_sequence(workflow_id: int, agent_id: int, agent_type: str, sequence_id: int):
"""Link an agent to a workflow"""
print("Sequence ID: ", sequence_id)
return dbmanager.link(
link_type="workflow_agent",
primary_id=workflow_id,
secondary_id=agent_id,
agent_type=agent_type,
sequence_id=sequence_id,
)
@api.delete("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}")
async def unlink_workflow_agent(workflow_id: int, agent_id: int, agent_type: str):
"""Unlink an agent from a workflow"""
return dbmanager.unlink(
link_type="workflow_agent",
primary_id=workflow_id,
secondary_id=agent_id,
agent_type=agent_type,
)
@api.delete("/workflows/link/agent/{workflow_id}/{agent_id}/{agent_type}/{sequence_id}")
async def unlink_workflow_agent_sequence(workflow_id: int, agent_id: int, agent_type: str, sequence_id: int):
"""Unlink an agent from a workflow sequence"""
return dbmanager.unlink(
link_type="workflow_agent",
primary_id=workflow_id,
secondary_id=agent_id,
agent_type=agent_type,
sequence_id=sequence_id,
)
@api.get("/workflows/link/agent/{workflow_id}")
async def get_linked_workflow_agents(workflow_id: int):
"""Get all agents linked to a workflow"""
return dbmanager.get_linked_entities(
link_type="workflow_agent",
primary_id=workflow_id,
return_json=True,
)
@api.get("/profiler/{message_id}")
async def profile_agent_task_run(message_id: int):
"""Profile an agent task run"""
try:
agent_message = dbmanager.get(Message, filters={"id": message_id}).data[0]
profile = profiler.profile(agent_message)
return {
"status": True,
"message": "Agent task run profiled successfully",
"data": profile,
}
except Exception as ex_error:
return {
"status": False,
"message": "Error occurred while profiling agent task run: " + str(ex_error),
}
@api.get("/sessions")
async def list_sessions(user_id: str):
"""List all sessions for a user"""
filters = {"user_id": user_id}
return list_entity(Session, filters=filters)
@api.post("/sessions")
async def create_session(session: Session):
"""Create a new session"""
return create_entity(session, Session)
@api.delete("/sessions/delete")
async def delete_session(session_id: int, user_id: str):
"""Delete a session"""
filters = {"id": session_id, "user_id": user_id}
return delete_entity(Session, filters=filters)
@api.get("/sessions/{session_id}/messages")
async def list_messages(user_id: str, session_id: int):
"""List all messages for a use session"""
filters = {"user_id": user_id, "session_id": session_id}
return list_entity(Message, filters=filters, order="asc", return_json=True)
@api.post("/sessions/{session_id}/workflow/{workflow_id}/run")
async def run_session_workflow(message: Message, session_id: int, workflow_id: int):
"""Runs a workflow on provided message"""
try:
user_message_history = (
dbmanager.get(
Message,
filters={"user_id": message.user_id, "session_id": message.session_id},
return_json=True,
).data
if session_id is not None
else []
)
# save incoming message
dbmanager.upsert(message)
user_dir = os.path.join(folders["files_static_root"], "user", sha256_hash(message.user_id))
os.makedirs(user_dir, exist_ok=True)
workflow = workflow_from_id(workflow_id, dbmanager=dbmanager)
agent_response: Message = await managers["chat"].a_chat(
message=message,
history=user_message_history,
user_dir=user_dir,
workflow=workflow,
connection_id=message.connection_id,
)
response: Response = dbmanager.upsert(agent_response)
return response.model_dump(mode="json")
except Exception as ex_error:
return {
"status": False,
"message": "Error occurred while processing message: " + str(ex_error),
}
# Version endpoint
@api.get("/version")
async def get_version():
"""Get API version"""
return {
"status": True,
"message": "Version retrieved successfully",
"data": {"version": VERSION},
}
# websockets
# Health check endpoint
async def process_socket_message(data: dict, websocket: WebSocket, client_id: str):
print(f"Client says: {data['type']}")
if data["type"] == "user_message":
user_message = Message(**data["data"])
session_id = data["data"].get("session_id", None)
workflow_id = data["data"].get("workflow_id", None)
response = await run_session_workflow(message=user_message, session_id=session_id, workflow_id=workflow_id)
response_socket_message = {
"type": "agent_response",
"data": response,
"connection_id": client_id,
}
await websocket_manager.send_message(response_socket_message, websocket)
@api.get("/health")
async def health_check():
"""API health check endpoint"""
return {
"status": True,
"message": "Service is healthy",
}
# Mount static file directories
app.mount("/api", api)
app.mount(
"/files",
StaticFiles(directory=initializer.static_root, html=True),
name="files",
)
app.mount("/", StaticFiles(directory=initializer.ui_root, html=True), name="ui")
# Error handlers
@api.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: str):
await websocket_manager.connect(websocket, client_id)
try:
while True:
data = await websocket.receive_json()
await process_socket_message(data, websocket, client_id)
except WebSocketDisconnect:
print(f"Client #{client_id} is disconnected")
await websocket_manager.disconnect(websocket)
@app.exception_handler(500)
async def internal_error_handler(request, exc):
logger.error(f"Internal error: {str(exc)}")
return {
"status": False,
"message": "Internal server error",
"detail": str(exc) if settings.API_DOCS else "Internal server error"
}
def create_app() -> FastAPI:
"""
Factory function to create and configure the FastAPI application.
Useful for testing and different deployment scenarios.
"""
return app

View File

@ -0,0 +1,18 @@
# api/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URI: str = "sqlite:///./autogen.db"
API_DOCS: bool = False
CLEANUP_INTERVAL: int = 300 # 5 minutes
SESSION_TIMEOUT: int = 3600 # 1 hour
CONFIG_DIR: str = "configs" # Default config directory relative to app_root
DEFAULT_USER_ID: str = "guestuser@gmail.com"
UPGRADE_DATABASE: bool = False
class Config:
env_prefix = "AUTOGENSTUDIO_"
settings = Settings()

View File

@ -0,0 +1,201 @@
# api/deps.py
from typing import Optional
from fastapi import Depends, HTTPException, status
import logging
from contextlib import contextmanager
from ..database import DatabaseManager
from .managers.connection import WebSocketManager
from ..teammanager import TeamManager
from .config import settings
from ..database import ConfigurationManager
logger = logging.getLogger(__name__)
# Global manager instances
_db_manager: Optional[DatabaseManager] = None
_websocket_manager: Optional[WebSocketManager] = None
_team_manager: Optional[TeamManager] = None
# Context manager for database sessions
@contextmanager
def get_db_context():
"""Provide a transactional scope around a series of operations."""
if not _db_manager:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database manager not initialized"
)
try:
yield _db_manager
except Exception as e:
logger.error(f"Database operation failed: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database operation failed"
)
# Dependency providers
async def get_db() -> DatabaseManager:
"""Dependency provider for database manager"""
if not _db_manager:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database manager not initialized"
)
return _db_manager
async def get_websocket_manager() -> WebSocketManager:
"""Dependency provider for connection manager"""
if not _websocket_manager:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Connection manager not initialized"
)
return _websocket_manager
async def get_team_manager() -> TeamManager:
"""Dependency provider for team manager"""
if not _team_manager:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Team manager not initialized"
)
return _team_manager
# Authentication dependency
async def get_current_user(
# Add your authentication logic here
# For example: token: str = Depends(oauth2_scheme)
) -> str:
"""
Dependency for getting the current authenticated user.
Replace with your actual authentication logic.
"""
# Implement your user authentication here
return "user_id" # Replace with actual user identification
# Manager initialization and cleanup
async def init_managers(database_uri: str, config_dir: str) -> None:
"""Initialize all manager instances"""
global _db_manager, _websocket_manager, _team_manager
logger.info("Initializing managers...")
try:
# Initialize database manager
_db_manager = DatabaseManager(
engine_uri=database_uri, auto_upgrade=settings.UPGRADE_DATABASE)
_db_manager.create_db_and_tables()
# init default team config
_team_config_manager = ConfigurationManager(db_manager=_db_manager)
import_result = await _team_config_manager.import_directory(
config_dir, settings.DEFAULT_USER_ID, check_exists=True)
# Initialize connection manager
_websocket_manager = WebSocketManager(
db_manager=_db_manager
)
logger.info("Connection manager initialized")
# Initialize team manager
_team_manager = TeamManager()
logger.info("Team manager initialized")
except Exception as e:
logger.error(f"Failed to initialize managers: {str(e)}")
await cleanup_managers() # Cleanup any partially initialized managers
raise
async def cleanup_managers() -> None:
"""Cleanup and shutdown all manager instances"""
global _db_manager, _websocket_manager, _team_manager
logger.info("Cleaning up managers...")
# Cleanup connection manager first to ensure all active connections are closed
if _websocket_manager:
try:
await _websocket_manager.cleanup()
except Exception as e:
logger.error(f"Error cleaning up connection manager: {str(e)}")
finally:
_websocket_manager = None
# TeamManager doesn't need explicit cleanup since WebSocketManager handles it
_team_manager = None
# Cleanup database manager last
if _db_manager:
try:
await _db_manager.close()
except Exception as e:
logger.error(f"Error cleaning up database manager: {str(e)}")
finally:
_db_manager = None
logger.info("All managers cleaned up")
# Utility functions for dependency management
def get_manager_status() -> dict:
"""Get the initialization status of all managers"""
return {
"database_manager": _db_manager is not None,
"websocket_manager": _websocket_manager is not None,
"team_manager": _team_manager is not None
}
# Combined dependencies
async def get_managers():
"""Get all managers in one dependency"""
return {
"db": await get_db(),
"connection": await get_websocket_manager(),
"team": await get_team_manager()
}
# Error handling for manager operations
class ManagerOperationError(Exception):
"""Custom exception for manager operation errors"""
def __init__(self, manager_name: str, operation: str, detail: str):
self.manager_name = manager_name
self.operation = operation
self.detail = detail
super().__init__(f"{manager_name} failed during {operation}: {detail}")
# Dependency for requiring specific managers
def require_managers(*manager_names: str):
"""Decorator to require specific managers for a route"""
async def dependency():
status = get_manager_status()
missing = [name for name in manager_names if not status.get(
f"{name}_manager")]
if missing:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Required managers not available: {', '.join(missing)}"
)
return True
return Depends(dependency)

View File

@ -0,0 +1,110 @@
# api/initialization.py
import os
from pathlib import Path
from typing import Dict
from pydantic import BaseModel
from loguru import logger
from dotenv import load_dotenv
from .config import Settings
class _AppPaths(BaseModel):
"""Internal model representing all application paths"""
app_root: Path
static_root: Path
user_files: Path
ui_root: Path
config_dir: Path
database_uri: str
class AppInitializer:
"""Handles application initialization including paths and environment setup"""
def __init__(self, settings: Settings, app_path: str):
"""
Initialize the application structure.
Args:
settings: Application settings
app_path: Path to the application code directory
"""
self.settings = settings
self._app_path = Path(app_path)
self._paths = self._init_paths()
self._create_directories()
self._load_environment()
logger.info(f"Initialized application data folder: {self.app_root}")
def _get_app_root(self) -> Path:
"""Determine application root directory"""
if app_dir := os.getenv("AUTOGENSTUDIO_APPDIR"):
return Path(app_dir)
return Path.home() / ".autogenstudio"
def _get_database_uri(self, app_root: Path) -> str:
"""Generate database URI based on settings or environment"""
if db_uri := os.getenv("AUTOGENSTUDIO_DATABASE_URI"):
return db_uri
return self.settings.DATABASE_URI.replace(
"./", str(app_root) + "/"
)
def _init_paths(self) -> _AppPaths:
"""Initialize and return AppPaths instance"""
app_root = self._get_app_root()
return _AppPaths(
app_root=app_root,
static_root=app_root / "files",
user_files=app_root / "files" / "user",
ui_root=self._app_path / "ui",
config_dir=app_root / self.settings.CONFIG_DIR,
database_uri=self._get_database_uri(app_root)
)
def _create_directories(self) -> None:
"""Create all required directories"""
self.app_root.mkdir(parents=True, exist_ok=True)
dirs = [self.static_root, self.user_files,
self.ui_root, self.config_dir]
for path in dirs:
path.mkdir(parents=True, exist_ok=True)
def _load_environment(self) -> None:
"""Load environment variables from .env file if it exists"""
env_file = self.app_root / ".env"
if env_file.exists():
logger.info(f"Loading environment variables from {env_file}")
load_dotenv(str(env_file))
# Properties for accessing paths
@property
def app_root(self) -> Path:
"""Root directory for the application"""
return self._paths.app_root
@property
def static_root(self) -> Path:
"""Directory for static files"""
return self._paths.static_root
@property
def user_files(self) -> Path:
"""Directory for user files"""
return self._paths.user_files
@property
def ui_root(self) -> Path:
"""Directory for UI files"""
return self._paths.ui_root
@property
def config_dir(self) -> Path:
"""Directory for configuration files"""
return self._paths.config_dir
@property
def database_uri(self) -> str:
"""Database connection URI"""
return self._paths.database_uri

View File

@ -0,0 +1,247 @@
# managers/connection.py
from fastapi import WebSocket, WebSocketDisconnect
from typing import Dict, Optional, Any
from uuid import UUID
import logging
from datetime import datetime, timezone
from ...datamodel import Run, RunStatus, TeamResult
from ...database import DatabaseManager
from autogen_agentchat.messages import InnerMessage, ChatMessage
from autogen_core.base import CancellationToken
logger = logging.getLogger(__name__)
class WebSocketManager:
"""Manages WebSocket connections and message streaming for team task execution"""
def __init__(self, db_manager: DatabaseManager):
self.db_manager = db_manager
self._connections: Dict[UUID, WebSocket] = {}
self._cancellation_tokens: Dict[UUID, CancellationToken] = {}
async def connect(self, websocket: WebSocket, run_id: UUID) -> bool:
"""Initialize WebSocket connection for a run
Args:
websocket: The WebSocket connection to initialize
run_id: UUID of the run to associate with this connection
Returns:
bool: True if connection was successful, False otherwise
"""
try:
await websocket.accept()
self._connections[run_id] = websocket
run = await self._get_run(run_id)
if run:
run.status = RunStatus.ACTIVE
self.db_manager.upsert(run)
await self._send_message(run_id, {
"type": "system",
"status": "connected",
"timestamp": datetime.now(timezone.utc).isoformat()
})
return True
except Exception as e:
logger.error(f"Connection error for run {run_id}: {e}")
return False
async def start_stream(
self,
run_id: UUID,
team_manager: Any,
task: str,
team_config: dict
) -> None:
"""Start streaming task execution
Args:
run_id: UUID of the run
team_manager: Instance of the team manager
task: Task string to execute
team_config: Team configuration dictionary
"""
if run_id not in self._connections:
raise ValueError(f"No active connection for run {run_id}")
cancellation_token = CancellationToken()
self._cancellation_tokens[run_id] = cancellation_token
try:
async for message in team_manager.run_stream(
task=task,
team_config=team_config,
cancellation_token=cancellation_token
):
if cancellation_token.is_cancelled():
logger.info(f"Stream cancelled for run {run_id}")
break
formatted_message = self._format_message(message)
if formatted_message:
await self._send_message(run_id, formatted_message)
# Only send completion if not cancelled
if not cancellation_token.is_cancelled():
# await self._send_message(run_id, {
# "type": "completion",
# "status": "complete",
# "timestamp": datetime.now(timezone.utc).isoformat()
# })
await self._update_run_status(run_id, RunStatus.COMPLETE)
else:
await self._send_message(run_id, {
"type": "completion",
"status": "cancelled",
"timestamp": datetime.now(timezone.utc).isoformat()
})
await self._update_run_status(run_id, RunStatus.STOPPED)
except Exception as e:
logger.error(f"Stream error for run {run_id}: {e}")
await self._handle_stream_error(run_id, e)
finally:
self._cancellation_tokens.pop(run_id, None)
async def stop_run(self, run_id: UUID) -> None:
"""Stop a running task"""
if run_id in self._cancellation_tokens:
logger.info(f"Stopping run {run_id}")
self._cancellation_tokens[run_id].cancel()
# Send final message if connection still exists
if run_id in self._connections:
try:
await self._send_message(run_id, {
"type": "completion",
"status": "cancelled",
"timestamp": datetime.now(timezone.utc).isoformat()
})
except Exception:
pass
async def disconnect(self, run_id: UUID) -> None:
"""Clean up connection and associated resources"""
logger.info(f"Disconnecting run {run_id}")
# First cancel any running tasks
await self.stop_run(run_id)
# Then clean up resources without trying to close the socket again
if run_id in self._connections:
self._connections.pop(run_id, None)
self._cancellation_tokens.pop(run_id, None)
async def _send_message(self, run_id: UUID, message: dict) -> None:
"""Send a message through the WebSocket
Args:
run_id: UUID of the run
message: Message dictionary to send
"""
try:
if run_id in self._connections:
await self._connections[run_id].send_json(message)
except WebSocketDisconnect:
logger.warning(
f"WebSocket disconnected while sending message for run {run_id}")
await self.disconnect(run_id)
except Exception as e:
logger.error(f"Error sending message for run {run_id}: {e}")
await self._handle_stream_error(run_id, e)
async def _handle_stream_error(self, run_id: UUID, error: Exception) -> None:
"""Handle stream errors consistently
Args:
run_id: UUID of the run
error: Exception that occurred
"""
try:
await self._send_message(run_id, {
"type": "completion",
"status": "error",
"error": str(error),
"timestamp": datetime.now(timezone.utc).isoformat()
})
except Exception as send_error:
logger.error(
f"Failed to send error message for run {run_id}: {send_error}")
await self._update_run_status(run_id, RunStatus.ERROR, str(error))
def _format_message(self, message: Any) -> Optional[dict]:
"""Format message for WebSocket transmission
Args:
message: Message to format
Returns:
Optional[dict]: Formatted message or None if formatting fails
"""
try:
if isinstance(message, (InnerMessage, ChatMessage)):
return {
"type": "message",
"data": message.model_dump()
}
elif isinstance(message, TeamResult):
return {
"type": "result",
"data": message.model_dump(),
"status": "complete",
}
return None
except Exception as e:
logger.error(f"Message formatting error: {e}")
return None
async def _get_run(self, run_id: UUID) -> Optional[Run]:
"""Get run from database
Args:
run_id: UUID of the run to retrieve
Returns:
Optional[Run]: Run object if found, None otherwise
"""
response = self.db_manager.get(
Run, filters={"id": run_id}, return_json=False)
return response.data[0] if response.status and response.data else None
async def _update_run_status(
self,
run_id: UUID,
status: RunStatus,
error: Optional[str] = None
) -> None:
"""Update run status in database
Args:
run_id: UUID of the run to update
status: New status to set
error: Optional error message
"""
run = await self._get_run(run_id)
if run:
run.status = status
run.error_message = error
self.db_manager.upsert(run)
@property
def active_connections(self) -> set[UUID]:
"""Get set of active run IDs"""
return set(self._connections.keys())
@property
def active_runs(self) -> set[UUID]:
"""Get set of runs with active cancellation tokens"""
return set(self._cancellation_tokens.keys())

View File

@ -0,0 +1,181 @@
# api/routes/agents.py
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict
from ..deps import get_db
from ...datamodel import Agent, Model, Tool
router = APIRouter()
@router.get("/")
async def list_agents(
user_id: str,
db=Depends(get_db)
) -> Dict:
"""List all agents for a user"""
response = db.get(Agent, filters={"user_id": user_id})
return {
"status": True,
"data": response.data
}
@router.get("/{agent_id}")
async def get_agent(
agent_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Get a specific agent"""
response = db.get(
Agent,
filters={"id": agent_id, "user_id": user_id}
)
if not response.status or not response.data:
raise HTTPException(status_code=404, detail="Agent not found")
return {
"status": True,
"data": response.data[0]
}
@router.post("/")
async def create_agent(
agent: Agent,
db=Depends(get_db)
) -> Dict:
"""Create a new agent"""
response = db.upsert(agent)
if not response.status:
raise HTTPException(status_code=400, detail=response.message)
return {
"status": True,
"data": response.data
}
@router.delete("/{agent_id}")
async def delete_agent(
agent_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Delete an agent"""
response = db.delete(
filters={"id": agent_id, "user_id": user_id},
model_class=Agent
)
return {
"status": True,
"message": "Agent deleted successfully"
}
# Agent-Model link endpoints
@router.post("/{agent_id}/models/{model_id}")
async def link_agent_model(
agent_id: int,
model_id: int,
db=Depends(get_db)
) -> Dict:
"""Link a model to an agent"""
response = db.link(
link_type="agent_model",
primary_id=agent_id,
secondary_id=model_id
)
return {
"status": True,
"message": "Model linked to agent successfully"
}
@router.delete("/{agent_id}/models/{model_id}")
async def unlink_agent_model(
agent_id: int,
model_id: int,
db=Depends(get_db)
) -> Dict:
"""Unlink a model from an agent"""
response = db.unlink(
link_type="agent_model",
primary_id=agent_id,
secondary_id=model_id
)
return {
"status": True,
"message": "Model unlinked from agent successfully"
}
@router.get("/{agent_id}/models")
async def get_agent_models(
agent_id: int,
db=Depends(get_db)
) -> Dict:
"""Get all models linked to an agent"""
response = db.get_linked_entities(
link_type="agent_model",
primary_id=agent_id,
return_json=True
)
return {
"status": True,
"data": response.data
}
# Agent-Tool link endpoints
@router.post("/{agent_id}/tools/{tool_id}")
async def link_agent_tool(
agent_id: int,
tool_id: int,
db=Depends(get_db)
) -> Dict:
"""Link a tool to an agent"""
response = db.link(
link_type="agent_tool",
primary_id=agent_id,
secondary_id=tool_id
)
return {
"status": True,
"message": "Tool linked to agent successfully"
}
@router.delete("/{agent_id}/tools/{tool_id}")
async def unlink_agent_tool(
agent_id: int,
tool_id: int,
db=Depends(get_db)
) -> Dict:
"""Unlink a tool from an agent"""
response = db.unlink(
link_type="agent_tool",
primary_id=agent_id,
secondary_id=tool_id
)
return {
"status": True,
"message": "Tool unlinked from agent successfully"
}
@router.get("/{agent_id}/tools")
async def get_agent_tools(
agent_id: int,
db=Depends(get_db)
) -> Dict:
"""Get all tools linked to an agent"""
response = db.get_linked_entities(
link_type="agent_tool",
primary_id=agent_id,
return_json=True
)
return {
"status": True,
"data": response.data
}

View File

@ -0,0 +1,95 @@
# api/routes/models.py
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict
from openai import OpenAIError
from ..deps import get_db
from ...datamodel import Model
from ...utils import test_model
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"""
response = db.delete(
filters={"id": model_id, "user_id": user_id},
model_class=Model
)
return {
"status": True,
"message": "Model deleted successfully"
}
@router.post("/test")
async def test_model_endpoint(model: Model) -> Dict:
"""Test a model configuration"""
try:
response = test_model(model)
return {
"status": True,
"message": "Model tested successfully",
"data": response
}
except OpenAIError as e:
raise HTTPException(
status_code=400,
detail=f"OpenAI API error: {str(e)}"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error testing model: {str(e)}"
)

View File

@ -0,0 +1,76 @@
# /api/runs routes
from fastapi import APIRouter, Body, Depends, HTTPException
from uuid import UUID
from typing import Dict
from pydantic import BaseModel
from ..deps import get_db, get_websocket_manager, get_team_manager
from ...datamodel import Run, Session, Message, Team, RunStatus, MessageConfig
from ...teammanager import TeamManager
from autogen_core.base import CancellationToken
router = APIRouter()
class CreateRunRequest(BaseModel):
session_id: int
user_id: str
@router.post("/")
async def create_run(
request: CreateRunRequest,
db=Depends(get_db),
) -> Dict:
"""Create a new run"""
session_response = db.get(
Session,
filters={"id": request.session_id, "user_id": request.user_id},
return_json=False
)
if not session_response.status or not session_response.data:
raise HTTPException(status_code=404, detail="Session not found")
try:
run = db.upsert(Run(session_id=request.session_id), return_json=False)
return {
"status": run.status,
"data": {"run_id": str(run.data.id)}
}
# }
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{run_id}/start")
async def start_run(
run_id: UUID,
message: Message = Body(...),
ws_manager=Depends(get_websocket_manager),
team_manager=Depends(get_team_manager),
db=Depends(get_db),
) -> Dict:
"""Start streaming task execution"""
if isinstance(message.config, dict):
message.config = MessageConfig(**message.config)
session = db.get(Session, filters={
"id": message.session_id}, return_json=False)
team = db.get(
Team, filters={"id": session.data[0].team_id}, return_json=False)
try:
await ws_manager.start_stream(run_id, team_manager, message.config.content, team.data[0].config)
return {
"status": True,
"message": "Stream started successfully",
"data": {"run_id": str(run_id)}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -0,0 +1,114 @@
# api/routes/sessions.py
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict
from ..deps import get_db
from ...datamodel import Session, Message
router = APIRouter()
@router.get("/")
async def list_sessions(
user_id: str,
db=Depends(get_db)
) -> Dict:
"""List all sessions for a user"""
response = db.get(Session, filters={"user_id": user_id})
return {
"status": True,
"data": response.data
}
@router.get("/{session_id}")
async def get_session(
session_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Get a specific session"""
response = db.get(
Session,
filters={"id": session_id, "user_id": user_id}
)
if not response.status or not response.data:
raise HTTPException(status_code=404, detail="Session not found")
return {
"status": True,
"data": response.data[0]
}
@router.post("/")
async def create_session(
session: Session,
db=Depends(get_db)
) -> Dict:
"""Create a new session"""
response = db.upsert(session)
if not response.status:
raise HTTPException(status_code=400, detail=response.message)
return {
"status": True,
"data": response.data
}
@router.put("/{session_id}")
async def update_session(
session_id: int,
user_id: str,
session: Session,
db=Depends(get_db)
) -> Dict:
"""Update an existing session"""
# First verify the session belongs to user
existing = db.get(
Session,
filters={"id": session_id, "user_id": user_id}
)
if not existing.status or not existing.data:
raise HTTPException(status_code=404, detail="Session not found")
# Update the session
response = db.upsert(session)
if not response.status:
raise HTTPException(status_code=400, detail=response.message)
return {
"status": True,
"data": response.data,
"message": "Session updated successfully"
}
@router.delete("/{session_id}")
async def delete_session(
session_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Delete a session"""
response = db.delete(
filters={"id": session_id, "user_id": user_id},
model_class=Session
)
return {
"status": True,
"message": "Session deleted successfully"
}
@router.get("/{session_id}/messages")
async def list_messages(
session_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""List all messages for a session"""
filters = {"session_id": session_id, "user_id": user_id}
response = db.get(Message, filters=filters, order="asc")
return {
"status": True,
"data": response.data
}

View File

@ -0,0 +1,146 @@
# api/routes/teams.py
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict
from ..deps import get_db
from ...datamodel import Team
router = APIRouter()
@router.get("/")
async def list_teams(
user_id: str,
db=Depends(get_db)
) -> Dict:
"""List all teams for a user"""
response = db.get(Team, filters={"user_id": user_id})
return {
"status": True,
"data": response.data
}
@router.get("/{team_id}")
async def get_team(
team_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Get a specific team"""
response = db.get(
Team,
filters={"id": team_id, "user_id": user_id}
)
if not response.status or not response.data:
raise HTTPException(status_code=404, detail="Team not found")
return {
"status": True,
"data": response.data[0]
}
@router.post("/")
async def create_team(
team: Team,
db=Depends(get_db)
) -> Dict:
"""Create a new team"""
response = db.upsert(team)
if not response.status:
raise HTTPException(status_code=400, detail=response.message)
return {
"status": True,
"data": response.data
}
@router.delete("/{team_id}")
async def delete_team(
team_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Delete a team"""
response = db.delete(
filters={"id": team_id, "user_id": user_id},
model_class=Team
)
return {
"status": True,
"message": "Team deleted successfully"
}
# Team-Agent link endpoints
@router.post("/{team_id}/agents/{agent_id}")
async def link_team_agent(
team_id: int,
agent_id: int,
db=Depends(get_db)
) -> Dict:
"""Link an agent to a team"""
response = db.link(
link_type="team_agent",
primary_id=team_id,
secondary_id=agent_id
)
return {
"status": True,
"message": "Agent linked to team successfully"
}
@router.post("/{team_id}/agents/{agent_id}/{sequence_id}")
async def link_team_agent_sequence(
team_id: int,
agent_id: int,
sequence_id: int,
db=Depends(get_db)
) -> Dict:
"""Link an agent to a team with sequence"""
response = db.link(
link_type="team_agent",
primary_id=team_id,
secondary_id=agent_id,
sequence_id=sequence_id
)
return {
"status": True,
"message": "Agent linked to team with sequence successfully"
}
@router.delete("/{team_id}/agents/{agent_id}")
async def unlink_team_agent(
team_id: int,
agent_id: int,
db=Depends(get_db)
) -> Dict:
"""Unlink an agent from a team"""
response = db.unlink(
link_type="team_agent",
primary_id=team_id,
secondary_id=agent_id
)
return {
"status": True,
"message": "Agent unlinked from team successfully"
}
@router.get("/{team_id}/agents")
async def get_team_agents(
team_id: int,
db=Depends(get_db)
) -> Dict:
"""Get all agents linked to a team"""
response = db.get_linked_entities(
link_type="team_agent",
primary_id=team_id,
return_json=True
)
return {
"status": True,
"data": response.data
}

View File

@ -0,0 +1,103 @@
# api/routes/tools.py
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict
from ..deps import get_db
from ...datamodel import Tool
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"""
response = db.delete(
filters={"id": tool_id, "user_id": user_id},
model_class=Tool
)
return {
"status": True,
"message": "Tool deleted successfully"
}
@router.post("/{tool_id}/test")
async def test_tool(
tool_id: int,
user_id: str,
db=Depends(get_db)
) -> Dict:
"""Test a tool configuration"""
# Get tool
tool_response = db.get(
Tool,
filters={"id": tool_id, "user_id": user_id}
)
if not tool_response.status or not tool_response.data:
raise HTTPException(status_code=404, detail="Tool not found")
tool = tool_response.data[0]
try:
# Implement tool testing logic here
# This would depend on the tool type and configuration
return {
"status": True,
"message": "Tool tested successfully",
"data": {"tool_id": tool_id}
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error testing tool: {str(e)}"
)

View File

@ -0,0 +1,74 @@
# api/ws.py
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException
from typing import Dict
from uuid import UUID
import logging
import json
from datetime import datetime
from ..deps import get_websocket_manager, get_db, get_team_manager
from ...datamodel import Run, RunStatus
router = APIRouter()
logger = logging.getLogger(__name__)
@router.websocket("/runs/{run_id}")
async def run_websocket(
websocket: WebSocket,
run_id: UUID,
ws_manager=Depends(get_websocket_manager),
db=Depends(get_db),
team_manager=Depends(get_team_manager)
):
"""WebSocket endpoint for run communication"""
# Verify run exists and is in valid state
run_response = db.get(Run, filters={"id": run_id}, return_json=False)
if not run_response.status or not run_response.data:
await websocket.close(code=4004, reason="Run not found")
return
run = run_response.data[0]
if run.status not in [RunStatus.CREATED, RunStatus.ACTIVE]:
await websocket.close(code=4003, reason="Run not in valid state")
return
# Connect websocket
connected = await ws_manager.connect(websocket, run_id)
if not connected:
await websocket.close(code=4002, reason="Failed to establish connection")
return
try:
logger.info(f"WebSocket connection established for run {run_id}")
while True:
try:
raw_message = await websocket.receive_text()
message = json.loads(raw_message)
if message.get("type") == "stop":
logger.info(f"Received stop request for run {run_id}")
await ws_manager.stop_run(run_id)
break
elif message.get("type") == "ping":
await websocket.send_json({
"type": "pong",
"timestamp": datetime.utcnow().isoformat()
})
except json.JSONDecodeError:
logger.warning(f"Invalid JSON received: {raw_message}")
await websocket.send_json({
"type": "error",
"error": "Invalid message format",
"timestamp": datetime.utcnow().isoformat()
})
except WebSocketDisconnect:
logger.info(f"WebSocket disconnected for run {run_id}")
except Exception as e:
logger.error(f"WebSocket error: {str(e)}")
finally:
await ws_manager.disconnect(run_id)

View File

@ -1,135 +0,0 @@
import asyncio
from typing import Any, Dict, List, Optional, Tuple, Union
import websockets
from fastapi import WebSocket, WebSocketDisconnect
class WebSocketConnectionManager:
"""
Manages WebSocket connections including sending, broadcasting, and managing the lifecycle of connections.
"""
def __init__(
self,
active_connections: List[Tuple[WebSocket, str]] = None,
active_connections_lock: asyncio.Lock = None,
) -> None:
"""
Initializes WebSocketConnectionManager with an optional list of active WebSocket connections.
:param active_connections: A list of tuples, each containing a WebSocket object and its corresponding client_id.
"""
if active_connections is None:
active_connections = []
self.active_connections_lock = active_connections_lock
self.active_connections: List[Tuple[WebSocket, str]] = active_connections
async def connect(self, websocket: WebSocket, client_id: str) -> None:
"""
Accepts a new WebSocket connection and appends it to the active connections list.
:param websocket: The WebSocket instance representing a client connection.
:param client_id: A string representing the unique identifier of the client.
"""
await websocket.accept()
async with self.active_connections_lock:
self.active_connections.append((websocket, client_id))
print(f"New Connection: {client_id}, Total: {len(self.active_connections)}")
async def disconnect(self, websocket: WebSocket) -> None:
"""
Disconnects and removes a WebSocket connection from the active connections list.
:param websocket: The WebSocket instance to remove.
"""
async with self.active_connections_lock:
try:
self.active_connections = [conn for conn in self.active_connections if conn[0] != websocket]
print(f"Connection Closed. Total: {len(self.active_connections)}")
except ValueError:
print("Error: WebSocket connection not found")
async def disconnect_all(self) -> None:
"""
Disconnects all active WebSocket connections.
"""
for connection, _ in self.active_connections[:]:
await self.disconnect(connection)
async def send_message(self, message: Union[Dict, str], websocket: WebSocket) -> None:
"""
Sends a JSON message to a single WebSocket connection.
:param message: A JSON serializable dictionary containing the message to send.
:param websocket: The WebSocket instance through which to send the message.
"""
try:
async with self.active_connections_lock:
await websocket.send_json(message)
except WebSocketDisconnect:
print("Error: Tried to send a message to a closed WebSocket")
await self.disconnect(websocket)
except websockets.exceptions.ConnectionClosedOK:
print("Error: WebSocket connection closed normally")
await self.disconnect(websocket)
except Exception as e:
print(f"Error in sending message: {str(e)}", message)
await self.disconnect(websocket)
async def get_input(self, prompt: Union[Dict, str], websocket: WebSocket, timeout: int = 60) -> str:
"""
Sends a JSON message to a single WebSocket connection as a prompt for user input.
Waits on a user response or until the given timeout elapses.
:param prompt: A JSON serializable dictionary containing the message to send.
:param websocket: The WebSocket instance through which to send the message.
"""
response = "Error: Unexpected response.\nTERMINATE"
try:
async with self.active_connections_lock:
await websocket.send_json(prompt)
result = await asyncio.wait_for(websocket.receive_json(), timeout=timeout)
data = result.get("data")
if data:
response = data.get("content", "Error: Unexpected response format\nTERMINATE")
else:
response = "Error: Unexpected response format\nTERMINATE"
except asyncio.TimeoutError:
response = f"The user was timed out after {timeout} seconds of inactivity.\nTERMINATE"
except WebSocketDisconnect:
print("Error: Tried to send a message to a closed WebSocket")
await self.disconnect(websocket)
response = "The user was disconnected\nTERMINATE"
except websockets.exceptions.ConnectionClosedOK:
print("Error: WebSocket connection closed normally")
await self.disconnect(websocket)
response = "The user was disconnected\nTERMINATE"
except Exception as e:
print(f"Error in sending message: {str(e)}", prompt)
await self.disconnect(websocket)
response = f"Error: {e}\nTERMINATE"
return response
async def broadcast(self, message: Dict) -> None:
"""
Broadcasts a JSON message to all active WebSocket connections.
:param message: A JSON serializable dictionary containing the message to broadcast.
"""
# Create a message dictionary with the desired format
message_dict = {"message": message}
for connection, _ in self.active_connections[:]:
try:
if connection.client_state == websockets.protocol.State.OPEN:
# Call send_message method with the message dictionary and current WebSocket connection
await self.send_message(message_dict, connection)
else:
print("Error: WebSocket connection is closed")
await self.disconnect(connection)
except (WebSocketDisconnect, websockets.exceptions.ConnectionClosedOK) as e:
print(f"Error: WebSocket disconnected or closed({str(e)})")
await self.disconnect(connection)

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
GATSBY_API_URL=http://127.0.0.1:8081/api
GATSBY_API_URL=http://127.0.0.1:8081/api

View File

@ -1,8 +1,6 @@
node_modules/
.cache/
public/
public
src/gatsby-types.d.ts
.env.development
.env.production
yarn.lock
.env.production

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Victor Dibia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -2,8 +2,8 @@
Run the UI in dev mode (make changes and see them reflected in the browser with hotreloading):
- npm install
- npm run start
- yarn install
- yarn start
This should start the server on port 8000.

View File

@ -14,22 +14,20 @@ require("dotenv").config({
});
const config: GatsbyConfig = {
pathPrefix: process.env.PREFIX_PATH_VALUE || '',
pathPrefix: process.env.PREFIX_PATH_VALUE || "",
siteMetadata: {
title: `AutoGen Studio [Beta]`,
description: `Build Multi-Agent Apps`,
siteUrl: `http://tbd.place`,
},
flags: {
LAZY_IMAGES: true,
FAST_DEV: true,
DEV_SSR: false,
},
// More easily incorporate content into your pages through automatic TypeScript type generation and better GraphQL IntelliSense.
// If you use VSCode you can also use the GraphQL plugin
// Learn more at: https://gatsby.dev/graphql-typegen
graphqlTypegen: true,
plugins: [
"gatsby-plugin-sass",
"gatsby-plugin-postcss",
"gatsby-plugin-image",
"gatsby-plugin-sitemap",
"gatsby-plugin-postcss",
{
resolve: "gatsby-plugin-manifest",
options: {

View File

@ -1,9 +1,9 @@
{
"name": "AutoGen_Studio",
"name": "autogentstudio",
"version": "1.0.0",
"private": true,
"description": "AutoGen Studio - Build LLM Enabled Agents",
"author": "SPIRAL Team",
"author": "Microsoft",
"keywords": [
"gatsby"
],
@ -17,55 +17,41 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ant-design/charts": "^1.3.6",
"@ant-design/charts": "^2.2.1",
"@ant-design/plots": "^2.2.2",
"@headlessui/react": "^1.7.16",
"@heroicons/react": "^2.0.18",
"@mdx-js/mdx": "^1.6.22",
"@mdx-js/react": "^1.6.22",
"@mdx-js/react": "^3.1.0",
"@monaco-editor/react": "^4.6.0",
"@tailwindcss/line-clamp": "^0.4.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash.debounce": "^4.0.9",
"@types/react-syntax-highlighter": "^15.5.10",
"antd": "^5.1.0",
"autoprefixer": "^10.4.7",
"gatsby": "^4.14.0",
"gatsby-plugin-image": "^2.14.1",
"gatsby-plugin-manifest": "^4.14.0",
"gatsby-plugin-mdx": "^3.14.0",
"gatsby-plugin-postcss": "^5.14.0",
"gatsby-plugin-sass": "^5.14.0",
"gatsby-plugin-sharp": "^4.14.1",
"gatsby-plugin-sitemap": "^5.14.0",
"gatsby-source-filesystem": "^4.14.0",
"gatsby-transformer-sharp": "^4.14.0",
"jszip": "^3.10.1",
"lodash.debounce": "^4.0.8",
"papaparse": "^5.4.1",
"postcss": "^8.4.13",
"autoprefixer": "^10.4.20",
"gatsby": "^5.13.7",
"gatsby-plugin-image": "^3.13.1",
"gatsby-plugin-manifest": "^5.13.1",
"gatsby-plugin-mdx": "^5.13.1",
"gatsby-plugin-postcss": "^6.13.1",
"gatsby-plugin-sharp": "^5.13.1",
"gatsby-plugin-sitemap": "^6.13.1",
"gatsby-source-filesystem": "^5.13.1",
"gatsby-transformer-sharp": "^5.13.1",
"install": "^0.13.0",
"lucide-react": "^0.454.0",
"postcss": "^8.4.47",
"react": "^18.2.0",
"react-contenteditable": "^3.3.6",
"react-dom": "^18.2.0",
"react-inner-image-zoom": "^3.0.2",
"react-markdown": "^8.0.7",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.3.0",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1",
"sass": "^1.51.0",
"tailwindcss": "^3.0.24",
"uuid": "^9.0.1",
"zustand": "^4.4.6"
"react-markdown": "^9.0.1",
"tailwindcss": "^3.4.14",
"yarn": "^1.22.22",
"zustand": "^5.0.1"
},
"devDependencies": {
"@types/node": "^18.7.13",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.15",
"@types/react-inner-image-zoom": "^3.0.0",
"@types/react-resizable": "^3.0.2",
"@types/uuid": "^9.0.8",
"typescript": "^4.6.4"
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^20.11.19",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-syntax-highlighter": "^15.5.10",
"@types/uuid": "^10.0.0",
"typescript": "^5.3.3"
}
}

View File

@ -1,4 +1,6 @@
module.exports = () => ({
plugins: [require("tailwindcss")],
})
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,873 +0,0 @@
import {
ChevronDownIcon,
ChevronUpIcon,
Cog8ToothIcon,
XMarkIcon,
ClipboardIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import React, { ReactNode, useEffect, useRef, useState } from "react";
import Icon from "./icons";
import { Modal, Table, Tooltip, theme } from "antd";
import Editor from "@monaco-editor/react";
import Papa from "papaparse";
import remarkGfm from "remark-gfm";
import ReactMarkdown from "react-markdown";
import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { truncateText } from "./utils";
const { useToken } = theme;
interface CodeProps {
node?: any;
inline?: any;
className?: any;
children?: React.ReactNode;
}
interface IProps {
children?: ReactNode;
title?: string | ReactNode;
subtitle?: string | ReactNode;
count?: number;
active?: boolean;
cursor?: string;
icon?: ReactNode;
padding?: string;
className?: string;
open?: boolean;
hoverable?: boolean;
onClick?: () => void;
loading?: boolean;
}
export const SectionHeader = ({
children,
title,
subtitle,
count,
icon,
}: IProps) => {
return (
<div id="section-header" className="mb-4">
<h1 className="text-primary text-2xl">
{/* {count !== null && <span className="text-accent mr-1">{count}</span>} */}
{icon && <>{icon}</>}
{title}
{count !== null && (
<span className="text-accent mr-1 ml-2 text-xs">{count}</span>
)}
</h1>
{subtitle && <span className="inline-block">{subtitle}</span>}
{children}
</div>
);
};
export const IconButton = ({
onClick,
icon,
className,
active = false,
}: IProps) => {
return (
<span
id="icon-button"
role={"button"}
onClick={onClick}
className={`inline-block mr-2 hover:text-accent transition duration-300 ${className} ${
active ? "border-accent border rounded text-accent" : ""
}`}
>
{icon}
</span>
);
};
export const LaunchButton = ({
children,
onClick,
className = "p-3 px-5 ",
}: any) => {
return (
<button
id="launch-button"
role={"button"}
className={` focus:ring ring-accent ring-l-none rounded cursor-pointer hover:brightness-110 bg-accent transition duration-500 text-white ${className} `}
onClick={onClick}
>
{children}
</button>
);
};
export const SecondaryButton = ({ children, onClick, className }: any) => {
return (
<button
id="secondary-button"
role={"button"}
className={` ${className} focus:ring ring-accent p-2 px-5 rounded cursor-pointer hover:brightness-90 bg-secondary transition duration-500 text-primary`}
onClick={onClick}
>
{children}
</button>
);
};
export const Card = ({
children,
title,
subtitle,
hoverable = true,
active,
cursor = "cursor-pointer",
className = "p-3",
onClick,
}: IProps) => {
let border = active
? "border-accent"
: "border-secondary hover:border-accent ";
border = hoverable ? border : "border-secondary";
return (
<button
id="card"
tabIndex={0}
onClick={onClick}
role={"button"}
className={`${border} border-2 bg-secondary group ${className} w-full text-left rounded ${cursor} transition duration-300`}
>
<div className="mt- text-sm text-secondary h-full break-words">
{title && (
<div className="text-accent rounded font-semibold text-xs pb-1">
{title}
</div>
)}
<div>{subtitle}</div>
{children}
</div>
</button>
);
};
export const CollapseBox = ({
title,
subtitle,
children,
className = " p-3",
open = false,
}: IProps) => {
const [isOpen, setIsOpen] = React.useState<boolean>(open);
const chevronClass = "h-4 cursor-pointer inline-block mr-1";
return (
<div
id="collapse-box"
onMouseDown={(e) => {
if (e.detail > 1) {
e.preventDefault();
}
}}
className="bordper border-secondary rounded"
>
<div
onClick={() => {
setIsOpen(!isOpen);
}}
className={`cursor-pointer bg-secondary p-2 rounded ${
isOpen ? "rounded-b-none " : " "
}"}`}
>
{isOpen && <ChevronUpIcon className={chevronClass} />}
{!isOpen && <ChevronDownIcon className={chevronClass} />}
<span className=" inline-block -mt-2 mb-2 text-xs">
{" "}
{/* {isOpen ? "hide" : "show"} section | */}
{title}
</span>
</div>
{isOpen && (
<div className={`${className} bg-tertiary rounded rounded-t-none`}>
{children}
</div>
)}
</div>
);
};
export const HighLight = ({ children }: IProps) => {
return <span id="highlight" className="border-b border-accent">{children}</span>;
};
export const LoadBox = ({
subtitle,
className = "my-2 text-accent ",
}: IProps) => {
return (
<div id="load-box" className={`${className} `}>
<span className="mr-2 ">
{" "}
<Icon size={5} icon="loading" />
</span>{" "}
{subtitle}
</div>
);
};
export const LoadingBar = ({ children }: IProps) => {
return (
<>
<div id="loading-bar" className="rounded bg-secondary p-3">
<span className="inline-block h-6 w-6 relative mr-2">
<Cog8ToothIcon className="animate-ping text-accent absolute inline-flex h-full w-full rounded-ful opacity-75" />
<Cog8ToothIcon className="relative text-accent animate-spin inline-flex rounded-full h-6 w-6" />
</span>
{children}
</div>
<div className="relative">
<div className="loadbar rounded-b"></div>
</div>
</>
);
};
export const MessageBox = ({ title, children, className }: IProps) => {
const messageBox = useRef<HTMLDivElement>(null);
const closeMessage = () => {
if (messageBox.current) {
messageBox.current.remove();
}
};
return (
<div
id="message-box"
ref={messageBox}
className={`${className} p-3 rounded bg-secondary transition duration-1000 ease-in-out overflow-hidden`}
>
{" "}
<div className="flex gap-2 mb-2">
<div className="flex-1">
{/* <span className="mr-2 text-accent">
<InformationCircleIcon className="h-6 w-6 inline-block" />
</span>{" "} */}
<span className="font-semibold text-primary text-base">{title}</span>
</div>
<div>
<span
onClick={() => {
closeMessage();
}}
className=" border border-secondary bg-secondary brightness-125 hover:brightness-100 cursor-pointer transition duration-200 inline-block px-1 pb-1 rounded text-primary"
>
<XMarkIcon className="h-4 w-4 inline-block" />
</span>
</div>
</div>
{children}
</div>
);
};
export const GroupView = ({
children,
title,
className = "text-primary bg-primary ",
}: any) => {
return (
<div id="group-view" className={`rounded mt-4 border-secondary ${className}`}>
<div className="mt-4 p-2 rounded border relative">
<div className={`absolute -top-3 inline-block ${className}`}>
{title}
</div>
<div className="mt-2"> {children}</div>
</div>
</div>
);
};
export const ExpandView = ({
children,
icon = null,
className = "",
title = "Detail View",
}: any) => {
const [isOpen, setIsOpen] = React.useState(false);
let windowAspect = 1;
if (typeof window !== "undefined") {
windowAspect = window.innerWidth / window.innerHeight;
}
const minImageWidth = 400;
return (
<div
id="expand-view"
style={{
minHeight: "100px",
}}
className={`h-full rounded mb-6 border-secondary ${className}`}
>
<div
role="button"
onClick={() => {
setIsOpen(true);
}}
className="text-xs mb-2 h-full w-full break-words"
>
{icon ? icon : children}
</div>
{isOpen && (
<Modal
title={title}
width={800}
open={isOpen}
onCancel={() => setIsOpen(false)}
footer={null}
>
{/* <ResizableBox
// handle={<span className="text-accent">resize</span>}
lockAspectRatio={false}
handle={
<div className="absolute right-0 bottom-0 cursor-se-resize font-semibold boprder p-3 bg-secondary">
<ArrowDownRightIcon className="h-4 w-4 inline-block" />
</div>
}
width={800}
height={minImageWidth * windowAspect}
minConstraints={[minImageWidth, minImageWidth * windowAspect]}
maxConstraints={[900, 900 * windowAspect]}
className="overflow-auto w-full rounded select-none "
> */}
{children}
{/* </ResizableBox> */}
</Modal>
)}
</div>
);
};
export const LoadingOverlay = ({ children, loading }: IProps) => {
return (
<>
{loading && (
<>
<div
id="loading-overlay"
className="absolute inset-0 bg-secondary flex pointer-events-none"
style={{ opacity: 0.5 }}
>
{/* Overlay background */}
</div>
<div
className="absolute inset-0 flex items-center justify-center"
style={{ pointerEvents: "none" }}
>
{/* Center BounceLoader without inheriting the opacity */}
<BounceLoader />
</div>
</>
)}
<div className="relative">{children}</div>
</>
);
};
export const MarkdownView = ({
data,
className = "",
showCode = true,
}: {
data: string;
className?: string;
showCode?: boolean;
}) => {
function processString(inputString: string): string {
// TODO: Had to add this temp measure while debugging. Why is it null?
if (!inputString) {
console.log("inputString is null!")
}
inputString = inputString && inputString.replace(/\n/g, " \n");
const markdownPattern = /```markdown\s+([\s\S]*?)\s+```/g;
return inputString?.replace(markdownPattern, (match, content) => content);
}
const [showCopied, setShowCopied] = React.useState(false);
const CodeView = ({ props, children, language }: any) => {
const [codeVisible, setCodeVisible] = React.useState(showCode);
return (
<div>
<div className=" flex ">
<div
role="button"
onClick={() => {
setCodeVisible(!codeVisible);
}}
className=" flex-1 mr-4 "
>
{!codeVisible && (
<div className=" text-white hover:text-accent duration-300">
<ChevronDownIcon className="inline-block w-5 h-5" />
<span className="text-xs"> show</span>
</div>
)}
{codeVisible && (
<div className=" text-white hover:text-accent duration-300">
{" "}
<ChevronUpIcon className="inline-block w-5 h-5" />
<span className="text-xs"> hide</span>
</div>
)}
</div>
{/* <div className="flex-1"></div> */}
<div>
{showCopied && (
<div className="inline-block text-sm text-white">
{" "}
🎉 Copied!{" "}
</div>
)}
<ClipboardIcon
role={"button"}
onClick={() => {
navigator.clipboard.writeText(data);
// message.success("Code copied to clipboard");
setShowCopied(true);
setTimeout(() => {
setShowCopied(false);
}, 3000);
}}
className=" inline-block duration-300 text-white hover:text-accent w-5 h-5"
/>
</div>
</div>
{codeVisible && (
<SyntaxHighlighter
{...props}
style={atomDark}
language={language}
className="rounded w-full"
PreTag="div"
wrapLongLines={true}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
)}
</div>
);
};
return (
<div
id="markdown-view"
className={` w-full chatbox prose dark:prose-invert text-primary rounded ${className}`}
>
<ReactMarkdown
className=" w-full"
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }: CodeProps) {
const match = /language-(\w+)/.exec(className || "");
const language = match ? match[1] : "text";
return !inline && match ? (
<CodeView props={props} children={children} language={language} />
) : (
<code {...props} className={className}>
{children}
</code>
);
},
}}
>
{processString(data)}
</ReactMarkdown>
</div>
);
};
interface ICodeProps {
code: string;
language: string;
title?: string;
showLineNumbers?: boolean;
className?: string | undefined;
wrapLines?: boolean;
maxWidth?: string;
maxHeight?: string;
minHeight?: string;
}
export const CodeBlock = ({
code,
language = "python",
showLineNumbers = false,
className = " ",
wrapLines = false,
maxHeight = "400px",
minHeight = "auto",
}: ICodeProps) => {
const codeString = code;
const [showCopied, setShowCopied] = React.useState(false);
return (
<div id="code-block" className="relative">
<div className=" rounded absolute right-5 top-4 z-10 ">
<div className="relative border border-transparent w-full h-full">
<div
style={{ zIndex: -1 }}
className="w-full absolute top-0 h-full bg-gray-900 hover:bg-opacity-0 duration-300 bg-opacity-50 rounded"
></div>
<div className=" ">
{showCopied && (
<div className="inline-block px-2 pl-3 text-white">
{" "}
🎉 Copied!{" "}
</div>
)}
<ClipboardIcon
role={"button"}
onClick={() => {
navigator.clipboard.writeText(codeString);
// message.success("Code copied to clipboard");
setShowCopied(true);
setTimeout(() => {
setShowCopied(false);
}, 6000);
}}
className="m-2 inline-block duration-300 text-white hover:text-accent w-5 h-5"
/>
</div>
</div>
</div>
<div
id="codeDivBox"
className={`rounded w-full overflow-auto overflow-y-scroll scroll ${className}`}
style={{ maxHeight: maxHeight, minHeight: minHeight }}
>
<SyntaxHighlighter
id="codeDiv"
className="rounded-sm h-full break-all"
language={language}
showLineNumbers={showLineNumbers}
style={atomDark}
wrapLines={wrapLines}
wrapLongLines={wrapLines}
>
{codeString}
</SyntaxHighlighter>
</div>
</div>
);
};
// Controls Row
export const ControlRowView = ({
title,
description,
value,
control,
className,
truncateLength = 20,
}: {
title: string;
description: string;
value: string | number | boolean;
control: any;
className?: string;
truncateLength?: number;
}) => {
return (
<div id="control-row-view" className={`${className}`}>
<div>
<span className="text-primary inline-block">{title} </span>
<span className="text-xs ml-1 text-accent -mt-2 inline-block">
{truncateText(value + "", truncateLength)}
</span>{" "}
<Tooltip title={description}>
<InformationCircleIcon className="text-gray-400 inline-block w-4 h-4" />
</Tooltip>
</div>
{control}
<div className="bordper-b border-secondary border-dashed pb-2 mxp-2"></div>
</div>
);
};
export const BounceLoader = ({
className,
title = "",
}: {
className?: string;
title?: string;
}) => {
return (
<div id="bounce-loader" className="inline-block">
<div className="inline-flex gap-2">
<span className=" rounded-full bg-accent h-2 w-2 inline-block"></span>
<span className="animate-bounce rounded-full bg-accent h-3 w-3 inline-block"></span>
<span className=" rounded-full bg-accent h-2 w-2 inline-block"></span>
</div>
<span className=" text-sm">{title}</span>
</div>
);
};
export const ImageLoader = ({
src,
className = "",
}: {
src: string;
className?: string;
}) => {
const [isLoading, setIsLoading] = useState(true);
return (
<div id="image-loader" className="w-full rounded relative">
{isLoading && (
<div className="absolute h-24 inset-0 flex items-center justify-center">
<BounceLoader title=" loading .." />
</div>
)}
<img
alt="Dynamic content"
src={src}
className={`w-full rounded ${
isLoading ? "opacity-0" : "opacity-100"
} ${className}`}
onLoad={() => setIsLoading(false)}
/>
</div>
);
};
type DataRow = { [key: string]: any };
export const CsvLoader = ({
csvUrl,
className,
}: {
csvUrl: string;
className?: string;
}) => {
const [data, setData] = useState<DataRow[]>([]);
const [columns, setColumns] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [pageSize, setPageSize] = useState<number>(50);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(csvUrl);
const csvString = await response.text();
const parsedData = Papa.parse(csvString, {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
});
setData(parsedData.data as DataRow[]);
// Use the keys of the first object for column headers
const firstRow = parsedData.data[0] as DataRow; // Type assertion
const columnHeaders: any[] = Object.keys(firstRow).map((key) => {
const val = {
title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the key for the title
dataIndex: key,
key: key,
};
if (typeof firstRow[key] === "number") {
return {
...val,
sorter: (a: DataRow, b: DataRow) => a[key] - b[key],
};
}
return val;
});
setColumns(columnHeaders);
setIsLoading(false);
} catch (error) {
console.error("Error fetching CSV data:", error);
setIsLoading(false);
}
};
fetchData();
}, [csvUrl]);
// calculate x scroll, based on number of columns
const scrollX = columns.length * 150;
return (
<div id="csv-loader" className={`CsvLoader ${className}`}>
<Table
dataSource={data}
columns={columns}
loading={isLoading}
pagination={{ pageSize: pageSize }}
scroll={{ y: 450, x: scrollX }}
onChange={(pagination) => {
setPageSize(pagination.pageSize || 50);
}}
/>
</div>
);
};
export const CodeLoader = ({
url,
className,
}: {
url: string;
className?: string;
}) => {
const [isLoading, setIsLoading] = useState(true);
const [code, setCode] = useState<string | null>(null);
React.useEffect(() => {
fetch(url)
.then((response) => response.text())
.then((data) => {
setCode(data);
setIsLoading(false);
});
}, [url]);
return (
<div id="code-loader" className={`w-full rounded relative ${className}`}>
{isLoading && (
<div className="absolute h-24 inset-0 flex items-center justify-center">
<BounceLoader />
</div>
)}
{!isLoading && <CodeBlock code={code || ""} language={"python"} />}
</div>
);
};
export const PdfViewer = ({ url }: { url: string }) => {
const [loading, setLoading] = useState<boolean>(true);
React.useEffect(() => {
// Assuming the URL is directly usable as the source for the <object> tag
setLoading(false);
// Note: No need to handle the creation and cleanup of a blob URL or converting file content as it's not provided anymore.
}, [url]);
// Render the PDF viewer
return (
<div id="pdf-viewer" className="h-full">
{loading && <p>Loading PDF...</p>}
{!loading && (
<object
className="w-full rounded"
data={url}
type="application/pdf"
width="100%"
style={{ height: "calc(90vh - 200px)" }}
>
<p>PDF cannot be displayed.</p>
</object>
)}
</div>
);
};
export const MonacoEditor = ({
value,
editorRef,
language,
onChange,
minimap = true,
}: {
value: string;
onChange?: (value: string) => void;
editorRef: any;
language: string;
minimap?: boolean;
}) => {
const [isEditorReady, setIsEditorReady] = useState(false);
const onEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor;
setIsEditorReady(true);
};
return (
<div id="monaco-editor" className="h-full rounded">
<Editor
height="100%"
className="h-full rounded"
defaultLanguage={language}
defaultValue={value}
value={value}
onChange={(value: string | undefined) => {
if (onChange && value) {
onChange(value);
}
}}
onMount={onEditorDidMount}
theme="vs-dark"
options={{
wordWrap: "on",
wrappingIndent: "indent",
wrappingStrategy: "advanced",
minimap: {
enabled: minimap,
},
}}
/>
</div>
);
};
export const CardHoverBar = ({
items,
}: {
items: {
title: string;
icon: any;
hoverText: string;
onClick: (e: any) => void;
}[];
}) => {
const itemRows = items.map((item, i) => {
return (
<div
key={"cardhoverrow" + i}
id={`card-hover-bar-item-${i}`}
role="button"
className="text-accent text-xs inline-block hover:bg-primary p-2 rounded"
onClick={item.onClick}
>
<Tooltip title={item.hoverText}>
<item.icon className=" w-5, h-5 cursor-pointer inline-block" />
</Tooltip>
</div>
);
});
return (
<div
id="card-hover-bar"
onMouseEnter={(e) => {
e.stopPropagation();
}}
className=" mt-2 text-right opacity-0 group-hover:opacity-100 "
>
{itemRows}
</div>
);
};
export const AgentRow = ({ message }: { message: any }) => {
return (
<GroupView
title={
<div className="rounded p-1 px-2 inline-block text-xs bg-secondary">
<span className="font-semibold">{message.sender}</span> ( to{" "}
{message.recipient} )
</div>
}
className="m"
>
<MarkdownView data={message.message?.content} className="text-sm" />
</GroupView>
);
};

View File

@ -0,0 +1,157 @@
import React from "react";
import { Menu } from "@headlessui/react";
import {
BellIcon,
MoonIcon,
SunIcon,
MagnifyingGlassIcon,
} from "@heroicons/react/24/outline";
import {
ChevronDown,
PanelLeftClose,
PanelLeftOpen,
Menu as MenuIcon,
} from "lucide-react";
import { Tooltip } from "antd";
import { appContext } from "../hooks/provider";
import { useConfigStore } from "../hooks/store";
type ContentHeaderProps = {
title?: string;
onMobileMenuToggle: () => void;
isMobileMenuOpen: boolean;
};
const classNames = (...classes: (string | undefined | boolean)[]) => {
return classes.filter(Boolean).join(" ");
};
const ContentHeader = ({
title,
onMobileMenuToggle,
isMobileMenuOpen,
}: ContentHeaderProps) => {
const { darkMode, setDarkMode, user, logout } = React.useContext(appContext);
const { sidebar, setSidebarState } = useConfigStore();
const { isExpanded } = sidebar;
return (
<div className="sticky top-0 z-40 bg-primary border-b border-secondary">
<div className="flex h-16 items-center gap-x-4 px-4">
{/* Mobile Menu Button */}
<button
onClick={onMobileMenuToggle}
className="md:hidden p-2 rounded-md hover:bg-secondary text-secondary hover:text-accent transition-colors"
aria-label="Toggle mobile menu"
>
<MenuIcon className="h-6 w-6" />
</button>
{/* Desktop Sidebar Toggle - Hidden on Mobile */}
<div className="hidden md:block">
<Tooltip title={isExpanded ? "Close Sidebar" : "Open Sidebar"}>
<button
onClick={() => setSidebarState({ isExpanded: !isExpanded })}
className={classNames(
"p-2 rounded-md hover:bg-secondary",
"hover:text-accent text-secondary transition-colors",
"focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50"
)}
>
{isExpanded ? (
<PanelLeftClose strokeWidth={1.5} className="h-6 w-6" />
) : (
<PanelLeftOpen strokeWidth={1.5} className="h-6 w-6" />
)}
</button>
</Tooltip>
</div>
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
{/* Search */}
<div className="flex flex-1 items-center">
<form className="hidden relative flex flex-1">
<label htmlFor="search-field" className="sr-only">
Search
</label>
<MagnifyingGlassIcon className="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-secondary" />
<input
id="search-field"
type="search"
placeholder="Search..."
className="block h-full w-full border-0 bg-primary py-0 pl-8 pr-0 text-primary placeholder:text-secondary focus:ring-0 sm:text-sm"
/>
</form>
</div>
{/* Right side header items */}
<div className="flex items-center gap-x-4 lg:gap-x-6 ml-auto">
{/* Dark Mode Toggle */}
<button
onClick={() =>
setDarkMode(darkMode === "dark" ? "light" : "dark")
}
className="text-secondary hover:text-primary"
>
{darkMode === "dark" ? (
<MoonIcon className="h-6 w-6" />
) : (
<SunIcon className="h-6 w-6" />
)}
</button>
{/* Notifications */}
<button className="text-secondary hidden hover:text-primary">
<BellIcon className="h-6 w-6" />
</button>
{/* Separator */}
<div className="hidden lg:block lg:h-6 lg:w-px lg:bg-secondary" />
{/* User Menu */}
{user && (
<Menu as="div" className="relative">
<Menu.Button className="flex items-center">
{user.avatar_url ? (
<img
className="h-8 w-8 rounded-full"
src={user.avatar_url}
alt={user.name}
/>
) : (
<div className="border-2 bg-accent h-8 w-8 rounded-full flex items-center justify-center text-white">
{user.name?.[0]}
</div>
)}
<span className="hidden lg:flex lg:items-center">
<span className="ml-4 text-sm text-primary">
{user.name}
</span>
<ChevronDown className="ml-2 h-5 w-5 text-secondary" />
</span>
</Menu.Button>
<Menu.Items className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-primary py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<a
href="#"
onClick={() => logout()}
className={`${
active ? "bg-secondary" : ""
} block px-4 py-2 text-sm text-primary`}
>
Sign out
</a>
)}
</Menu.Item>
</Menu.Items>
</Menu>
)}
</div>
</div>
</div>
</div>
);
};
export default ContentHeader;

View File

@ -17,7 +17,7 @@ const Footer = () => {
}
}, []);
return (
<div className=" mt-4 text-primary p-3 border-t border-secondary flex ">
<div className=" text-primary p-3 border-t border-secondary flex ">
<div className="text-xs flex-1">
Maintained by the AutoGen{" "}
<a

View File

@ -208,20 +208,62 @@ const Icon = ({ icon = "app", size = 4, className = "" }: Props) => {
className={` ${sizeClass} inline-block `}
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 93 90"
viewBox="0 0 290 264"
>
<path
d="M44.0471 0H31.0006C28.3399 0 25.997 1.75225 25.2451 4.30449L2.26702 82.3045C1.13478 86.1479 4.01575 90 8.02249 90H21.776C24.4553 90 26.8098 88.2236 27.5454 85.6473L49.8165 7.64732C50.9108 3.81465 48.0329 0 44.0471 0Z"
fill="#3F9447"
d="M112.233 104.244C106.455 104.242 100.778 105.756 95.7684 108.635C90.7586 111.513 86.5912 115.655 83.6823 120.647L0 264H38.456C44.2308 263.999 49.9048 262.486 54.9137 259.613C59.9226 256.739 64.0919 252.603 67.0068 247.618L86.2185 214.713L97.1033 196.079L141.807 119.548L150.733 104.244H112.233Z"
fill="url(#paint0_linear_149_456)"
/>
<path
d="M61.8267 39H51.7524C49.9425 39 48.3581 40.2153 47.8891 41.9634L36.3514 84.9634C35.6697 87.5042 37.5841 90 40.2148 90H50.644C52.4654 90 54.0568 88.7695 54.5153 87.0068L65.6979 44.0068C66.3568 41.4731 64.4446 39 61.8267 39Z"
fill="#D9D9D9"
d="M111.547 33.2857L130.813 0L212.939 144.278C215.795 149.3 217.285 154.982 217.262 160.759C217.239 166.535 215.704 172.205 212.809 177.205L193.532 210.49L111.417 66.2122C108.559 61.1911 107.068 55.5088 107.091 49.7316C107.114 43.9544 108.65 38.284 111.547 33.2857Z"
fill="url(#paint1_linear_149_456)"
/>
<path
d="M90.1629 84.234L77.2698 58.0311C77.0912 57.668 76.8537 57.337 76.5672 57.0514C74.5514 55.0426 71.1154 55.9917 70.4166 58.7504L63.7622 85.0177C63.1219 87.5453 65.0322 90 67.6397 90H86.5738C89.5362 90 91.4707 86.8921 90.1629 84.234Z"
fill="#3F9447"
d="M289.285 245.714H123.281C117.498 245.714 111.815 244.199 106.8 241.319C101.785 238.439 97.6121 234.295 94.6976 229.3L86.1748 214.714L97.0596 196.079H241.348C247.134 196.075 252.82 197.588 257.837 200.468C262.855 203.349 267.029 207.495 269.943 212.493L289.285 245.714Z"
fill="url(#paint2_linear_149_456)"
/>
<defs>
<linearGradient
id="paint0_linear_149_456"
x1="116.173"
y1="107.901"
x2="37.3131"
y2="255.118"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2314CC" />
<stop offset="0.22" stopColor="#234CE4" />
<stop offset="1" stopColor="#4081FF" />
</linearGradient>
<linearGradient
id="paint1_linear_149_456"
x1="200.792"
y1="184.508"
x2="134.199"
y2="47.8169"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#7215D4" />
<stop offset="0.11" stopColor="#7554D5" />
<stop offset="0.56" stopColor="#9E8AE9" />
<stop offset="1" stopColor="#CC99FF" />
</linearGradient>
<linearGradient
id="paint2_linear_149_456"
x1="107.651"
y1="220.896"
x2="271.108"
y2="220.896"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2E31F0" />
<stop offset="0.2" stopColor="#4081FF" />
<stop offset="0.39" stopColor="#848EE5" />
<stop offset="0.49" stopColor="#8183E2" />
<stop offset="0.65" stopColor="#7866DA" />
<stop offset="0.75" stopColor="#7251D4" />
</linearGradient>
</defs>
</svg>
);
}

View File

@ -1,10 +1,16 @@
import * as React from "react";
import Header from "./header";
import { Dialog } from "@headlessui/react";
import { X } from "lucide-react";
import { appContext } from "../hooks/provider";
import { useConfigStore } from "../hooks/store";
import Footer from "./footer";
/// import ant css
import "antd/dist/reset.css";
import SideBar from "./sidebar";
import ContentHeader from "./contentheader";
const classNames = (...classes: (string | undefined | boolean)[]) => {
return classes.filter(Boolean).join(" ");
};
type Props = {
title: string;
@ -23,38 +29,96 @@ const Layout = ({
showHeader = true,
restricted = false,
}: Props) => {
const layoutContent = (
<div
// style={{ height: "calc(100vh - 64px)" }}
className={` h-full flex flex-col`}
>
{showHeader && <Header meta={meta} link={link} />}
<div className="flex-1 text-primary ">
<title>{meta?.title + " | " + title}</title>
<div className=" h-full text-primary">{children}</div>
</div>
<Footer />
</div>
);
const { darkMode } = React.useContext(appContext);
const { sidebar } = useConfigStore();
const { isExpanded } = sidebar;
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
// Close mobile menu on route change
React.useEffect(() => {
setIsMobileMenuOpen(false);
}, [link]);
React.useEffect(() => {
document.getElementsByTagName("html")[0].className = `${
darkMode === "dark" ? "dark bg-primary" : "light bg-primary"
} `;
}`;
}, [darkMode]);
return (
<appContext.Consumer>
{(context: any) => {
if (restricted) {
return <div className="h-full ">{context.user && layoutContent}</div>;
} else {
return layoutContent;
}
}}
</appContext.Consumer>
const layoutContent = (
<div className="min-h-screen flex">
{/* Mobile menu */}
<Dialog
as="div"
open={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
className="relative z-50 md:hidden"
>
{/* Backdrop */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
{/* Mobile Sidebar Container */}
<div className="fixed inset-0 flex">
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
<div className="absolute right-0 top-0 flex w-16 justify-center pt-5">
<button
type="button"
className="text-secondary"
onClick={() => setIsMobileMenuOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<X className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<SideBar link={link} meta={meta} isMobile={true} />
</Dialog.Panel>
</div>
</Dialog>
{/* Desktop sidebar */}
<div className="hidden md:flex md:flex-col md:fixed md:inset-y-0">
<SideBar link={link} meta={meta} isMobile={false} />
</div>
{/* Content area */}
<div
className={classNames(
"flex-1 flex flex-col min-h-screen",
"transition-all duration-300 ease-in-out",
"md:pl-16",
isExpanded ? "md:pl-72" : "md:pl-16"
)}
>
{showHeader && (
<ContentHeader
title={title}
isMobileMenuOpen={isMobileMenuOpen}
onMobileMenuToggle={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
/>
)}
<main className="flex-1 p-2 text-primary">{children}</main>
<Footer />
</div>
</div>
);
// Handle restricted content
if (restricted) {
return (
<appContext.Consumer>
{(context: any) => {
if (context.user) {
return layoutContent;
}
return null;
}}
</appContext.Consumer>
);
}
return layoutContent;
};
export default Layout;

View File

@ -0,0 +1,148 @@
import React from "react";
import { Link } from "gatsby";
import { useConfigStore } from "../hooks/store";
import { Tooltip } from "antd";
import { Blocks, Settings, MessagesSquare } from "lucide-react";
import Icon from "./icons";
const navigation = [
// { name: "Build", href: "/build", icon: Blocks },
{ name: "Playground", href: "/", icon: MessagesSquare },
];
const classNames = (...classes: (string | undefined | boolean)[]) => {
return classes.filter(Boolean).join(" ");
};
type SidebarProps = {
link: string;
meta?: {
title: string;
description: string;
};
isMobile: boolean;
};
const Sidebar = ({ link, meta, isMobile }: SidebarProps) => {
const { sidebar } = useConfigStore();
const { isExpanded } = sidebar;
// Always show full sidebar in mobile view
const showFull = isMobile || isExpanded;
return (
<div
className={classNames(
"flex grow flex-col gap-y-5 overflow-y-auto border-r border-secondary bg-primary",
"transition-all duration-300 ease-in-out",
showFull ? "w-72 px-6" : "w-16 px-2"
)}
>
{/* App Logo/Title */}
<div
className={`flex h-16 items-center ${showFull ? "gap-x-3" : "ml-2"}`}
>
<div className="w-8 text-right text-accent">
<Icon icon="app" size={8} />
</div>
{showFull && (
<div className="flex flex-col" style={{ minWidth: "200px" }}>
<span className="text-base font-semibold text-primary">
{meta?.title}
</span>
<span className="text-xs text-secondary">{meta?.description}</span>
</div>
)}
</div>
{/* Navigation */}
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
{/* Main Navigation */}
<li>
<ul
role="list"
className={classNames(
"-mx-2 space-y-1",
!showFull && "items-center"
)}
>
{navigation.map((item) => {
const isActive = item.href === link;
const IconComponent = item.icon;
const navLink = (
<Link
to={item.href}
className={classNames(
isActive
? "text-accent"
: "text-primary hover:text-accent hover:bg-secondary",
"group flex gap-x-3 rounded-md p-2 text-sm font-medium",
!showFull && "justify-center"
)}
>
<IconComponent
className={classNames(
isActive
? "text-accent"
: "text-secondary group-hover:text-accent",
"h-6 w-6 shrink-0"
)}
/>
{showFull && item.name}
</Link>
);
return (
<li key={item.name}>
{!showFull && !isMobile ? (
<Tooltip title={item.name} placement="right">
{navLink}
</Tooltip>
) : (
navLink
)}
</li>
);
})}
</ul>
</li>
{/* Settings at bottom */}
<li
className={classNames(
"mt-auto -mx-2 mb-4",
!showFull && "flex justify-center"
)}
>
{!showFull && !isMobile ? (
<Tooltip title="Settings" placement="right">
<Link
to="/settings"
className={classNames(
"group flex gap-x-3 rounded-md p-2 text-sm font-medium",
"text-primary hover:text-accent hover:bg-secondary",
!showFull && "justify-center"
)}
>
<Settings className="h-6 w-6 shrink-0 text-secondary group-hover:text-accent" />
</Link>
</Tooltip>
) : (
<Link
to="/settings"
className="group flex gap-x-3 rounded-md p-2 text-sm font-medium text-primary hover:text-accent hover:bg-secondary"
>
<Settings className="h-6 w-6 shrink-0 text-secondary group-hover:text-accent" />
{showFull && "Settings"}
</Link>
)}
</li>
</ul>
</nav>
</div>
);
};
export default Sidebar;

View File

@ -1,127 +0,0 @@
export type NotificationType = "success" | "info" | "warning" | "error";
export interface IMessage {
user_id: string;
role: string;
content: string;
created_at?: string;
updated_at?: string;
session_id?: number;
connection_id?: string;
workflow_id?: number;
meta?: any;
id?: number;
}
export interface IStatus {
message: string;
status: boolean;
data?: any;
}
export interface IChatMessage {
text: string;
sender: "user" | "bot";
meta?: any;
id?: number;
}
export interface ILLMConfig {
config_list: Array<IModelConfig>;
timeout?: number;
cache_seed?: number | null;
temperature: number;
max_tokens: number;
}
export interface IAgentConfig {
name: string;
llm_config?: ILLMConfig | false;
human_input_mode: string;
max_consecutive_auto_reply: number;
system_message: string | "";
is_termination_msg?: boolean | string;
default_auto_reply?: string | null;
code_execution_config?: "none" | "local" | "docker";
description?: string;
admin_name?: string;
messages?: Array<IMessage>;
max_round?: number;
speaker_selection_method?: string;
allow_repeat_speaker?: boolean;
}
export interface IAgent {
type?: "assistant" | "userproxy" | "groupchat";
config: IAgentConfig;
created_at?: string;
updated_at?: string;
id?: number;
skills?: Array<ISkill>;
user_id?: string;
}
export interface IWorkflow {
name: string;
description: string;
sender?: IAgent;
receiver?: IAgent;
type?: "autonomous" | "sequential";
created_at?: string;
updated_at?: string;
summary_method?: "none" | "last" | "llm";
id?: number;
user_id?: string;
}
export interface IModelConfig {
model: string;
api_key?: string;
api_version?: string;
base_url?: string;
api_type?: "open_ai" | "azure" | "google" | "anthropic" | "mistral";
user_id?: string;
created_at?: string;
updated_at?: string;
description?: string;
id?: number;
}
export interface IMetadataFile {
name: string;
path: string;
extension: string;
content: string;
type: string;
}
export interface IChatSession {
id?: number;
user_id: string;
workflow_id?: number;
created_at?: string;
updated_at?: string;
name: string;
}
export interface IGalleryItem {
id: number;
messages: Array<IMessage>;
session: IChatSession;
tags: Array<string>;
created_at: string;
updated_at: string;
}
export interface ISkill {
name: string;
content: string;
secrets?: any[];
libraries?: string[];
id?: number;
description?: string;
user_id?: string;
created_at?: string;
updated_at?: string;
}

View File

@ -0,0 +1,5 @@
export interface IStatus {
message: string;
status: boolean;
data?: any;
}

View File

@ -0,0 +1,86 @@
export interface RequestUsage {
prompt_tokens: number;
completion_tokens: number;
}
export interface MessageConfig {
source: string;
content: string;
models_usage?: RequestUsage;
}
export interface DBModel {
id?: number;
user_id?: string;
created_at?: string;
updated_at?: string;
}
export interface Message extends DBModel {
config: MessageConfig;
session_id: number;
run_id: string;
}
export interface Session extends DBModel {
name: string;
team_id?: string;
}
export interface TeamConfig {
name: string;
participants: AgentConfig[];
team_type: TeamTypes;
model_client?: ModelConfig;
termination_condition?: TerminationConfig;
}
export interface Team extends DBModel {
config: TeamConfig;
}
export type ModelTypes = "OpenAIChatCompletionClient";
export type AgentTypes = "AssistantAgent" | "CodingAssistantAgent";
export type TeamTypes = "RoundRobinGroupChat" | "SelectorGroupChat";
export type TerminationTypes =
| "MaxMessageTermination"
| "StopMessageTermination"
| "TextMentionTermination";
export interface ModelConfig {
model: string;
model_type: ModelTypes;
api_key?: string;
base_url?: string;
}
export interface ToolConfig {
name: string;
description: string;
content: string;
}
export interface AgentConfig {
name: string;
agent_type: AgentTypes;
system_message?: string;
model_client?: ModelConfig;
tools?: ToolConfig[];
description?: string;
}
export interface TerminationConfig {
termination_type: TerminationTypes;
max_messages?: number;
text?: string;
}
export interface TaskResult {
messages: MessageConfig[];
usage: string;
duration: number;
stop_reason?: string;
}

View File

@ -1,12 +1,4 @@
import {
IAgent,
IAgentConfig,
ILLMConfig,
IModelConfig,
ISkill,
IStatus,
IWorkflow,
} from "./types";
import { IStatus } from "./types";
export const getServerUrl = () => {
return process.env.GATSBY_API_URL || "/api";
@ -100,10 +92,6 @@ export function fetchJSON(
onFinal();
});
}
export const capitalize = (s: string) => {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
};
export function eraseCookie(name: string) {
document.cookie = name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
@ -116,435 +104,6 @@ export function truncateText(text: string, length = 50) {
return text;
}
export const getCaretCoordinates = () => {
let caretX, caretY;
const selection = window.getSelection();
if (selection && selection?.rangeCount !== 0) {
const range = selection.getRangeAt(0).cloneRange();
range.collapse(false);
const rect = range.getClientRects()[0];
if (rect) {
caretX = rect.left;
caretY = rect.top;
}
}
return { caretX, caretY };
};
export const getPrefixSuffix = (container: any) => {
let prefix = "";
let suffix = "";
if (window.getSelection) {
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
let range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(container!, 0);
prefix = range.toString();
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setEnd(container, container.childNodes.length);
suffix = range.toString();
console.log("prefix", prefix);
console.log("suffix", suffix);
}
}
return { prefix, suffix };
};
export const uid = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
export const setCaretToEnd = (element: HTMLElement) => {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(element);
range.collapse(false);
selection?.removeAllRanges();
selection?.addRange(range);
element.focus();
};
// return a color between a start and end color using a percentage
export const ColorTween = (
startColor: string,
endColor: string,
percent: number
) => {
// example startColor = "#ff0000" endColor = "#0000ff" percent = 0.5
const start = {
r: parseInt(startColor.substring(1, 3), 16),
g: parseInt(startColor.substring(3, 5), 16),
b: parseInt(startColor.substring(5, 7), 16),
};
const end = {
r: parseInt(endColor.substring(1, 3), 16),
g: parseInt(endColor.substring(3, 5), 16),
b: parseInt(endColor.substring(5, 7), 16),
};
const r = Math.floor(start.r + (end.r - start.r) * percent);
const g = Math.floor(start.g + (end.g - start.g) * percent);
const b = Math.floor(start.b + (end.b - start.b) * percent);
return `rgb(${r}, ${g}, ${b})`;
};
export const guid = () => {
var w = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
return `${w()}${w()}-${w()}-${w()}-${w()}-${w()}${w()}${w()}`;
};
/**
* Takes a string and returns the first n characters followed by asterisks.
* @param {string} str - The string to obscure
* @param {number} n - Number of characters to show before obscuring
* @returns {string} The obscured string with first n characters in clear text
*/
export const obscureString = (str: string, n: number = 3) => {
if (n < 0 || n > str.length) {
console.log("n cannot be less than 0 or greater than the string length.");
return str;
}
// First n characters in clear text
var clearText = str.substring(0, n);
// Remaining characters replaced with asterisks
var obscured = clearText + "*".repeat(str.length - n);
return obscured;
};
/**
* Converts a number of seconds into a human-readable string representing the duration in days, hours, minutes, and seconds.
* @param {number} seconds - The number of seconds to convert.
* @returns {string} A well-formatted duration string.
*/
export const formatDuration = (seconds: number) => {
const units = [
{ label: " day", seconds: 86400 },
{ label: " hr", seconds: 3600 },
{ label: " min", seconds: 60 },
{ label: " sec", seconds: 1 },
];
let remainingSeconds = seconds;
const parts = [];
for (const { label, seconds: unitSeconds } of units) {
const count = Math.floor(remainingSeconds / unitSeconds);
if (count > 0) {
parts.push(count + (count > 1 ? label + "s" : label));
remainingSeconds -= count * unitSeconds;
}
}
return parts.length > 0 ? parts.join(" ") : "0 sec";
};
export const sampleModelConfig = (modelType: string = "open_ai") => {
const openaiConfig: IModelConfig = {
model: "gpt-4-1106-preview",
api_type: "open_ai",
description: "OpenAI GPT-4 model",
};
const azureConfig: IModelConfig = {
model: "gpt-4",
api_type: "azure",
api_version: "v1",
base_url: "https://youazureendpoint.azure.com/",
description: "Azure model",
};
const googleConfig: IModelConfig = {
model: "gemini-1.0-pro",
api_type: "google",
description: "Google Gemini Model model",
};
const anthropicConfig: IModelConfig = {
model: "claude-3-5-sonnet-20240620",
api_type: "anthropic",
description: "Claude 3.5 Sonnet model",
};
const mistralConfig: IModelConfig = {
model: "mistral",
api_type: "mistral",
description: "Mistral model",
};
switch (modelType) {
case "open_ai":
return openaiConfig;
case "azure":
return azureConfig;
case "google":
return googleConfig;
case "anthropic":
return anthropicConfig;
case "mistral":
return mistralConfig;
default:
return openaiConfig;
}
};
export const getRandomIntFromDateAndSalt = (salt: number = 43444) => {
const currentDate = new Date();
const seed = currentDate.getTime() + salt;
const randomValue = Math.sin(seed) * 10000;
const randomInt = Math.floor(randomValue) % 100;
return randomInt;
};
export const getSampleWorkflow = (workflow_type: string = "autonomous") => {
const autonomousWorkflow: IWorkflow = {
name: "Default Chat Workflow",
description: "Autonomous Workflow",
type: "autonomous",
summary_method: "llm",
};
const sequentialWorkflow: IWorkflow = {
name: "Default Sequential Workflow",
description: "Sequential Workflow",
type: "sequential",
summary_method: "llm",
};
if (workflow_type === "autonomous") {
return autonomousWorkflow;
} else if (workflow_type === "sequential") {
return sequentialWorkflow;
} else {
return autonomousWorkflow;
}
};
export const sampleAgentConfig = (agent_type: string = "assistant") => {
const llm_config: ILLMConfig = {
config_list: [],
temperature: 0.1,
timeout: 600,
cache_seed: null,
max_tokens: 4000,
};
const userProxyConfig: IAgentConfig = {
name: "userproxy",
human_input_mode: "NEVER",
description: "User Proxy",
max_consecutive_auto_reply: 25,
system_message: "You are a helpful assistant.",
default_auto_reply: "TERMINATE",
llm_config: false,
code_execution_config: "local",
};
const userProxyFlowSpec: IAgent = {
type: "userproxy",
config: userProxyConfig,
};
const assistantConfig: IAgentConfig = {
name: "primary_assistant",
description: "Primary Assistant",
llm_config: llm_config,
human_input_mode: "NEVER",
max_consecutive_auto_reply: 25,
code_execution_config: "none",
system_message:
"You are a helpful AI assistant. Solve tasks using your coding and language skills. In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the user to execute. 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly. Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill. When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user. If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. Reply 'TERMINATE' in the end when everything is done.",
};
const assistantFlowSpec: IAgent = {
type: "assistant",
config: assistantConfig,
};
const groupChatAssistantConfig = Object.assign(
{
admin_name: "groupchat_assistant",
messages: [],
max_round: 10,
speaker_selection_method: "auto",
allow_repeat_speaker: false,
},
assistantConfig
);
groupChatAssistantConfig.name = "groupchat_assistant";
groupChatAssistantConfig.system_message =
"You are a helpful assistant skilled at cordinating a group of other assistants to solve a task. ";
groupChatAssistantConfig.description = "Group Chat Assistant";
const groupChatFlowSpec: IAgent = {
type: "groupchat",
config: groupChatAssistantConfig,
};
if (agent_type === "userproxy") {
return userProxyFlowSpec;
} else if (agent_type === "assistant") {
return assistantFlowSpec;
} else if (agent_type === "groupchat") {
return groupChatFlowSpec;
} else {
return assistantFlowSpec;
}
};
export const getSampleSkill = () => {
const content = `
from typing import List
import uuid
import requests # to perform HTTP requests
from pathlib import Path
from openai import OpenAI
def generate_and_save_images(query: str, image_size: str = "1024x1024") -> List[str]:
"""
Function to paint, draw or illustrate images based on the users query or request. Generates images from a given query using OpenAI's DALL-E model and saves them to disk. Use the code below anytime there is a request to create an image.
:param query: A natural language description of the image to be generated.
:param image_size: The size of the image to be generated. (default is "1024x1024")
:return: A list of filenames for the saved images.
"""
client = OpenAI() # Initialize the OpenAI client
response = client.images.generate(model="dall-e-3", prompt=query, n=1, size=image_size) # Generate images
# List to store the file names of saved images
saved_files = []
# Check if the response is successful
if response.data:
for image_data in response.data:
# Generate a random UUID as the file name
file_name = str(uuid.uuid4()) + ".png" # Assuming the image is a PNG
file_path = Path(file_name)
img_url = image_data.url
img_response = requests.get(img_url)
if img_response.status_code == 200:
# Write the binary content to a file
with open(file_path, "wb") as img_file:
img_file.write(img_response.content)
print(f"Image saved to {file_path}")
saved_files.append(str(file_path))
else:
print(f"Failed to download the image from {img_url}")
else:
print("No image data found in the response!")
# Return the list of saved files
return saved_files
# Example usage of the function:
# generate_and_save_images("A cute baby sea otter")
`;
const skill: ISkill = {
name: "generate_and_save_images",
description: "Generate and save images based on a user's query.",
content: content,
};
return skill;
};
export const timeAgo = (
dateString: string,
returnFormatted: boolean = false
): string => {
// if dateStr is empty, return empty string
if (!dateString) {
return "";
}
// Parse the date string into a Date object
const timestamp = new Date(dateString);
// Check for invalid date
if (isNaN(timestamp.getTime())) {
throw new Error("Invalid date string provided.");
}
// Get the current time
const now = new Date();
// Calculate the difference in milliseconds
const timeDifference = now.getTime() - timestamp.getTime();
// Convert time difference to minutes and hours
const minutesAgo = Math.floor(timeDifference / (1000 * 60));
const hoursAgo = Math.floor(minutesAgo / 60);
// Format the date into a readable format e.g. "November 27, 2021, 3:45 PM"
const options: Intl.DateTimeFormatOptions = {
month: "long",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
};
const formattedDate = timestamp.toLocaleDateString(undefined, options);
if (returnFormatted) {
return formattedDate;
}
// Determine the time difference string
let timeAgoStr: string;
if (minutesAgo < 1) {
timeAgoStr = "just now";
} else if (minutesAgo < 60) {
// Less than an hour ago, display minutes
timeAgoStr = `${minutesAgo} ${minutesAgo === 1 ? "minute" : "minutes"} ago`;
} else if (hoursAgo < 24) {
// Less than a day ago, display hours
timeAgoStr = `${hoursAgo} ${hoursAgo === 1 ? "hour" : "hours"} ago`;
} else {
// More than a day ago, display the formatted date
timeAgoStr = formattedDate;
}
// Return the final readable string
return timeAgoStr;
};
export const examplePrompts = [
{
title: "Stock Price",
prompt:
"Plot a chart of NVDA and TESLA stock price for 2023. Save the result to a file named nvda_tesla.png",
},
{
title: "Sine Wave",
prompt:
"Write a python script to plot a sine wave and save it to disc as a png file sine_wave.png",
},
{
title: "Markdown",
prompt:
"List out the top 5 rivers in africa and their length and return that as a markdown table. Do not try to write any code, just write the table",
},
{
title: "Paint",
prompt:
"paint a picture of a glass of ethiopian coffee, freshly brewed in a tall glass cup, on a table right in front of a lush green forest scenery",
},
{
title: "Travel",
prompt:
"Plan a 2 day trip to hawaii. Limit to 3 activities per day, be as brief as possible!",
},
];
export const fetchVersion = () => {
const versionUrl = getServerUrl() + "/version";
return fetch(versionUrl)
@ -557,128 +116,3 @@ export const fetchVersion = () => {
return null;
});
};
/**
* Recursively sanitizes JSON objects by replacing specific keys with a given value.
* @param {JsonValue} data - The JSON data to be sanitized.
* @param {string[]} keys - An array of keys to be replaced in the JSON object.
* @param {string} replacement - The value to use as replacement for the specified keys.
* @returns {JsonValue} - The sanitized JSON data.
*/
export const sanitizeConfig = (
data: any,
keys: string[] = ["api_key", "id", "created_at", "updated_at", "secrets"]
): any => {
if (Array.isArray(data)) {
return data.map((item) => sanitizeConfig(item, keys));
} else if (typeof data === "object" && data !== null) {
Object.keys(data).forEach((key) => {
if (keys.includes(key)) {
delete data[key];
} else {
data[key] = sanitizeConfig(data[key], keys);
}
});
}
return data;
};
/**
* Checks the input text against the regex '^[a-zA-Z0-9_-]{1,64}$' and returns an object with
* status, message, and sanitizedText. Status is boolean indicating whether input text is valid,
* message provides information about the outcome, and sanitizedText contains a valid version
* of the input text or the original text if it was already valid.
*
* @param text - The input string to be checked and sanitized.
* @returns An object containing a status, a message, and sanitizedText.
*/
export const checkAndSanitizeInput = (
text: string
): { status: boolean; message: string; sanitizedText: string } => {
// Create a regular expression pattern to match valid characters
const regexPattern: RegExp = /^[a-zA-Z0-9_-]{1,64}$/;
let status: boolean = true;
let message: string;
let sanitizedText: string;
// Check if the input text matches the pattern
if (regexPattern.test(text)) {
// Text already adheres to the pattern
message = `The text '${text}' is valid.`;
sanitizedText = text;
} else {
// The text does not match; sanitize the input
status = false;
sanitizedText = text.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
message = `'${text}' is invalid. Consider using '${sanitizedText}' instead.`;
}
return { status, message, sanitizedText };
};
export const isValidConfig = (
jsonObj: any,
templateObj: any,
diffThreshold: number = 4
): {
status: boolean;
message: string;
} => {
// Check if both parameters are indeed objects and not null
if (
typeof jsonObj !== "object" ||
jsonObj === null ||
Array.isArray(jsonObj) ||
typeof templateObj !== "object" ||
templateObj === null ||
Array.isArray(templateObj)
) {
return {
status: false,
message:
"Invalid input: One or both parameters are not objects, or are null or arrays.",
};
}
const jsonKeys = new Set(Object.keys(jsonObj));
const templateKeys = new Set(Object.keys(templateObj));
if (jsonKeys.size !== templateKeys.size) {
if (Math.abs(jsonKeys.size - templateKeys.size) > diffThreshold) {
return {
status: false,
message:
"Configuration does not match template: Number of keys differ.",
};
}
}
for (const key of templateKeys) {
if (!jsonKeys.has(key)) {
return {
status: false,
message: `Configuration does not match template: Missing key '${key}' in configuration.`,
};
}
// If the value is an object, recursively validate
if (
typeof templateObj[key] === "object" &&
templateObj[key] !== null &&
!Array.isArray(templateObj[key])
) {
const result = isValidConfig(jsonObj[key], templateObj[key]);
if (!result.status) {
return {
status: false,
message: `Configuration error in nested key '${key}': ${result.message}`,
};
}
}
}
return {
status: true,
message: "Configuration is valid.",
};
};

View File

@ -1,385 +0,0 @@
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
DocumentDuplicateIcon,
InformationCircleIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Dropdown, MenuProps, Modal, message } from "antd";
import * as React from "react";
import { IAgent, IStatus } from "../../types";
import { appContext } from "../../../hooks/provider";
import {
fetchJSON,
getServerUrl,
sanitizeConfig,
timeAgo,
truncateText,
} from "../../utils";
import { BounceLoader, Card, CardHoverBar, LoadingOverlay } from "../../atoms";
import { AgentViewer } from "./utils/agentconfig";
const AgentsView = ({}: any) => {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const listAgentsUrl = `${serverUrl}/agents?user_id=${user?.email}`;
const [agents, setAgents] = React.useState<IAgent[] | null>([]);
const [selectedAgent, setSelectedAgent] = React.useState<IAgent | null>(null);
const [showNewAgentModal, setShowNewAgentModal] = React.useState(false);
const [showAgentModal, setShowAgentModal] = React.useState(false);
const sampleAgent = {
config: {
name: "sample_agent",
description: "Sample agent description",
human_input_mode: "NEVER",
max_consecutive_auto_reply: 3,
system_message: "",
},
};
const [newAgent, setNewAgent] = React.useState<IAgent | null>(sampleAgent);
const deleteAgent = (agent: IAgent) => {
setError(null);
setLoading(true);
const deleteAgentUrl = `${serverUrl}/agents/delete?user_id=${user?.email}&agent_id=${agent.id}`;
// const fetch;
const payLoad = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user?.email,
agent: agent,
}),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
fetchAgents();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(deleteAgentUrl, payLoad, onSuccess, onError);
};
const fetchAgents = () => {
setError(null);
setLoading(true);
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
setAgents(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(listAgentsUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user) {
// console.log("fetching messages", messages);
fetchAgents();
}
}, []);
const agentRows = (agents || []).map((agent: IAgent, i: number) => {
const cardItems = [
{
title: "Download",
icon: ArrowDownTrayIcon,
onClick: (e: any) => {
e.stopPropagation();
// download workflow as workflow.name.json
const element = document.createElement("a");
const sanitizedAgent = sanitizeConfig(agent);
const file = new Blob([JSON.stringify(sanitizedAgent)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
element.download = `agent_${agent.config.name}.json`;
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
},
hoverText: "Download",
},
{
title: "Make a Copy",
icon: DocumentDuplicateIcon,
onClick: (e: any) => {
e.stopPropagation();
let newAgent = { ...sanitizeConfig(agent) };
newAgent.config.name = `${agent.config.name}_copy`;
console.log("newAgent", newAgent);
setNewAgent(newAgent);
setShowNewAgentModal(true);
},
hoverText: "Make a Copy",
},
{
title: "Delete",
icon: TrashIcon,
onClick: (e: any) => {
e.stopPropagation();
deleteAgent(agent);
},
hoverText: "Delete",
},
];
return (
<li
role="listitem"
key={"agentrow" + i}
className=" "
style={{ width: "200px" }}
>
<Card
className="h-full p-2 cursor-pointer"
title={
<div className=" ">
{truncateText(agent.config.name || "", 25)}
</div>
}
onClick={() => {
setSelectedAgent(agent);
setShowAgentModal(true);
}}
>
<div
style={{ minHeight: "65px" }}
aria-hidden="true"
className="my-2 break-words"
>
<div className="text-xs mb-2">{agent.type}</div>{" "}
{truncateText(agent.config.description || "", 70)}
</div>
<div
aria-label={`Updated ${timeAgo(agent.updated_at || "")}`}
className="text-xs"
>
{timeAgo(agent.updated_at || "")}
</div>
<CardHoverBar items={cardItems} />
</Card>
</li>
);
});
const AgentModal = ({
agent,
setAgent,
showAgentModal,
setShowAgentModal,
handler,
}: {
agent: IAgent | null;
setAgent: (agent: IAgent | null) => void;
showAgentModal: boolean;
setShowAgentModal: (show: boolean) => void;
handler?: (agent: IAgent | null) => void;
}) => {
const [localAgent, setLocalAgent] = React.useState<IAgent | null>(agent);
const closeModal = () => {
setShowAgentModal(false);
if (handler) {
handler(localAgent);
}
};
return (
<Modal
title={<>Agent Configuration</>}
width={800}
open={showAgentModal}
onOk={() => {
closeModal();
}}
onCancel={() => {
closeModal();
}}
footer={[]}
>
{agent && (
<AgentViewer
agent={localAgent || agent}
setAgent={setLocalAgent}
close={closeModal}
/>
)}
{/* {JSON.stringify(localAgent)} */}
</Modal>
);
};
const uploadAgent = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e: any) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e: any) => {
const contents = e.target.result;
if (contents) {
try {
const agent = JSON.parse(contents);
// TBD validate that it is a valid agent
if (!agent.config) {
throw new Error(
"Invalid agent file. An agent must have a config"
);
}
setNewAgent(agent);
setShowNewAgentModal(true);
} catch (err) {
message.error(
"Invalid agent file. Please upload a valid agent file."
);
}
}
};
reader.readAsText(file);
};
input.click();
};
const agentsMenuItems: MenuProps["items"] = [
// {
// type: "divider",
// },
{
key: "uploadagent",
label: (
<div>
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
Upload Agent
</div>
),
},
];
const agentsMenuItemOnClick: MenuProps["onClick"] = ({ key }) => {
if (key === "uploadagent") {
uploadAgent();
return;
}
};
return (
<div className="text-primary ">
<AgentModal
agent={selectedAgent}
setAgent={setSelectedAgent}
setShowAgentModal={setShowAgentModal}
showAgentModal={showAgentModal}
handler={(agent: IAgent | null) => {
fetchAgents();
}}
/>
<AgentModal
agent={newAgent || sampleAgent}
setAgent={setNewAgent}
setShowAgentModal={setShowNewAgentModal}
showAgentModal={showNewAgentModal}
handler={(agent: IAgent | null) => {
fetchAgents();
}}
/>
<div className="mb-2 relative">
<div className=" rounded ">
<div className="flex mt-2 pb-2 mb-2 border-b">
<div className="flex-1 font-semibold mb-2 ">
{" "}
Agents ({agentRows.length}){" "}
</div>
<div>
<Dropdown.Button
type="primary"
menu={{
items: agentsMenuItems,
onClick: agentsMenuItemOnClick,
}}
placement="bottomRight"
trigger={["click"]}
onClick={() => {
setShowNewAgentModal(true);
}}
>
<PlusIcon className="w-5 h-5 inline-block mr-1" />
New Agent
</Dropdown.Button>
</div>
</div>
<div className="text-xs mb-2 pb-1 ">
{" "}
Configure an agent that can reused in your agent workflow .
<div>
Tip: You can also create a Group of Agents ( New Agent -
GroupChat) which can have multiple agents in it.
</div>
</div>
{agents && agents.length > 0 && (
<div className="w-full relative">
<LoadingOverlay loading={loading} />
<ul className=" flex flex-wrap gap-3">{agentRows}</ul>
</div>
)}
{agents && agents.length === 0 && !loading && (
<div className="text-sm border mt-4 rounded text-secondary p-2">
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
No agents found. Please create a new agent.
</div>
)}
{loading && (
<div className=" w-full text-center">
{" "}
<BounceLoader />{" "}
<span className="inline-block"> loading .. </span>
</div>
)}
</div>
</div>
</div>
);
};
export default AgentsView;

View File

@ -1,81 +0,0 @@
import * as React from "react";
import SkillsView from "./skills";
import AgentsView from "./agents";
import WorkflowView from "./workflow";
import { Tabs } from "antd";
import {
BugAntIcon,
CpuChipIcon,
Square2StackIcon,
Square3Stack3DIcon,
} from "@heroicons/react/24/outline";
import ModelsView from "./models";
const BuildView = () => {
return (
<div className=" ">
{/* <div className="mb-4 text-2xl">Build </div> */}
<div className="mb-6 text-sm hidden text-secondary">
{" "}
Create skills, agents and workflows for building multiagent capabilities{" "}
</div>
<div className="mb-4 text-primary">
{" "}
<Tabs
tabBarStyle={{ paddingLeft: 0, marginLeft: 0 }}
defaultActiveKey="4"
tabPosition="left"
items={[
{
label: (
<div className="w-full ">
{" "}
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
Skills
</div>
),
key: "1",
children: <SkillsView />,
},
{
label: (
<div className="w-full ">
{" "}
<CpuChipIcon className="h-4 w-4 inline-block mr-1" />
Models
</div>
),
key: "2",
children: <ModelsView />,
},
{
label: (
<>
<Square2StackIcon className="h-4 w-4 inline-block mr-1" />
Agents
</>
),
key: "3",
children: <AgentsView />,
},
{
label: (
<>
<Square3Stack3DIcon className="h-4 w-4 inline-block mr-1" />
Workflows
</>
),
key: "4",
children: <WorkflowView />,
},
]}
/>
</div>
<div></div>
</div>
);
};
export default BuildView;

View File

@ -1,403 +0,0 @@
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
DocumentDuplicateIcon,
InformationCircleIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Dropdown, MenuProps, Modal, message } from "antd";
import * as React from "react";
import { IModelConfig, IStatus } from "../../types";
import { appContext } from "../../../hooks/provider";
import {
fetchJSON,
getServerUrl,
sanitizeConfig,
timeAgo,
truncateText,
} from "../../utils";
import { BounceLoader, Card, CardHoverBar, LoadingOverlay } from "../../atoms";
import { ModelConfigView } from "./utils/modelconfig";
const ModelsView = ({}: any) => {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const listModelsUrl = `${serverUrl}/models?user_id=${user?.email}`;
const createModelUrl = `${serverUrl}/models`;
const testModelUrl = `${serverUrl}/models/test`;
const defaultModel: IModelConfig = {
model: "gpt-4-1106-preview",
description: "Sample OpenAI GPT-4 model",
user_id: user?.email,
};
const [models, setModels] = React.useState<IModelConfig[] | null>([]);
const [selectedModel, setSelectedModel] = React.useState<IModelConfig | null>(
null
);
const [newModel, setNewModel] = React.useState<IModelConfig | null>(
defaultModel
);
const [showNewModelModal, setShowNewModelModal] = React.useState(false);
const [showModelModal, setShowModelModal] = React.useState(false);
const deleteModel = (model: IModelConfig) => {
setError(null);
setLoading(true);
const deleteModelUrl = `${serverUrl}/models/delete?user_id=${user?.email}&model_id=${model.id}`;
const payLoad = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
fetchModels();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(deleteModelUrl, payLoad, onSuccess, onError);
};
const fetchModels = () => {
setError(null);
setLoading(true);
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
setModels(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(listModelsUrl, payLoad, onSuccess, onError);
};
const createModel = (model: IModelConfig) => {
setError(null);
setLoading(true);
model.user_id = user?.email;
const payLoad = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(model),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
const updatedModels = [data.data].concat(models || []);
setModels(updatedModels);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(createModelUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user) {
// console.log("fetching messages", messages);
fetchModels();
}
}, []);
const modelRows = (models || []).map((model: IModelConfig, i: number) => {
const cardItems = [
{
title: "Download",
icon: ArrowDownTrayIcon,
onClick: (e: any) => {
e.stopPropagation();
// download workflow as workflow.name.json
const element = document.createElement("a");
const sanitizedSkill = sanitizeConfig(model);
const file = new Blob([JSON.stringify(sanitizedSkill)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
element.download = `model_${model.model}.json`;
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
},
hoverText: "Download",
},
{
title: "Make a Copy",
icon: DocumentDuplicateIcon,
onClick: (e: any) => {
e.stopPropagation();
let newModel = { ...sanitizeConfig(model) };
newModel.model = `${model.model}_copy`;
setNewModel(newModel);
setShowNewModelModal(true);
},
hoverText: "Make a Copy",
},
{
title: "Delete",
icon: TrashIcon,
onClick: (e: any) => {
e.stopPropagation();
deleteModel(model);
},
hoverText: "Delete",
},
];
return (
<li
role="listitem"
key={"modelrow" + i}
className=" "
style={{ width: "200px" }}
>
<Card
className="h-full p-2 cursor-pointer"
title={
<div className=" ">{truncateText(model.model || "", 20)}</div>
}
onClick={() => {
setSelectedModel(model);
setShowModelModal(true);
}}
>
<div style={{ minHeight: "65px" }} className="my-2 break-words">
{" "}
{truncateText(model.description || model.model || "", 70)}
</div>
<div
aria-label={`Updated ${timeAgo(model.updated_at || "")} `}
className="text-xs"
>
{timeAgo(model.updated_at || "")}
</div>
<CardHoverBar items={cardItems} />
</Card>
</li>
);
});
const ModelModal = ({
model,
setModel,
showModelModal,
setShowModelModal,
handler,
}: {
model: IModelConfig;
setModel: (model: IModelConfig | null) => void;
showModelModal: boolean;
setShowModelModal: (show: boolean) => void;
handler?: (agent: IModelConfig) => void;
}) => {
const [localModel, setLocalModel] = React.useState<IModelConfig>(model);
const closeModal = () => {
setModel(null);
setShowModelModal(false);
if (handler) {
handler(model);
}
};
return (
<Modal
title={
<>
Model Specification{" "}
<span className="text-accent font-normal">{model?.model}</span>{" "}
</>
}
width={800}
open={showModelModal}
footer={[]}
onOk={() => {
closeModal();
}}
onCancel={() => {
closeModal();
}}
>
{model && (
<ModelConfigView
model={localModel}
setModel={setLocalModel}
close={closeModal}
/>
)}
</Modal>
);
};
const uploadModel = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e: any) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e: any) => {
const contents = e.target.result;
if (contents) {
try {
const model = JSON.parse(contents);
if (model) {
setNewModel(model);
setShowNewModelModal(true);
}
} catch (e) {
message.error("Invalid model file");
}
}
};
reader.readAsText(file);
};
input.click();
};
const modelsMenuItems: MenuProps["items"] = [
// {
// type: "divider",
// },
{
key: "uploadmodel",
label: (
<div>
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
Upload Model
</div>
),
},
];
const modelsMenuItemOnClick: MenuProps["onClick"] = ({ key }) => {
if (key === "uploadmodel") {
uploadModel();
return;
}
};
return (
<div className="text-primary ">
{selectedModel && (
<ModelModal
model={selectedModel}
setModel={setSelectedModel}
setShowModelModal={setShowModelModal}
showModelModal={showModelModal}
handler={(model: IModelConfig | null) => {
fetchModels();
}}
/>
)}
<ModelModal
model={newModel || defaultModel}
setModel={setNewModel}
setShowModelModal={setShowNewModelModal}
showModelModal={showNewModelModal}
handler={(model: IModelConfig | null) => {
fetchModels();
}}
/>
<div className="mb-2 relative">
<div className=" rounded ">
<div className="flex mt-2 pb-2 mb-2 border-b">
<div className="flex-1 font-semibold mb-2 ">
{" "}
Models ({modelRows.length}){" "}
</div>
<div>
<Dropdown.Button
type="primary"
menu={{
items: modelsMenuItems,
onClick: modelsMenuItemOnClick,
}}
placement="bottomRight"
trigger={["click"]}
onClick={() => {
setShowNewModelModal(true);
}}
>
<PlusIcon className="w-5 h-5 inline-block mr-1" />
New Model
</Dropdown.Button>
</div>
</div>
<div className="text-xs mb-2 pb-1 ">
{" "}
Create model configurations that can be reused in your agents and
workflows. {selectedModel?.model}
</div>
{models && models.length > 0 && (
<div className="w-full relative">
<LoadingOverlay loading={loading} />
<ul className=" flex flex-wrap gap-3">{modelRows}</ul>
</div>
)}
{models && models.length === 0 && !loading && (
<div className="text-sm border mt-4 rounded text-secondary p-2">
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
No models found. Please create a new model which can be reused
with agents.
</div>
)}
{loading && (
<div className=" w-full text-center">
{" "}
<BounceLoader />{" "}
<span className="inline-block"> loading .. </span>
</div>
)}
</div>
</div>
</div>
);
};
export default ModelsView;

View File

@ -1,380 +0,0 @@
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
CodeBracketIcon,
CodeBracketSquareIcon,
DocumentDuplicateIcon,
InformationCircleIcon,
KeyIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Button, Input, Modal, message, MenuProps, Dropdown, Tabs } from "antd";
import * as React from "react";
import { ISkill, IStatus } from "../../types";
import { appContext } from "../../../hooks/provider";
import {
fetchJSON,
getSampleSkill,
getServerUrl,
sanitizeConfig,
timeAgo,
truncateText,
} from "../../utils";
import {
BounceLoader,
Card,
CardHoverBar,
LoadingOverlay,
MonacoEditor,
} from "../../atoms";
import { SkillSelector } from "./utils/selectors";
import { SkillConfigView } from "./utils/skillconfig";
const SkillsView = ({}: any) => {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const listSkillsUrl = `${serverUrl}/skills?user_id=${user?.email}`;
const saveSkillsUrl = `${serverUrl}/skills`;
const [skills, setSkills] = React.useState<ISkill[] | null>([]);
const [selectedSkill, setSelectedSkill] = React.useState<any>(null);
const [showSkillModal, setShowSkillModal] = React.useState(false);
const [showNewSkillModal, setShowNewSkillModal] = React.useState(false);
const sampleSkill = getSampleSkill();
const [newSkill, setNewSkill] = React.useState<ISkill | null>(sampleSkill);
const deleteSkill = (skill: ISkill) => {
setError(null);
setLoading(true);
// const fetch;
const deleteSkillUrl = `${serverUrl}/skills/delete?user_id=${user?.email}&skill_id=${skill.id}`;
const payLoad = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user?.email,
skill: skill,
}),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
fetchSkills();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(deleteSkillUrl, payLoad, onSuccess, onError);
};
const fetchSkills = () => {
setError(null);
setLoading(true);
// const fetch;
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
// message.success(data.message);
console.log("skills", data.data);
setSkills(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(listSkillsUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user) {
// console.log("fetching messages", messages);
fetchSkills();
}
}, []);
const skillRows = (skills || []).map((skill: ISkill, i: number) => {
const cardItems = [
{
title: "Download",
icon: ArrowDownTrayIcon,
onClick: (e: any) => {
e.stopPropagation();
// download workflow as workflow.name.json
const element = document.createElement("a");
const sanitizedSkill = sanitizeConfig(skill);
const file = new Blob([JSON.stringify(sanitizedSkill)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
element.download = `skill_${skill.name}.json`;
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
},
hoverText: "Download",
},
{
title: "Make a Copy",
icon: DocumentDuplicateIcon,
onClick: (e: any) => {
e.stopPropagation();
let newSkill = { ...sanitizeConfig(skill) };
newSkill.name = `${skill.name}_copy`;
setNewSkill(newSkill);
setShowNewSkillModal(true);
},
hoverText: "Make a Copy",
},
{
title: "Delete",
icon: TrashIcon,
onClick: (e: any) => {
e.stopPropagation();
deleteSkill(skill);
},
hoverText: "Delete",
},
];
return (
<li key={"skillrow" + i} className=" " style={{ width: "200px" }}>
<div>
{" "}
<Card
className="h-full p-2 cursor-pointer group"
title={truncateText(skill.name, 25)}
onClick={() => {
setSelectedSkill(skill);
setShowSkillModal(true);
}}
>
<div
style={{ minHeight: "65px" }}
className="my-2 break-words"
aria-hidden="true"
>
{" "}
{skill.description
? truncateText(skill.description || "", 70)
: truncateText(skill.content || "", 70)}
</div>
<div
aria-label={`Updated ${timeAgo(skill.updated_at || "")}`}
className="text-xs"
>
{timeAgo(skill.updated_at || "")}
</div>
<CardHoverBar items={cardItems} />
</Card>
<div className="text-right mt-2"></div>
</div>
</li>
);
});
const SkillModal = ({
skill,
setSkill,
showSkillModal,
setShowSkillModal,
handler,
}: {
skill: ISkill | null;
setSkill: any;
showSkillModal: boolean;
setShowSkillModal: any;
handler: any;
}) => {
const editorRef = React.useRef<any | null>(null);
const [localSkill, setLocalSkill] = React.useState<ISkill | null>(skill);
const closeModal = () => {
setSkill(null);
setShowSkillModal(false);
if (handler) {
handler(skill);
}
};
return (
<Modal
title={
<>
Skill Specification{" "}
<span className="text-accent font-normal">{localSkill?.name}</span>{" "}
</>
}
width={800}
open={showSkillModal}
onCancel={() => {
setShowSkillModal(false);
}}
footer={[]}
>
{localSkill && (
<SkillConfigView
skill={localSkill}
setSkill={setLocalSkill}
close={closeModal}
/>
)}
</Modal>
);
};
const uploadSkill = () => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".json";
fileInput.onchange = (e: any) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
if (content) {
try {
const skill = JSON.parse(content as string);
if (skill) {
setNewSkill(skill);
setShowNewSkillModal(true);
}
} catch (e) {
message.error("Invalid skill file");
}
}
};
reader.readAsText(file);
};
fileInput.click();
};
const skillsMenuItems: MenuProps["items"] = [
// {
// type: "divider",
// },
{
key: "uploadskill",
label: (
<div>
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
Upload Skill
</div>
),
},
];
const skillsMenuItemOnClick: MenuProps["onClick"] = ({ key }) => {
if (key === "uploadskill") {
uploadSkill();
return;
}
};
return (
<div className=" text-primary ">
<SkillModal
skill={selectedSkill}
setSkill={setSelectedSkill}
showSkillModal={showSkillModal}
setShowSkillModal={setShowSkillModal}
handler={(skill: ISkill) => {
fetchSkills();
}}
/>
<SkillModal
skill={newSkill || sampleSkill}
setSkill={setNewSkill}
showSkillModal={showNewSkillModal}
setShowSkillModal={setShowNewSkillModal}
handler={(skill: ISkill) => {
fetchSkills();
}}
/>
<div className="mb-2 relative">
<div className="">
<div className="flex mt-2 pb-2 mb-2 border-b">
<ul className="flex-1 font-semibold mb-2 ">
{" "}
Skills ({skillRows.length}){" "}
</ul>
<div>
<Dropdown.Button
type="primary"
menu={{
items: skillsMenuItems,
onClick: skillsMenuItemOnClick,
}}
placement="bottomRight"
trigger={["click"]}
onClick={() => {
setShowNewSkillModal(true);
}}
>
<PlusIcon className="w-5 h-5 inline-block mr-1" />
New Skill
</Dropdown.Button>
</div>
</div>
<div className="text-xs mb-2 pb-1 ">
{" "}
Skills are python functions that agents can use to solve tasks.{" "}
</div>
{skills && skills.length > 0 && (
<div
// style={{ height: "400px" }}
className="w-full relative"
>
<LoadingOverlay loading={loading} />
<div className=" flex flex-wrap gap-3">{skillRows}</div>
</div>
)}
{skills && skills.length === 0 && !loading && (
<div className="text-sm border mt-4 rounded text-secondary p-2">
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
No skills found. Please create a new skill.
</div>
)}
{loading && (
<div className=" w-full text-center">
{" "}
<BounceLoader />{" "}
<span className="inline-block"> loading .. </span>
</div>
)}
</div>
</div>
</div>
);
};
export default SkillsView;

View File

@ -1,517 +0,0 @@
import React from "react";
import { CollapseBox, ControlRowView } from "../../../atoms";
import { checkAndSanitizeInput, fetchJSON, getServerUrl } from "../../../utils";
import {
Button,
Form,
Input,
Select,
Slider,
Tabs,
message,
theme,
} from "antd";
import {
BugAntIcon,
CpuChipIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { appContext } from "../../../../hooks/provider";
import {
AgentSelector,
AgentTypeSelector,
ModelSelector,
SkillSelector,
} from "./selectors";
import { IAgent, ILLMConfig } from "../../../types";
import TextArea from "antd/es/input/TextArea";
const { useToken } = theme;
export const AgentConfigView = ({
agent,
setAgent,
close,
}: {
agent: IAgent;
setAgent: (agent: IAgent) => void;
close: () => void;
}) => {
const nameValidation = checkAndSanitizeInput(agent?.config?.name);
const [error, setError] = React.useState<any>(null);
const [loading, setLoading] = React.useState<boolean>(false);
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const createAgentUrl = `${serverUrl}/agents`;
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
const onControlChange = (value: any, key: string) => {
// if (key === "llm_config") {
// if (value.config_list.length === 0) {
// value = false;
// }
// }
const updatedAgent = {
...agent,
config: { ...agent.config, [key]: value },
};
setAgent(updatedAgent);
setControlChanged(true);
};
const llm_config: ILLMConfig = agent?.config?.llm_config || {
config_list: [],
temperature: 0.1,
max_tokens: 4000,
};
const createAgent = (agent: IAgent) => {
setError(null);
setLoading(true);
// const fetch;
console.log("agent", agent);
agent.user_id = user?.email;
const payLoad = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(agent),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
console.log("agents", data.data);
const newAgent = data.data;
setAgent(newAgent);
} else {
message.error(data.message);
}
setLoading(false);
// setNewAgent(sampleAgent);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
const onFinal = () => {
setLoading(false);
setControlChanged(false);
};
fetchJSON(createAgentUrl, payLoad, onSuccess, onError, onFinal);
};
const hasChanged =
(!controlChanged || !nameValidation.status) && agent?.id !== undefined;
return (
<div className="text-primary">
<Form>
<div
className={`grid gap-3 ${
agent.type === "groupchat" ? "grid-cols-2" : "grid-cols-1"
}`}
>
<div className="">
<ControlRowView
title="Agent Name"
className=""
description="Name of the agent"
value={agent?.config?.name}
control={
<>
<Input
className="mt-2"
placeholder="Agent Name"
value={agent?.config?.name}
onChange={(e) => {
onControlChange(e.target.value, "name");
}}
/>
{!nameValidation.status && (
<div className="text-xs text-red-500 mt-2">
{nameValidation.message}
</div>
)}
</>
}
/>
<ControlRowView
title="Agent Description"
className="mt-4"
description="Description of the agent, used by other agents
(e.g. the GroupChatManager) to decide when to call upon this agent. (Default: system_message)"
value={agent.config.description || ""}
control={
<Input
className="mt-2"
placeholder="Agent Description"
value={agent.config.description || ""}
onChange={(e) => {
onControlChange(e.target.value, "description");
}}
/>
}
/>
<ControlRowView
title="Max Consecutive Auto Reply"
className="mt-4"
description="Max consecutive auto reply messages before termination."
value={agent.config?.max_consecutive_auto_reply}
control={
<Slider
min={1}
max={agent.type === "groupchat" ? 600 : 30}
defaultValue={agent.config.max_consecutive_auto_reply}
step={1}
onChange={(value: any) => {
onControlChange(value, "max_consecutive_auto_reply");
}}
/>
}
/>
<ControlRowView
title="Human Input Mode"
description="Defines when to request human input"
value={agent.config.human_input_mode}
control={
<Select
className="mt-2 w-full"
defaultValue={agent.config.human_input_mode}
onChange={(value: any) => {
onControlChange(value, "human_input_mode");
}}
options={
[
{ label: "NEVER", value: "NEVER" },
{ label: "TERMINATE", value: "TERMINATE" },
{ label: "ALWAYS", value: "ALWAYS" },
] as any
}
/>
}
/>
<ControlRowView
title="System Message"
className="mt-4"
description="Free text to control agent behavior"
value={agent.config.system_message}
control={
<TextArea
className="mt-2 w-full"
value={agent.config.system_message}
rows={3}
onChange={(e) => {
onControlChange(e.target.value, "system_message");
}}
/>
}
/>
<div className="mt-4">
{" "}
<CollapseBox
className="bg-secondary mt-4"
open={false}
title="Advanced Options"
>
<ControlRowView
title="Temperature"
className="mt-4"
description="Defines the randomness of the agent's response."
value={llm_config.temperature}
control={
<Slider
min={0}
max={2}
step={0.1}
defaultValue={llm_config.temperature || 0.1}
onChange={(value: any) => {
const llm_config = {
...agent.config.llm_config,
temperature: value,
};
onControlChange(llm_config, "llm_config");
}}
/>
}
/>
<ControlRowView
title="Agent Default Auto Reply"
className="mt-4"
description="Default auto reply when no code execution or llm-based reply is generated."
value={agent.config.default_auto_reply || ""}
control={
<Input
className="mt-2"
placeholder="Agent Description"
value={agent.config.default_auto_reply || ""}
onChange={(e) => {
onControlChange(e.target.value, "default_auto_reply");
}}
/>
}
/>
<ControlRowView
title="Max Tokens"
description="Max tokens generated by LLM used in the agent's response."
value={llm_config.max_tokens}
className="mt-4"
control={
<Slider
min={100}
max={50000}
defaultValue={llm_config.max_tokens || 1000}
onChange={(value: any) => {
const llm_config = {
...agent.config.llm_config,
max_tokens: value,
};
onControlChange(llm_config, "llm_config");
}}
/>
}
/>
<ControlRowView
title="Code Execution Config"
className="mt-4"
description="Determines if and where code execution is done."
value={agent.config.code_execution_config || "none"}
control={
<Select
className="mt-2 w-full"
defaultValue={
agent.config.code_execution_config || "none"
}
onChange={(value: any) => {
onControlChange(value, "code_execution_config");
}}
options={
[
{ label: "None", value: "none" },
{ label: "Local", value: "local" },
{ label: "Docker", value: "docker" },
] as any
}
/>
}
/>
</CollapseBox>
</div>
</div>
{/* ====================== Group Chat Config ======================= */}
{agent.type === "groupchat" && (
<div>
<ControlRowView
title="Speaker Selection Method"
description="How the next speaker is selected"
className=""
value={agent?.config?.speaker_selection_method || "auto"}
control={
<Select
className="mt-2 w-full"
defaultValue={
agent?.config?.speaker_selection_method || "auto"
}
onChange={(value: any) => {
if (agent?.config) {
onControlChange(value, "speaker_selection_method");
}
}}
options={
[
{ label: "Auto", value: "auto" },
{ label: "Round Robin", value: "round_robin" },
{ label: "Random", value: "random" },
] as any
}
/>
}
/>
<ControlRowView
title="Admin Name"
className="mt-4"
description="Name of the admin of the group chat"
value={agent.config.admin_name || ""}
control={
<Input
className="mt-2"
placeholder="Agent Description"
value={agent.config.admin_name || ""}
onChange={(e) => {
onControlChange(e.target.value, "admin_name");
}}
/>
}
/>
<ControlRowView
title="Max Rounds"
className="mt-4"
description="Max rounds before termination."
value={agent.config?.max_round || 10}
control={
<Slider
min={10}
max={600}
defaultValue={agent.config.max_round}
step={1}
onChange={(value: any) => {
onControlChange(value, "max_round");
}}
/>
}
/>
<ControlRowView
title="Allow Repeat Speaker"
className="mt-4"
description="Allow the same speaker to speak multiple times in a row"
value={agent.config?.allow_repeat_speaker || false}
control={
<Select
className="mt-2 w-full"
defaultValue={agent.config.allow_repeat_speaker}
onChange={(value: any) => {
onControlChange(value, "allow_repeat_speaker");
}}
options={
[
{ label: "True", value: true },
{ label: "False", value: false },
] as any
}
/>
}
/>
</div>
)}
</div>
</Form>
<div className="w-full mt-4 text-right">
{" "}
{!hasChanged && (
<Button
type="primary"
onClick={() => {
createAgent(agent);
setAgent(agent);
}}
loading={loading}
>
{agent.id ? "Update Agent" : "Create Agent"}
</Button>
)}
<Button
className="ml-2"
key="close"
type="default"
onClick={() => {
close();
}}
>
Close
</Button>
</div>
</div>
);
};
export const AgentViewer = ({
agent,
setAgent,
close,
}: {
agent: IAgent | null;
setAgent: (newAgent: IAgent) => void;
close: () => void;
}) => {
let items = [
{
label: (
<div className="w-full ">
{" "}
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
Agent Configuration
</div>
),
key: "1",
children: (
<div>
{!agent?.type && (
<AgentTypeSelector agent={agent} setAgent={setAgent} />
)}
{agent?.type && agent && (
<AgentConfigView agent={agent} setAgent={setAgent} close={close} />
)}
</div>
),
},
];
if (agent) {
if (agent?.id) {
if (agent.type && agent.type === "groupchat") {
items.push({
label: (
<div className="w-full ">
{" "}
<UserGroupIcon className="h-4 w-4 inline-block mr-1" />
Agents
</div>
),
key: "2",
children: <AgentSelector agentId={agent?.id} />,
});
}
items.push({
label: (
<div className="w-full ">
{" "}
<CpuChipIcon className="h-4 w-4 inline-block mr-1" />
Models
</div>
),
key: "3",
children: <ModelSelector agentId={agent?.id} />,
});
items.push({
label: (
<>
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
Skills
</>
),
key: "4",
children: <SkillSelector agentId={agent?.id} />,
});
}
}
return (
<div className="text-primary">
{/* <RenderView viewIndex={currentViewIndex} /> */}
<Tabs
tabBarStyle={{ paddingLeft: 0, marginLeft: 0 }}
defaultActiveKey="1"
items={items}
/>
</div>
);
};

View File

@ -1,207 +0,0 @@
import { Button, Modal, message } from "antd";
import * as React from "react";
import { IWorkflow } from "../../../types";
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
import {
checkAndSanitizeInput,
fetchJSON,
getServerUrl,
sanitizeConfig,
} from "../../../utils";
import { appContext } from "../../../../hooks/provider";
import { CodeBlock } from "../../../atoms";
export const ExportWorkflowModal = ({
workflow,
show,
setShow,
}: {
workflow: IWorkflow | null;
show: boolean;
setShow: (show: boolean) => void;
}) => {
const serverUrl = getServerUrl();
const { user } = React.useContext(appContext);
const [error, setError] = React.useState<any>(null);
const [loading, setLoading] = React.useState<boolean>(false);
const [workflowDetails, setWorkflowDetails] = React.useState<any>(null);
const getWorkflowCode = (workflow: IWorkflow) => {
const workflowCode = `from autogenstudio import WorkflowManager
# load workflow from exported json workflow file.
workflow_manager = WorkflowManager(workflow="path/to/your/workflow_.json")
# run the workflow on a task
task_query = "What is the height of the Eiffel Tower?. Dont write code, just respond to the question."
workflow_manager.run(message=task_query)`;
return workflowCode;
};
const getCliWorkflowCode = (workflow: IWorkflow) => {
const workflowCode = `autogenstudio serve --workflow=workflow.json --port=5000
`;
return workflowCode;
};
const getGunicornWorkflowCode = (workflow: IWorkflow) => {
const workflowCode = `gunicorn -w $((2 * $(getconf _NPROCESSORS_ONLN) + 1)) --timeout 12600 -k uvicorn.workers.UvicornWorker autogenstudio.web.app:app --bind `;
return workflowCode;
};
const fetchWorkFlow = (workflow: IWorkflow) => {
setError(null);
setLoading(true);
// const fetch;
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const downloadWorkflowUrl = `${serverUrl}/workflows/export/${workflow.id}?user_id=${user?.email}`;
const onSuccess = (data: any) => {
if (data && data.status) {
setWorkflowDetails(data.data);
console.log("workflow details", data.data);
const sanitized_name =
checkAndSanitizeInput(workflow.name).sanitizedText || workflow.name;
const file_name = `workflow_${sanitized_name}.json`;
const workflowData = sanitizeConfig(data.data);
const file = new Blob([JSON.stringify(workflowData)], {
type: "application/json",
});
const downloadUrl = URL.createObjectURL(file);
const a = document.createElement("a");
a.href = downloadUrl;
a.download = file_name;
a.click();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(downloadWorkflowUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (workflow && workflow.id && show) {
// fetchWorkFlow(workflow.id);
console.log("workflow modal ... component loaded", workflow);
}
}, [show]);
return (
<Modal
title={
<>
Export Workflow
<span className="text-accent font-normal ml-2">
{workflow?.name}
</span>{" "}
</>
}
width={800}
open={show}
onOk={() => {
setShow(false);
}}
onCancel={() => {
setShow(false);
}}
footer={[]}
>
<div>
<div>
{" "}
You can use the following steps to start integrating your workflow
into your application.{" "}
</div>
{workflow && workflow.id && (
<>
<div className="flex mt-2 gap-3">
<div>
<div className="text-sm mt-2 mb-2 pb-1 font-bold">Step 1</div>
<div className="mt-2 mb-2 pb-1 text-xs">
Download your workflow as a JSON file by clicking the button
below.
</div>
<div className="text-sm mt-2 mb-2 pb-1">
<Button
type="primary"
loading={loading}
onClick={() => {
fetchWorkFlow(workflow);
}}
>
Download
<ArrowDownTrayIcon className="h-4 w-4 inline-block ml-2 -mt-1" />
</Button>
</div>
</div>
<div>
<div className="text-sm mt-2 mb-2 pb-1 font-bold">Step 2</div>
<div className=" mt-2 mb-2 pb-1 text-xs">
Copy the following code snippet and paste it into your
application to run your workflow on a task.
</div>
<div className="text-sm mt-2 mb-2 pb-1">
<CodeBlock
className="text-xs"
code={getWorkflowCode(workflow)}
language="python"
wrapLines={true}
/>
</div>
</div>
</div>
<div>
<div className="text-sm mt-2 mb-2 pb-1 font-bold">
Step 3 (Deploy)
</div>
<div className=" mt-2 mb-2 pb-1 text-xs">
You can also deploy your workflow as an API endpoint using the
autogenstudio python CLI.
</div>
<div className="text-sm mt-2 mb-2 pb-1">
<CodeBlock
className="text-xs"
code={getCliWorkflowCode(workflow)}
language="bash"
wrapLines={true}
/>
<div className="text-xs mt-2">
Note: this will start a endpoint on port 5000. You can change
the port by changing the port number. You can also scale this
using multiple workers (e.g., via an application server like
gunicorn) or wrap it in a docker container and deploy on a
cloud provider like Azure.
</div>
<CodeBlock
className="text-xs"
code={getGunicornWorkflowCode(workflow)}
language="bash"
wrapLines={true}
/>
</div>
</div>
</>
)}
</div>
</Modal>
);
};

View File

@ -1,388 +0,0 @@
import React from "react";
import { fetchJSON, getServerUrl, sampleModelConfig } from "../../../utils";
import { Button, Input, message, theme } from "antd";
import {
CpuChipIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { IModelConfig, IStatus } from "../../../types";
import { Card, ControlRowView } from "../../../atoms";
import TextArea from "antd/es/input/TextArea";
import { appContext } from "../../../../hooks/provider";
const ModelTypeSelector = ({
model,
setModel,
}: {
model: IModelConfig;
setModel: (newModel: IModelConfig) => void;
}) => {
const modelTypes = [
{
label: "OpenAI",
value: "open_ai",
description: "OpenAI or other endpoints that implement the OpenAI API",
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
hint: "In addition to OpenAI models, You can also use OSS models via tools like Ollama, vLLM, LMStudio etc. that provide OpenAI compatible endpoint.",
},
{
label: "Azure OpenAI",
value: "azure",
description: "Azure OpenAI endpoint",
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
hint: "Azure OpenAI endpoint",
},
{
label: "Gemini",
value: "google",
description: "Gemini",
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
hint: "Gemini",
},
{
label: "Claude",
value: "anthropic",
description: "Anthropic Claude",
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
hint: "Anthropic Claude models",
},
{
label: "Mistral",
value: "mistral",
description: "Mistral",
icon: <CpuChipIcon className="h-6 w-6 text-primary" />,
hint: "Mistral models",
},
];
const [selectedType, setSelectedType] = React.useState<string | undefined>(
model?.api_type
);
const modelTypeRows = modelTypes.map((modelType: any, i: number) => {
return (
<li
onMouseEnter={() => {
setSelectedHint(modelType.hint);
}}
role="listitem"
key={"modeltype" + i}
className="w-36"
>
<Card
active={selectedType === modelType.value}
className="h-full p-2 cursor-pointer"
title={<div className=" ">{modelType.label}</div>}
onClick={() => {
setSelectedType(modelType.value);
if (model) {
const sampleModel = sampleModelConfig(modelType.value);
setModel(sampleModel);
// setAgent(sampleAgent);
}
}}
>
<div style={{ minHeight: "35px" }} className="my-2 break-words ">
{" "}
<div className="mb-2">{modelType.icon}</div>
<span className="text-secondary tex-sm">
{" "}
{modelType.description}
</span>
</div>
</Card>
</li>
);
});
const [selectedHint, setSelectedHint] = React.useState<string>("open_ai");
return (
<>
<div className="pb-3">Select Model Type</div>
<ul className="inline-flex gap-2">{modelTypeRows}</ul>
<div className="text-xs mt-4">
<InformationCircleIcon className="h-4 w-4 inline mr-1 -mt-1" />
{selectedHint}
</div>
</>
);
};
const ModelConfigMainView = ({
model,
setModel,
close,
}: {
model: IModelConfig;
setModel: (newModel: IModelConfig) => void;
close: () => void;
}) => {
const [loading, setLoading] = React.useState(false);
const [modelStatus, setModelStatus] = React.useState<IStatus | null>(null);
const serverUrl = getServerUrl();
const { user } = React.useContext(appContext);
const testModelUrl = `${serverUrl}/models/test`;
const createModelUrl = `${serverUrl}/models`;
// const [model, setmodel] = React.useState<IModelConfig | null>(
// model
// );
const testModel = (model: IModelConfig) => {
setModelStatus(null);
setLoading(true);
model.user_id = user?.email;
const payLoad = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(model),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
setModelStatus(data.data);
} else {
message.error(data.message);
}
setLoading(false);
setModelStatus(data);
};
const onError = (err: any) => {
message.error(err.message);
setLoading(false);
};
fetchJSON(testModelUrl, payLoad, onSuccess, onError);
};
const createModel = (model: IModelConfig) => {
setLoading(true);
model.user_id = user?.email;
const payLoad = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(model),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
setModel(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
message.error(err.message);
setLoading(false);
};
const onFinal = () => {
setLoading(false);
setControlChanged(false);
};
fetchJSON(createModelUrl, payLoad, onSuccess, onError, onFinal);
};
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
const updateModelConfig = (key: string, value: string) => {
if (model) {
const updatedModelConfig = { ...model, [key]: value };
// setmodel(updatedModelConfig);
setModel(updatedModelConfig);
}
setControlChanged(true);
};
const hasChanged = !controlChanged && model.id !== undefined;
return (
<div className="relative ">
<div className="text-sm my-2">
Enter parameters for your{" "}
<span className="mx-1 text-accent">{model.api_type}</span> model.
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<ControlRowView
title="Model"
className=""
description="Model name"
value={model?.model || ""}
control={
<Input
className="mt-2 w-full"
value={model?.model}
onChange={(e) => {
updateModelConfig("model", e.target.value);
}}
/>
}
/>
<ControlRowView
title="Base URL"
className=""
description="Base URL for Model Endpoint"
value={model?.base_url || ""}
control={
<Input
className="mt-2 w-full"
value={model?.base_url}
onChange={(e) => {
updateModelConfig("base_url", e.target.value);
}}
/>
}
/>
</div>
<div>
<ControlRowView
title="API Key"
className=""
description="API Key"
value={model?.api_key || ""}
truncateLength={5}
control={
<Input.Password
className="mt-2 w-full"
value={model?.api_key}
onChange={(e) => {
updateModelConfig("api_key", e.target.value);
}}
/>
}
/>
{model?.api_type == "azure" && (
<ControlRowView
title="API Version"
className=" "
description="API Version, required by Azure Models"
value={model?.api_version || ""}
control={
<Input
className="mt-2 w-full"
value={model?.api_version}
onChange={(e) => {
updateModelConfig("api_version", e.target.value);
}}
/>
}
/>
)}
</div>
</div>
<ControlRowView
title="Description"
className="mt-4"
description="Description of the model"
value={model?.description || ""}
control={
<TextArea
className="mt-2 w-full"
value={model?.description}
onChange={(e) => {
updateModelConfig("description", e.target.value);
}}
/>
}
/>
{model?.api_type === "azure" && (
<div className="mt-4 text-xs">
Note: For Azure OAI models, you will need to specify all fields.
</div>
)}
{modelStatus && (
<div
className={`text-sm border mt-4 rounded text-secondary p-2 ${
modelStatus.status ? "border-accent" : " border-red-500 "
}`}
>
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
{modelStatus.message}
{/* <span className="block"> Note </span> */}
</div>
)}
<div className="w-full mt-4 text-right">
<Button
key="test"
type="primary"
loading={loading}
onClick={() => {
if (model) {
testModel(model);
}
}}
>
Test Model
</Button>
{!hasChanged && (
<Button
className="ml-2"
key="save"
type="primary"
onClick={() => {
if (model) {
createModel(model);
setModel(model);
}
}}
>
{model?.id ? "Update Model" : "Save Model"}
</Button>
)}
<Button
className="ml-2"
key="close"
type="default"
onClick={() => {
close();
}}
>
Close
</Button>
</div>
</div>
);
};
export const ModelConfigView = ({
model,
setModel,
close,
}: {
model: IModelConfig;
setModel: (newModel: IModelConfig) => void;
close: () => void;
}) => {
return (
<div className="text-primary">
<div>
{!model?.api_type && (
<ModelTypeSelector model={model} setModel={setModel} />
)}
{model?.api_type && model && (
<ModelConfigMainView
model={model}
setModel={setModel}
close={close}
/>
)}
</div>
</div>
);
};

View File

@ -1,295 +0,0 @@
import React from "react";
import { fetchJSON, getServerUrl, sampleModelConfig } from "../../../utils";
import { Button, Input, message, theme } from "antd";
import {
CpuChipIcon,
EyeIcon,
EyeSlashIcon,
InformationCircleIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { ISkill, IStatus } from "../../../types";
import { Card, ControlRowView, MonacoEditor } from "../../../atoms";
import TextArea from "antd/es/input/TextArea";
import { appContext } from "../../../../hooks/provider";
const SecretsEditor = ({
secrets = [],
updateSkillConfig,
}: {
secrets: { secret: string; value: string }[];
updateSkillConfig: (key: string, value: any) => void;
}) => {
const [editingIndex, setEditingIndex] = React.useState<number | null>(null);
const [newSecret, setNewSecret] = React.useState<string>("");
const [newValue, setNewValue] = React.useState<string>("");
const toggleEditing = (index: number) => {
setEditingIndex(editingIndex === index ? null : index);
};
const handleAddSecret = () => {
if (newSecret && newValue) {
const updatedSecrets = [
...secrets,
{ secret: newSecret, value: newValue },
];
updateSkillConfig("secrets", updatedSecrets);
setNewSecret("");
setNewValue("");
}
};
const handleRemoveSecret = (index: number) => {
const updatedSecrets = secrets.filter((_, i) => i !== index);
updateSkillConfig("secrets", updatedSecrets);
};
const handleSecretChange = (index: number, key: string, value: string) => {
const updatedSecrets = secrets.map((item, i) =>
i === index ? { ...item, [key]: value } : item
);
updateSkillConfig("secrets", updatedSecrets);
};
return (
<div className="mt-4">
{secrets && (
<div className="flex flex-col gap-2">
{secrets.map((secret, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={secret.secret}
disabled={editingIndex !== index}
onChange={(e) =>
handleSecretChange(index, "secret", e.target.value)
}
className="flex-1"
/>
<Input.Password
value={secret.value}
visibilityToggle
disabled={editingIndex !== index}
onChange={(e) =>
handleSecretChange(index, "value", e.target.value)
}
className="flex-1"
/>
<Button
icon={
editingIndex === index ? (
<EyeSlashIcon className="h-5 w-5" />
) : (
<EyeIcon className="h-5 w-5" />
)
}
onClick={() => toggleEditing(index)}
/>
<Button
icon={<TrashIcon className="h-5 w-5" />}
onClick={() => handleRemoveSecret(index)}
/>
</div>
))}
</div>
)}
<div className="flex items-center gap-2 mt-2">
<Input
placeholder="New Secret"
value={newSecret}
onChange={(e) => setNewSecret(e.target.value)}
className="flex-1"
/>
<Input.Password
placeholder="New Value"
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
className="flex-1"
/>
<Button
icon={<PlusIcon className="h-5 w-5" />}
onClick={handleAddSecret}
/>
</div>
</div>
);
};
export const SkillConfigView = ({
skill,
setSkill,
close,
}: {
skill: ISkill;
setSkill: (newModel: ISkill) => void;
close: () => void;
}) => {
const [loading, setLoading] = React.useState(false);
const serverUrl = getServerUrl();
const { user } = React.useContext(appContext);
const testModelUrl = `${serverUrl}/skills/test`;
const createSkillUrl = `${serverUrl}/skills`;
const createSkill = (skill: ISkill) => {
setLoading(true);
skill.user_id = user?.email;
const payLoad = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(skill),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
setSkill(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
message.error(err.message);
setLoading(false);
};
const onFinal = () => {
setLoading(false);
setControlChanged(false);
};
fetchJSON(createSkillUrl, payLoad, onSuccess, onError, onFinal);
};
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
const updateSkillConfig = (key: string, value: string) => {
if (skill) {
const updatedSkill = { ...skill, [key]: value };
// setSkill(updatedModelConfig);
setSkill(updatedSkill);
}
setControlChanged(true);
};
const hasChanged = !controlChanged && skill.id !== undefined;
const editorRef = React.useRef<any | null>(null);
return (
<div className="relative ">
{skill && (
<div style={{ minHeight: "65vh" }}>
<div className="flex gap-3">
<div className="h-ful flex-1 ">
<div className="mb-2 h-full" style={{ minHeight: "65vh" }}>
<div className="h-full mt-2" style={{ height: "65vh" }}>
<MonacoEditor
value={skill?.content}
language="python"
editorRef={editorRef}
onChange={(value: string) => {
updateSkillConfig("content", value);
}}
/>
</div>
</div>
</div>
<div className="w-72 ">
<div className="">
<ControlRowView
title="Name"
className=""
description="Skill name, should match function name"
value={skill?.name || ""}
control={
<Input
className="mt-2 w-full"
value={skill?.name}
onChange={(e) => {
updateSkillConfig("name", e.target.value);
}}
/>
}
/>
<ControlRowView
title="Description"
className="mt-4"
description="Description of the skill"
value={skill?.description || ""}
control={
<TextArea
className="mt-2 w-full"
value={skill?.description}
onChange={(e) => {
updateSkillConfig("description", e.target.value);
}}
/>
}
/>
<ControlRowView
title="Secrets"
className="mt-4"
description="Environment variables"
value=""
control={
<SecretsEditor
secrets={skill?.secrets || []}
updateSkillConfig={updateSkillConfig}
/>
}
/>
</div>
</div>
</div>
</div>
)}
<div className="w-full mt-4 text-right">
{/* <Button
key="test"
type="primary"
loading={loading}
onClick={() => {
if (skill) {
testModel(skill);
}
}}
>
Test Model
</Button> */}
{!hasChanged && (
<Button
className="ml-2"
key="save"
type="primary"
onClick={() => {
if (skill) {
createSkill(skill);
setSkill(skill);
}
}}
>
{skill?.id ? "Update Skill" : "Save Skill"}
</Button>
)}
<Button
className="ml-2"
key="close"
type="default"
onClick={() => {
close();
}}
>
Close
</Button>
</div>
</div>
);
};

View File

@ -1,279 +0,0 @@
import React from "react";
import { IWorkflow, IStatus, IChatSession } from "../../../types";
import { ControlRowView } from "../../../atoms";
import {
fetchJSON,
getRandomIntFromDateAndSalt,
getServerUrl,
} from "../../../utils";
import { Button, Drawer, Input, Select, Tabs, message, theme } from "antd";
import { appContext } from "../../../../hooks/provider";
import { BugAntIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import { WorkflowAgentSelector, WorkflowTypeSelector } from "./selectors";
import ChatBox from "../../playground/chatbox";
export const WorkflowViewConfig = ({
workflow,
setWorkflow,
close,
}: {
workflow: IWorkflow;
setWorkflow: (newFlowConfig: IWorkflow) => void;
close: () => void;
}) => {
const [loading, setLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<IStatus | null>(null);
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const createWorkflowUrl = `${serverUrl}/workflows`;
const [controlChanged, setControlChanged] = React.useState<boolean>(false);
const [localWorkflow, setLocalWorkflow] = React.useState<IWorkflow>(workflow);
const updateFlowConfig = (key: string, value: string) => {
// When an updatedFlowConfig is created using localWorkflow, if the contents of FlowConfigViewer Modal are changed after the Agent Specification Modal is updated, the updated contents of the Agent Specification Modal are not saved. Fixed to localWorkflow->flowConfig. Fixed a bug.
const updatedFlowConfig = { ...workflow, [key]: value };
setLocalWorkflow(updatedFlowConfig);
setWorkflow(updatedFlowConfig);
setControlChanged(true);
};
const createWorkflow = (workflow: IWorkflow) => {
setError(null);
setLoading(true);
// const fetch;
workflow.user_id = user?.email;
const payLoad = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(workflow),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
const newWorkflow = data.data;
setWorkflow(newWorkflow);
} else {
message.error(data.message);
}
setLoading(false);
// setNewAgent(sampleAgent);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
const onFinal = () => {
setLoading(false);
setControlChanged(false);
};
fetchJSON(createWorkflowUrl, payLoad, onSuccess, onError, onFinal);
};
const hasChanged = !controlChanged && workflow.id !== undefined;
const [drawerOpen, setDrawerOpen] = React.useState<boolean>(false);
const openDrawer = () => {
setDrawerOpen(true);
};
const closeDrawer = () => {
setDrawerOpen(false);
};
const dummySession: IChatSession = {
user_id: user?.email || "test_session_user_id",
workflow_id: workflow?.id,
name: "test_session",
};
return (
<>
{/* <div className="mb-2">{flowConfig.name}</div> */}
<div>
<ControlRowView
title="Workflow Name"
className="mt-4 mb-2"
description="Name of the workflow"
value={localWorkflow.name}
control={
<Input
className="mt-2 w-full"
value={localWorkflow.name}
onChange={(e) => updateFlowConfig("name", e.target.value)}
/>
}
/>
<ControlRowView
title="Workflow Description"
className="mt-4 mb-2"
description="Description of the workflow"
value={localWorkflow.description}
control={
<Input
className="mt-2 w-full"
value={localWorkflow.description}
onChange={(e) => updateFlowConfig("description", e.target.value)}
/>
}
/>
<ControlRowView
title="Summary Method"
description="Defines the method to summarize the conversation"
value={localWorkflow.summary_method || "last"}
control={
<Select
className="mt-2 w-full"
defaultValue={localWorkflow.summary_method || "last"}
onChange={(value: any) =>
updateFlowConfig("summary_method", value)
}
options={
[
{ label: "last", value: "last" },
{ label: "none", value: "none" },
{ label: "llm", value: "llm" },
] as any
}
/>
}
/>
</div>
<div className="w-full mt-4 text-right">
{" "}
{!hasChanged && (
<Button
type="primary"
onClick={() => {
createWorkflow(localWorkflow);
}}
loading={loading}
>
{workflow.id ? "Update Workflow" : "Create Workflow"}
</Button>
)}
{workflow?.id && (
<Button
className="ml-2 text-primary"
type="primary"
onClick={() => {
setDrawerOpen(true);
}}
>
Test Workflow
</Button>
)}
<Button
className="ml-2"
key="close text-primary"
type="default"
onClick={() => {
close();
}}
>
Close
</Button>
</div>
<Drawer
title={<div>{workflow?.name || "Test Workflow"}</div>}
size="large"
onClose={closeDrawer}
open={drawerOpen}
>
<div className="h-full ">
{drawerOpen && (
<ChatBox
initMessages={[]}
session={dummySession}
heightOffset={100}
/>
)}
</div>
</Drawer>
</>
);
};
export const WorflowViewer = ({
workflow,
setWorkflow,
close,
}: {
workflow: IWorkflow;
setWorkflow: (workflow: IWorkflow) => void;
close: () => void;
}) => {
let items = [
{
label: (
<div className="w-full ">
{" "}
<BugAntIcon className="h-4 w-4 inline-block mr-1" />
Workflow Configuration
</div>
),
key: "1",
children: (
<div>
{!workflow?.type && (
<WorkflowTypeSelector
workflow={workflow}
setWorkflow={setWorkflow}
/>
)}
{workflow?.type && workflow && (
<WorkflowViewConfig
workflow={workflow}
setWorkflow={setWorkflow}
close={close}
/>
)}
</div>
),
},
];
if (workflow) {
if (workflow?.id) {
items.push({
label: (
<div className="w-full ">
{" "}
<UserGroupIcon className="h-4 w-4 inline-block mr-1" />
Agents
</div>
),
key: "2",
children: (
<>
<WorkflowAgentSelector workflow={workflow} />{" "}
</>
),
});
}
}
const { user } = React.useContext(appContext);
return (
<div className="text-primary">
<Tabs
tabBarStyle={{ paddingLeft: 0, marginLeft: 0 }}
defaultActiveKey="1"
items={items}
/>
</div>
);
};

View File

@ -1,428 +0,0 @@
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
CodeBracketSquareIcon,
DocumentDuplicateIcon,
InformationCircleIcon,
PlusIcon,
TrashIcon,
UserGroupIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { Dropdown, MenuProps, Modal, message } from "antd";
import * as React from "react";
import { IWorkflow, IStatus } from "../../types";
import { appContext } from "../../../hooks/provider";
import {
fetchJSON,
getServerUrl,
sanitizeConfig,
timeAgo,
truncateText,
} from "../../utils";
import { BounceLoader, Card, CardHoverBar, LoadingOverlay } from "../../atoms";
import { WorflowViewer } from "./utils/workflowconfig";
import { ExportWorkflowModal } from "./utils/export";
const WorkflowView = ({}: any) => {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const listWorkflowsUrl = `${serverUrl}/workflows?user_id=${user?.email}`;
const saveWorkflowsUrl = `${serverUrl}/workflows`;
const [workflows, setWorkflows] = React.useState<IWorkflow[] | null>([]);
const [selectedWorkflow, setSelectedWorkflow] =
React.useState<IWorkflow | null>(null);
const [selectedExportWorkflow, setSelectedExportWorkflow] =
React.useState<IWorkflow | null>(null);
const sampleWorkflow: IWorkflow = {
name: "Sample Agent Workflow",
description: "Sample Agent Workflow",
};
const [newWorkflow, setNewWorkflow] = React.useState<IWorkflow | null>(
sampleWorkflow
);
const [showWorkflowModal, setShowWorkflowModal] = React.useState(false);
const [showNewWorkflowModal, setShowNewWorkflowModal] = React.useState(false);
const fetchWorkFlow = () => {
setError(null);
setLoading(true);
// const fetch;
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
setWorkflows(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(listWorkflowsUrl, payLoad, onSuccess, onError);
};
const deleteWorkFlow = (workflow: IWorkflow) => {
setError(null);
setLoading(true);
// const fetch;
const deleteWorkflowsUrl = `${serverUrl}/workflows/delete?user_id=${user?.email}&workflow_id=${workflow.id}`;
const payLoad = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user?.email,
workflow: workflow,
}),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
fetchWorkFlow();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(deleteWorkflowsUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user) {
// console.log("fetching messages", messages);
fetchWorkFlow();
}
}, []);
React.useEffect(() => {
if (selectedWorkflow) {
setShowWorkflowModal(true);
}
}, [selectedWorkflow]);
const [showExportModal, setShowExportModal] = React.useState(false);
const workflowRows = (workflows || []).map(
(workflow: IWorkflow, i: number) => {
const cardItems = [
{
title: "Export",
icon: CodeBracketSquareIcon,
onClick: (e: any) => {
e.stopPropagation();
setSelectedExportWorkflow(workflow);
setShowExportModal(true);
},
hoverText: "Export",
},
{
title: "Download",
icon: ArrowDownTrayIcon,
onClick: (e: any) => {
e.stopPropagation();
// download workflow as workflow.name.json
const element = document.createElement("a");
const sanitizedWorkflow = sanitizeConfig(workflow);
const file = new Blob([JSON.stringify(sanitizedWorkflow)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
element.download = `workflow_${workflow.name}.json`;
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
},
hoverText: "Download",
},
{
title: "Make a Copy",
icon: DocumentDuplicateIcon,
onClick: (e: any) => {
e.stopPropagation();
let newWorkflow = { ...sanitizeConfig(workflow) };
newWorkflow.name = `${workflow.name}_copy`;
setNewWorkflow(newWorkflow);
setShowNewWorkflowModal(true);
},
hoverText: "Make a Copy",
},
{
title: "Delete",
icon: TrashIcon,
onClick: (e: any) => {
e.stopPropagation();
deleteWorkFlow(workflow);
},
hoverText: "Delete",
},
];
return (
<li
key={"workflowrow" + i}
className="block h-full"
style={{ width: "200px" }}
>
<Card
className=" block p-2 cursor-pointer"
title={<div className=" ">{truncateText(workflow.name, 25)}</div>}
onClick={() => {
setSelectedWorkflow(workflow);
}}
>
<div
style={{ minHeight: "65px" }}
className="break-words my-2"
aria-hidden="true"
>
<div className="text-xs mb-2">{workflow.type}</div>{" "}
{truncateText(workflow.description, 70)}
</div>
<div
aria-label={`Updated ${timeAgo(workflow.updated_at || "")} ago`}
className="text-xs"
>
{timeAgo(workflow.updated_at || "")}
</div>
<CardHoverBar items={cardItems} />
</Card>
</li>
);
}
);
const WorkflowModal = ({
workflow,
setWorkflow,
showModal,
setShowModal,
handler,
}: {
workflow: IWorkflow | null;
setWorkflow?: (workflow: IWorkflow | null) => void;
showModal: boolean;
setShowModal: (show: boolean) => void;
handler?: (workflow: IWorkflow) => void;
}) => {
const [localWorkflow, setLocalWorkflow] = React.useState<IWorkflow | null>(
workflow
);
const closeModal = () => {
setShowModal(false);
if (handler) {
handler(localWorkflow as IWorkflow);
}
};
return (
<Modal
title={
<>
Workflow Specification{" "}
<span className="text-accent font-normal">
{localWorkflow?.name}
</span>{" "}
</>
}
width={800}
open={showModal}
onOk={() => {
closeModal();
}}
onCancel={() => {
closeModal();
}}
footer={[]}
>
<>
{localWorkflow && (
<WorflowViewer
workflow={localWorkflow}
setWorkflow={setLocalWorkflow}
close={closeModal}
/>
)}
</>
</Modal>
);
};
const uploadWorkflow = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e: any) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e: any) => {
const contents = e.target.result;
if (contents) {
try {
const workflow = JSON.parse(contents);
// TBD validate that it is a valid workflow
setNewWorkflow(workflow);
setShowNewWorkflowModal(true);
} catch (err) {
message.error("Invalid workflow file");
}
}
};
reader.readAsText(file);
};
input.click();
};
const workflowTypes: MenuProps["items"] = [
// {
// key: "twoagents",
// label: (
// <div>
// {" "}
// <UsersIcon className="w-5 h-5 inline-block mr-2" />
// Two Agents
// </div>
// ),
// },
// {
// key: "groupchat",
// label: (
// <div>
// <UserGroupIcon className="w-5 h-5 inline-block mr-2" />
// Group Chat
// </div>
// ),
// },
// {
// type: "divider",
// },
{
key: "uploadworkflow",
label: (
<div>
<ArrowUpTrayIcon className="w-5 h-5 inline-block mr-2" />
Upload Workflow
</div>
),
},
];
const showWorkflow = (config: IWorkflow) => {
setSelectedWorkflow(config);
setShowWorkflowModal(true);
};
const workflowTypesOnClick: MenuProps["onClick"] = ({ key }) => {
if (key === "uploadworkflow") {
uploadWorkflow();
return;
}
showWorkflow(sampleWorkflow);
};
return (
<div className=" text-primary ">
<WorkflowModal
workflow={selectedWorkflow}
setWorkflow={setSelectedWorkflow}
showModal={showWorkflowModal}
setShowModal={setShowWorkflowModal}
handler={(workflow: IWorkflow) => {
fetchWorkFlow();
}}
/>
<WorkflowModal
workflow={newWorkflow}
showModal={showNewWorkflowModal}
setShowModal={setShowNewWorkflowModal}
handler={(workflow: IWorkflow) => {
fetchWorkFlow();
}}
/>
<ExportWorkflowModal
workflow={selectedExportWorkflow}
show={showExportModal}
setShow={setShowExportModal}
/>
<div className="mb-2 relative">
<div className=" rounded ">
<div className="flex mt-2 pb-2 mb-2 border-b">
<div className="flex-1 font-semibold mb-2 ">
{" "}
Workflows ({workflowRows.length}){" "}
</div>
<div className=" ">
<Dropdown.Button
type="primary"
menu={{ items: workflowTypes, onClick: workflowTypesOnClick }}
placement="bottomRight"
trigger={["click"]}
onClick={() => {
showWorkflow(sampleWorkflow);
}}
>
<PlusIcon className="w-5 h-5 inline-block mr-1" />
New Workflow
</Dropdown.Button>
</div>
</div>
<div className="text-xs mb-2 pb-1 ">
{" "}
Configure an agent workflow that can be used to handle tasks.
</div>
{workflows && workflows.length > 0 && (
<div
// style={{ minHeight: "500px" }}
className="w-full relative"
>
<LoadingOverlay loading={loading} />
<ul className="flex flex-wrap gap-3">{workflowRows}</ul>
</div>
)}
{workflows && workflows.length === 0 && !loading && (
<div className="text-sm border mt-4 rounded text-secondary p-2">
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
No workflows found. Please create a new workflow.
</div>
)}
{loading && (
<div className=" w-full text-center">
{" "}
<BounceLoader />{" "}
<span className="inline-block"> loading .. </span>
</div>
)}
</div>
</div>
</div>
);
};
export default WorkflowView;

View File

@ -1,207 +0,0 @@
import * as React from "react";
import { appContext } from "../../../hooks/provider";
import { fetchJSON, getServerUrl, timeAgo, truncateText } from "../../utils";
import { IGalleryItem, IStatus } from "../../types";
import { Button, message } from "antd";
import { BounceLoader, Card } from "../../atoms";
import {
ChevronLeftIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { navigate } from "gatsby";
import ChatBox from "../playground/chatbox";
const GalleryView = ({ location }: any) => {
const serverUrl = getServerUrl();
const { user } = React.useContext(appContext);
const [loading, setLoading] = React.useState(false);
const [gallery, setGallery] = React.useState<null | IGalleryItem[]>(null);
const [currentGallery, setCurrentGallery] =
React.useState<null | IGalleryItem>(null);
const listGalleryUrl = `${serverUrl}/gallery?user_id=${user?.email}`;
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const [currentGalleryId, setCurrentGalleryId] = React.useState<string | null>(
null
);
React.useEffect(() => {
// get gallery id from url
const urlParams = new URLSearchParams(location.search);
const galleryId = urlParams.get("id");
if (galleryId) {
// Fetch gallery details using the galleryId
fetchGallery(galleryId);
setCurrentGalleryId(galleryId);
} else {
// Redirect to an error page or home page if the id is not found
// navigate("/");
fetchGallery(null);
}
}, []);
const fetchGallery = (galleryId: string | null) => {
const fetchGalleryUrl = galleryId
? `${serverUrl}/gallery?gallery_id=${galleryId}`
: listGalleryUrl;
setError(null);
setLoading(true);
// const fetch;
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
// message.success(data.message);
console.log("gallery", data);
if (galleryId) {
// Set the currently viewed gallery item
setCurrentGallery(data.data[0]);
} else {
setGallery(data.data);
}
// Set the list of gallery items
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(fetchGalleryUrl, payLoad, onSuccess, onError);
};
const GalleryContent = ({ item }: { item: IGalleryItem }) => {
return (
<div>
<div className="mb-4 text-sm">
This session contains {item.messages.length} messages and was created{" "}
{timeAgo(item.timestamp)}
</div>
<div className="">
<ChatBox initMessages={item.messages} editable={false} />
</div>
</div>
);
};
const TagsView = ({ tags }: { tags: string[] }) => {
const tagsView = tags.map((tag: string, index: number) => {
return (
<div key={"tag" + index} className="mr-2 inline-block">
<span className="text-xs bg-secondary border px-3 p-1 rounded">
{tag}
</span>
</div>
);
});
return <div className="flex flex-wrap">{tagsView}</div>;
};
const galleryRows = gallery?.map((item: IGalleryItem, index: number) => {
const isSelected = currentGallery?.id === item.id;
return (
<div key={"galleryrow" + index} className="">
<Card
active={isSelected}
onClick={() => {
setCurrentGallery(item);
// add to history
navigate(`/gallery?id=${item.id}`);
}}
className="h-full p-2 cursor-pointer"
title={truncateText(item.messages[0]?.content || "", 20)}
>
<div className="my-2">
{" "}
{truncateText(item.messages[0]?.content || "", 80)}
</div>
<div className="text-xs">
{" "}
{item.messages.length} message{item.messages.length > 1 && "s"}
</div>
<div className="my-2 border-t border-dashed w-full pt-2 inline-flex gap-2 ">
<TagsView tags={item.tags} />{" "}
</div>
<div className="text-xs">{timeAgo(item.timestamp)}</div>
</Card>
</div>
);
});
return (
<div className=" ">
<div className="mb-4 text-2xl">Gallery</div>
{/* back to gallery button */}
{currentGallery && (
<div className="mb-4 w-full">
<Button
type="primary"
onClick={() => {
setCurrentGallery(null);
// add to history
navigate(`/gallery?_=${Date.now()}`);
if (currentGalleryId) {
fetchGallery(null);
setCurrentGalleryId(null);
}
}}
className="bg-primary text-white px-2 py-1 rounded"
>
<ChevronLeftIcon className="h-4 w-4 inline mr-1" />
Back to gallery
</Button>
</div>
)}
{!currentGallery && (
<>
<div>
View a collection of AutoGen agent specifications and sessions{" "}
</div>
<div className="mt-4 grid gap-3 grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{galleryRows}
</div>
</>
)}
{gallery && gallery.length === 0 && (
<div className="text-sm border rounded text-secondary p-2">
<InformationCircleIcon className="h-4 w-4 inline mr-1" />
No gallery items found. Please create a chat session and publish to
gallery.
</div>
)}
{currentGallery && (
<div className="mt-4 border-t pt-2">
<GalleryContent item={currentGallery} />
</div>
)}
{loading && (
<div className="w-full text-center boder mt-4">
<div>
{" "}
<BounceLoader />
</div>
loading gallery
</div>
)}
</div>
);
};
export default GalleryView;

View File

@ -0,0 +1,449 @@
import * as React from "react";
import { message } from "antd";
import { getServerUrl } from "../../../utils";
import { SessionManager } from "../../shared/session/manager";
import { IStatus } from "../../../types/app";
import { Message } from "../../../types/datamodel";
import { useConfigStore } from "../../../../hooks/store";
import { appContext } from "../../../../hooks/provider";
import ChatInput from "./chatinput";
import { ModelUsage, SocketMessage, ThreadState, ThreadStatus } from "./types";
import { MessageList } from "./messagelist";
import TeamManager from "../../shared/team/manager";
const logo = require("../../../../images/landing/welcome.svg").default;
export default function ChatView({
initMessages,
}: {
initMessages: Message[];
}) {
const serverUrl = getServerUrl();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const [messages, setMessages] = React.useState<Message[]>(initMessages);
const [threadMessages, setThreadMessages] = React.useState<
Record<string, ThreadState>
>({});
const chatContainerRef = React.useRef<HTMLDivElement>(null);
const { user } = React.useContext(appContext);
const { session, sessions } = useConfigStore();
const [activeSockets, setActiveSockets] = React.useState<
Record<string, WebSocket>
>({});
React.useEffect(() => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: "smooth",
});
}
}, [messages, threadMessages]);
React.useEffect(() => {
return () => {
Object.values(activeSockets).forEach((socket) => socket.close());
};
}, [activeSockets]);
const getBaseUrl = (url: string): string => {
try {
// Remove protocol (http:// or https://)
let baseUrl = url.replace(/(^\w+:|^)\/\//, "");
// Handle both localhost and production cases
if (baseUrl.startsWith("localhost")) {
// For localhost, keep the port if it exists
baseUrl = baseUrl.replace("/api", "");
} else if (baseUrl === "/api") {
// For production where url is just '/api'
baseUrl = window.location.host;
} else {
// For other cases, remove '/api' and trailing slash
baseUrl = baseUrl.replace("/api", "").replace(/\/$/, "");
}
return baseUrl;
} catch (error) {
console.error("Error processing server URL:", error);
throw new Error("Invalid server URL configuration");
}
};
const createRun = async (sessionId: number): Promise<string> => {
const payload = { session_id: sessionId, user_id: user?.email || "" };
const response = await fetch(`${serverUrl}/runs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Failed to create run");
}
const data = await response.json();
return data.data.run_id;
};
const startRun = async (runId: string, query: string) => {
const messagePayload = {
user_id: user?.email,
session_id: session?.id,
config: {
content: query,
source: "user",
},
};
const response = await fetch(`${serverUrl}/runs/${runId}/start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(messagePayload),
});
if (!response.ok) {
throw new Error("Failed to start run");
}
return await response.json();
};
interface RequestUsage {
prompt_tokens: number;
completion_tokens: number;
}
const connectWebSocket = (runId: string, query: string) => {
const baseUrl = getBaseUrl(serverUrl);
// Determine if we should use ws:// or wss:// based on current protocol
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${wsProtocol}//${baseUrl}/api/ws/runs/${runId}`;
console.log("Connecting to WebSocket URL:", wsUrl); // For debugging
const socket = new WebSocket(wsUrl);
let isClosing = false;
const closeSocket = () => {
if (!isClosing && socket.readyState !== WebSocket.CLOSED) {
isClosing = true;
socket.close();
setActiveSockets((prev) => {
const newSockets = { ...prev };
delete newSockets[runId];
return newSockets;
});
}
};
socket.onopen = async () => {
try {
setActiveSockets((prev) => ({
...prev,
[runId]: socket,
}));
setThreadMessages((prev) => ({
...prev,
[runId]: {
messages: [],
status: "streaming",
isExpanded: true,
},
}));
setMessages((prev: Message[]) =>
prev.map((msg: Message) => {
if (msg.run_id === runId && msg.config.source === "bot") {
return {
...msg,
config: {
...msg.config,
content: "Starting...",
},
};
}
return msg;
})
);
// Start the run only after socket is connected
await startRun(runId, query);
} catch (error) {
console.error("Error starting run:", error);
message.error("Failed to start run");
closeSocket();
setThreadMessages((prev) => ({
...prev,
[runId]: {
...prev[runId],
status: "error",
isExpanded: true,
},
}));
}
};
socket.onmessage = (event) => {
const message: SocketMessage = JSON.parse(event.data);
switch (message.type) {
case "message":
setThreadMessages((prev) => {
const currentThread = prev[runId] || {
messages: [],
status: "streaming",
isExpanded: true,
};
const models_usage: ModelUsage | undefined = message.data
?.models_usage
? {
prompt_tokens: message.data.models_usage.prompt_tokens,
completion_tokens:
message.data.models_usage.completion_tokens,
}
: undefined;
const newMessage = {
source: message.data?.source || "",
content: message.data?.content || "",
models_usage,
};
return {
...prev,
[runId]: {
...currentThread,
messages: [...currentThread.messages, newMessage],
status: "streaming",
},
};
});
break;
case "result":
case "completion":
setThreadMessages((prev) => {
const currentThread = prev[runId];
if (!currentThread) return prev;
const finalMessage = message.data?.task_result?.messages
?.filter((msg: any) => msg.content !== "TERMINATE")
.pop();
const status: ThreadStatus = message.status || "complete";
// Capture completion reason from task_result
const reason =
message.data?.task_result?.stop_reason ||
(message.error ? `Error: ${message.error}` : undefined);
return {
...prev,
[runId]: {
...currentThread,
status: status,
reason: reason,
isExpanded: true,
finalResult: finalMessage,
messages: currentThread.messages,
},
};
});
closeSocket();
break;
}
};
socket.onclose = (event) => {
console.log(
`WebSocket closed for run ${runId}. Code: ${event.code}, Reason: ${event.reason}`
);
if (!isClosing) {
setActiveSockets((prev) => {
const newSockets = { ...prev };
delete newSockets[runId];
return newSockets;
});
setThreadMessages((prev) => {
const thread = prev[runId];
if (thread && thread.status === "streaming") {
return {
...prev,
[runId]: {
...thread,
status: "complete",
reason: event.reason || "Connection closed",
},
};
}
return prev;
});
}
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
message.error("WebSocket connection error");
setThreadMessages((prev) => {
const thread = prev[runId];
if (!thread) return prev;
return {
...prev,
[runId]: {
...thread,
status: "error",
reason: "WebSocket connection error occurred",
isExpanded: true,
},
};
});
closeSocket();
};
return socket;
};
const cancelRun = async (runId: string) => {
const socket = activeSockets[runId];
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "stop" }));
setThreadMessages((prev) => ({
...prev,
[runId]: {
...prev[runId],
status: "cancelled",
reason: "Cancelled by user",
isExpanded: true,
},
}));
}
};
const runTask = async (query: string) => {
setError(null);
setLoading(true);
if (!session?.id) {
setLoading(false);
return;
}
let runId: string | null = null;
try {
runId = (await createRun(session.id)) + "";
const userMessage: Message = {
config: {
content: query,
source: "user",
},
session_id: session.id,
run_id: runId,
};
const botMessage: Message = {
config: {
content: "Thinking...",
source: "bot",
},
session_id: session.id,
run_id: runId,
};
setMessages((prev) => [...prev, userMessage, botMessage]);
connectWebSocket(runId, query); // Now passing query to connectWebSocket
} catch (err) {
console.error("Error:", err);
message.error("Error during request processing");
if (runId) {
if (activeSockets[runId]) {
activeSockets[runId].close();
}
setThreadMessages((prev) => ({
...prev,
[runId!]: {
...prev[runId!],
status: "error",
isExpanded: true,
},
}));
}
setError({
status: false,
message: err instanceof Error ? err.message : "Unknown error occurred",
});
} finally {
setLoading(false);
}
};
React.useEffect(() => {
// session changed
if (session) {
setMessages([]);
setThreadMessages({});
}
}, [session]);
return (
<div className="text-primary h-[calc(100vh-195px)] bg-primary relative rounded flex-1 scroll">
<div className="flex gap-4 w-full">
<div className="flex-1">
<SessionManager />
</div>
<TeamManager />
</div>
<div className="flex flex-col h-full">
<div
className="flex-1 overflow-y-auto scroll relative min-h-0"
ref={chatContainerRef}
>
<MessageList
messages={messages}
threadMessages={threadMessages}
setThreadMessages={setThreadMessages}
onRetry={runTask}
onCancel={cancelRun}
loading={loading}
/>
</div>
{sessions?.length === 0 ? (
<div className="flex h-[calc(100%-100px)] flex-col items-center justify-center w-full">
<div className="mt-4 text-sm text-secondary text-center">
<img src={logo} alt="Welcome" className="w-72 h-72 mb-4" />
Welcome! Create a session to get started!
</div>
</div>
) : (
<>
{session && (
<div className="flex-shrink-0">
<ChatInput onSubmit={runTask} loading={loading} error={error} />
</div>
)}
</>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,128 @@
"use client";
import {
PaperAirplaneIcon,
Cog6ToothIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import * as React from "react";
import { IStatus } from "../../../types/app";
interface ChatInputProps {
onSubmit: (text: string) => void;
loading: boolean;
error: IStatus | null;
}
export default function ChatInput({
onSubmit,
loading,
error,
}: ChatInputProps) {
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
const [previousLoading, setPreviousLoading] = React.useState(loading);
const [text, setText] = React.useState("");
const textAreaDefaultHeight = "64px";
// Handle textarea auto-resize
React.useEffect(() => {
if (textAreaRef.current) {
textAreaRef.current.style.height = textAreaDefaultHeight;
const scrollHeight = textAreaRef.current.scrollHeight;
textAreaRef.current.style.height = `${scrollHeight}px`;
}
}, [text]);
// Clear input when loading changes from true to false (meaning the response is complete)
React.useEffect(() => {
if (previousLoading && !loading && !error) {
resetInput();
}
setPreviousLoading(loading);
}, [loading, error, previousLoading]);
const resetInput = () => {
if (textAreaRef.current) {
textAreaRef.current.value = "";
textAreaRef.current.style.height = textAreaDefaultHeight;
setText("");
}
};
const handleTextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setText(event.target.value);
};
const handleSubmit = () => {
if (textAreaRef.current?.value && !loading) {
const query = textAreaRef.current.value;
onSubmit(query);
// Don't reset immediately - wait for response to complete
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
};
return (
<div className="mt-2 w-full">
<div
className={`mt-2 rounded shadow-sm flex mb-1 ${
loading ? "opacity-50 pointer-events-none" : ""
}`}
>
<form
className="flex-1 relative"
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<textarea
id="queryInput"
name="queryInput"
ref={textAreaRef}
defaultValue={"what is the height of the eiffel tower"}
onChange={handleTextChange}
onKeyDown={handleKeyDown}
className="flex items-center w-full resize-none text-gray-600 rounded border border-accent bg-white p-2 pl-5 pr-16"
style={{
maxHeight: "120px",
overflowY: "auto",
minHeight: "50px",
}}
placeholder="Type your message here..."
disabled={loading}
/>
<div
role="button"
onClick={handleSubmit}
style={{ width: "45px", height: "35px" }}
className="absolute right-3 bottom-2 bg-accent hover:brightness-75 transition duration-300 rounded cursor-pointer flex justify-center items-center"
>
{!loading ? (
<div className="inline-block">
<PaperAirplaneIcon className="h-6 w-6 text-white" />
</div>
) : (
<div className="inline-block">
<Cog6ToothIcon className="text-white animate-spin rounded-full h-6 w-6" />
</div>
)}
</div>
</form>
</div>
{error && !error.status && (
<div className="p-2 border rounded mt-4 text-orange-500 text-sm">
<ExclamationTriangleIcon className="h-5 text-orange-500 inline-block mr-2" />
{error.message}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,139 @@
import React, { useEffect, useRef } from "react";
import { ChevronDown, ChevronRight, RotateCcw, User, Bot } from "lucide-react";
import { ThreadView } from "./thread";
import { MessageListProps } from "./types";
export const MessageList: React.FC<MessageListProps> = ({
messages,
threadMessages,
setThreadMessages,
onRetry,
onCancel,
loading,
}) => {
const messageListRef = useRef<HTMLDivElement>(null);
const messagePairs = React.useMemo(() => {
const pairs = [];
for (let i = 0; i < messages.length; i += 2) {
if (messages[i] && messages[i + 1]) {
pairs.push({
userMessage: messages[i],
botMessage: messages[i + 1],
});
}
}
return pairs;
}, [messages]);
const toggleThread = (runId: string) => {
setThreadMessages((prev) => ({
...prev,
[runId]: {
...prev[runId],
isExpanded: !prev[runId].isExpanded,
},
}));
};
return (
<div
ref={messageListRef}
className="flex flex-col space-y-4 p-4 overflow-y-hidden overflow-x-hidden"
>
{messagePairs.map(({ userMessage, botMessage }, idx) => {
const thread = threadMessages[botMessage.run_id];
const isStreaming = thread?.status === "streaming";
const isError = thread?.status === "error";
return (
<div key={idx} className="space-y-2 text-primary">
{/* User Message */}
<div className="flex items-start gap-2 ]">
<div className="p-1.5 rounded bg-light text-accent">
<User size={24} />
</div>
<div className="flex-1">
<div className="p-2 bg-accent rounded text-white">
{userMessage.config.content}
</div>
{userMessage.config.models_usage && (
<div className="text-xs text-secondary mt-1">
Tokens:{" "}
{userMessage.config.models_usage.prompt_tokens +
userMessage.config.models_usage.completion_tokens}
</div>
)}
</div>
</div>
{/* Bot Response */}
<div className="flex items-start gap-2 ml-auto ">
<div className="flex-1">
<div className="p-2 bg-secondary rounded text-primary">
<div className="flex items-center justify-between">
<div className="flex-1 text-sm">
{isStreaming ? (
<>Processing...</>
) : (
<>
{" "}
{thread?.finalResult?.content}
<div className="mt-2 mb-2 text-sm text-secondary">
{thread?.reason}
</div>
</>
)}
</div>
<div className="flex items-center gap-1">
{isError && (
<button
onClick={() => onRetry(userMessage.config.content)}
className="p-1 text-secondary hover:text-primary transition-colors"
>
<RotateCcw size={14} />
</button>
)}
{thread && thread.messages?.length > 0 && (
<button
onClick={() => toggleThread(botMessage.run_id)}
className="p-1 text-secondary hover:text-primary transition-colors"
>
{thread.isExpanded ? (
<ChevronDown size={14} />
) : (
<ChevronRight size={14} />
)}
</button>
)}
</div>
</div>
{botMessage.config.models_usage && (
<div className="text-sm text-secondary -mt-4">
{botMessage.config.models_usage.prompt_tokens +
botMessage.config.models_usage.completion_tokens}{" "}
tokens | {thread.messages.length} messages
</div>
)}
{/* Thread View */}
{thread && thread.isExpanded && (
<ThreadView
messages={thread.messages}
status={thread.status}
onCancel={onCancel}
runId={botMessage.run_id}
/>
)}
</div>
</div>
<div className="p-1.5 rounded bg-light text-primary">
<Bot size={24} />
</div>
</div>
</div>
);
})}
</div>
);
};

View File

@ -0,0 +1,212 @@
import React, { useEffect, useRef } from "react";
import {
CheckCircle,
AlertCircle,
Loader2,
User,
Bot,
StopCircle,
} from "lucide-react";
import { Tooltip } from "antd";
import { MessageConfig } from "../../../types/datamodel";
import { ThreadState } from "./types";
interface ThreadViewProps {
messages: MessageConfig[];
status: ThreadState["status"];
reason?: string; // Add reason
onCancel: (runId: string) => void;
runId: string;
}
interface StatusBannerProps {
status: ThreadState["status"];
reason?: string; // Add reason prop
onCancel: (runId: string) => void;
runId: string;
}
const StatusBanner = ({
status,
reason,
onCancel,
runId,
}: StatusBannerProps) => {
// Define variants FIRST
const variants = {
streaming: {
wrapper: "bg-secondary border border-secondary",
text: "text-primary",
icon: "text-accent",
},
complete: {
wrapper: "bg-secondary p-2",
text: "text-accent",
icon: " ",
},
error: {
wrapper: "bg-secondary border border-secondary",
text: "text-primary",
icon: "text-red-500",
},
cancelled: {
wrapper: "bg-secondary border border-secondary",
text: "text-red-500",
icon: "text-red-500",
},
} as const;
const content = {
streaming: {
icon: <Loader2 className="animate-spin" size={16} />,
text: "Processing",
showStop: true,
},
complete: {
icon: <CheckCircle size={16} />,
text: reason || "Completed",
showStop: false,
},
error: {
icon: <AlertCircle size={16} />,
text: reason || "Error occurred",
showStop: false,
},
cancelled: {
icon: <StopCircle size={16} />,
text: reason || "Cancelled",
showStop: false,
},
} as const;
// THEN check valid status and use variants
const validStatus = status && status in variants ? status : "error";
const currentVariant = variants[validStatus];
const currentContent = content[validStatus];
// Rest of component remains the same...
return (
<div
className={`flex items-center mt-2 justify-between p-2 rounded transition-all duration-200 ${currentVariant.wrapper}`}
>
<div className={`flex items-center gap-2 ${currentVariant.text}`}>
<span className={currentVariant.icon}>{currentContent.icon}</span>
<span className="text-sm font-medium">{currentContent.text}</span>
{currentContent.showStop && (
<Tooltip title="Stop processing" placement="right">
<button
onClick={() => onCancel(runId)}
className="ml-2 flex items-center gap-1 px-2 py-1 rounded bg-red-500 hover:bg-red-600 text-white text-xs font-medium transition-colors duration-200"
>
<StopCircle size={12} />
<span>Stop</span>
</button>
</Tooltip>
)}
</div>
</div>
);
};
const Message = ({ msg, isLast }: { msg: MessageConfig; isLast: boolean }) => {
const isUser = msg.source === "user";
return (
<div className={`relative group ${!isLast ? "mb-2" : ""}`}>
<div
className={`
flex items-start gap-2 p-2 rounded
${isUser ? "bg-secondary" : "bg-tertiary"}
border border-secondary
transition-all duration-200
`}
>
<div
className={`
p-1.5 rounded bg-light
${isUser ? " text-accent" : "text-primary"}
`}
>
{isUser ? <User size={14} /> : <Bot size={14} />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-primary">
{msg.source}
</span>
</div>
<div className="text-sm text-secondary whitespace-pre-wrap">
{msg.content || ""}
</div>
{msg.models_usage && (
<div className="text-xs text-secondary mt-1">
Tokens:{" "}
{(msg.models_usage.prompt_tokens || 0) +
(msg.models_usage.completion_tokens || 0)}
</div>
)}
</div>
</div>
</div>
);
};
export const ThreadView: React.FC<ThreadViewProps> = ({
messages = [],
status,
reason,
onCancel,
runId,
}) => {
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current && status === "streaming") {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages, status]);
// const displayMessages = messages.filter(
// (msg): msg is MessageConfig =>
// msg && typeof msg.content === "string" && msg.content !== "TERMINATE"
// );
const displayMessages = messages;
return (
<div>
<div className="space-y-2">
<StatusBanner
status={status}
reason={reason}
onCancel={onCancel}
runId={runId}
/>
<div
ref={scrollRef}
style={{ maxHeight: "300px" }}
className="overflow-y-scroll scroll p-2 space-y-2 bg-primary rounded border border-secondary"
>
{displayMessages.map((msg, idx) => (
<Message
key={idx}
msg={msg}
isLast={idx === displayMessages.length - 1}
/>
))}
{displayMessages.length === 0 && status !== "streaming" && (
<div className="text-sm text-secondary text-center p-2">
No messages to display
</div>
)}
</div>
</div>
</div>
);
};
export default ThreadView;

View File

@ -0,0 +1,47 @@
import { Message, TaskResult } from "../../../types/datamodel";
export type ThreadStatus = "streaming" | "complete" | "error" | "cancelled";
export interface ThreadState {
messages: any[];
finalResult?: any;
status: ThreadStatus;
isExpanded: boolean;
}
export interface ThreadState {
messages: any[];
finalResult?: any;
status: "streaming" | "complete" | "error" | "cancelled";
isExpanded: boolean;
reason?: string;
}
export interface MessageListProps {
messages: Message[];
threadMessages: Record<string, ThreadState>;
setThreadMessages: React.Dispatch<
React.SetStateAction<Record<string, ThreadState>>
>; // Add this
onRetry: (query: string) => void;
onCancel: (runId: string) => void;
loading: boolean;
}
export interface ModelUsage {
prompt_tokens: number;
completion_tokens: number;
}
export interface SocketMessage {
type: "message" | "result" | "completion";
data?: {
source?: string;
models_usage?: ModelUsage | null;
content?: string;
task_result?: TaskResult;
};
status?: ThreadStatus;
timestamp?: string;
error?: string;
}

View File

@ -1,824 +0,0 @@
import {
ArrowPathIcon,
ChatBubbleLeftRightIcon,
Cog6ToothIcon,
DocumentDuplicateIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
PaperAirplaneIcon,
SignalSlashIcon,
} from "@heroicons/react/24/outline";
import {
Button,
Dropdown,
MenuProps,
Tabs,
message as ToastMessage,
Tooltip,
message,
} from "antd";
import * as React from "react";
import {
IChatMessage,
IChatSession,
IMessage,
IStatus,
IWorkflow,
} from "../../types";
import { examplePrompts, fetchJSON, getServerUrl, guid } from "../../utils";
import { appContext } from "../../../hooks/provider";
import MetaDataView from "./metadata";
import {
AgentRow,
BounceLoader,
CollapseBox,
LoadingBar,
MarkdownView,
} from "../../atoms";
import { useConfigStore } from "../../../hooks/store";
import ProfilerView from "./utils/profiler";
let socketMsgs: any[] = [];
const ChatBox = ({
initMessages,
session,
editable = true,
heightOffset = 160,
}: {
initMessages: IMessage[] | null;
session: IChatSession | null;
editable?: boolean;
heightOffset?: number;
}) => {
// const session: IChatSession | null = useConfigStore((state) => state.session);
const textAreaInputRef = React.useRef<HTMLTextAreaElement>(null);
const messageBoxInputRef = React.useRef<HTMLDivElement>(null);
const { user } = React.useContext(appContext);
const wsClient = React.useRef<WebSocket | null>(null);
const wsMessages = React.useRef<IChatMessage[]>([]);
const [wsConnectionStatus, setWsConnectionStatus] =
React.useState<string>("disconnected");
const [workflow, setWorkflow] = React.useState<IWorkflow | null>(null);
const [socketMessages, setSocketMessages] = React.useState<any[]>([]);
const [awaitingUserInput, setAwaitingUserInput] = React.useState(false); // New state for tracking user input
const setAreSessionButtonsDisabled = useConfigStore(
(state) => state.setAreSessionButtonsDisabled
);
const MAX_RETRIES = 10;
const RETRY_INTERVAL = 2000;
const [retries, setRetries] = React.useState(0);
const serverUrl = getServerUrl();
let websocketUrl = serverUrl.replace("http", "ws") + "/ws/";
// check if there is a protocol in the serverUrl e.g. /api. if use the page url
if (!serverUrl.includes("http")) {
const pageUrl = window.location.href;
const url = new URL(pageUrl);
const protocol = url.protocol;
const host = url.host;
const baseUrl = protocol + "//" + host + serverUrl;
websocketUrl = baseUrl.replace("http", "ws") + "/ws/";
} else {
websocketUrl = serverUrl.replace("http", "ws") + "/ws/";
}
const [loading, setLoading] = React.useState(false);
const [text, setText] = React.useState("");
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const socketDivRef = React.useRef<HTMLDivElement>(null);
const connectionId = useConfigStore((state) => state.connectionId);
const messages = useConfigStore((state) => state.messages);
const setMessages = useConfigStore((state) => state.setMessages);
const parseMessage = (message: IMessage) => {
let meta;
try {
meta = JSON.parse(message.meta);
} catch (e) {
meta = message?.meta;
}
const msg: IChatMessage = {
text: message.content,
sender: message.role === "user" ? "user" : "bot",
meta: meta,
id: message.id,
};
return msg;
};
const parseMessages = (messages: any) => {
return messages?.map(parseMessage);
};
React.useEffect(() => {
// console.log("initMessages changed", initMessages);
const initMsgs: IChatMessage[] = parseMessages(initMessages);
setMessages(initMsgs);
wsMessages.current = initMsgs;
}, [initMessages]);
const promptButtons = examplePrompts.map((prompt, i) => {
return (
<Button
key={"prompt" + i}
type="primary"
className=""
onClick={() => {
runWorkflow(prompt.prompt);
}}
>
{" "}
{prompt.title}{" "}
</Button>
);
});
const messageListView = messages && messages?.map((message: IChatMessage, i: number) => {
const isUser = message.sender === "user";
const css = isUser ? "bg-accent text-white " : "bg-light";
// console.log("message", message);
let hasMeta = false;
if (message.meta) {
hasMeta =
message.meta.code !== null ||
message.meta.images?.length > 0 ||
message.meta.files?.length > 0 ||
message.meta.scripts?.length > 0;
}
let items: MenuProps["items"] = [];
if (isUser) {
items.push({
label: (
<div
onClick={() => {
console.log("retrying");
runWorkflow(message.text);
}}
>
<ArrowPathIcon
role={"button"}
title={"Retry"}
className="h-4 w-4 mr-1 inline-block"
/>
Retry
</div>
),
key: "retrymessage",
});
items.push({
label: (
<div
onClick={() => {
// copy to clipboard
navigator.clipboard.writeText(message.text);
ToastMessage.success("Message copied to clipboard");
}}
>
<DocumentDuplicateIcon
role={"button"}
title={"Copy"}
className="h-4 w-4 mr-1 inline-block"
/>
Copy
</div>
),
key: "copymessage",
});
}
const menu = (
<Dropdown menu={{ items }} trigger={["click"]} placement="bottomRight">
<div
role="button"
className="float-right ml-2 duration-100 hover:bg-secondary font-semibold px-2 pb-1 rounded"
>
<span className="block -mt-2 text-primary "> ...</span>
</div>
</Dropdown>
);
return (
<div
id={"message" + i} className={`align-right ${isUser ? "text-righpt" : ""} mb-2 border-b`}
key={"message" + i}
>
{" "}
<div className={` ${isUser ? "" : " w-full"} inline-flex gap-2`}>
<div className=""></div>
<div className="font-semibold text-secondary text-sm w-16">{`${
isUser ? "USER" : "AGENTS"
}`}</div>
<div
className={`inline-block group relative w-full p-2 rounded ${css}`}
>
{" "}
{items.length > 0 && editable && (
<div className=" group-hover:opacity-100 opacity-0 ">{menu}</div>
)}
{isUser && (
<>
<div className="inline-block">{message.text}</div>
</>
)}
{!isUser && (
<div
className={` w-full chatbox prose dark:prose-invert text-primary rounded `}
>
<MarkdownView
className="text-sm"
data={message.text}
showCode={false}
/>
</div>
)}
{message.meta && !isUser && (
<>
{" "}
<Tabs
defaultActiveKey="1"
items={[
{
label: (
<>
{" "}
<ChatBubbleLeftRightIcon className="h-4 w-4 inline-block mr-1" />
Agent Messages
</>
),
key: "1",
children: (
<div className="text-primary">
<MetaDataView metadata={message.meta} />
</div>
),
},
{
label: (
<div>
{" "}
<SignalSlashIcon className="h-4 w-4 inline-block mr-1" />{" "}
Profiler
</div>
),
key: "2",
children: (
<div className="text-primary">
<ProfilerView agentMessage={message} />
</div>
),
},
]}
/>
</>
)}
</div>
</div>
</div>
);
});
React.useEffect(() => {
// console.log("messages updated, scrolling");
setTimeout(() => {
scrollChatBox(messageBoxInputRef);
}, 500);
}, [messages]);
const textAreaDefaultHeight = "64px";
// clear text box if loading has just changed to false and there is no error
React.useEffect(() => {
if ((awaitingUserInput || loading === false) && textAreaInputRef.current) {
if (textAreaInputRef.current) {
if (error === null || (error && error.status === false)) {
textAreaInputRef.current.value = "";
textAreaInputRef.current.style.height = textAreaDefaultHeight;
}
}
}
}, [loading]);
React.useEffect(() => {
if (textAreaInputRef.current) {
textAreaInputRef.current.style.height = textAreaDefaultHeight; // Reset height to shrink if text is deleted
const scrollHeight = textAreaInputRef.current.scrollHeight;
textAreaInputRef.current.style.height = `${scrollHeight}px`;
}
}, [text]);
const [waitingToReconnect, setWaitingToReconnect] = React.useState<
boolean | null
>(null);
React.useEffect(() => {
if (waitingToReconnect) {
return;
}
// Only set up the websocket once
const socketUrl = websocketUrl + connectionId;
console.log("socketUrl", socketUrl);
if (!wsClient.current) {
const client = new WebSocket(socketUrl);
wsClient.current = client;
client.onerror = (e) => {
console.log("ws error", e);
};
client.onopen = () => {
setWsConnectionStatus("connected");
console.log("ws opened");
};
client.onclose = () => {
if (wsClient.current) {
// Connection failed
console.log("ws closed by server");
} else {
// Cleanup initiated from app side, can return here, to not attempt a reconnect
return;
}
if (waitingToReconnect) {
return;
}
setWsConnectionStatus("disconnected");
setWaitingToReconnect(true);
setWsConnectionStatus("reconnecting");
setTimeout(() => {
setWaitingToReconnect(null);
}, RETRY_INTERVAL);
};
client.onmessage = (message) => {
const data = JSON.parse(message.data);
console.log("received message", data);
if (data && data.type === "agent_message") {
// indicates an intermediate agent message update
const newsocketMessages = Object.assign([], socketMessages);
newsocketMessages.push(data.data);
setSocketMessages(newsocketMessages);
socketMsgs.push(data.data);
setTimeout(() => {
scrollChatBox(socketDivRef);
scrollChatBox(messageBoxInputRef);
}, 200);
// console.log("received message", data, socketMsgs.length);
} else if (data && data.type === "user_input_request") {
setAwaitingUserInput(true); // Set awaiting input state
textAreaInputRef.current.value = ""
textAreaInputRef.current.placeholder = data.data.message.content
const newsocketMessages = Object.assign([], socketMessages);
newsocketMessages.push(data.data);
setSocketMessages(newsocketMessages);
socketMsgs.push(data.data);
setTimeout(() => {
scrollChatBox(socketDivRef);
scrollChatBox(messageBoxInputRef);
}, 200);
ToastMessage.info(data.data.message)
} else if (data && data.type === "agent_status") {
// indicates a status message update
const agentStatusSpan = document.getElementById("agentstatusspan");
if (agentStatusSpan) {
agentStatusSpan.innerHTML = data.data.message;
}
} else if (data && data.type === "agent_response") {
// indicates a final agent response
setAwaitingUserInput(false); // Set awaiting input state
setAreSessionButtonsDisabled(false);
processAgentResponse(data.data);
}
};
return () => {
console.log("Cleanup");
// Dereference, so it will set up next time
wsClient.current = null;
client.close();
};
}
}, [waitingToReconnect]);
const scrollChatBox = (element: any) => {
element.current?.scroll({
top: element.current.scrollHeight,
behavior: "smooth",
});
};
const mainDivRef = React.useRef<HTMLDivElement>(null);
const processAgentResponse = (data: any) => {
if (data && data.status) {
const msg = parseMessage(data.data);
wsMessages.current.push(msg);
setMessages(wsMessages.current);
setLoading(false);
setAwaitingUserInput(false);
} else {
console.log("error", data);
// setError(data);
ToastMessage.error(data.message);
setLoading(false);
setAwaitingUserInput(false);
}
};
const fetchWorkFlow = (workflowId: number) => {
const fetchUrl = `${serverUrl}/workflows/${workflowId}?user_id=${user?.email}`;
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
if (data.data && data.data.length > 0) {
setWorkflow(data.data[0]);
}
} else {
message.error(data.message);
}
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
};
fetchJSON(fetchUrl, payLoad, onSuccess, onError);
};
const runWorkflow = (query: string) => {
setError(null);
setAreSessionButtonsDisabled(true);
socketMsgs = [];
let messageHolder = Object.assign([], messages);
const userMessage: IChatMessage = {
text: query,
sender: "user",
};
messageHolder.push(userMessage);
setMessages(messageHolder);
wsMessages.current.push(userMessage);
const messagePayload: IMessage = {
role: "user",
content: query,
user_id: user?.email || "",
session_id: session?.id,
workflow_id: session?.workflow_id,
connection_id: connectionId,
};
const runWorkflowUrl = `${serverUrl}/sessions/${session?.id}/workflow/${session?.workflow_id}/run`;
const postData = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(messagePayload),
};
setLoading(true);
// check if socket connected, send on socket
// else send on fetch
if (wsClient.current && wsClient.current.readyState === 1) {
wsClient.current.send(
JSON.stringify({
connection_id: connectionId,
data: messagePayload,
type: "user_message",
session_id: session?.id,
workflow_id: session?.workflow_id,
})
);
} else {
fetch(runWorkflowUrl, postData)
.then((res) => {
if (res.status === 200) {
res.json().then((data) => {
processAgentResponse(data);
});
} else {
res.json().then((data) => {
console.log("error", data);
ToastMessage.error(data.message);
setLoading(false);
});
ToastMessage.error(
"Connection error. Ensure server is up and running."
);
}
})
.catch(() => {
setLoading(false);
ToastMessage.error(
"Connection error. Ensure server is up and running."
);
})
.finally(() => {
setTimeout(() => {
scrollChatBox(messageBoxInputRef);
}, 500);
});
}
};
const sendUserResponse = (userResponse: string) => {
setAwaitingUserInput(false);
setError(null);
setLoading(true);
textAreaInputRef.current.placeholder = "Write message here..."
const userMessage: IChatMessage = {
text: userResponse,
sender: "system",
};
const messagePayload: IMessage = {
role: "user",
content: userResponse,
user_id: user?.email || "",
session_id: session?.id,
workflow_id: session?.workflow_id,
connection_id: connectionId,
};
// check if socket connected,
if (wsClient.current && wsClient.current.readyState === 1) {
wsClient.current.send(
JSON.stringify({
connection_id: connectionId,
data: messagePayload,
type: "user_message",
session_id: session?.id,
workflow_id: session?.workflow_id,
})
);
} else {
console.err("websocket client error")
}
};
const handleTextChange = (
event: React.ChangeEvent<HTMLTextAreaElement>
): void => {
setText(event.target.value);
};
const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>
): void => {
if (event.key === "Enter" && !event.shiftKey) {
if (textAreaInputRef.current &&(awaitingUserInput || !loading)) {
event.preventDefault();
if (awaitingUserInput) {
sendUserResponse(textAreaInputRef.current.value); // New function call for sending user input
textAreaInputRef.current.value = "";
} else {
runWorkflow(textAreaInputRef.current.value);
}
}
}
};
const getConnectionColor = (status: string) => {
if (status === "connected") {
return "bg-green-500";
} else if (status === "reconnecting") {
return "bg-orange-500";
} else if (status === "disconnected") {
return "bg-red-500";
}
};
React.useEffect(() => {
if (session && session.workflow_id) {
fetchWorkFlow(session.workflow_id);
}
}, [session]);
const WorkflowView = ({ workflow }: { workflow: IWorkflow }) => {
return (
<div id="workflow-view" className="text-xs cursor-pointer inline-block">
{" "}
{workflow.name}
</div>
);
};
return (
<div
id="chatbox-main"
style={{ height: "calc(100vh - " + heightOffset + "px)" }}
className="text-primary relative rounded "
ref={mainDivRef}
>
<div
id="workflow-name"
style={{ zIndex: 100 }}
className=" absolute right-3 bg-primary rounded text-secondary -top-6 p-2"
>
{" "}
{workflow && <div className="text-xs"> {workflow.name}</div>}
</div>
<div
id="message-box"
ref={messageBoxInputRef}
className="flex h-full flex-col rounded scroll pr-2 overflow-auto "
style={{ minHeight: "30px", height: "calc(100vh - 310px)" }}
>
<div id="scroll-gradient" className="scroll-gradient h-10">
{" "}
<span className=" inline-block h-6"></span>{" "}
</div>
<div className="flex-1 boder mt-4"></div>
{!messages && messages !== null && (
<div id="loading-messages" className="w-full text-center boder mt-4">
<div>
{" "}
<BounceLoader />
</div>
loading messages
</div>
)}
{messages && messages?.length === 0 && (
<div id="no-messages" className="ml-2 text-sm text-secondary ">
<InformationCircleIcon className="inline-block h-6 mr-2" />
No messages in the current session. Start a conversation to begin.
</div>
)}
<div id="message-list" className="ml-2"> {messageListView}</div>
{(loading || awaitingUserInput) && (
<div id="loading-bar" className={` inline-flex gap-2 duration-300 `}>
<div className=""></div>
<div className="font-semibold text-secondary text-sm w-16">
AGENTS
</div>
<div className="relative w-full ">
<div className="mb-2 ">
<LoadingBar>
<div className="mb-1 inline-block ml-2 text-xs text-secondary">
<span className="innline-block text-sm ml-2">
{" "}
<span id="agentstatusspan">
{" "}
agents working on task ..
</span>
</span>{" "}
{socketMsgs.length > 0 && (
<span className="border-l inline-block text-right ml-2 pl-2">
{socketMsgs.length} agent message
{socketMsgs.length > 1 && "s"} sent/received.
</span>
)}
</div>
</LoadingBar>
</div>
{socketMsgs.length > 0 && (
<div
id="agent-messages"
ref={socketDivRef}
style={{
minHeight: "300px",
maxHeight: "400px",
overflowY: "auto",
}}
className={`inline-block scroll group relative p-2 rounded w-full bg-light `}
>
<CollapseBox
open={true}
title={`Agent Messages (${socketMsgs.length} message${
socketMsgs.length > 1 ? "s" : ""
}) `}
>
{socketMsgs?.map((message: any, i: number) => {
return (
<div key={i}>
<AgentRow message={message} />
</div>
);
})}
</CollapseBox>
</div>
)}
</div>
</div>
)}
</div>
{editable && (
<div id="input-area" className="mt-2 p-2 absolute bg-primary bottom-0 w-full">
<div
id="input-form"
className={`rounded p-2 shadow-lg flex mb-1 gap-2 ${
loading && !awaitingUserInput ? " opacity-50 pointer-events-none" : ""
}`}
>
{/* <input className="flex-1 p-2 ring-2" /> */}
<form
autoComplete="on"
className="flex-1 relative"
onSubmit={(e) => {
e.preventDefault();
}}
>
<textarea
id="queryInput"
name="queryInput"
autoComplete="on"
onKeyDown={handleKeyDown}
onChange={handleTextChange}
placeholder="Write message here..."
ref={textAreaInputRef}
className="flex items-center w-full resize-none text-gray-600 bg-white p-2 ring-2 rounded-sm pl-5 pr-16 h-64"
style={{
maxHeight: "120px",
overflowY: "auto",
minHeight: "50px",
}}
/>
<div
id="send-button"
role={"button"}
style={{ width: "45px", height: "35px" }}
title="Send message"
onClick={() => {
if (textAreaInputRef.current && (awaitingUserInput || !loading)) {
if (awaitingUserInput) {
sendUserResponse(textAreaInputRef.current.value); // Use the new function for user input
} else {
runWorkflow(textAreaInputRef.current.value);
}
}
}}
className="absolute right-3 bottom-2 bg-accent hover:brightness-75 transition duration-300 rounded cursor-pointer flex justify-center items-center"
>
{" "}
{(awaitingUserInput || !loading) && (
<div className="inline-block ">
<PaperAirplaneIcon className="h-6 w-6 text-white " />{" "}
</div>
)}
{loading && !awaitingUserInput && (
<div className="inline-block ">
<Cog6ToothIcon className="text-white animate-spin rounded-full h-6 w-6" />
</div>
)}
</div>
</form>
</div>{" "}
<div>
<div className="mt-2 text-xs text-secondary">
<Tooltip title={`Socket ${wsConnectionStatus}`}>
<div
className={`w-1 h-3 rounded inline-block mr-1 ${getConnectionColor(
wsConnectionStatus
)}`}
></div>{" "}
</Tooltip>
Blank slate? Try one of the example prompts below{" "}
</div>
<div
id="prompt-buttons"
className={`mt-2 inline-flex gap-2 flex-wrap ${
(loading && !awaitingUserInput) ? "brightness-75 pointer-events-none" : ""
}`}
>
{promptButtons}
</div>
</div>
{error && !error.status && (
<div id="error-message" className="p-2 rounded mt-4 text-orange-500 text-sm">
{" "}
<ExclamationTriangleIcon className="h-5 text-orange-500 inline-block mr-2" />{" "}
{error.message}
</div>
)}
</div>
)}
</div>
);
};
export default ChatBox;

View File

@ -1,242 +0,0 @@
import {
DocumentTextIcon,
PhotoIcon,
VideoCameraIcon,
} from "@heroicons/react/24/outline";
import * as React from "react";
import {
CodeBlock,
CodeLoader,
CsvLoader,
CollapseBox,
ExpandView,
GroupView,
ImageLoader,
MarkdownView,
PdfViewer,
AgentRow,
} from "../../atoms";
import { formatDuration, getServerUrl } from "../../utils";
import { IMetadataFile } from "../../types";
import Icon from "../../icons";
const MetaDataView = ({ metadata }: { metadata: any | null }) => {
const serverUrl = getServerUrl();
const renderFileContent = (file: IMetadataFile, i: number) => {
const file_type = file.extension;
const is_image = ["image"].includes(file.type);
const is_code = ["code"].includes(file.type);
const is_csv = ["csv"].includes(file.type);
const is_pdf = ["pdf"].includes(file.type);
const is_video = ["video"].includes(file.type);
const file_name = file.name || "unknown";
const file_path = file.path || "unknown";
let fileView = null;
let fileTitle = (
<div>
{file.type === "image" ? (
<PhotoIcon className="h-4 mr-1 inline-block" />
) : (
<DocumentTextIcon className="h-4 mr-1 inline-block" />
)}{" "}
<span className="break-all ">{file_name}</span>{" "}
</div>
);
let icon = (
<div className="p-2 bg-secondary rounded flex items-center justify-center h-full">
<div className="">
<div style={{ fontSize: "2em" }} className=" text-center mb-2">
{file.extension}
</div>
<div>{fileTitle}</div>
</div>
</div>
);
if (is_image) {
fileView = (
<div>
<div className="mb-2">{fileTitle}</div>
<ImageLoader
src={`${serverUrl}/${file_path}`}
className="w-full rounded"
/>
</div>
);
icon = fileView;
} else if (is_video) {
fileView = (
<div className="mb-2">
<a href={`${serverUrl}/${file_path}`}>{fileTitle}</a>
<video controls className="w-full rounded">
<source
src={`${serverUrl}/${file_path}`}
type={`video/${file_type}`}
/>
Your browser does not support the video tag.
</video>
</div>
);
// Update icon to show a video-related icon
icon = (
<div className=" relative rounded h-full">
<div className="absolute rounded p-2 bg-secondary top-0 ">
{fileTitle}
</div>
<div
style={{ minHeight: "150px" }}
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary "
>
<VideoCameraIcon className="h-14 w-14" />
</div>
</div>
);
} else if (is_csv) {
fileView = (
<div className="h">
<a href={`${serverUrl}/${file_path}`}>
<div className="mb-4">{fileTitle}</div>
</a>
<CsvLoader
csvUrl={`${serverUrl}/${file_path}`}
className="w-full rounded"
/>
</div>
);
icon = (
<div className=" relative rounded h-full">
<div className="absolute rounded p-2 bg-secondary top-0 ">
{fileTitle}
</div>
<div
style={{ minHeight: "150px" }}
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary"
>
<Icon icon="csv" size={14} />
</div>
</div>
);
} else if (is_code) {
fileView = (
<div className="h">
<a
href={`${serverUrl}/${file_path}`}
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-4">{fileTitle}</div>
</a>
<CodeLoader
url={`${serverUrl}/${file_path}`}
className="w-full rounded"
/>
</div>
);
icon = (
<div className=" relative rounded h-full">
<div className="absolute rounded p-2 bg-secondary top-0 ">
{fileTitle}
</div>
<div
style={{ minHeight: "150px" }}
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary"
>
<Icon icon="python" size={14} />
</div>
</div>
);
} else if (is_pdf) {
fileView = (
<div className="h-full">
<div className="mb-4">
<a
href={`${serverUrl}/${file_path}`}
target="_blank"
rel="noopener noreferrer"
>
{fileTitle}
</a>
</div>
<PdfViewer url={`${serverUrl}/${file_path}`} />
</div>
);
icon = (
<div className=" relative rounded h-full">
<div className="absolute rounded p-2 bg-secondary top-0 ">
{fileTitle}
</div>
<div
style={{ minHeight: "150px" }}
className="bg-secondary h-full w-full rounded flex items-center justify-center text-primary "
>
<Icon icon="pdf" size={14} />
</div>
</div>
);
} else {
fileView = <span>Unsupported file type.</span>;
}
return (
<div className=" h-full rounded">
<ExpandView className="mb-1" icon={icon} title={file_name}>
{fileView}
</ExpandView>
</div>
);
};
const renderFile = (file: IMetadataFile, i: number) => (
<div key={"metafilesrow" + i} className="text-primary ">
{renderFileContent(file, i)}
</div>
);
const files = (metadata.files || []).map(renderFile);
const messages = (metadata.messages || []).map((message: any, i: number) => {
return (
<div className=" mb-2 border-dashed" key={"messagerow" + i}>
<AgentRow message={message} />
</div>
);
});
const hasContent = files.length > 0;
const hasMessages = messages.length > 0;
return (
<div>
{hasMessages && (
<div className="rounded bg-primary ">
<CollapseBox
open={false}
title={`Agent Messages (${messages.length} message${
messages.length > 1 ? "s" : ""
}) | ${formatDuration(metadata?.time)}`}
>
{messages}
</CollapseBox>
</div>
)}
{hasContent && (
<div className="rounded mt-2">
<CollapseBox
open={true}
title={`Results (${files.length} file${
files.length > 1 ? "s" : ""
})`}
>
<div className="mt-2 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{files}
</div>
</CollapseBox>
</div>
)}
</div>
);
};
export default MetaDataView;

View File

@ -1,94 +0,0 @@
import * as React from "react";
import { IChatSession, IMessage, IStatus } from "../../types";
import { fetchJSON, getServerUrl, setLocalStorage } from "../../utils";
import ChatBox from "./chatbox";
import { appContext } from "../../../hooks/provider";
import { message } from "antd";
import SideBarView from "./sidebar";
import { useConfigStore } from "../../../hooks/store";
import SessionsView from "./sessions";
const RAView = () => {
const session: IChatSession | null = useConfigStore((state) => state.session);
const [loading, setLoading] = React.useState(false);
const [messages, setMessages] = React.useState<IMessage[] | null>(null);
const [config, setConfig] = React.useState(null);
React.useEffect(() => {
setLocalStorage("ara_config", config);
}, [config]);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const fetchMessagesUrl = `${serverUrl}/sessions/${session?.id}/messages?user_id=${user?.email}`;
const fetchMessages = () => {
setError(null);
setLoading(true);
setMessages(null);
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
setMessages(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(fetchMessagesUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user && session) {
fetchMessages();
}
}, [session]);
return (
<div className="h-full ">
<div className="flex h-full ">
<div className=" mr-2 rounded">
<SideBarView />
</div>
<div className=" flex-1 ">
{!session && (
<div className=" w-full h-full flex items-center justify-center">
<div className="w-2/3" id="middle">
<div className="w-full text-center">
{" "}
<img
src="/images/svgs/welcome.svg"
alt="welcome"
className="text-accent inline-block object-cover w-56"
/>
</div>
<SessionsView />
</div>
</div>
)}
{session !== null && (
<ChatBox initMessages={messages} session={session} />
)}
</div>
</div>
</div>
);
};
export default RAView;

View File

@ -1,460 +0,0 @@
import {
ChatBubbleLeftRightIcon,
ExclamationTriangleIcon,
GlobeAltIcon,
PencilIcon,
PlusIcon,
Square3Stack3DIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Button, Dropdown, Input, MenuProps, Modal, message } from "antd";
import * as React from "react";
import { IChatSession, IWorkflow, IStatus } from "../../types";
import { appContext } from "../../../hooks/provider";
import { fetchJSON, getServerUrl, timeAgo } from "../../utils";
import { LaunchButton, LoadingOverlay } from "../../atoms";
import { useConfigStore } from "../../../hooks/store";
import WorkflowSelector from "./utils/selectors";
const SessionsView = ({}: any) => {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const listSessionUrl = `${serverUrl}/sessions?user_id=${user?.email}`;
const createSessionUrl = `${serverUrl}/sessions`;
const publishSessionUrl = `${serverUrl}/sessions/publish`;
const sessions = useConfigStore((state) => state.sessions);
const setSessions = useConfigStore((state) => state.setSessions);
const sampleSession: IChatSession = {
user_id: user?.email || "",
name:
"New Session " +
new Date().toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
}),
};
const [selectedSession, setSelectedSession] =
React.useState<IChatSession | null>(sampleSession);
// const [session, setSession] =
// React.useState<IChatSession | null>(null);
const session = useConfigStore((state) => state.session);
const setSession = useConfigStore((state) => state.setSession);
const isSessionButtonsDisabled = useConfigStore(
(state) => state.areSessionButtonsDisabled
);
const deleteSession = (session: IChatSession) => {
setError(null);
setLoading(true);
// const fetch;
const deleteSessionUrl = `${serverUrl}/sessions/delete?user_id=${user?.email}&session_id=${session.id}`;
const payLoad = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user?.email,
session: session,
}),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
fetchSessions();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(deleteSessionUrl, payLoad, onSuccess, onError);
};
const [newSessionModalVisible, setNewSessionModalVisible] =
React.useState(false);
const fetchSessions = () => {
setError(null);
setLoading(true);
// const fetch;
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
// message.success(data.message);
// console.log("sessions", data);
setSessions(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(listSessionUrl, payLoad, onSuccess, onError);
};
const publishSession = () => {
setError(null);
setLoading(true);
const body = {
user_id: user?.email,
session: session,
tags: ["published"],
};
// const fetch;
const payLoad = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
// setSessions(data.data);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(publishSessionUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (sessions && sessions.length > 0) {
const firstSession = sessions[0];
setSession(firstSession);
} else {
setSession(null);
}
}, [sessions]);
const createSession = (session: IChatSession) => {
setError(null);
setLoading(true);
// const fetch;
const payLoad = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(session),
};
const onSuccess = (data: any) => {
if (data && data.status) {
message.success(data.message);
fetchSessions();
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(createSessionUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user) {
fetchSessions();
}
}, []);
const sessionRows = sessions.map((data: IChatSession, index: number) => {
const isSelected = session?.id === data.id;
const rowClass = isSelected
? "bg-accent text-white"
: "bg-secondary text-primary";
let items: MenuProps["items"] = [
{
label: (
<div
onClick={() => {
console.log("deleting session");
deleteSession(data);
}}
>
<TrashIcon
role={"button"}
title={"Delete"}
className="h-4 w-4 mr-1 inline-block"
/>
Delete
</div>
),
key: "delete",
},
{
label: (
<div
onClick={() => {
// get current clicked session
setSelectedSession(data);
setNewSessionModalVisible(true);
}}
>
<PencilIcon
role={"button"}
title={"Edit"}
className="h-4 w-4 mr-1 inline-block"
/>
Edit
</div>
),
key: "edit",
},
// {
// label: (
// <div
// onClick={() => {
// console.log("publishing session");
// publishSession();
// }}
// >
// <GlobeAltIcon
// role={"button"}
// title={"Publish"}
// className="h-4 w-4 mr-1 inline-block"
// />
// Publish
// </div>
// ),
// key: "publish",
// },
];
items.push();
const menu = (
<Dropdown
menu={{ items: items }}
trigger={["click"]}
placement="bottomRight"
>
<div
role="button"
className={`float-right ml-2 duration-100 hover:bg-secondary font-semibold px-2 pb-1 rounded ${
isSelected ? "hover:text-accent" : ""
}`}
>
<span className={`block -mt-2 ${isSelected ? "text-white" : ""}`}>
{" "}
...
</span>
</div>
</Dropdown>
);
return (
<div
key={"sessionsrow" + index}
className={`group relative mb-2 pb-1 border-b border-dashed ${
isSessionButtonsDisabled ? "opacity-50 pointer-events-none" : ""
}`}
>
{items.length > 0 && (
<div className=" absolute right-2 top-2 group-hover:opacity-100 opacity-0 ">
{menu}
</div>
)}
<div
className={`rounded p-2 cursor-pointer ${rowClass}`}
role="button"
onClick={() => {
// setWorkflowConfig(data.flow_config);
if (!isSessionButtonsDisabled) {
setSession(data);
}
}}
>
<div className="text-xs mt-1">
<Square3Stack3DIcon className="h-4 w-4 inline-block mr-1" />
{data.name}
</div>
<div className="text-xs text-right ">
{timeAgo(data.created_at || "")}
</div>
</div>
</div>
);
});
let windowHeight, skillsMaxHeight;
if (typeof window !== "undefined") {
windowHeight = window.innerHeight;
skillsMaxHeight = windowHeight - 400 + "px";
}
const NewSessionModal = ({ session }: { session: IChatSession | null }) => {
const [workflow, setWorkflow] = React.useState<IWorkflow | null>(null);
const [localSession, setLocalSession] = React.useState<IChatSession | null>(
session
);
React.useEffect(() => {
if (workflow && workflow.id && localSession) {
setLocalSession({ ...localSession, workflow_id: workflow.id });
}
}, [workflow]);
const sessionExists =
localSession !== null && localSession.id !== undefined;
return (
<Modal
onCancel={() => {
setNewSessionModalVisible(false);
}}
title={
<div className="font-semibold mb-2 pb-1 border-b">
<Square3Stack3DIcon className="h-5 w-5 inline-block mr-1" />
New Session{" "}
</div>
}
open={newSessionModalVisible}
footer={[
<Button
key="back"
onClick={() => {
setNewSessionModalVisible(false);
}}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
disabled={!workflow}
onClick={() => {
setNewSessionModalVisible(false);
if (localSession) {
createSession(localSession);
}
}}
>
Create
</Button>,
]}
>
<WorkflowSelector
workflow={workflow}
setWorkflow={setWorkflow}
workflow_id={selectedSession?.workflow_id}
disabled={sessionExists}
/>
<div className="my-2 text-xs"> Session Name </div>
<Input
placeholder="Session Name"
value={localSession?.name || ""}
onChange={(event) => {
if (localSession) {
setLocalSession({ ...localSession, name: event.target.value });
}
}}
/>
<div className="text-xs mt-4">
{" "}
{timeAgo(localSession?.created_at || "", true)}
</div>
</Modal>
);
};
return (
<div className=" ">
<NewSessionModal session={selectedSession || sampleSession} />
<div className="mb-2 relative">
<div className="">
<div className="font-semibold mb-2 pb-1 border-b">
<ChatBubbleLeftRightIcon className="h-5 w-5 inline-block mr-1" />
Sessions{" "}
</div>
{sessions && sessions.length > 0 && (
<div className="text-xs hidden mb-2 pb-1 ">
{" "}
Create a new session or select an existing session to view chat.
</div>
)}
<div
style={{
maxHeight: skillsMaxHeight,
}}
className="mb-4 overflow-y-auto scroll rounded relative "
>
{sessionRows}
<LoadingOverlay loading={loading} />
</div>
{(!sessions || sessions.length == 0) && !loading && (
<div className="text-xs text-gray-500">
No sessions found. Create a new session to get started.
</div>
)}
</div>
<div className="flex gap-x-2">
<div className="flex-1"></div>
<LaunchButton
className={`text-sm p-2 px-3 ${isSessionButtonsDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => {
setSelectedSession(sampleSession);
setNewSessionModalVisible(true);
}}
>
{" "}
<PlusIcon className="w-5 h-5 inline-block mr-1" />
New
</LaunchButton>
</div>
</div>
{error && !error.status && (
<div className="p-2 border border-orange-500 text-secondary rounded mt-4 text-sm">
{" "}
<ExclamationTriangleIcon className="h-5 text-orange-500 inline-block mr-2" />{" "}
{error.message}
</div>
)}
</div>
);
};
export default SessionsView;

View File

@ -1,49 +0,0 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import * as React from "react";
import SessionsView from "./sessions";
const SideBarView = () => {
const [isOpen, setIsOpen] = React.useState(true);
const minWidth = isOpen ? "270px" : "50px";
let windowHeight, sidebarMaxHeight;
if (typeof window !== "undefined") {
windowHeight = window.innerHeight;
sidebarMaxHeight = windowHeight - 180 + "px";
}
return (
<div
style={{
minWidth: minWidth,
maxWidth: minWidth,
height: "calc(100vh - 190px)",
}}
className=" "
>
<div className=" transition overflow-hidden duration-300 flex flex-col h-full p-2 overflow-y-scroll scroll rounded ">
<div className={`${isOpen ? "" : "hidden"} `}>
{/* <AgentsView /> */}
{<SessionsView />}
</div>
</div>
<div
onClick={() => setIsOpen(!isOpen)}
role="button"
className=" hover:text-accent duration-150 "
>
{isOpen ? (
<div className="mt-4 ">
{" "}
<ChevronLeftIcon className="w-6 h-6 inline-block rounded" />{" "}
<span className="text-xs "> close sidebar</span>
</div>
) : (
<ChevronRightIcon className="w-6 h-6 inline-block font-bold rounded " />
)}
</div>
</div>
);
};
export default SideBarView;

View File

@ -1,58 +0,0 @@
import { Bar, Line } from "@ant-design/plots";
import * as React from "react";
import { IStatus } from "../../../../types";
const BarChartViewer = ({ data }: { data: any | null }) => {
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const [loading, setLoading] = React.useState(false);
const config = {
data: data.bar,
xField: "agent",
yField: "message",
colorField: "tool_call",
stack: true,
axis: {
y: { labelFormatter: "" },
x: {
labelSpacing: 4,
},
},
style: {
radiusTopLeft: 10,
radiusTopRight: 10,
},
height: 60 * data.agents.length,
};
const config_code_exec = Object.assign({}, config);
config_code_exec.colorField = "code_execution";
return (
<div className="bg-white rounded relative">
<div>
<div className="grid grid-cols-2">
<div>
<div className=" text-gray-700 border-b border-dashed p-4">
{" "}
Tool Call
</div>
<Bar {...config} />
</div>
<div className=" ">
<div className=" text-gray-700 border-b border-dashed p-4">
{" "}
Code Execution Status
</div>
<Bar {...config_code_exec} />
</div>
</div>
</div>
</div>
);
};
export default BarChartViewer;

View File

@ -1,125 +0,0 @@
import { Tooltip, message } from "antd";
import * as React from "react";
import { IStatus, IChatMessage } from "../../../types";
import { fetchJSON, getServerUrl } from "../../../utils";
import { appContext } from "../../../../hooks/provider";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
const BarChartViewer = React.lazy(() => import("./charts/bar"));
const ProfilerView = ({
agentMessage,
}: {
agentMessage: IChatMessage | null;
}) => {
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const [loading, setLoading] = React.useState(false);
const [profile, setProfile] = React.useState<any | null>(null);
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const fetchProfile = (messageId: number) => {
const profilerUrl = `${serverUrl}/profiler/${messageId}?user_id=${user?.email}`;
setError(null);
setLoading(true);
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
console.log(data);
if (data && data.status) {
setProfile(data.data);
setTimeout(() => {
// scroll parent to bottom
const parent = document.getElementById("chatbox");
if (parent) {
parent.scrollTop = parent.scrollHeight;
}
}, 4000);
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(profilerUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user && agentMessage && agentMessage.id) {
fetchProfile(agentMessage.id);
}
}, []);
const UsageViewer = ({ usage }: { usage: any }) => {
const usageRows = usage.map((usage: any, index: number) => (
<div key={index} className=" borpder rounded">
{(usage.total_cost != 0 || usage.total_tokens != 0) && (
<>
<div className="bg-secondary p-2 text-xs rounded-t">
{usage.agent}
</div>
<div className="bg-tertiary p-3 rounded-b inline-flex gap-2 w-full">
{usage.total_tokens && usage.total_tokens != 0 && (
<div className="flex flex-col text-center w-full">
<div className="w-full px-2 text-2xl ">
{usage.total_tokens}
</div>
<div className="w-full text-xs">tokens</div>
</div>
)}
{usage.total_cost && usage.total_cost != 0 && (
<div className="flex flex-col text-center w-full">
<div className="w-full px-2 text-2xl ">
{usage.total_cost?.toFixed(3)}
</div>
<div className="w-full text-xs">USD</div>
</div>
)}
</div>
</>
)}
</div>
));
return (
<div className="inline-flex gap-3 flex-wrap">{usage && usageRows}</div>
);
};
return (
<div className=" relative">
<div className="text-sm ">
{/* {profile && <RadarMetrics profileData={profile} />} */}
{profile && <BarChartViewer data={profile} />}
<div className="mt-4">
<div className="mt-4 mb-4 txt">
LLM Costs
<Tooltip
title={
"LLM tokens below based on data returned by the model. Support for exact costs may vary."
}
>
<InformationCircleIcon className="ml-1 text-gray-400 inline-block w-4 h-4" />
</Tooltip>
</div>
{profile && profile.usage && <UsageViewer usage={profile.usage} />}
</div>
</div>
</div>
);
};
export default ProfilerView;

View File

@ -1,122 +0,0 @@
import { Select, message } from "antd";
import * as React from "react";
import { LoadingOverlay } from "../../../atoms";
import { IWorkflow, IStatus } from "../../../types";
import { fetchJSON, getServerUrl } from "../../../utils";
import { appContext } from "../../../../hooks/provider";
import { Link } from "gatsby";
const WorkflowSelector = ({
workflow,
setWorkflow,
workflow_id,
disabled,
}: {
workflow: IWorkflow | null;
setWorkflow: (workflow: IWorkflow) => void;
workflow_id: number | undefined;
disabled?: boolean;
}) => {
const [error, setError] = React.useState<IStatus | null>({
status: true,
message: "All good",
});
const [loading, setLoading] = React.useState(false);
const [workflows, setWorkflows] = React.useState<IWorkflow[]>([]);
const [selectedWorkflow, setSelectedWorkflow] = React.useState<number>(0);
const { user } = React.useContext(appContext);
const serverUrl = getServerUrl();
const listWorkflowsUrl = `${serverUrl}/workflows?user_id=${user?.email}`;
const fetchWorkFlow = () => {
setError(null);
setLoading(true);
const payLoad = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const onSuccess = (data: any) => {
if (data && data.status) {
// message.success(data.message);
setWorkflows(data.data);
if (data.data.length > 0) {
if (!disabled) {
setWorkflow(data.data[0]);
} else {
const index = data.data.findIndex((item:IWorkflow) => item.id === workflow_id);
if (index !== -1) {
setSelectedWorkflow(index);
setWorkflow(data.data[index]);
}
}
}
} else {
message.error(data.message);
}
setLoading(false);
};
const onError = (err: any) => {
setError(err);
message.error(err.message);
setLoading(false);
};
fetchJSON(listWorkflowsUrl, payLoad, onSuccess, onError);
};
React.useEffect(() => {
if (user) {
fetchWorkFlow();
}
}, []);
return (
<div className=" mb-4 relative">
<div className="text-sm mt-2 mb-2 pb-1 ">
{" "}
Please select an agent workflow to begin.{" "}
</div>
<div className="relative mt-2 ">
<LoadingOverlay loading={loading} />
{workflows && workflows.length > 0 && (
<Select
disabled={disabled}
className="w-full"
value={workflows[selectedWorkflow].name}
onChange={(value: any) => {
setSelectedWorkflow(value);
setWorkflow(workflows[value]);
}}
options={
workflows.map((config, index) => {
return { label: config.name, value: index };
}) as any
}
/>
)}
<div className="mt-2 text-xs hidden">
{" "}
<div className="my-2 text-xs"> {workflow?.name}</div>
View all workflows{" "}
<span className="text-accent">
{" "}
<Link to="/build">here</Link>
</span>{" "}
</div>
</div>
{!workflows ||
(workflows && workflows.length === 0 && (
<div className="p-1 border rounded text-xs px-2 text-secondary">
{" "}
No agent workflows found.
</div>
))}
</div>
);
};
export default WorkflowSelector;

View File

@ -0,0 +1,20 @@
import Markdown from "react-markdown";
import React from "react";
interface MarkdownViewProps {
children: string;
className?: string;
}
export const MarkdownView: React.FC<MarkdownViewProps> = ({
children,
className = "",
}) => {
return (
<div
className={`text-sm w-full prose dark:prose-invert text-primary rounded p-2 ${className}`}
>
<Markdown>{children}</Markdown>
</div>
);
};

View File

@ -0,0 +1,48 @@
import React, { useState } from "react";
import Editor from "@monaco-editor/react";
export const MonacoEditor = ({
value,
editorRef,
language,
onChange,
minimap = true,
}: {
value: string;
onChange?: (value: string) => void;
editorRef: any;
language: string;
minimap?: boolean;
}) => {
const [isEditorReady, setIsEditorReady] = useState(false);
const onEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor;
setIsEditorReady(true);
};
return (
<div id="monaco-editor" className="h-full rounded">
<Editor
height="100%"
className="h-full rounded"
defaultLanguage={language}
defaultValue={value}
value={value}
onChange={(value: string | undefined) => {
if (onChange && value) {
onChange(value);
}
}}
onMount={onEditorDidMount}
theme="vs-dark"
options={{
wordWrap: "on",
wrappingIndent: "indent",
wrappingStrategy: "advanced",
minimap: {
enabled: minimap,
},
}}
/>
</div>
);
};

View File

@ -0,0 +1,115 @@
import { Session } from "../../../types/datamodel";
import { getServerUrl } from "../../../utils";
export class SessionAPI {
private getBaseUrl(): string {
return getServerUrl();
}
private getHeaders(): HeadersInit {
return {
"Content-Type": "application/json",
};
}
async listSessions(userId: string): Promise<Session[]> {
const response = await fetch(
`${this.getBaseUrl()}/sessions?user_id=${userId}`,
{
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to fetch sessions");
return data.data;
}
async getSession(sessionId: number, userId: string): Promise<Session> {
const response = await fetch(
`${this.getBaseUrl()}/sessions/${sessionId}?user_id=${userId}`,
{
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to fetch session");
return data.data;
}
async createSession(
sessionData: Partial<Session>,
userId: string
): Promise<Session> {
const session = {
...sessionData,
user_id: userId, // Ensure user_id is included
};
const response = await fetch(`${this.getBaseUrl()}/sessions`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify(session),
});
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to create session");
return data.data;
}
async updateSession(
sessionId: number,
sessionData: Partial<Session>,
userId: string
): Promise<Session> {
const session = {
...sessionData,
id: sessionId,
user_id: userId, // Ensure user_id is included
};
const response = await fetch(
`${this.getBaseUrl()}/sessions/${sessionId}?user_id=${userId}`,
{
method: "PUT",
headers: this.getHeaders(),
body: JSON.stringify(session),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to update session");
return data.data;
}
async deleteSession(sessionId: number, userId: string): Promise<void> {
const response = await fetch(
`${this.getBaseUrl()}/sessions/${sessionId}?user_id=${userId}`,
{
method: "DELETE",
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to delete session");
}
// Adding messages endpoint
async listSessionMessages(sessionId: number, userId: string): Promise<any[]> {
// Replace 'any' with proper message type
const response = await fetch(
`${this.getBaseUrl()}/sessions/${sessionId}/messages?user_id=${userId}`,
{
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to fetch messages");
return data.data;
}
}
export const sessionAPI = new SessionAPI();

View File

@ -0,0 +1,157 @@
import React, { useContext, useEffect, useState } from "react";
import { Modal, Form, Input, message, Button, Select, Spin } from "antd";
import { TriangleAlertIcon } from "lucide-react";
import type { FormProps } from "antd";
import { SessionEditorProps } from "./types";
import { Team } from "../../../types/datamodel";
import { teamAPI } from "../team/api";
import { appContext } from "../../../../hooks/provider";
type FieldType = {
name: string;
team_id?: string;
};
export const SessionEditor: React.FC<SessionEditorProps> = ({
session,
onSave,
onCancel,
isOpen,
}) => {
const [form] = Form.useForm();
const [teams, setTeams] = useState<Team[]>([]);
const [loading, setLoading] = useState(false);
const { user } = useContext(appContext);
// Fetch teams when modal opens
useEffect(() => {
const fetchTeams = async () => {
if (isOpen) {
try {
setLoading(true);
const userId = user?.email || "";
const teamsData = await teamAPI.listTeams(userId);
setTeams(teamsData);
} catch (error) {
message.error("Failed to load teams");
console.error("Error loading teams:", error);
} finally {
setLoading(false);
}
}
};
fetchTeams();
}, [isOpen, user?.email]);
// Set form values when modal opens or session changes
useEffect(() => {
if (isOpen) {
form.setFieldsValue({
name: session?.name || "",
team_id: session?.team_id || undefined,
});
} else {
form.resetFields();
}
}, [form, session, isOpen]);
const onFinish: FormProps<FieldType>["onFinish"] = async (values) => {
try {
await onSave({
...values,
id: session?.id,
});
message.success(
`Session ${session ? "updated" : "created"} successfully`
);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
};
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
errorInfo
) => {
message.error("Please check the form for errors");
console.error("Form validation failed:", errorInfo);
};
const hasNoTeams = !loading && teams.length === 0;
return (
<Modal
title={session ? "Edit Session" : "Create Session"}
open={isOpen}
onCancel={onCancel}
footer={null}
className="text-primary"
forceRender
>
<Form
form={form}
name="session-form"
layout="vertical"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item<FieldType>
label="Session Name"
name="name"
rules={[
{ required: true, message: "Please enter a session name" },
{ max: 100, message: "Session name cannot exceed 100 characters" },
]}
>
<Input />
</Form.Item>
<div className="space-y-2">
<Form.Item<FieldType>
label="Team"
name="team_id"
rules={[{ required: true, message: "Please select a team" }]}
>
<Select
placeholder="Select a team"
loading={loading}
disabled={loading || hasNoTeams}
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
options={teams.map((team) => ({
value: team.id,
label: `${team.config.name} (${team.config.team_type})`,
}))}
notFoundContent={loading ? <Spin size="small" /> : null}
/>
</Form.Item>
</div>
{hasNoTeams && (
<div className="flex border p-1 rounded -mt-2 mb-4 items-center gap-1.5 text-sm text-yellow-600">
<TriangleAlertIcon className="h-4 w-4" />
<span>No teams found. Please create a team first.</span>
</div>
)}
<Form.Item className="flex justify-end mb-0">
<div className="flex gap-2">
<Button onClick={onCancel}>Cancel</Button>
<Button type="primary" htmlType="submit" disabled={hasNoTeams}>
{session ? "Update" : "Create"}
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default SessionEditor;

View File

@ -0,0 +1,76 @@
import React from "react";
import { Select, Button, Popconfirm } from "antd";
import { Edit, Trash2 } from "lucide-react";
import type { SessionListProps } from "./types";
import type { SelectProps } from "antd";
export const SessionList: React.FC<SessionListProps> = ({
sessions,
currentSession,
onSelect,
onEdit,
onDelete,
isLoading,
}) => {
const options: SelectProps["options"] = [
{
label: "Sessions",
options: sessions.map((session) => ({
label: (
<div className="flex items-center justify-between w-full pr-2">
<span className="flex-1 truncate">{session.name}</span>
<div className="flex gap-2 ml-2">
<Button
type="text"
size="small"
className="p-0 min-w-[24px] h-6 text-primary"
icon={<Edit className="w-4 h-4 text-primary" />}
onClick={(e) => {
e.stopPropagation();
onEdit(session);
}}
/>
<Popconfirm
title="Delete Session"
description="Are you sure you want to delete this session?"
onConfirm={(e) => {
e?.stopPropagation();
if (session.id) onDelete(session.id);
}}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
size="small"
className="p-0 min-w-[24px] h-6"
danger
icon={<Trash2 className="w-4 h-4 text-primary" />}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</div>
</div>
),
value: session.id,
})),
},
];
return (
<Select
className="w-64"
placeholder={isLoading ? "Loading sessions..." : "Select a session"}
loading={isLoading}
disabled={isLoading}
value={currentSession?.id}
onChange={(value) => {
const session = sessions.find((s) => s.id === value);
if (session) onSelect(session);
}}
options={options}
notFoundContent={sessions.length === 0 ? "No sessions found" : undefined}
dropdownStyle={{ minWidth: "256px" }}
listHeight={256}
/>
);
};

View File

@ -0,0 +1,190 @@
import React, { useCallback, useEffect, useState, useContext } from "react";
import { Button, message, Collapse, Badge, CollapseProps } from "antd";
import { Plus, ChevronDown } from "lucide-react";
import { useConfigStore } from "../../../../hooks/store";
import { appContext } from "../../../../hooks/provider";
import { sessionAPI } from "./api";
import { SessionList } from "./list";
import { SessionEditor } from "./editor";
import type { Session } from "../../../types/datamodel";
export const SessionManager: React.FC = () => {
// UI State
const [isLoading, setIsLoading] = useState(false);
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [editingSession, setEditingSession] = useState<Session | undefined>();
// Global context and store
const { user } = useContext(appContext);
const { session, setSession, sessions, setSessions } = useConfigStore();
// Fetch all sessions
const fetchSessions = useCallback(async () => {
if (!user?.email) return;
try {
setIsLoading(true);
const data = await sessionAPI.listSessions(user.email);
setSessions(data);
if (!session && data.length > 0) {
setSession(data[0]);
}
} catch (error) {
console.error("Error fetching sessions:", error);
message.error("Error loading sessions");
} finally {
setIsLoading(false);
}
}, [user?.email, setSessions]);
// Handle session operations
const handleSaveSession = async (sessionData: Partial<Session>) => {
if (!user?.email) return;
try {
if (sessionData.id) {
const updated = await sessionAPI.updateSession(
sessionData.id,
sessionData,
user.email
);
setSessions(
sessions.map((s) => (s.id && s.id === updated.id ? updated : s))
);
if (session?.id === updated.id) {
setSession(updated);
}
} else {
const created = await sessionAPI.createSession(sessionData, user.email);
setSession(created);
setSessions([...sessions, created]);
}
setIsEditorOpen(false);
setEditingSession(undefined);
} catch (error) {
throw error;
}
};
const handleDeleteSession = async (sessionId: number) => {
if (!user?.email) return;
try {
await sessionAPI.deleteSession(sessionId, user.email);
setSessions(sessions.filter((s) => s.id !== sessionId));
if (sessions.length > 0) {
setSession(sessions[0]);
}
if (session?.id === sessionId) {
setSession(null);
}
message.success("Session deleted");
} catch (error) {
console.error("Error deleting session:", error);
message.error("Error deleting session");
}
};
const handleSelectSession = async (selectedSession: Session) => {
if (!user?.email || !selectedSession.id) return;
try {
setIsLoading(true);
const data = await sessionAPI.getSession(selectedSession.id, user.email);
setSession(data);
} catch (error) {
console.error("Error loading session:", error);
message.error("Error loading session");
} finally {
setIsLoading(false);
}
};
// Load sessions on mount
useEffect(() => {
fetchSessions();
}, [fetchSessions]);
// Custom header with session count
const CollapsibleHeader = () => (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-3">
<span className="font-medium">Sessions</span>
<Badge
count={sessions.length}
showZero
className="site-badge-count-4"
style={{ backgroundColor: "#52525b" }}
/>
</div>
{session && (
<span className="text-sm text-gray-500">Current: {session.name}</span>
)}
</div>
);
// Session management content
const SessionContent = () => (
<div className="flex gap-2 items-center">
{sessions && sessions.length > 0 && (
<SessionList
sessions={sessions}
currentSession={session}
onSelect={handleSelectSession}
onEdit={(session) => {
setEditingSession(session);
setIsEditorOpen(true);
}}
onDelete={handleDeleteSession}
isLoading={isLoading}
/>
)}
<div className="relative">
<Button
type="primary"
onClick={() => {
setEditingSession(undefined);
setIsEditorOpen(true);
}}
icon={<Plus className="w-4 h-4" />}
>
New Session{" "}
{sessions.length === 0 && (
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-secondary"></span>
</span>
)}
</Button>
</div>
</div>
);
const items: CollapseProps["items"] = [
{
key: "1",
label: <CollapsibleHeader />,
children: <SessionContent />,
},
];
return (
<>
<div className="bg-secondary rounded p-2">
<div className="text-xs pb-2">
Sessions <span className="px-1 text-accent">{sessions.length} </span>
</div>
<SessionContent />
</div>
<SessionEditor
session={editingSession}
isOpen={isEditorOpen}
onSave={handleSaveSession}
onCancel={() => {
setIsEditorOpen(false);
setEditingSession(undefined);
}}
/>
</>
);
};

View File

@ -0,0 +1,24 @@
import type { Session } from "../../../types/datamodel";
export interface SessionEditorProps {
session?: Session;
onSave: (session: Partial<Session>) => Promise<void>;
onCancel: () => void;
isOpen: boolean;
}
export interface SessionListProps {
sessions: Session[];
currentSession?: Session | null;
onSelect: (session: Session) => void;
onEdit: (session: Session) => void;
onDelete: (sessionId: number) => void;
isLoading?: boolean;
}
export interface SessionFormState {
name: string;
team_id: string;
isSubmitting: boolean;
error?: string;
}

View File

@ -0,0 +1,127 @@
import { Team, AgentConfig } from "../../../types/datamodel";
import { getServerUrl } from "../../../utils";
export class TeamAPI {
private getBaseUrl(): string {
return getServerUrl();
}
private getHeaders(): HeadersInit {
return {
"Content-Type": "application/json",
};
}
async listTeams(userId: string): Promise<Team[]> {
const response = await fetch(
`${this.getBaseUrl()}/teams?user_id=${userId}`,
{
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status) throw new Error(data.message || "Failed to fetch teams");
return data.data;
}
async getTeam(teamId: number, userId: string): Promise<Team> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}?user_id=${userId}`,
{
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status) throw new Error(data.message || "Failed to fetch team");
return data.data;
}
async createTeam(teamData: Partial<Team>, userId: string): Promise<Team> {
const team = {
...teamData,
user_id: userId,
};
const response = await fetch(`${this.getBaseUrl()}/teams`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify(team),
});
const data = await response.json();
if (!data.status) throw new Error(data.message || "Failed to create team");
return data.data;
}
async deleteTeam(teamId: number, userId: string): Promise<void> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}?user_id=${userId}`,
{
method: "DELETE",
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status) throw new Error(data.message || "Failed to delete team");
}
// Team-Agent Link Management
async linkAgent(teamId: number, agentId: number): Promise<void> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}`,
{
method: "POST",
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to link agent to team");
}
async linkAgentWithSequence(
teamId: number,
agentId: number,
sequenceId: number
): Promise<void> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}/${sequenceId}`,
{
method: "POST",
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(
data.message || "Failed to link agent to team with sequence"
);
}
async unlinkAgent(teamId: number, agentId: number): Promise<void> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}/agents/${agentId}`,
{
method: "DELETE",
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to unlink agent from team");
}
async getTeamAgents(teamId: number): Promise<AgentConfig[]> {
const response = await fetch(
`${this.getBaseUrl()}/teams/${teamId}/agents`,
{
headers: this.getHeaders(),
}
);
const data = await response.json();
if (!data.status)
throw new Error(data.message || "Failed to fetch team agents");
return data.data;
}
}
export const teamAPI = new TeamAPI();

View File

@ -0,0 +1,171 @@
import React, { useEffect, useState, useRef } from "react";
import { Modal, Form, message, Button } from "antd";
import { TriangleAlertIcon } from "lucide-react";
import { TeamEditorProps } from "./types";
import type { FormProps } from "antd";
import type { Team, TeamConfig } from "../../../types/datamodel";
import { MonacoEditor } from "../monaco";
const defaultTeamConfig: TeamConfig = {
name: "",
participants: [],
team_type: "RoundRobinGroupChat",
};
type FieldType = {
config: string;
};
export const TeamEditor: React.FC<TeamEditorProps> = ({
team,
onSave,
onCancel,
isOpen,
}) => {
const [form] = Form.useForm();
const [jsonError, setJsonError] = useState<string | null>(null);
const editorRef = useRef(null);
const [editorValue, setEditorValue] = useState<string>("");
// Set form values when modal opens or team changes
useEffect(() => {
if (isOpen) {
const configStr = team
? JSON.stringify(team.config, null, 2)
: JSON.stringify(defaultTeamConfig, null, 2);
setEditorValue(configStr);
form.setFieldsValue({
config: configStr,
});
} else {
form.resetFields();
setJsonError(null);
}
}, [form, team, isOpen]);
const validateJSON = (jsonString: string): TeamConfig | null => {
try {
const parsed = JSON.parse(jsonString);
// Basic validation of required fields
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
throw new Error("Team name is required");
}
if (!Array.isArray(parsed.participants)) {
throw new Error("Participants must be an array");
}
if (
!["RoundRobinGroupChat", "SelectorGroupChat"].includes(parsed.team_type)
) {
throw new Error("Invalid team_type");
}
return parsed;
} catch (error) {
if (error instanceof Error) {
setJsonError(error.message);
} else {
setJsonError("Invalid JSON format");
}
return null;
}
};
const onFinish: FormProps<FieldType>["onFinish"] = async () => {
const config = validateJSON(editorValue);
if (!config) {
return;
}
try {
// When updating, keep the existing id and dates
const teamData: Partial<Team> = team
? {
...team,
config,
// Remove date fields to let the backend handle them
created_at: undefined,
updated_at: undefined,
}
: { config };
await onSave(teamData);
message.success(`Team ${team ? "updated" : "created"} successfully`);
setJsonError(null);
} catch (error) {
if (error instanceof Error) {
message.error(error.message);
}
}
};
const handleEditorChange = (value: string) => {
setEditorValue(value);
form.setFieldsValue({ config: value });
// Clear error if JSON becomes valid
try {
JSON.parse(value);
setJsonError(null);
} catch (e) {
// Don't set error while typing - Monaco will show syntax errors
}
};
return (
<Modal
title={team ? "Edit Team" : "Create Team"}
open={isOpen}
onCancel={onCancel}
footer={null}
className="text-primary"
width={800}
forceRender
>
<Form
form={form}
name="team-form"
layout="vertical"
onFinish={onFinish}
autoComplete="off"
>
<div className="mb-2 text-xs text-gray-500">
Required fields: name (string), team_type ("RoundRobinGroupChat" |
"SelectorGroupChat"), participants (array)
</div>
<div className="h-[500px] mb-4">
<MonacoEditor
value={editorValue}
onChange={handleEditorChange}
editorRef={editorRef}
language="json"
minimap={false}
/>
</div>
{jsonError && (
<div className="flex items-center gap-1.5 text-sm text-red-500 mb-4">
<TriangleAlertIcon className="h-4 w-4" />
<span>{jsonError}</span>
</div>
)}
<Form.Item className="flex justify-end mb-0">
<div className="flex gap-2">
<Button onClick={onCancel}>Cancel</Button>
<Button
type="primary"
onClick={() => form.submit()}
disabled={!!jsonError}
>
{team ? "Update" : "Create"}
</Button>
</div>
</Form.Item>
</Form>
</Modal>
);
};
export default TeamEditor;

View File

@ -0,0 +1,76 @@
import React from "react";
import { Select, Button, Popconfirm } from "antd";
import { Edit, Trash2 } from "lucide-react";
import type { TeamListProps } from "./types";
import type { SelectProps } from "antd";
export const TeamList: React.FC<TeamListProps> = ({
teams,
currentTeam,
onSelect,
onEdit,
onDelete,
isLoading,
}) => {
const options: SelectProps["options"] = [
{
label: "Teams",
options: teams.map((team) => ({
label: (
<div className="flex items-center justify-between w-full pr-2">
<span className="flex-1 truncate">{team.config.name}</span>
<div className="flex gap-2 ml-2">
<Button
type="text"
size="small"
className="p-0 min-w-[24px] h-6 text-primary"
icon={<Edit className="w-4 h-4 text-primary" />}
onClick={(e) => {
e.stopPropagation();
onEdit(team);
}}
/>
<Popconfirm
title="Delete Team"
description="Are you sure you want to delete this team?"
onConfirm={(e) => {
e?.stopPropagation();
if (team.id) onDelete(team.id);
}}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
size="small"
className="p-0 min-w-[24px] h-6"
danger
icon={<Trash2 className="w-4 h-4 text-primary" />}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</div>
</div>
),
value: team.id,
})),
},
];
return (
<Select
className="w-64"
placeholder={isLoading ? "Loading teams..." : "Select a team"}
loading={isLoading}
disabled={isLoading}
value={currentTeam?.id}
onChange={(value) => {
const team = teams.find((t) => t.id === value);
if (team) onSelect(team);
}}
options={options}
notFoundContent={teams.length === 0 ? "No teams found" : undefined}
dropdownStyle={{ minWidth: "256px" }}
listHeight={256}
/>
);
};

View File

@ -0,0 +1,153 @@
import React, { useCallback, useEffect, useState, useContext } from "react";
import { Button, message, Badge } from "antd";
import { Plus } from "lucide-react";
import { appContext } from "../../../../hooks/provider";
import { teamAPI } from "./api";
import { TeamList } from "./list";
import { TeamEditor } from "./editor";
import type { Team } from "../../../types/datamodel";
export const TeamManager: React.FC = () => {
// UI State
const [isLoading, setIsLoading] = useState(false);
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [editingTeam, setEditingTeam] = useState<Team | undefined>();
const [teams, setTeams] = useState<Team[]>([]);
const [currentTeam, setCurrentTeam] = useState<Team | null>(null);
// Global context
const { user } = useContext(appContext);
// Fetch all teams
const fetchTeams = useCallback(async () => {
if (!user?.email) return;
try {
setIsLoading(true);
const data = await teamAPI.listTeams(user.email);
setTeams(data);
if (!currentTeam && data.length > 0) {
setCurrentTeam(data[0]);
}
} catch (error) {
console.error("Error fetching teams:", error);
message.error("Error loading teams");
} finally {
setIsLoading(false);
}
}, [user?.email, currentTeam]);
// Handle team operations
const handleSaveTeam = async (teamData: Partial<Team>) => {
if (!user?.email) return;
try {
console.log("teamData", teamData);
const savedTeam = await teamAPI.createTeam(teamData, user.email);
// Update teams list
if (teamData.id) {
setTeams(teams.map((t) => (t.id === savedTeam.id ? savedTeam : t)));
if (currentTeam?.id === savedTeam.id) {
setCurrentTeam(savedTeam);
}
} else {
setTeams([...teams, savedTeam]);
}
setIsEditorOpen(false);
setEditingTeam(undefined);
} catch (error) {
throw error;
}
};
const handleDeleteTeam = async (teamId: number) => {
if (!user?.email) return;
try {
await teamAPI.deleteTeam(teamId, user.email);
setTeams(teams.filter((t) => t.id !== teamId));
if (currentTeam?.id === teamId) {
setCurrentTeam(null);
}
message.success("Team deleted");
} catch (error) {
console.error("Error deleting team:", error);
message.error("Error deleting team");
}
};
const handleSelectTeam = async (selectedTeam: Team) => {
if (!user?.email || !selectedTeam.id) return;
try {
setIsLoading(true);
const data = await teamAPI.getTeam(selectedTeam.id, user.email);
setCurrentTeam(data);
} catch (error) {
console.error("Error loading team:", error);
message.error("Error loading team");
} finally {
setIsLoading(false);
}
};
// Load teams on mount
useEffect(() => {
fetchTeams();
}, [fetchTeams]);
// Content component
const TeamContent = () => (
<div className="flex gap-2 items-center">
{teams && teams.length > 0 && (
<div className="flex items-center gap-3">
<TeamList
teams={teams}
currentTeam={currentTeam}
onSelect={handleSelectTeam}
onEdit={(team) => {
setEditingTeam(team);
setIsEditorOpen(true);
}}
onDelete={handleDeleteTeam}
isLoading={isLoading}
/>
</div>
)}
<Button
type="primary"
onClick={() => {
setEditingTeam(undefined);
setIsEditorOpen(true);
}}
icon={<Plus className="w-4 h-4" />}
>
New Team
</Button>
</div>
);
return (
<>
<div className="bg-secondary rounded p-2">
<div className="text-xs pb-2">
Teams <span className="px-1 text-accent">{teams.length} </span>
</div>
<TeamContent />
</div>
<TeamEditor
team={editingTeam}
isOpen={isEditorOpen}
onSave={handleSaveTeam}
onCancel={() => {
setIsEditorOpen(false);
setEditingTeam(undefined);
}}
/>
</>
);
};
export default TeamManager;

View File

@ -0,0 +1,17 @@
import type { Team } from "../../../types/datamodel";
export interface TeamEditorProps {
team?: Team;
onSave: (team: Partial<Team>) => Promise<void>;
onCancel: () => void;
isOpen: boolean;
}
export interface TeamListProps {
teams: Team[];
currentTeam?: Team | null;
onSelect: (team: Team) => void;
onEdit: (team: Team) => void;
onDelete: (teamId: number) => void;
isLoading?: boolean;
}

View File

@ -1,25 +1,33 @@
import { create } from "zustand";
import { v4 as uuidv4 } from "uuid";
import { Message, Session } from "../components/types/datamodel";
import { IChatMessage, IChatSession } from "../components/types";
interface ConfigState {
messages: IChatMessage[] | null;
setMessages: (messages: IChatMessage[]) => void;
session: IChatSession | null;
setSession: (session: IChatSession | null) => void;
sessions: IChatSession[];
setSessions: (sessions: IChatSession[]) => void;
version: string | null;
setVersion: (version: string) => void;
connectionId: string;
setConnectionId: (connectionId: string) => void;
areSessionButtonsDisabled: boolean;
setAreSessionButtonsDisabled: (disabled: boolean) => void;
interface ISidebarState {
isExpanded: boolean;
isPinned: boolean;
}
export const useConfigStore = create<ConfigState>()((set) => ({
messages: null,
export interface IConfigState {
messages: Message[];
setMessages: (messages: Message[]) => void;
session: Session | null;
setSession: (session: Session | null) => void;
sessions: Session[];
setSessions: (sessions: Session[]) => void;
version: string | null;
setVersion: (version: string | null) => void;
// Sidebar state
sidebar: ISidebarState;
setSidebarState: (state: Partial<ISidebarState>) => void;
collapseSidebar: () => void;
expandSidebar: () => void;
toggleSidebar: () => void;
}
export const useConfigStore = create<IConfigState>((set) => ({
// Existing state
messages: [],
setMessages: (messages) => set({ messages }),
session: null,
setSession: (session) => set({ session }),
@ -28,7 +36,30 @@ export const useConfigStore = create<ConfigState>()((set) => ({
version: null,
setVersion: (version) => set({ version }),
connectionId: uuidv4(),
setConnectionId: (connectionId) => set({ connectionId }),
areSessionButtonsDisabled: false,
setAreSessionButtonsDisabled: (disabled) => set({ areSessionButtonsDisabled: disabled }),
// Sidebar state and actions
sidebar: {
isExpanded: true,
isPinned: false,
},
setSidebarState: (newState) =>
set((state) => ({
sidebar: { ...state.sidebar, ...newState },
})),
collapseSidebar: () =>
set((state) => ({
sidebar: { ...state.sidebar, isExpanded: false },
})),
expandSidebar: () =>
set((state) => ({
sidebar: { ...state.sidebar, isExpanded: true },
})),
toggleSidebar: () =>
set((state) => ({
sidebar: { ...state.sidebar, isExpanded: !state.sidebar.isExpanded },
})),
}));

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f7307728cfd62bc0d07d8cc3ab3809e36d8859a5ad314d020b21648642cf574
size 12710
oid sha256:7ec19e710d8b38d99b5f1b170db18eb192cd3a50fe80f1c103f88cce68e057b4
size 33034

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/><circle cx="190.15351" cy="24.95465" r="20" fill="#6c63ff"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/><circle cx="433.63626" cy="105.17383" r="20" fill="#6c63ff"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,3 @@
declare module "*.jpg";
declare module "*.png";
declare module "*.svg";

View File

@ -1,7 +1,6 @@
import * as React from "react"
import { Link } from "gatsby"
import { Link, HeadFC, PageProps } from "gatsby"
// styles
const pageStyles = {
color: "#232129",
padding: "96px",
@ -24,18 +23,12 @@ const codeStyles = {
borderRadius: 4,
}
// markup
const NotFoundPage = () => {
const NotFoundPage: React.FC<PageProps> = () => {
return (
<main style={pageStyles}>
<title>Not found</title>
<h1 style={headingStyles}>Page not found</h1>
<p style={paragraphStyles}>
Sorry{" "}
<span role="img" aria-label="Pensive emoji">
😔
</span>{" "}
we couldnt find what you were looking for.
Sorry 😔, we couldnt find what you were looking for.
<br />
{process.env.NODE_ENV === "development" ? (
<>
@ -52,3 +45,5 @@ const NotFoundPage = () => {
}
export default NotFoundPage
export const Head: HeadFC = () => <title>Not found</title>

View File

@ -1,14 +1,13 @@
import * as React from "react";
import Layout from "../components/layout";
import { graphql } from "gatsby";
import BuildView from "../components/views/builder/build";
// markup
const IndexPage = ({ data }: any) => {
return (
<Layout meta={data.site.siteMetadata} title="Home" link={"/build"}>
<main style={{ height: "100%" }} className=" h-full ">
<BuildView />
Build
</main>
</Layout>
);

View File

@ -1,28 +0,0 @@
import * as React from "react";
import { graphql } from "gatsby";
import Layout from "../../components/layout";
import GalleryView from "../../components/views/gallery/gallery";
// markup
const GalleryPage = ({ location, data }: any) => {
return (
<Layout meta={data.site.siteMetadata} title="Gallery" link={"/gallery"}>
<main style={{ height: "100%" }} className=" h-full ">
<GalleryView location={location} />
</main>
</Layout>
);
};
export const query = graphql`
query HomePageQuery {
site {
siteMetadata {
description
title
}
}
}
`;
export default GalleryPage;

Some files were not shown because too many files have changed in this diff Show More