Fix warnings in AGS (#5320)

This PR does the following: 

- Fix warning messages in AGS on launch.
- Improve Cli message to include app URL on startup from command line
- Minor improvements default gallery generator. (add more default tools)
- Improve new session behaviour.



## Related issue number

Closes #5097

## Checks
This commit is contained in:
Victor Dibia 2025-02-03 22:32:34 -08:00 committed by GitHub
parent fbda70320d
commit b89ca2a5ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1532 additions and 262 deletions

View File

@ -64,7 +64,7 @@ class AssistantAgentConfig(BaseModel):
model_context: ComponentModel | None = None
description: str
system_message: str | None = None
model_client_stream: bool
model_client_stream: bool = False
reflect_on_tool_use: bool
tool_call_summary_format: str

View File

@ -1,4 +1,6 @@
import os
import tempfile
import warnings
from typing import Optional
import typer
@ -9,6 +11,10 @@ from .version import VERSION
app = typer.Typer()
# Ignore deprecation warnings from websockets
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated*")
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated*")
@app.command()
def ui(
@ -33,14 +39,25 @@ def ui(
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.
"""
# Create temporary env file to share configuration with uvicorn workers
temp_env = tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=True)
os.environ["AUTOGENSTUDIO_API_DOCS"] = str(docs)
# Write configuration
env_vars = {
"AUTOGENSTUDIO_HOST": host,
"AUTOGENSTUDIO_PORT": port,
"AUTOGENSTUDIO_API_DOCS": str(docs),
}
if appdir:
os.environ["AUTOGENSTUDIO_APPDIR"] = appdir
env_vars["AUTOGENSTUDIO_APPDIR"] = appdir
if database_uri:
os.environ["AUTOGENSTUDIO_DATABASE_URI"] = database_uri
env_vars["AUTOGENSTUDIO_DATABASE_URI"] = database_uri
if upgrade_database:
os.environ["AUTOGENSTUDIO_UPGRADE_DATABASE"] = "1"
env_vars["AUTOGENSTUDIO_UPGRADE_DATABASE"] = "1"
for key, value in env_vars.items():
temp_env.write(f"{key}={value}\n")
temp_env.flush()
uvicorn.run(
"autogenstudio.web.app:app",
@ -49,6 +66,7 @@ def ui(
workers=workers,
reload=reload,
reload_excludes=["**/alembic/*", "**/alembic.ini", "**/versions/*"] if reload else None,
env_file=temp_env.name,
)

View File

@ -1,5 +1,4 @@
from .db_manager import DatabaseManager
from .gallery_builder import GalleryBuilder, create_default_gallery
__all__ = [
"DatabaseManager",

View File

@ -1,5 +1,7 @@
import io
import os
import shutil
from contextlib import redirect_stdout
from pathlib import Path
from typing import List, Optional, Tuple
@ -45,7 +47,7 @@ class SchemaManager:
def initialize_migrations(self, force: bool = False) -> bool:
try:
if force:
logger.info("Force reinitialization of migrations...")
# logger.info("Force reinitialization of migrations...")
self._cleanup_existing_alembic()
if not self._initialize_alembic():
return False
@ -60,7 +62,7 @@ class SchemaManager:
return False
# Only generate initial revision if alembic is properly initialized
logger.info("Creating initial migration...")
# logger.info("Creating initial migration...")
return self.generate_revision("Initial schema") is not None
except Exception as e:
@ -88,7 +90,7 @@ class SchemaManager:
Completely remove existing Alembic configuration including versions.
For fresh initialization, we don't need to preserve anything.
"""
logger.info("Cleaning up existing Alembic configuration...")
# logger.info("Cleaning up existing Alembic configuration...")
# Remove entire alembic directory if it exists
if self.alembic_dir.exists():
@ -130,7 +132,7 @@ class SchemaManager:
self.alembic_dir.parent.mkdir(exist_ok=True)
# Run alembic init to create fresh directory structure
logger.info("Initializing alembic directory structure...")
# logger.info("Initializing alembic directory structure...")
# Create initial config file for alembic init
config_content = self._generate_alembic_ini_content()
@ -139,7 +141,9 @@ class SchemaManager:
# Use the config we just created
config = Config(str(self.alembic_ini_path))
command.init(config, str(self.alembic_dir))
with redirect_stdout(io.StringIO()):
command.init(config, str(self.alembic_dir))
# Update script template after initialization
self.update_script_template()
@ -265,7 +269,6 @@ datefmt = %H:%M:%S
with open(template_path, "w") as f:
f.write(content)
logger.info("Updated script template")
return True
except Exception as e:
@ -320,8 +323,6 @@ datefmt = %H:%M:%S
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
@ -481,8 +482,9 @@ datefmt = %H:%M:%S
"""
try:
config = self.get_alembic_config()
command.revision(config, message=message, autogenerate=True)
return self.get_head_revision()
with redirect_stdout(io.StringIO()):
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)}")

View File

@ -6,6 +6,7 @@ from typing import List, Optional, Union
from uuid import UUID, uuid4
from autogen_core import ComponentModel
from pydantic import ConfigDict
from sqlalchemy import ForeignKey, Integer
from sqlmodel import JSON, Column, DateTime, Field, SQLModel, func
@ -101,5 +102,4 @@ class Run(SQLModel, table=True):
version: Optional[str] = "0.0.1"
messages: Union[List[Message], List[dict]] = Field(default_factory=list, sa_column=Column(JSON))
class Config:
json_encoders = {UUID: str, datetime: lambda v: v.isoformat()}
model_config = ConfigDict(json_encoders={UUID: str, datetime: lambda v: v.isoformat()})

View File

@ -0,0 +1,3 @@
from .builder import GalleryBuilder, create_default_gallery
__all__ = ["GalleryBuilder", "create_default_gallery"]

View File

@ -6,20 +6,21 @@ from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermi
from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat
from autogen_core import ComponentModel
from autogen_core.models import ModelInfo
from autogen_core.tools import FunctionTool
from autogen_ext.agents.web_surfer import MultimodalWebSurfer
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogenstudio.datamodel import Gallery, GalleryComponents, GalleryItems, GalleryMetadata
from . import tools as tools
class GalleryBuilder:
"""Builder class for creating AutoGen component galleries."""
"""Enhanced builder class for creating AutoGen component galleries with custom labels."""
def __init__(self, id: str, name: str):
def __init__(self, id: str, name: str, url: Optional[str] = None):
self.id = id
self.name = name
self.url: Optional[str] = None
self.url: Optional[str] = url
self.teams: List[ComponentModel] = []
self.agents: List[ComponentModel] = []
self.models: List[ComponentModel] = []
@ -38,6 +39,16 @@ class GalleryBuilder:
category="conversation",
)
def _update_component_metadata(
self, component: ComponentModel, label: Optional[str] = None, description: Optional[str] = None
) -> ComponentModel:
"""Helper method to update component metadata."""
if label is not None:
component.label = label
if description is not None:
component.description = description
return component
def set_metadata(
self,
author: Optional[str] = None,
@ -62,29 +73,39 @@ class GalleryBuilder:
self.metadata.category = category
return self
def add_team(self, team: ComponentModel) -> "GalleryBuilder":
"""Add a team component to the gallery."""
self.teams.append(team)
def add_team(
self, team: ComponentModel, label: Optional[str] = None, description: Optional[str] = None
) -> "GalleryBuilder":
"""Add a team component to the gallery with optional custom label and description."""
self.teams.append(self._update_component_metadata(team, label, description))
return self
def add_agent(self, agent: ComponentModel) -> "GalleryBuilder":
"""Add an agent component to the gallery."""
self.agents.append(agent)
def add_agent(
self, agent: ComponentModel, label: Optional[str] = None, description: Optional[str] = None
) -> "GalleryBuilder":
"""Add an agent component to the gallery with optional custom label and description."""
self.agents.append(self._update_component_metadata(agent, label, description))
return self
def add_model(self, model: ComponentModel) -> "GalleryBuilder":
"""Add a model component to the gallery."""
self.models.append(model)
def add_model(
self, model: ComponentModel, label: Optional[str] = None, description: Optional[str] = None
) -> "GalleryBuilder":
"""Add a model component to the gallery with optional custom label and description."""
self.models.append(self._update_component_metadata(model, label, description))
return self
def add_tool(self, tool: ComponentModel) -> "GalleryBuilder":
"""Add a tool component to the gallery."""
self.tools.append(tool)
def add_tool(
self, tool: ComponentModel, label: Optional[str] = None, description: Optional[str] = None
) -> "GalleryBuilder":
"""Add a tool component to the gallery with optional custom label and description."""
self.tools.append(self._update_component_metadata(tool, label, description))
return self
def add_termination(self, termination: ComponentModel) -> "GalleryBuilder":
"""Add a termination condition component to the gallery."""
self.terminations.append(termination)
def add_termination(
self, termination: ComponentModel, label: Optional[str] = None, description: Optional[str] = None
) -> "GalleryBuilder":
"""Add a termination condition component with optional custom label and description."""
self.terminations.append(self._update_component_metadata(termination, label, description))
return self
def build(self) -> Gallery:
@ -108,12 +129,14 @@ class GalleryBuilder:
def create_default_gallery() -> Gallery:
"""Create a default gallery with all components including calculator and web surfer teams."""
# url = "https://raw.githubusercontent.com/microsoft/autogen/refs/heads/main/python/packages/autogen-studio/autogenstudio/gallery/default.json"
builder = GalleryBuilder(id="gallery_default", name="Default Component Gallery")
# Set metadata
builder.set_metadata(
description="A default gallery containing basic components for human-in-loop conversations",
tags=["human-in-loop", "assistant"],
tags=["human-in-loop", "assistant", "web agents"],
category="conversation",
)
@ -121,44 +144,36 @@ def create_default_gallery() -> Gallery:
base_model = OpenAIChatCompletionClient(model="gpt-4o-mini")
builder.add_model(base_model.dump_component())
# Create Mistral vllm model
mistral_vllm_model = OpenAIChatCompletionClient(
model="TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
base_url="http://localhost:1234/v1",
model_info=ModelInfo(vision=False, function_calling=True, json_output=False),
)
builder.add_model(mistral_vllm_model.dump_component())
# Create websurfer model client
websurfer_model = OpenAIChatCompletionClient(model="gpt-4o-mini")
builder.add_model(websurfer_model.dump_component())
def calculator(a: float, b: float, operator: str) -> str:
try:
if operator == "+":
return str(a + b)
elif operator == "-":
return str(a - b)
elif operator == "*":
return str(a * b)
elif operator == "/":
if b == 0:
return "Error: Division by zero"
return str(a / b)
else:
return "Error: Invalid operator. Please use +, -, *, or /"
except Exception as e:
return f"Error: {str(e)}"
# Create calculator tool
calculator_tool = FunctionTool(
name="calculator",
description="A simple calculator that performs basic arithmetic operations",
func=calculator,
global_imports=[],
builder.add_model(
mistral_vllm_model.dump_component(),
label="Mistral-7B vllm",
description="Example on how to use the OpenAIChatCopletionClient with local models (Ollama, vllm etc).",
)
builder.add_tool(calculator_tool.dump_component())
# Create termination conditions for calculator team
builder.add_tool(
tools.calculator_tool.dump_component(),
label="Calculator Tool",
description="A tool that performs basic arithmetic operations (addition, subtraction, multiplication, division).",
)
# Create calculator assistant agent
calc_assistant = AssistantAgent(
name="assistant_agent",
system_message="You are a helpful assistant. Solve tasks carefully. When done, say TERMINATE.",
model_client=base_model,
tools=[tools.calculator_tool],
)
builder.add_agent(
calc_assistant.dump_component(), description="An agent that provides assistance with ability to use tools."
)
# Create termination conditions
calc_text_term = TextMentionTermination(text="TERMINATE")
calc_max_term = MaxMessageTermination(max_messages=10)
calc_or_term = calc_text_term | calc_max_term
@ -167,38 +182,33 @@ def create_default_gallery() -> Gallery:
builder.add_termination(calc_max_term.dump_component())
builder.add_termination(calc_or_term.dump_component())
# Create calculator assistant agent
calc_assistant = AssistantAgent(
name="assistant_agent",
system_message="You are a helpful assistant. Solve tasks carefully. When done, say TERMINATE.",
model_client=base_model,
tools=[calculator_tool],
)
builder.add_agent(calc_assistant.dump_component())
# Create calculator team
calc_team = RoundRobinGroupChat(participants=[calc_assistant], termination_condition=calc_or_term)
builder.add_team(calc_team.dump_component())
builder.add_team(
calc_team.dump_component(),
label="Default Team",
description="A single AssistantAgent (with a calculator tool) in a RoundRobinGroupChat team. ",
)
# Create web surfer agent
websurfer_agent = MultimodalWebSurfer(
name="websurfer_agent",
description="an agent that solves tasks by browsing the web",
model_client=websurfer_model,
model_client=base_model,
headless=True,
)
builder.add_agent(websurfer_agent.dump_component())
# Create web surfer verification assistant
# Create verification assistant
verification_assistant = AssistantAgent(
name="assistant_agent",
description="an agent that verifies and summarizes information",
system_message="You are a task verification assistant who is working with a web surfer agent to solve tasks. At each point, check if the task has been completed as requested by the user. If the websurfer_agent responds and the task has not yet been completed, respond with what is left to do and then say 'keep going'. If and only when the task has been completed, summarize and present a final answer that directly addresses the user task in detail and then respond with TERMINATE.",
model_client=websurfer_model,
model_client=base_model,
)
builder.add_agent(verification_assistant.dump_component())
# Create web surfer user proxy
# Create user proxy
web_user_proxy = UserProxyAgent(
name="user_proxy",
description="a human user that should be consulted only when the assistant_agent is unable to verify the information provided by the websurfer_agent",
@ -209,11 +219,11 @@ def create_default_gallery() -> Gallery:
web_max_term = MaxMessageTermination(max_messages=20)
web_text_term = TextMentionTermination(text="TERMINATE")
web_termination = web_max_term | web_text_term
builder.add_termination(web_termination.dump_component())
# Create web surfer team
selector_prompt = """You are the cordinator of role play game. The following roles are available:
{roles}. Given a task, the websurfer_agent will be tasked to address it by browsing the web and providing information. The assistant_agent will be tasked with verifying the information provided by the websurfer_agent and summarizing the information to present a final answer to the user. If the task needs assistance from a human user (e.g., providing feedback, preferences, or the task is stalled), you should select the user_proxy role to provide the necessary information.
Read the following conversation. Then select the next role from {participants} to play. Only return the role.
{history}
@ -226,18 +236,49 @@ Read the above conversation. Then select the next role from {participants} to pl
model_client=base_model,
termination_condition=web_termination,
)
builder.add_team(websurfer_team.dump_component())
builder.add_team(
websurfer_team.dump_component(),
label="Web Agent Team (Operator)",
description="A group chat team that have participants takes turn to publish a message\n to all, using a ChatCompletion model to select the next speaker after each message.",
)
builder.add_tool(
tools.generate_image_tool.dump_component(),
label="Image Generation Tool",
description="A tool that generates images based on a text description using OpenAI's DALL-E model. Note: Requires OpenAI API key to function.",
)
builder.add_tool(
tools.generate_pdf_tool.dump_component(),
label="PDF Generation Tool",
description="A tool that generates a PDF file from a list of images.Requires the PyFPDF and pillow library to function.",
)
builder.add_tool(
tools.fetch_webpage_tool.dump_component(),
label="Webpage Generation Tool",
description="A tool that generates a webpage from a list of images. Requires beautifulsoup4 html2text library to function.",
)
builder.add_tool(
tools.bing_search_tool.dump_component(),
label="Bing Search Tool",
description="A tool that performs Bing searches using the Bing Web Search API. Requires the requests library, BING_SEARCH_KEY env variable to function.",
)
builder.add_tool(
tools.google_search_tool.dump_component(),
label="Google Search Tool",
description="A tool that performs Google searches using the Google Custom Search API. Requires the requests library, [GOOGLE_API_KEY, GOOGLE_CSE_ID] to be set, env variable to function.",
)
return builder.build()
# if __name__ == "__main__":
# # Create and save the gallery
# gallery = create_default_gallery()
if __name__ == "__main__":
# Create and save the gallery
gallery = create_default_gallery()
# # Print as JSON
# print(gallery.model_dump_json(indent=2))
# # Save to file
# with open("gallery_default.json", "w") as f:
# f.write(gallery.model_dump_json(indent=2))
# Save to file
with open("gallery_default.json", "w") as f:
f.write(gallery.model_dump_json(indent=2))

View File

@ -0,0 +1,15 @@
from .bing_search import bing_search_tool
from .calculator import calculator_tool
from .fetch_webpage import fetch_webpage_tool
from .generate_image import generate_image_tool
from .generate_pdf import generate_pdf_tool
from .google_search import google_search_tool
__all__ = [
"bing_search_tool",
"calculator_tool",
"google_search_tool",
"generate_image_tool",
"generate_pdf_tool",
"fetch_webpage_tool",
]

View File

@ -0,0 +1,218 @@
import json
import os
from typing import Dict, List, Optional
from urllib.parse import urljoin
import html2text
import httpx
from autogen_core.code_executor import ImportFromModule
from autogen_core.tools import FunctionTool
from bs4 import BeautifulSoup
async def bing_search(
query: str,
num_results: int = 5,
include_snippets: bool = True,
include_content: bool = True,
content_max_length: Optional[int] = 15000,
language: str = "en",
country: Optional[str] = None,
safe_search: str = "moderate",
response_filter: str = "webpages",
) -> List[Dict[str, str]]:
"""
Perform a Bing search using the Bing Web Search API.
Args:
query: Search query string
num_results: Number of results to return (max 50)
include_snippets: Include result snippets in output
include_content: Include full webpage content in markdown format
content_max_length: Maximum length of webpage content (if included)
language: Language code for search results (e.g., 'en', 'es', 'fr')
country: Optional market code for search results (e.g., 'us', 'uk')
safe_search: SafeSearch setting ('off', 'moderate', or 'strict')
response_filter: Type of results ('webpages', 'news', 'images', or 'videos')
Returns:
List[Dict[str, str]]: List of search results
Raises:
ValueError: If API credentials are invalid or request fails
"""
# Get and validate API key
api_key = os.getenv("BING_SEARCH_KEY", "").strip()
if not api_key:
raise ValueError(
"BING_SEARCH_KEY environment variable is not set. " "Please obtain an API key from Azure Portal."
)
# Validate safe_search parameter
valid_safe_search = ["off", "moderate", "strict"]
if safe_search.lower() not in valid_safe_search:
raise ValueError(f"Invalid safe_search value. Must be one of: {', '.join(valid_safe_search)}")
# Validate response_filter parameter
valid_filters = ["webpages", "news", "images", "videos"]
if response_filter.lower() not in valid_filters:
raise ValueError(f"Invalid response_filter value. Must be one of: {', '.join(valid_filters)}")
async def fetch_page_content(url: str, max_length: Optional[int] = 50000) -> str:
"""Helper function to fetch and convert webpage content to markdown"""
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Remove script and style elements
for script in soup(["script", "style"]):
script.decompose()
# Convert relative URLs to absolute
for tag in soup.find_all(["a", "img"]):
if tag.get("href"):
tag["href"] = urljoin(url, tag["href"])
if tag.get("src"):
tag["src"] = urljoin(url, tag["src"])
h2t = html2text.HTML2Text()
h2t.body_width = 0
h2t.ignore_images = False
h2t.ignore_emphasis = False
h2t.ignore_links = False
h2t.ignore_tables = False
markdown = h2t.handle(str(soup))
if max_length and len(markdown) > max_length:
markdown = markdown[:max_length] + "\n...(truncated)"
return markdown.strip()
except Exception as e:
return f"Error fetching content: {str(e)}"
# Build request headers and parameters
headers = {"Ocp-Apim-Subscription-Key": api_key, "Accept": "application/json"}
params = {
"q": query,
"count": min(max(1, num_results), 50),
"mkt": f"{language}-{country.upper()}" if country else language,
"safeSearch": safe_search.capitalize(),
"responseFilter": response_filter,
"textFormat": "raw",
}
# Make the request
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.bing.microsoft.com/v7.0/search", headers=headers, params=params, timeout=10
)
# Handle common error cases
if response.status_code == 401:
raise ValueError("Authentication failed. Please verify your Bing Search API key.")
elif response.status_code == 403:
raise ValueError(
"Access forbidden. This could mean:\n"
"1. The API key is invalid\n"
"2. The API key has expired\n"
"3. You've exceeded your API quota"
)
elif response.status_code == 429:
raise ValueError("API quota exceeded. Please try again later.")
response.raise_for_status()
data = response.json()
# Process results based on response_filter
results = []
if response_filter == "webpages" and "webPages" in data:
items = data["webPages"]["value"]
elif response_filter == "news" and "news" in data:
items = data["news"]["value"]
elif response_filter == "images" and "images" in data:
items = data["images"]["value"]
elif response_filter == "videos" and "videos" in data:
items = data["videos"]["value"]
else:
if not any(key in data for key in ["webPages", "news", "images", "videos"]):
return [] # No results found
raise ValueError(f"No {response_filter} results found in API response")
# Extract relevant information based on result type
for item in items:
result = {"title": item.get("name", "")}
if response_filter == "webpages":
result["link"] = item.get("url", "")
if include_snippets:
result["snippet"] = item.get("snippet", "")
if include_content:
result["content"] = await fetch_page_content(result["link"], max_length=content_max_length)
elif response_filter == "news":
result["link"] = item.get("url", "")
if include_snippets:
result["snippet"] = item.get("description", "")
result["date"] = item.get("datePublished", "")
if include_content:
result["content"] = await fetch_page_content(result["link"], max_length=content_max_length)
elif response_filter == "images":
result["link"] = item.get("contentUrl", "")
result["thumbnail"] = item.get("thumbnailUrl", "")
if include_snippets:
result["snippet"] = item.get("description", "")
elif response_filter == "videos":
result["link"] = item.get("contentUrl", "")
result["thumbnail"] = item.get("thumbnailUrl", "")
if include_snippets:
result["snippet"] = item.get("description", "")
result["duration"] = item.get("duration", "")
results.append(result)
return results[:num_results]
except httpx.RequestException as e:
error_msg = str(e)
if "InvalidApiKey" in error_msg:
raise ValueError("Invalid API key. Please check your BING_SEARCH_KEY environment variable.") from e
elif "KeyExpired" in error_msg:
raise ValueError("API key has expired. Please generate a new key.") from e
else:
raise ValueError(f"Search request failed: {error_msg}") from e
except json.JSONDecodeError:
raise ValueError("Failed to parse API response. " "Please verify your API credentials and try again.") from None
except Exception as e:
raise ValueError(f"Unexpected error during search: {str(e)}") from e
# Create the Bing search tool
bing_search_tool = FunctionTool(
func=bing_search,
description="""
Perform Bing searches using the Bing Web Search API. Requires BING_SEARCH_KEY environment variable.
Supports web, news, image, and video searches.
See function documentation for detailed setup instructions.
""",
global_imports=[
ImportFromModule("typing", ("List", "Dict", "Optional")),
"os",
"httpx",
"json",
"html2text",
ImportFromModule("bs4", ("BeautifulSoup",)),
ImportFromModule("urllib.parse", ("urljoin",)),
],
)

View File

@ -0,0 +1,28 @@
from autogen_core.tools import FunctionTool
def calculator(a: float, b: float, operator: str) -> str:
try:
if operator == "+":
return str(a + b)
elif operator == "-":
return str(a - b)
elif operator == "*":
return str(a * b)
elif operator == "/":
if b == 0:
return "Error: Division by zero"
return str(a / b)
else:
return "Error: Invalid operator. Please use +, -, *, or /"
except Exception as e:
return f"Error: {str(e)}"
# Create calculator tool
calculator_tool = FunctionTool(
name="calculator",
description="A simple calculator that performs basic arithmetic operations",
func=calculator,
global_imports=[],
)

View File

@ -0,0 +1,88 @@
from typing import Dict, Optional
from urllib.parse import urljoin
import html2text
import httpx
from autogen_core.code_executor import ImportFromModule
from autogen_core.tools import FunctionTool
from bs4 import BeautifulSoup
async def fetch_webpage(
url: str, include_images: bool = True, max_length: Optional[int] = None, headers: Optional[Dict[str, str]] = None
) -> str:
"""Fetch a webpage and convert it to markdown format.
Args:
url: The URL of the webpage to fetch
include_images: Whether to include image references in the markdown
max_length: Maximum length of the output markdown (if None, no limit)
headers: Optional HTTP headers for the request
Returns:
str: Markdown version of the webpage content
Raises:
ValueError: If the URL is invalid or the page can't be fetched
"""
# Use default headers if none provided
if headers is None:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
try:
# Fetch the webpage
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers, timeout=10)
response.raise_for_status()
# Parse HTML
soup = BeautifulSoup(response.text, "html.parser")
# Remove script and style elements
for script in soup(["script", "style"]):
script.decompose()
# Convert relative URLs to absolute
for tag in soup.find_all(["a", "img"]):
if tag.get("href"):
tag["href"] = urljoin(url, tag["href"])
if tag.get("src"):
tag["src"] = urljoin(url, tag["src"])
# Configure HTML to Markdown converter
h2t = html2text.HTML2Text()
h2t.body_width = 0 # No line wrapping
h2t.ignore_images = not include_images
h2t.ignore_emphasis = False
h2t.ignore_links = False
h2t.ignore_tables = False
# Convert to markdown
markdown = h2t.handle(str(soup))
# Trim if max_length is specified
if max_length and len(markdown) > max_length:
markdown = markdown[:max_length] + "\n...(truncated)"
return markdown.strip()
except httpx.RequestError as e:
raise ValueError(f"Failed to fetch webpage: {str(e)}") from e
except Exception as e:
raise ValueError(f"Error processing webpage: {str(e)}") from e
# Create the webpage fetching tool
fetch_webpage_tool = FunctionTool(
func=fetch_webpage,
description="Fetch a webpage and convert it to markdown format, with options for including images and limiting length",
global_imports=[
"os",
"html2text",
ImportFromModule("typing", ("Optional", "Dict")),
"httpx",
ImportFromModule("bs4", ("BeautifulSoup",)),
ImportFromModule("html2text", ("HTML2Text",)),
ImportFromModule("urllib.parse", ("urljoin",)),
],
)

