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
117 changed files with 20736 additions and 13600 deletions

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)