mirror of https://github.com/microsoft/autogen.git
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:
parent
fbda70320d
commit
b89ca2a5ae
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from .db_manager import DatabaseManager
|
||||
from .gallery_builder import GalleryBuilder, create_default_gallery
|
||||
|
||||
__all__ = [
|
||||
"DatabaseManager",
|
||||
|
|
|
@ -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)}")
|
||||
|
|
|
@ -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()})
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from .builder import GalleryBuilder, create_default_gallery
|
||||
|
||||
__all__ = ["GalleryBuilder", "create_default_gallery"]
|
|
@ -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))
|
|
@ -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",
|
||||
]
|
|
@ -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",)),
|
||||
],
|
||||
)
|
|
@ -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=[],
|
||||
)
|
|
@ -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",)),
|
||||
],
|
||||
)
|
|
@ -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",)),
|
||||
],
|
||||
)
|
|
@ -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",)),
|
||||
],
|
||||
)
|
|
@ -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",)),
|
||||
],
|
||||
)
|
|
@ -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)}")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
@ -151,7 +151,7 @@ export const useGalleryStore = create<GalleryStore>()(
|
|||
},
|
||||
}),
|
||||
{
|
||||
name: "gallery-storage-v1",
|
||||
name: "gallery-storage-v2",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
137
python/uv.lock
137
python/uv.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue