You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

studio.py 8.7 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import json
  2. import os
  3. import subprocess
  4. import tempfile
  5. import threading
  6. import time
  7. import webbrowser
  8. from pathlib import Path
  9. from typing import Any, Dict, Union
  10. import uvicorn
  11. from autogen_core import ComponentModel
  12. class LiteStudio:
  13. """
  14. Core class for managing AutoGen Studio lite mode instances.
  15. Supports both file-based and programmatic team configurations.
  16. """
  17. def __init__(
  18. self,
  19. team: Union[str, Path, Dict[str, Any], ComponentModel, None] = None,
  20. host: str = "127.0.0.1",
  21. port: int = 8080,
  22. session_name: str = "Lite Session",
  23. auto_open: bool = True,
  24. ):
  25. """
  26. Initialize LiteStudio instance.
  27. Args:
  28. team: Team configuration - can be:
  29. - str: Path to team JSON file
  30. - Path: Path object to team JSON file
  31. - Dict[str, Any]: Team configuration dictionary
  32. - ComponentModel: AutoGen ComponentModel instance
  33. - None: Creates default team
  34. host: Host to run server on
  35. port: Port to run server on
  36. session_name: Name for the auto-created session
  37. auto_open: Whether to auto-open browser
  38. """
  39. self.host = host
  40. self.port = port
  41. self.session_name = session_name
  42. self.auto_open = auto_open
  43. self.server_process = None
  44. self.server_thread = None # Handle team loading
  45. self.team_file_path = self._load_team(team)
  46. def _load_team(self, team: Union[str, Path, Dict[str, Any], ComponentModel, None]) -> str:
  47. """
  48. Load team from file path, object, or create default.
  49. Returns the file path to the team JSON.
  50. Args:
  51. team: Can be file path (str/Path), dict, ComponentModel, or None
  52. """
  53. if team is None:
  54. # Create default team
  55. from autogenstudio.gallery.builder import create_default_lite_team
  56. return create_default_lite_team()
  57. elif isinstance(team, (str, Path)):
  58. # File path provided
  59. team_path = Path(team)
  60. if not team_path.exists():
  61. raise FileNotFoundError(f"Team file not found: {team_path}")
  62. return str(team_path.absolute())
  63. elif isinstance(team, dict):
  64. # Team dict provided - save to temp file
  65. temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
  66. try:
  67. json.dump(team, temp_file, indent=2)
  68. temp_file.flush()
  69. return temp_file.name
  70. finally:
  71. temp_file.close()
  72. elif isinstance(team, ComponentModel):
  73. # ComponentModel - use model_dump directly
  74. team_dict = team.model_dump()
  75. temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
  76. try:
  77. json.dump(team_dict, temp_file, indent=2)
  78. temp_file.flush()
  79. return temp_file.name
  80. finally:
  81. temp_file.close()
  82. else:
  83. # Try to serialize other team objects
  84. team_dict = None
  85. # Try dump_component() method (AutoGen teams)
  86. if hasattr(team, "dump_component"):
  87. component = team.dump_component()
  88. if hasattr(component, "model_dump"):
  89. team_dict = component.model_dump()
  90. elif hasattr(component, "dict"):
  91. team_dict = component.dict()
  92. else:
  93. team_dict = dict(component)
  94. # Try model_dump() method (Pydantic v2)
  95. elif hasattr(team, "model_dump"):
  96. team_dict = team.model_dump()
  97. # Try dict() method (Pydantic v1)
  98. elif hasattr(team, "dict"):
  99. team_dict = team.dict()
  100. if team_dict is None:
  101. raise ValueError(
  102. f"Cannot serialize team object of type {type(team)}. "
  103. f"Expected: file path, dict, ComponentModel, or object with dump_component()/model_dump()/dict() method."
  104. )
  105. # Save serialized team to temp file
  106. temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
  107. try:
  108. json.dump(team_dict, temp_file, indent=2)
  109. temp_file.flush()
  110. return temp_file.name
  111. finally:
  112. temp_file.close()
  113. def _get_env_file_path(self) -> str:
  114. """Get path for environment variables file."""
  115. app_dir = os.path.join(os.path.expanduser("~"), ".autogenstudio")
  116. if not os.path.exists(app_dir):
  117. os.makedirs(app_dir, exist_ok=True)
  118. return os.path.join(app_dir, "temp_env_vars.env")
  119. def _setup_environment(self) -> str:
  120. """
  121. Setup environment variables for lite mode.
  122. Returns path to the environment file.
  123. """
  124. env_vars = {
  125. "AUTOGENSTUDIO_HOST": self.host,
  126. "AUTOGENSTUDIO_PORT": str(self.port),
  127. "AUTOGENSTUDIO_LITE_MODE": "true",
  128. "AUTOGENSTUDIO_API_DOCS": "false",
  129. "AUTOGENSTUDIO_AUTH_DISABLED": "true",
  130. "AUTOGENSTUDIO_LITE_SESSION_NAME": self.session_name,
  131. "AUTOGENSTUDIO_LITE_TEAM_FILE": self.team_file_path,
  132. "AUTOGENSTUDIO_DATABASE_URI": "sqlite:///:memory:",
  133. }
  134. env_file_path = self._get_env_file_path()
  135. with open(env_file_path, "w") as temp_env:
  136. for key, value in env_vars.items():
  137. temp_env.write(f"{key}={value}\n")
  138. return env_file_path
  139. def _setup_browser_opening(self):
  140. """Setup browser opening in a separate thread."""
  141. if self.auto_open:
  142. def open_browser():
  143. time.sleep(3) # Wait for server startup
  144. url = f"http://{self.host}:{self.port}/lite"
  145. webbrowser.open(url)
  146. threading.Thread(target=open_browser, daemon=True).start()
  147. def start(self, background: bool = False):
  148. """
  149. Start the lite studio server.
  150. Args:
  151. background: If True, run server in background thread
  152. """
  153. # Check if already running
  154. if self.server_thread and self.server_thread.is_alive():
  155. raise RuntimeError("LiteStudio is already running")
  156. # Setup environment
  157. env_file_path = self._setup_environment()
  158. # Setup browser opening
  159. self._setup_browser_opening()
  160. if background:
  161. # Run server in background thread
  162. def run_server():
  163. uvicorn.run(
  164. "autogenstudio.web.app:app",
  165. host=self.host,
  166. port=self.port,
  167. workers=1,
  168. env_file=env_file_path,
  169. )
  170. self.server_thread = threading.Thread(target=run_server, daemon=True)
  171. self.server_thread.start()
  172. else:
  173. # Run server in foreground (blocking)
  174. uvicorn.run(
  175. "autogenstudio.web.app:app",
  176. host=self.host,
  177. port=self.port,
  178. workers=1,
  179. env_file=env_file_path,
  180. )
  181. def stop(self):
  182. """Stop the lite studio server."""
  183. if self.server_thread and self.server_thread.is_alive():
  184. # For background threads, we can't easily stop uvicorn
  185. # This is a limitation - in production you'd want proper shutdown
  186. self.server_thread.join(timeout=5)
  187. self.server_thread = None
  188. def __enter__(self):
  189. """Context manager entry - start in background."""
  190. self.start(background=True)
  191. return self
  192. def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
  193. """Context manager exit - stop server."""
  194. self.stop()
  195. @classmethod
  196. def shutdown_port(cls, port: int):
  197. """
  198. Utility to shutdown any process running on the specified port.
  199. Args:
  200. port: Port number to shutdown
  201. """
  202. try:
  203. # Try to find and kill process on port (Unix/Linux/Mac)
  204. result = subprocess.run(["lsof", "-ti", f":{port}"], capture_output=True, text=True)
  205. if result.returncode == 0 and result.stdout.strip():
  206. pids = result.stdout.strip().split("\n")
  207. for pid in pids:
  208. subprocess.run(["kill", "-9", pid], check=False)
  209. except (subprocess.SubprocessError, FileNotFoundError):
  210. # lsof might not be available on all systems
  211. pass