Victor Dibia b2cef7f47c
Update AGS (Support Workbenches ++) (#6736)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-16 10:03:02 -07:00

246 lines
8.5 KiB
Python

import json
import os
import subprocess
import tempfile
import threading
import time
import webbrowser
from pathlib import Path
from typing import Any, Dict, Union
import uvicorn
from autogen_core import ComponentModel
class LiteStudio:
"""
Core class for managing AutoGen Studio lite mode instances.
Supports both file-based and programmatic team configurations.
"""
def __init__(
self,
team: Union[str, Path, Dict[str, Any], ComponentModel, None] = None,
host: str = "127.0.0.1",
port: int = 8080,
session_name: str = "Lite Session",
auto_open: bool = True,
):
"""
Initialize LiteStudio instance.
Args:
team: Team configuration - can be:
- str: Path to team JSON file
- Path: Path object to team JSON file
- Dict[str, Any]: Team configuration dictionary
- ComponentModel: AutoGen ComponentModel instance
- None: Creates default team
host: Host to run server on
port: Port to run server on
session_name: Name for the auto-created session
auto_open: Whether to auto-open browser
"""
self.host = host
self.port = port
self.session_name = session_name
self.auto_open = auto_open
self.server_process = None
self.server_thread = None # Handle team loading
self.team_file_path = self._load_team(team)
def _load_team(self, team: Union[str, Path, Dict[str, Any], ComponentModel, None]) -> str:
"""
Load team from file path, object, or create default.
Returns the file path to the team JSON.
Args:
team: Can be file path (str/Path), dict, ComponentModel, or None
"""
if team is None:
# Create default team
from autogenstudio.gallery.builder import create_default_lite_team
return create_default_lite_team()
elif isinstance(team, (str, Path)):
# File path provided
team_path = Path(team)
if not team_path.exists():
raise FileNotFoundError(f"Team file not found: {team_path}")
return str(team_path.absolute())
elif isinstance(team, dict):
# Team dict provided - save to temp file
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
try:
json.dump(team, temp_file, indent=2)
temp_file.flush()
return temp_file.name
finally:
temp_file.close()
elif isinstance(team, ComponentModel):
# ComponentModel - use model_dump directly
team_dict = team.model_dump()
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
try:
json.dump(team_dict, temp_file, indent=2)
temp_file.flush()
return temp_file.name
finally:
temp_file.close()
else:
# Try to serialize other team objects
team_dict = None
# Try dump_component() method (AutoGen teams)
if hasattr(team, "dump_component"):
component = team.dump_component()
if hasattr(component, "model_dump"):
team_dict = component.model_dump()
elif hasattr(component, "dict"):
team_dict = component.dict()
else:
team_dict = dict(component)
# Try model_dump() method (Pydantic v2)
elif hasattr(team, "model_dump"):
team_dict = team.model_dump()
# Try dict() method (Pydantic v1)
elif hasattr(team, "dict"):
team_dict = team.dict()
if team_dict is None:
raise ValueError(
f"Cannot serialize team object of type {type(team)}. "
f"Expected: file path, dict, ComponentModel, or object with dump_component()/model_dump()/dict() method."
)
# Save serialized team to temp file
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
try:
json.dump(team_dict, temp_file, indent=2)
temp_file.flush()
return temp_file.name
finally:
temp_file.close()
def _get_env_file_path(self) -> str:
"""Get path for environment variables file."""
app_dir = os.path.join(os.path.expanduser("~"), ".autogenstudio")
if not os.path.exists(app_dir):
os.makedirs(app_dir, exist_ok=True)
return os.path.join(app_dir, "temp_env_vars.env")
def _setup_environment(self) -> str:
"""
Setup environment variables for lite mode.
Returns path to the environment file.
"""
env_vars = {
"AUTOGENSTUDIO_HOST": self.host,
"AUTOGENSTUDIO_PORT": str(self.port),
"AUTOGENSTUDIO_LITE_MODE": "true",
"AUTOGENSTUDIO_API_DOCS": "false",
"AUTOGENSTUDIO_AUTH_DISABLED": "true",
"AUTOGENSTUDIO_LITE_SESSION_NAME": self.session_name,
"AUTOGENSTUDIO_LITE_TEAM_FILE": self.team_file_path,
"AUTOGENSTUDIO_DATABASE_URI": "sqlite:///:memory:",
}
env_file_path = self._get_env_file_path()
with open(env_file_path, "w") as temp_env:
for key, value in env_vars.items():
temp_env.write(f"{key}={value}\n")
return env_file_path
def _setup_browser_opening(self):
"""Setup browser opening in a separate thread."""
if self.auto_open:
def open_browser():
time.sleep(3) # Wait for server startup
url = f"http://{self.host}:{self.port}/lite"
webbrowser.open(url)
threading.Thread(target=open_browser, daemon=True).start()
def start(self, background: bool = False):
"""
Start the lite studio server.
Args:
background: If True, run server in background thread
"""
# Check if already running
if self.server_thread and self.server_thread.is_alive():
raise RuntimeError("LiteStudio is already running")
# Setup environment
env_file_path = self._setup_environment()
# Setup browser opening
self._setup_browser_opening()
if background:
# Run server in background thread
def run_server():
uvicorn.run(
"autogenstudio.web.app:app",
host=self.host,
port=self.port,
workers=1,
env_file=env_file_path,
)
self.server_thread = threading.Thread(target=run_server, daemon=True)
self.server_thread.start()
else:
# Run server in foreground (blocking)
uvicorn.run(
"autogenstudio.web.app:app",
host=self.host,
port=self.port,
workers=1,
env_file=env_file_path,
)
def stop(self):
"""Stop the lite studio server."""
if self.server_thread and self.server_thread.is_alive():
# For background threads, we can't easily stop uvicorn
# This is a limitation - in production you'd want proper shutdown
self.server_thread.join(timeout=5)
self.server_thread = None
def __enter__(self):
"""Context manager entry - start in background."""
self.start(background=True)
return self
def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
"""Context manager exit - stop server."""
self.stop()
@classmethod
def shutdown_port(cls, port: int):
"""
Utility to shutdown any process running on the specified port.
Args:
port: Port number to shutdown
"""
try:
# Try to find and kill process on port (Unix/Linux/Mac)
result = subprocess.run(["lsof", "-ti", f":{port}"], capture_output=True, text=True)
if result.returncode == 0 and result.stdout.strip():
pids = result.stdout.strip().split("\n")
for pid in pids:
subprocess.run(["kill", "-9", pid], check=False)
except (subprocess.SubprocessError, FileNotFoundError):
# lsof might not be available on all systems
pass