View File

@ -0,0 +1,68 @@
import base64
import io
import uuid
from pathlib import Path
from typing import List, Literal, Optional
from autogen_core.code_executor import ImportFromModule
from autogen_core.tools import FunctionTool
from openai import OpenAI
from PIL import Image
async def generate_image(
query: str, output_dir: Optional[Path] = None, image_size: Literal["1024x1024", "512x512", "256x256"] = "1024x1024"
) -> List[str]:
"""
Generate images using OpenAI's DALL-E model based on a text description.
Args:
query: Natural language description of the desired image
output_dir: Directory to save generated images (default: current directory)
image_size: Size of generated image (1024x1024, 512x512, or 256x256)
Returns:
List[str]: Paths to the generated image files
"""
# Initialize the OpenAI client
client = OpenAI()
# Generate images using DALL-E 3
response = client.images.generate(model="dall-e-3", prompt=query, n=1, response_format="b64_json", size=image_size)
saved_files = []
# Process the response
if response.data:
for image_data in response.data:
# Generate a unique filename
file_name = f"{uuid.uuid4()}.png"
# Use output_dir if provided, otherwise use current directory
file_path = Path(output_dir) / file_name if output_dir else Path(file_name)
base64_str = image_data.b64_json
img = Image.open(io.BytesIO(base64.decodebytes(bytes(base64_str, "utf-8"))))
# Save the image to a file
img.save(file_path)
saved_files.append(str(file_path))
return saved_files
# Create the image generation tool
generate_image_tool = FunctionTool(
func=generate_image,
description="Generate images using DALL-E based on text descriptions.",
global_imports=[
"io",
"uuid",
"base64",
ImportFromModule("typing", ("List", "Optional", "Literal")),
ImportFromModule("pathlib", ("Path",)),
ImportFromModule("openai", ("OpenAI",)),
ImportFromModule("PIL", ("Image",)),
],
)

View File

@ -0,0 +1,128 @@
import unicodedata
import uuid
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional
import requests
from autogen_core.code_executor import ImportFromModule
from autogen_core.tools import FunctionTool
from fpdf import FPDF
from PIL import Image, ImageDraw, ImageOps
async def generate_pdf(
sections: List[Dict[str, Optional[str]]], output_file: str = "report.pdf", report_title: str = "PDF Report"
) -> str:
"""
Generate a PDF report with formatted sections including text and images.
Args:
sections: List of dictionaries containing section details with keys:
- title: Section title
- level: Heading level (title, h1, h2)
- content: Section text content
- image: Optional image URL or file path
output_file: Name of output PDF file
report_title: Title shown at top of report
Returns:
str: Path to the generated PDF file
"""
def normalize_text(text: str) -> str:
"""Normalize Unicode text to ASCII."""
return unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
def get_image(image_url_or_path):
"""Fetch image from URL or local path."""
if image_url_or_path.startswith(("http://", "https://")):
response = requests.get(image_url_or_path)
if response.status_code == 200:
return BytesIO(response.content)
elif Path(image_url_or_path).is_file():
return open(image_url_or_path, "rb")
return None
def add_rounded_corners(img, radius=6):
"""Add rounded corners to an image."""
mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle([(0, 0), img.size], radius, fill=255)
img = ImageOps.fit(img, mask.size, centering=(0.5, 0.5))
img.putalpha(mask)
return img
class PDF(FPDF):
"""Custom PDF class with header and content formatting."""
def header(self):
self.set_font("Arial", "B", 12)
normalized_title = normalize_text(report_title)
self.cell(0, 10, normalized_title, 0, 1, "C")
def chapter_title(self, txt):
self.set_font("Arial", "B", 12)
normalized_txt = normalize_text(txt)
self.cell(0, 10, normalized_txt, 0, 1, "L")
self.ln(2)
def chapter_body(self, body):
self.set_font("Arial", "", 12)
normalized_body = normalize_text(body)
self.multi_cell(0, 10, normalized_body)
self.ln()
def add_image(self, img_data):
img = Image.open(img_data)
img = add_rounded_corners(img)
img_path = Path(f"temp_{uuid.uuid4().hex}.png")
img.save(img_path, format="PNG")
self.image(str(img_path), x=None, y=None, w=190 if img.width > 190 else img.width)
self.ln(10)
img_path.unlink()
# Initialize PDF
pdf = PDF()
pdf.add_page()
font_size = {"title": 16, "h1": 14, "h2": 12, "body": 12}
# Add sections
for section in sections:
title = section.get("title", "")
level = section.get("level", "h1")
content = section.get("content", "")
image = section.get("image")
pdf.set_font("Arial", "B" if level in font_size else "", font_size.get(level, font_size["body"]))
pdf.chapter_title(title)
if content:
pdf.chapter_body(content)
if image:
img_data = get_image(image)
if img_data:
pdf.add_image(img_data)
if isinstance(img_data, BytesIO):
img_data.close()
pdf.output(output_file)
return output_file
# Create the PDF generation tool
generate_pdf_tool = FunctionTool(
func=generate_pdf,
description="Generate PDF reports with formatted sections containing text and images",
global_imports=[
"uuid",
"requests",
"unicodedata",
ImportFromModule("typing", ("List", "Dict", "Optional")),
ImportFromModule("pathlib", ("Path",)),
ImportFromModule("fpdf", ("FPDF",)),
ImportFromModule("PIL", ("Image", "ImageDraw", "ImageOps")),
ImportFromModule("io", ("BytesIO",)),
],
)

View File

@ -0,0 +1,144 @@
import os
from typing import Dict, List, Optional
from urllib.parse import urljoin
import html2text
import httpx
from autogen_core.code_executor import ImportFromModule
from autogen_core.tools import FunctionTool
from bs4 import BeautifulSoup
async def google_search(
query: str,
num_results: int = 5,
include_snippets: bool = True,
include_content: bool = True,
content_max_length: Optional[int] = 15000,
language: str = "en",
country: Optional[str] = None,
safe_search: bool = True,
) -> List[Dict[str, str]]:
"""
Perform a Google search using the Custom Search API and optionally fetch webpage content.
Args:
query: Search query string
num_results: Number of results to return (max 10)
include_snippets: Include result snippets in output
include_content: Include full webpage content in markdown format
content_max_length: Maximum length of webpage content (if included)
language: Language code for search results (e.g., en, es, fr)
country: Optional country code for search results (e.g., us, uk)
safe_search: Enable safe search filtering
Returns:
List[Dict[str, str]]: List of search results, each containing:
- title: Result title
- link: Result URL
- snippet: Result description (if include_snippets=True)
- content: Webpage content in markdown (if include_content=True)
"""
api_key = os.getenv("GOOGLE_API_KEY")
cse_id = os.getenv("GOOGLE_CSE_ID")
if not api_key or not cse_id:
raise ValueError("Missing required environment variables. Please set GOOGLE_API_KEY and GOOGLE_CSE_ID.")
num_results = min(max(1, num_results), 10)
async def fetch_page_content(url: str, max_length: Optional[int] = 50000) -> str:
"""Helper function to fetch and convert webpage content to markdown"""
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Remove script and style elements
for script in soup(["script", "style"]):
script.decompose()
# Convert relative URLs to absolute
for tag in soup.find_all(["a", "img"]):
if tag.get("href"):
tag["href"] = urljoin(url, tag["href"])
if tag.get("src"):
tag["src"] = urljoin(url, tag["src"])
h2t = html2text.HTML2Text()
h2t.body_width = 0
h2t.ignore_images = False
h2t.ignore_emphasis = False
h2t.ignore_links = False
h2t.ignore_tables = False
markdown = h2t.handle(str(soup))
if max_length and len(markdown) > max_length:
markdown = markdown[:max_length] + "\n...(truncated)"
return markdown.strip()
except Exception as e:
return f"Error fetching content: {str(e)}"
params = {
"key": api_key,
"cx": cse_id,
"q": query,
"num": num_results,
"hl": language,
"safe": "active" if safe_search else "off",
}
if country:
params["gl"] = country
try:
async with httpx.AsyncClient() as client:
response = await client.get("https://www.googleapis.com/customsearch/v1", params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
if "items" in data:
for item in data["items"]:
result = {"title": item.get("title", ""), "link": item.get("link", "")}
if include_snippets:
result["snippet"] = item.get("snippet", "")
if include_content:
result["content"] = await fetch_page_content(result["link"], max_length=content_max_length)
results.append(result)
return results
except httpx.RequestError as e:
raise ValueError(f"Failed to perform search: {str(e)}") from e
except KeyError as e:
raise ValueError(f"Invalid API response format: {str(e)}") from e
except Exception as e:
raise ValueError(f"Error during search: {str(e)}") from e
# Create the enhanced Google search tool
google_search_tool = FunctionTool(
func=google_search,
description="""
Perform Google searches using the Custom Search API with optional webpage content fetching.
Requires GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables to be set.
""",
global_imports=[
ImportFromModule("typing", ("List", "Dict", "Optional")),
"os",
"httpx",
"html2text",
ImportFromModule("bs4", ("BeautifulSoup",)),
ImportFromModule("urllib.parse", ("urljoin",)),
],
)

View File

@ -31,15 +31,15 @@ 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, initializer.app_root)
logger.info("Managers initialized successfully")
# Any other initialization code
logger.info("Application startup complete")
logger.info(
f"Application startup complete. Navigate to http://{os.environ.get('AUTOGENSTUDIO_HOST', '127.0.0.1')}:{os.environ.get('AUTOGENSTUDIO_PORT', '8081')}"
)
except Exception as e:
logger.error(f"Failed to initialize application: {str(e)}")

View File

@ -1,4 +1,5 @@
# api/config.py
from pydantic_settings import BaseSettings
@ -11,8 +12,7 @@ class Settings(BaseSettings):
DEFAULT_USER_ID: str = "guestuser@gmail.com"
UPGRADE_DATABASE: bool = False
class Config:
env_prefix = "AUTOGENSTUDIO_"
model_config = {"env_prefix": "AUTOGENSTUDIO_"}
settings = Settings()

View File

@ -37,7 +37,7 @@ class AppInitializer:
self._paths = self._init_paths()
self._create_directories()
self._load_environment()
logger.info(f"Initialized application data folder: {self.app_root}")
logger.info(f"Initializing application data folder: {self.app_root} ")
def _get_app_root(self) -> Path:
"""Determine application root directory"""

File diff suppressed because one or more lines are too long

View File

@ -151,7 +151,7 @@ export const useGalleryStore = create<GalleryStore>()(
},
}),
{
name: "gallery-storage-v1",
name: "gallery-storage-v2",
}
)
);

View File

@ -283,13 +283,13 @@ const AgentFlow: React.FC<AgentFlowProps> = ({ teamConfig, run }) => {
// Add first message node if it exists
if (messages.length > 0) {
const firstAgentConfig = teamConfig.config.participants.find(
(p) => p.config.name === messages[0].source
(p) => p.config.name === messages[0]?.source
);
nodeMap.set(
messages[0].source,
messages[0]?.source,
createNode(
messages[0].source,
messages[0].source === "user" ? "user" : "agent",
messages[0]?.source,
messages[0]?.source === "user" ? "user" : "agent",
firstAgentConfig,
false,
run
@ -301,28 +301,28 @@ const AgentFlow: React.FC<AgentFlowProps> = ({ teamConfig, run }) => {
for (let i = 0; i < messages.length - 1; i++) {
const currentMsg = messages[i];
const nextMsg = messages[i + 1];
const transitionKey = `${currentMsg.source}->${nextMsg.source}`;
const transitionKey = `${currentMsg?.source}->${nextMsg?.source}`;
if (!transitionCounts.has(transitionKey)) {
transitionCounts.set(transitionKey, {
source: currentMsg.source,
target: nextMsg.source,
source: currentMsg?.source,
target: nextMsg?.source,
count: 1,
totalTokens:
(currentMsg.models_usage?.prompt_tokens || 0) +
(currentMsg.models_usage?.completion_tokens || 0),
(currentMsg?.models_usage?.prompt_tokens || 0) +
(currentMsg?.models_usage?.completion_tokens || 0),
messages: [currentMsg],
});
} else {
const transition = transitionCounts.get(transitionKey)!;
transition.count++;
transition.totalTokens +=
(currentMsg.models_usage?.prompt_tokens || 0) +
(currentMsg.models_usage?.completion_tokens || 0);
(currentMsg?.models_usage?.prompt_tokens || 0) +
(currentMsg?.models_usage?.completion_tokens || 0);
transition.messages.push(currentMsg);
}
if (!nodeMap.has(nextMsg.source)) {
if (!nodeMap.has(nextMsg?.source)) {
const agentConfig = teamConfig.config.participants.find(
(p) => p.config.name === nextMsg.source
);
@ -460,8 +460,8 @@ const AgentFlow: React.FC<AgentFlowProps> = ({ teamConfig, run }) => {
}[run.status];
newEdges.push({
id: `${lastMessage.source}-end`,
source: lastMessage.source,
id: `${lastMessage?.source}-end`,
source: lastMessage?.source,
target: "end",
type: "custom",
data: {

View File

@ -1,5 +1,5 @@
import React, { useState, memo } from "react";
import { User, Bot, Maximize2, Minimize2 } from "lucide-react";
import { User, Bot, Maximize2, Minimize2, DraftingCompass } from "lucide-react";
import {
AgentMessageConfig,
FunctionCall,
@ -45,8 +45,15 @@ const RenderMultiModal: React.FC<{ content: (string | ImageContent)[] }> = ({
const RenderToolCall: React.FC<{ content: FunctionCall[] }> = ({ content }) => (
<div className="space-y-2">
{content.map((call) => (
<div key={call.id} className="border border-secondary rounded p-2">
<div className="font-medium">Function: {call.name}</div>
<div
key={call.id}
className="relative pl-3 border border-secondary rounded p-2"
>
<div className="absolute top-0 -left-0.5 w-1 bg-secondary h-full rounded"></div>
<div className="font-medium">
<DraftingCompass className="w-4 h-4 text-accent inline-block mr-1.5 -mt-0.5" />{" "}
Calling {call.name} tool with arguments
</div>
<TruncatableText
content={JSON.stringify(JSON.parse(call.arguments), null, 2)}
isJson={true}
@ -62,8 +69,15 @@ const RenderToolResult: React.FC<{ content: FunctionExecutionResult[] }> = ({
}) => (
<div className="space-y-2">
{content.map((result) => (
<div key={result.call_id} className="rounded p-2">
<div className="font-medium">Result ID: {result.call_id}</div>
<div
key={result.call_id}
className="rounded p-2 pl-3 relative border border-secondary"
>
<div className="absolute top-0 -left-0.5 w-1 bg-secondary h-full rounded"></div>
<div className="font-medium">
<DraftingCompass className="w-4 text-accent h-4 inline-block mr-1.5 -mt-0.5" />{" "}
Tool Result
</div>
<TruncatableText
content={result.content}
className="text-sm mt-1 bg-secondary p-2 border border-secondary rounded scroll overflow-x-scroll"

View File

@ -10,6 +10,9 @@ import {
ChevronDown,
ChevronUp,
Bot,
PanelRightClose,
PanelLeftOpen,
PanelRightOpen,
} from "lucide-react";
import { Run, Message, TeamConfig, Component } from "../../../types/datamodel";
import AgentFlow from "./agentflow/agentflow";
@ -40,6 +43,7 @@ const RunView: React.FC<RunViewProps> = ({
const [isExpanded, setIsExpanded] = useState(true);
const threadContainerRef = useRef<HTMLDivElement | null>(null);
const isActive = run.status === "active" || run.status === "awaiting_input";
const [isFlowVisible, setIsFlowVisible] = useState(true);
// Replace existing scroll effect with this simpler one
useEffect(() => {
@ -56,7 +60,7 @@ const RunView: React.FC<RunViewProps> = ({
const calculateThreadTokens = (messages: Message[]) => {
// console.log("messages", messages);
return messages.reduce((total, msg) => {
if (!msg.config.models_usage) return total;
if (!msg.config?.models_usage) return total;
return (
total +
(msg.config.models_usage.prompt_tokens || 0) +
@ -121,6 +125,12 @@ const RunView: React.FC<RunViewProps> = ({
const lastResultMessage = run.team_result?.task_result.messages.slice(-1)[0];
const lastMessage = run.messages.slice(-1)[0];
console.log("lastResultMessage", lastResultMessage);
console.log(
"lastMessage",
run.messages[run.messages.length - 1]?.config?.content
);
return (
<div className="space-y-6 mr-2 ">
{/* Run Header */}
@ -198,14 +208,18 @@ const RunView: React.FC<RunViewProps> = ({
</div>
{lastMessage ? (
<TruncatableText
key={"_" + run.id}
textThreshold={700}
content={
run.messages[run.messages.length - 1]?.config?.content +
""
}
className="break-all"
// <TruncatableText
// key={"_" + run.id}
// textThreshold={700}
// content={
// run.messages[run.messages.length - 1]?.config?.content +
// ""
// }
// className="break-all"
// />
<RenderMessage
message={run.messages[run.messages.length - 1]?.config}
isLast={true}
/>
) : (
<>
@ -260,7 +274,19 @@ const RunView: React.FC<RunViewProps> = ({
</div>
{isExpanded && (
<div className="flex flex-row gap-4">
<div className="flex relative flex-row gap-4">
{!isFlowVisible && (
<div className="z-50 absolute right-2 top-2 bg-tertiary rounded p-2 hover:opacity-100 opacity-80">
<Tooltip title="Show message flow graph">
<button
onClick={() => setIsFlowVisible(true)}
className=" p-1 rounded-md bg-tertiary hover:bg-secondary transition-colors"
>
<PanelRightOpen strokeWidth={1.5} size={22} />
</button>
</Tooltip>
</div>
)}
{/* Messages Thread */}
<div
ref={threadContainerRef}
@ -300,11 +326,23 @@ const RunView: React.FC<RunViewProps> = ({
</div>
{/* Agent Flow Visualization */}
<div className="bg-tertiary flex-1 rounded mt-2">
{teamConfig && (
<AgentFlow teamConfig={teamConfig} run={run} />
)}
</div>
{isFlowVisible && (
<div className="bg-tertiary flex-1 rounded mt-2 relative">
<div className="z-50 absolute left-2 top-2 p-2 hover:opacity-100 opacity-80">
<Tooltip title="Hide message flow">
<button
onClick={() => setIsFlowVisible(false)}
className=" p-1 rounded-md bg-tertiary hover:bg-secondary transition-colors"
>
<PanelRightClose strokeWidth={1.5} size={22} />
</button>
</Tooltip>
</div>
{teamConfig && (
<AgentFlow teamConfig={teamConfig} run={run} />
)}
</div>
)}
</div>
)}
</div>

View File

@ -18,34 +18,13 @@ export const SessionEditor: React.FC<SessionEditorProps> = ({
onSave,
onCancel,
isOpen,
teams,
}) => {
const [form] = Form.useForm();
const [teams, setTeams] = useState<Team[]>([]);
const [loading, setLoading] = useState(false);
const { user } = useContext(appContext);
const [messageApi, contextHolder] = message.useMessage();
// 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) {
messageApi.error("Error loading 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) {

View File

@ -1,14 +1,17 @@
import React, { useCallback, useEffect, useState, useContext } from "react";
import { message } from "antd";
import { Button, message } from "antd";
import { useConfigStore } from "../../../hooks/store";
import { appContext } from "../../../hooks/provider";
import { sessionAPI } from "./api";
import { SessionEditor } from "./editor";
import type { Session } from "../../types/datamodel";
import type { Session, Team } from "../../types/datamodel";
import ChatView from "./chat/chat";
import { Sidebar } from "./sidebar";
import { teamAPI } from "../team/api";
import { useGalleryStore } from "../gallery/store";
export const SessionManager: React.FC = () => {
const [teams, setTeams] = useState<Team[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [editingSession, setEditingSession] = useState<Session | undefined>();
@ -24,6 +27,8 @@ export const SessionManager: React.FC = () => {
const { user } = useContext(appContext);
const { session, setSession, sessions, setSessions } = useConfigStore();
const defaultGallery = useGalleryStore((state) => state.getDefaultGallery());
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("sessionSidebar", JSON.stringify(isSidebarOpen));
@ -121,6 +126,30 @@ export const SessionManager: React.FC = () => {
}
};
const handleQuickStart = async (teamId: number, teamName: string) => {
if (!user?.email) return;
console.log("Quick start session", teamId, teamName);
try {
const defaultName = `${teamName.substring(
0,
20
)} - ${new Date().toLocaleString()} Session`;
const created = await sessionAPI.createSession(
{
name: defaultName,
team_id: teamId,
},
user.email
);
setSessions([created, ...sessions]);
setSession(created);
messageApi.success("Session created!");
} catch (error) {
messageApi.error("Error creating session");
}
};
const handleSelectSession = async (selectedSession: Session) => {
if (!user?.email || !selectedSession.id) return;
@ -158,6 +187,39 @@ export const SessionManager: React.FC = () => {
fetchSessions();
}, [fetchSessions]);
// Add teams fetching
const fetchTeams = useCallback(async () => {
if (!user?.email) return;
try {
setIsLoading(true);
const teamsData = await teamAPI.listTeams(user.email);
if (teamsData.length > 0) {
setTeams(teamsData);
} else {
const sampleTeam = defaultGallery.items.teams[0];
// If no teams, create a default team
const defaultTeam = await teamAPI.createTeam(
{
component: sampleTeam,
},
user.email
);
setTeams([defaultTeam]);
}
} catch (error) {
console.error("Error fetching teams:", error);
messageApi.error("Error loading teams");
} finally {
setIsLoading(false);
}
}, [user?.email, messageApi]);
// Fetch teams on mount
useEffect(() => {
fetchTeams();
}, [fetchTeams]);
return (
<div className="relative flex h-full w-full">
{contextHolder}
@ -168,6 +230,8 @@ export const SessionManager: React.FC = () => {
>
<Sidebar
isOpen={isSidebarOpen}
teams={teams}
onStartSession={handleQuickStart}
sessions={sessions}
currentSession={session}
onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
@ -198,6 +262,7 @@ export const SessionManager: React.FC = () => {
</div>
<SessionEditor
teams={teams}
session={editingSession}
isOpen={isEditorOpen}
onSave={handleSaveSession}

View File

@ -0,0 +1,158 @@
import React, { useState, useEffect } from "react";
import { Button, Dropdown, MenuProps, message, Select, Space } from "antd";
import { Plus, InfoIcon, Bot, TextSearch, ChevronDown } from "lucide-react";
import { Team } from "../../types/datamodel";
import { truncateText } from "../../utils";
import Input from "antd/es/input/Input";
interface NewSessionControlsProps {
teams: Team[];
isLoading: boolean;
onStartSession: (teamId: number, teamName: string) => void;
}
const NewSessionControls = ({
teams,
isLoading,
onStartSession,
}: NewSessionControlsProps) => {
const [selectedTeamId, setSelectedTeamId] = useState<number | undefined>();
const [lastUsedTeamId, setLastUsedTeamId] = useState<number | undefined>(
() => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem("lastUsedTeamId");
return stored ? parseInt(stored) : undefined;
}
}
);
const [search, setSearch] = useState<string>("");
// Filter teams based on search
const filteredTeams = teams.filter((team) => {
return (
team.component.label?.toLowerCase().includes(search.toLowerCase()) ||
team.component.description?.toLowerCase().includes(search.toLowerCase())
);
});
// Auto-select last used team on load
useEffect(() => {
if (lastUsedTeamId && teams.some((team) => team.id === lastUsedTeamId)) {
setSelectedTeamId(lastUsedTeamId);
} else if (teams.length > 0) {
setSelectedTeamId(teams[0].id);
}
}, [teams, lastUsedTeamId]);
const handleStartSession = async () => {
if (!selectedTeamId) return;
if (typeof window !== "undefined") {
localStorage.setItem("lastUsedTeamId", selectedTeamId.toString());
}
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
if (!selectedTeam) return;
// Give UI time to update before starting session
await new Promise((resolve) => setTimeout(resolve, 100));
onStartSession(selectedTeamId, selectedTeam.component.label || "");
};
const handleMenuClick: MenuProps["onClick"] = async (e) => {
const newTeamId = parseInt(e.key);
const selectedTeam = teams.find((team) => team.id === newTeamId);
if (!selectedTeam) {
console.error("Selected team not found:", newTeamId);
return;
}
// Update state first
setSelectedTeamId(newTeamId);
// Save to localStorage
if (typeof window !== "undefined") {
localStorage.setItem("lastUsedTeamId", e.key);
}
// Delay the session start to allow UI to update
await new Promise((resolve) => setTimeout(resolve, 100));
onStartSession(newTeamId, selectedTeam.component.label || "");
};
const hasNoTeams = !isLoading && teams.length === 0;
const items: MenuProps["items"] = [
{
type: "group",
label: (
<div>
<div className="text-xs text-secondary mb-1">Select a team</div>
<Input
prefix={<TextSearch className="w-4 h-4" />}
placeholder="Search teams"
onChange={(e) => setSearch(e.target.value)}
/>
</div>
),
key: "from-team",
},
{
type: "divider",
},
...filteredTeams.map((team) => ({
label: (
<div>
<div>{truncateText(team.component.label || "", 20)}</div>
<div className="text-xs text-secondary">
{team.component.component_type}
</div>
</div>
),
key: team?.id?.toString() || "",
icon: <Bot className="w-4 h-4" />,
})),
];
const menuProps = {
items,
onClick: handleMenuClick,
};
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
return (
<div className="space-y-2 w-full">
<Dropdown.Button
menu={menuProps}
type="primary"
className="w-full"
placement="bottomRight"
icon={<ChevronDown className="w-4 h-4" />}
onClick={handleStartSession}
disabled={!selectedTeamId || isLoading}
>
<div className="" style={{ width: "183px" }}>
<Plus className="w-4 h-4 inline-block -mt-1" /> New Session
</div>
</Dropdown.Button>
<div
className="text-xs text-secondary"
title={selectedTeam?.component.label}
>
{truncateText(selectedTeam?.component.label || "", 30)}
</div>
{hasNoTeams && (
<div className="flex items-center gap-1.5 text-xs text-yellow-600 mt-1">
<InfoIcon className="h-3 w-3" />
<span>Create a team to get started</span>
</div>
)}
</div>
);
};
export default NewSessionControls;

View File

@ -9,8 +9,9 @@ import {
InfoIcon,
RefreshCcw,
} from "lucide-react";
import type { Session } from "../../types/datamodel";
import type { Session, Team } from "../../types/datamodel";
import { getRelativeTimeString } from "../atoms";
import NewSessionControls from "./newsession";
interface SidebarProps {
isOpen: boolean;
@ -21,6 +22,8 @@ interface SidebarProps {
onEditSession: (session?: Session) => void;
onDeleteSession: (sessionId: number) => void;
isLoading?: boolean;
onStartSession: (teamId: number, teamName: string) => void;
teams: Team[];
}
export const Sidebar: React.FC<SidebarProps> = ({
@ -32,6 +35,8 @@ export const Sidebar: React.FC<SidebarProps> = ({
onEditSession,
onDeleteSession,
isLoading = false,
onStartSession,
teams,
}) => {
if (!isOpen) {
return (
@ -85,17 +90,14 @@ export const Sidebar: React.FC<SidebarProps> = ({
</div>
<div className="my-4 flex text-sm ">
<div className=" mr-2 w-full">
<Tooltip title="Create new session">
<Button
type="primary"
className="w-full"
icon={<Plus className="w-4 h-4" />}
onClick={() => onEditSession()}
>
New Session
</Button>
</Tooltip>
<div className=" mr-2 w-full pr-2">
{isOpen && (
<NewSessionControls
teams={teams}
isLoading={isLoading}
onStartSession={onStartSession}
/>
)}
</div>
</div>
@ -128,22 +130,22 @@ export const Sidebar: React.FC<SidebarProps> = ({
</div>
<div
className={`group ml-1 flex items-center justify-between rounded-l p-2 py-1 text-sm cursor-pointer hover:bg-tertiary ${
currentSession?.id === s.id
? " border-accent bg-secondary"
: ""
currentSession?.id === s.id ? "border-accent bg-secondary" : ""
}`}
onClick={() => onSelectSession(s)}
>
<span className="truncate text-sm flex-1">{s.name}</span>
<span className="ml-2 truncate text-xs text-secondary flex-1">
{getRelativeTimeString(s.updated_at || "")}
</span>
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex flex-col min-w-0 flex-1 mr-2">
<div className="truncate text-sm">{s.name}</div>
<span className="truncate text-xs text-secondary">
{getRelativeTimeString(s.updated_at || "")}
</span>
</div>
<div className="py-3 flex gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<Tooltip title="Edit session">
<Button
type="text"
size="small"
className="p-0 min-w-[24px] h-6"
className="p-1 min-w-[24px] h-6"
icon={<Edit className="w-4 h-4" />}
onClick={(e) => {
e.stopPropagation();
@ -155,9 +157,9 @@ export const Sidebar: React.FC<SidebarProps> = ({
<Button
type="text"
size="small"
className="p-0 min-w-[24px] h-6"
className="p-1 min-w-[24px] h-6"
danger
icon={<Trash2 className="w-4 h-4 text-red-500" />}
icon={<Trash2 className="w-4 h-4 text-red-500" />}
onClick={(e) => {
e.stopPropagation();
if (s.id) onDeleteSession(s.id);

View File

@ -1,10 +1,11 @@
import type { Session } from "../../types/datamodel";
import type { Session, Team } from "../../types/datamodel";
export interface SessionEditorProps {
session?: Session;
onSave: (session: Partial<Session>) => Promise<void>;
onCancel: () => void;
isOpen: boolean;
teams: Team[];
}
export interface SessionListProps {

View File

@ -324,7 +324,7 @@ export const TeamNode = memo<NodeProps<CustomNode>>((props) => {
</div>
)}
<DroppableZone
id={`${props.id}-termination-zone`}
id={`${props.id}@@@termination-zone`}
accepts={["termination"]}
>
<div className="text-secondary text-xs my-1 text-center">

View File

@ -1,11 +1,10 @@
import React, { useCallback, useEffect, useState, useContext } from "react";
import { Button, message, Modal } from "antd";
import { message, Modal } from "antd";
import { ChevronRight } from "lucide-react";
import { appContext } from "../../../hooks/provider";
import { teamAPI } from "./api";
import { TeamSidebar } from "./sidebar";
import type { Team } from "../../types/datamodel";
import { defaultTeam } from "./types";
import { TeamBuilder } from "./builder/builder";
export const TeamManager: React.FC = () => {

View File

@ -102,7 +102,7 @@ export const TeamSidebar: React.FC<TeamSidebarProps> = ({
<div className="my-4 flex text-sm ">
<div className=" mr-2 w-full">
<Tooltip title="Create new session">
<Tooltip title="Create a new team">
<Button
type="primary"
className="w-full"

View File

@ -57,14 +57,6 @@ export const defaultTeamConfig: Component<TeamConfig> = {
},
],
handoffs: [],
model_context: {
provider:
"autogen_core.model_context.UnboundedChatCompletionContext",
component_type: "chat_completion_context",
version: 1,
component_version: 1,
config: {},
},
description:
"An agent that provides assistance with ability to use tools.",
system_message:

View File

@ -21,9 +21,8 @@ classifiers = [
dependencies = [
"pydantic",
"pydantic-settings",
"fastapi",
"typer",
"uvicorn",
"fastapi[standard]",
"typer",
"aiofiles",
"python-dotenv",
"websockets",
@ -33,8 +32,8 @@ dependencies = [
"alembic",
"loguru",
"pyyaml",
"autogen-core>=0.4.2,<0.5",
"autogen-agentchat>=0.4.2,<0.5",
"autogen-core>=0.4.5,<0.5",
"autogen-agentchat>=0.4.5,<0.5",
"autogen-ext[magentic-one, openai, azure]>=0.4.2,<0.5",
"azure-identity"
]

View File

@ -758,7 +758,7 @@ dependencies = [
{ name = "autogen-core" },
{ name = "autogen-ext", extra = ["azure", "magentic-one", "openai"] },
{ name = "azure-identity" },
{ name = "fastapi" },
{ name = "fastapi", extra = ["standard"] },
{ name = "loguru" },
{ name = "numpy" },
{ name = "psycopg" },
@ -768,7 +768,6 @@ dependencies = [
{ name = "pyyaml" },
{ name = "sqlmodel" },
{ name = "typer" },
{ name = "uvicorn" },
{ name = "websockets" },
]
@ -789,8 +788,8 @@ requires-dist = [
{ name = "autogen-core", editable = "packages/autogen-core" },
{ name = "autogen-ext", extras = ["azure", "magentic-one", "openai"], editable = "packages/autogen-ext" },
{ name = "azure-identity" },
{ name = "fastapi" },
{ name = "fastapi", marker = "extra == 'web'" },
{ name = "fastapi", extras = ["standard"] },
{ name = "loguru" },
{ name = "numpy", specifier = "<2.0.0" },
{ name = "psycopg" },
@ -801,7 +800,6 @@ requires-dist = [
{ name = "pyyaml" },
{ name = "sqlmodel" },
{ name = "typer" },
{ name = "uvicorn" },
{ name = "uvicorn", marker = "extra == 'web'" },
{ name = "websockets" },
]
@ -1587,6 +1585,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
]
[[package]]
name = "dnspython"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
]
[[package]]
name = "docker"
version = "7.1.0"
@ -1619,6 +1626,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 },
]
[[package]]
name = "email-validator"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 },
]
[[package]]
name = "environs"
version = "11.2.1"
@ -1691,6 +1711,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/7f/bbd4dcf0faf61bc68a01939256e2ed02d681e9334c1a3cef24d5f77aba9f/fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e", size = 94777 },
]
[package.optional-dependencies]
standard = [
{ name = "email-validator" },
{ name = "fastapi-cli", extra = ["standard"] },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "python-multipart" },
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "fastapi-cli"
version = "0.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "rich-toolkit" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705 },
]
[package.optional-dependencies]
standard = [
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "fastjsonschema"
version = "2.21.1"
@ -2460,6 +2509,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854 },
]
[[package]]
name = "httptools"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 },
{ url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 },
{ url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 },
{ url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 },
{ url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 },
{ url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 },
{ url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 },
{ url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 },
{ url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 },
{ url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 },
{ url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 },
{ url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 },
{ url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 },
{ url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 },
{ url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 },
{ url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 },
{ url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 },
{ url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 },
{ url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 },
{ url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 },
{ url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 },
]
[[package]]
name = "httpx"
version = "0.28.1"
@ -5940,6 +6018,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "rich-toolkit"
version = "0.13.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/71cfbf6bf6257ea785d1f030c22468f763eea1b3e5417620f2ba9abd6dca/rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3", size = 72288 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 },
]
[[package]]
name = "rpds-py"
version = "0.22.3"
@ -7464,6 +7556,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]
[package.optional-dependencies]
standard = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "httptools" },
{ name = "python-dotenv" },
{ name = "pyyaml" },
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
{ name = "watchfiles" },
{ name = "websockets" },
]
[[package]]
name = "uvloop"
version = "0.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 },
{ url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 },
{ url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 },
{ url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 },
{ url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 },
{ url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 },
{ url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 },
{ url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 },
{ url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 },
{ url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 },
{ url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 },
{ url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 },
{ url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 },
{ url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 },
{ url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 },
{ url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 },
{ url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 },
{ url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 },
]
[[package]]
name = "watchdog"
version = "6.0.0"