mirror of
https://github.com/microsoft/autogen.git
synced 2025-12-02 01:49:53 +00:00
Enable LLM Call Observability in AGS (#5457)
<!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> It is often helpful to inspect the raw request and response to/from an LLM as agents act. This PR, does the following: - Updates TeamManager to yield LLMCallEvents from core library - Run in an async background task to listen for LLMCallEvent JIT style when a team is run - Add events to an async queue and - yield those events in addition to whatever actual agentchat team.run_stream yields. - Update the AGS UI to show those LLMCallEvents in the messages section as a team runs - Add settings panel to show/hide llm call events in messages. - Minor updates to default team <img width="1539" alt="image" src="https://github.com/user-attachments/assets/bfbb19fe-3560-4faa-b600-7dd244e0e974" /> <img width="1554" alt="image" src="https://github.com/user-attachments/assets/775624f5-ba83-46e8-81ff-512bfeed6bab" /> <img width="1538" alt="image" src="https://github.com/user-attachments/assets/3becbf85-b75a-4506-adb7-e5575ebbaec4" /> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5440 ## Checks - [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation 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.
This commit is contained in:
parent
b868e32b05
commit
7fc7f383f0
@ -4,6 +4,7 @@ from .types import (
|
||||
GalleryComponents,
|
||||
GalleryItems,
|
||||
GalleryMetadata,
|
||||
LLMCallEventMessage,
|
||||
MessageConfig,
|
||||
MessageMeta,
|
||||
Response,
|
||||
@ -22,4 +23,5 @@ __all__ = [
|
||||
"TeamResult",
|
||||
"Response",
|
||||
"SocketMessage",
|
||||
"LLMCallEventMessage",
|
||||
]
|
||||
|
||||
@ -2,6 +2,7 @@ from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from autogen_agentchat.base import TaskResult
|
||||
from autogen_agentchat.messages import BaseChatMessage
|
||||
from autogen_core import ComponentModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
@ -18,6 +19,11 @@ class TeamResult(BaseModel):
|
||||
duration: float
|
||||
|
||||
|
||||
class LLMCallEventMessage(BaseChatMessage):
|
||||
source: str = "llm_call_event"
|
||||
content: str
|
||||
|
||||
|
||||
class MessageMeta(BaseModel):
|
||||
task: Optional[str] = None
|
||||
task_result: Optional[TaskResult] = None
|
||||
|
||||
@ -240,7 +240,7 @@ Read the above conversation. Then select the next role from {participants} to pl
|
||||
builder.add_team(
|
||||
websurfer_team.dump_component(),
|
||||
label="Web Agent Team (Operator)",
|
||||
description="A group chat team that have participants takes turn to publish a message\n to all, using a ChatCompletion model to select the next speaker after each message.",
|
||||
description="A team with 3 agents - a Web Surfer agent that can browse the web, a Verification Assistant that verifies and summarizes information, and a User Proxy that provides human feedback when needed.",
|
||||
)
|
||||
|
||||
builder.add_tool(
|
||||
@ -347,7 +347,7 @@ Read the above conversation. Then select the next role from {participants} to pl
|
||||
builder.add_team(
|
||||
deep_research_team.dump_component(),
|
||||
label="Deep Research Team",
|
||||
description="A team that performs deep research using web searches, verification, and summarization.",
|
||||
description="A team with 3 agents - a Research Assistant that performs web searches and analyzes information, a Verifier that ensures research quality and completeness, and a Summary Agent that provides a detailed markdown summary of the research as a report to the user.",
|
||||
)
|
||||
|
||||
return builder.build()
|
||||
|
||||
@ -12,10 +12,10 @@ from bs4 import BeautifulSoup
|
||||
|
||||
async def bing_search(
|
||||
query: str,
|
||||
num_results: int = 5,
|
||||
num_results: int = 3,
|
||||
include_snippets: bool = True,
|
||||
include_content: bool = True,
|
||||
content_max_length: Optional[int] = 15000,
|
||||
content_max_length: Optional[int] = 10000,
|
||||
language: str = "en",
|
||||
country: Optional[str] = None,
|
||||
safe_search: str = "moderate",
|
||||
|
||||
@ -11,10 +11,10 @@ from bs4 import BeautifulSoup
|
||||
|
||||
async def google_search(
|
||||
query: str,
|
||||
num_results: int = 5,
|
||||
num_results: int = 3,
|
||||
include_snippets: bool = True,
|
||||
include_content: bool = True,
|
||||
content_max_length: Optional[int] = 15000,
|
||||
content_max_length: Optional[int] = 10000,
|
||||
language: str = "en",
|
||||
country: Optional[str] = None,
|
||||
safe_search: bool = True,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
@ -8,13 +9,26 @@ import aiofiles
|
||||
import yaml
|
||||
from autogen_agentchat.base import TaskResult, Team
|
||||
from autogen_agentchat.messages import AgentEvent, ChatMessage
|
||||
from autogen_core import CancellationToken, Component, ComponentModel
|
||||
from autogen_core import EVENT_LOGGER_NAME, CancellationToken, Component, ComponentModel
|
||||
from autogen_core.logging import LLMCallEvent
|
||||
|
||||
from ..datamodel.types import TeamResult
|
||||
from ..datamodel.types import LLMCallEventMessage, TeamResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunEventLogger(logging.Handler):
|
||||
"""Event logger that queues LLMCallEvents for streaming"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.events = asyncio.Queue()
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
if isinstance(record.msg, LLMCallEvent):
|
||||
self.events.put_nowait(LLMCallEventMessage(content=str(record.msg)))
|
||||
|
||||
|
||||
class TeamManager:
|
||||
"""Manages team operations including loading configs and running teams"""
|
||||
|
||||
@ -35,14 +49,7 @@ class TeamManager:
|
||||
|
||||
@staticmethod
|
||||
async def load_from_directory(directory: Union[str, Path]) -> List[dict]:
|
||||
"""Load all team configurations from a directory
|
||||
|
||||
Args:
|
||||
directory (Union[str, Path]): Path to directory containing config files
|
||||
|
||||
Returns:
|
||||
List[dict]: List of loaded team configurations
|
||||
"""
|
||||
"""Load all team configurations from a directory"""
|
||||
directory = Path(directory)
|
||||
configs = []
|
||||
valid_extensions = {".json", ".yaml", ".yml"}
|
||||
@ -61,7 +68,6 @@ class TeamManager:
|
||||
self, team_config: Union[str, Path, dict, ComponentModel], input_func: Optional[Callable] = None
|
||||
) -> Component:
|
||||
"""Create team instance from config"""
|
||||
# Handle different input types
|
||||
if isinstance(team_config, (str, Path)):
|
||||
config = await self.load_from_file(team_config)
|
||||
elif isinstance(team_config, dict):
|
||||
@ -69,14 +75,12 @@ class TeamManager:
|
||||
else:
|
||||
config = team_config.model_dump()
|
||||
|
||||
# Use Component.load_component directly
|
||||
team = Team.load_component(config)
|
||||
|
||||
for agent in team._participants:
|
||||
if hasattr(agent, "input_func"):
|
||||
agent.input_func = input_func
|
||||
|
||||
# TBD - set input function
|
||||
return team
|
||||
|
||||
async def run_stream(
|
||||
@ -85,11 +89,17 @@ class TeamManager:
|
||||
team_config: Union[str, Path, dict, ComponentModel],
|
||||
input_func: Optional[Callable] = None,
|
||||
cancellation_token: Optional[CancellationToken] = None,
|
||||
) -> AsyncGenerator[Union[AgentEvent | ChatMessage, ChatMessage, TaskResult], None]:
|
||||
) -> AsyncGenerator[Union[AgentEvent | ChatMessage | LLMCallEvent, ChatMessage, TeamResult], None]:
|
||||
"""Stream team execution results"""
|
||||
start_time = time.time()
|
||||
team = None
|
||||
|
||||
# Setup logger correctly
|
||||
logger = logging.getLogger(EVENT_LOGGER_NAME)
|
||||
logger.setLevel(logging.INFO)
|
||||
llm_event_logger = RunEventLogger()
|
||||
logger.handlers = [llm_event_logger] # Replace all handlers
|
||||
|
||||
try:
|
||||
team = await self._create_team(team_config, input_func)
|
||||
|
||||
@ -102,7 +112,15 @@ class TeamManager:
|
||||
else:
|
||||
yield message
|
||||
|
||||
# Check for any LLM events
|
||||
while not llm_event_logger.events.empty():
|
||||
event = await llm_event_logger.events.get()
|
||||
yield event
|
||||
|
||||
finally:
|
||||
# Cleanup - remove our handler
|
||||
logger.handlers.remove(llm_event_logger)
|
||||
|
||||
# Ensure cleanup happens
|
||||
if team and hasattr(team, "_participants"):
|
||||
for agent in team._participants:
|
||||
@ -127,7 +145,6 @@ class TeamManager:
|
||||
return TeamResult(task_result=result, usage="", duration=time.time() - start_time)
|
||||
|
||||
finally:
|
||||
# Ensure cleanup happens
|
||||
if team and hasattr(team, "_participants"):
|
||||
for agent in team._participants:
|
||||
if hasattr(agent, "close"):
|
||||
|
||||
@ -15,11 +15,6 @@ from .deps import cleanup_managers, init_managers
|
||||
from .initialization import AppInitializer
|
||||
from .routes import runs, sessions, teams, ws
|
||||
|
||||
# Configure logging
|
||||
# logger = logging.getLogger(__name__)
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
# Initialize application
|
||||
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
||||
initializer = AppInitializer(settings, app_file_path)
|
||||
|
||||
@ -21,7 +21,7 @@ from autogen_core import Image as AGImage
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
from ...database import DatabaseManager
|
||||
from ...datamodel import Message, MessageConfig, Run, RunStatus, TeamResult
|
||||
from ...datamodel import LLMCallEventMessage, Message, MessageConfig, Run, RunStatus, TeamResult
|
||||
from ...teammanager import TeamManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -111,6 +111,7 @@ class WebSocketManager:
|
||||
HandoffMessage,
|
||||
ToolCallRequestEvent,
|
||||
ToolCallExecutionEvent,
|
||||
LLMCallEventMessage,
|
||||
),
|
||||
):
|
||||
await self._save_message(run_id, message)
|
||||
@ -328,7 +329,15 @@ class WebSocketManager:
|
||||
}
|
||||
|
||||
elif isinstance(
|
||||
message, (TextMessage, StopMessage, HandoffMessage, ToolCallRequestEvent, ToolCallExecutionEvent)
|
||||
message,
|
||||
(
|
||||
TextMessage,
|
||||
StopMessage,
|
||||
HandoffMessage,
|
||||
ToolCallRequestEvent,
|
||||
ToolCallExecutionEvent,
|
||||
LLMCallEventMessage,
|
||||
),
|
||||
):
|
||||
return {"type": "message", "data": message.model_dump()}
|
||||
|
||||
|
||||
@ -220,7 +220,7 @@ const Sidebar = ({ link, meta, isMobile }: SidebarProps) => {
|
||||
],
|
||||
})
|
||||
}
|
||||
className="group hidden flex gap-x-3 rounded-md p-2 text-sm font-medium text-primary hover:text-accent hover:bg-secondary justify-center"
|
||||
className="group flex gap-x-3 rounded-md p-2 text-sm font-medium text-primary hover:text-accent hover:bg-secondary justify-center"
|
||||
>
|
||||
<Settings className="h-6 w-6 shrink-0 text-secondary group-hover:text-accent" />
|
||||
</Link>
|
||||
@ -248,7 +248,7 @@ const Sidebar = ({ link, meta, isMobile }: SidebarProps) => {
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full ">
|
||||
<div className="hidden">
|
||||
<div className="">
|
||||
{" "}
|
||||
<Link
|
||||
to="/settings"
|
||||
|
||||
@ -252,7 +252,13 @@ export interface SessionRuns {
|
||||
}
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: "message" | "result" | "completion" | "input_request" | "error";
|
||||
type:
|
||||
| "message"
|
||||
| "result"
|
||||
| "completion"
|
||||
| "input_request"
|
||||
| "error"
|
||||
| "llm_call_event";
|
||||
data?: AgentMessageConfig | TaskResult;
|
||||
status?: RunStatus;
|
||||
error?: string;
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
import React, { memo, useState } from "react";
|
||||
import { Loader2, Maximize2, Minimize2, X } from "lucide-react";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Fullscreen,
|
||||
Loader2,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Tooltip } from "antd";
|
||||
|
||||
export const LoadingIndicator = ({ size = 16 }: { size: number }) => (
|
||||
<div className="inline-flex items-center gap-2 text-accent mr-2">
|
||||
@ -41,6 +52,11 @@ export const LoadingDots = ({ size = 8 }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// import { memo, useState } from 'react';
|
||||
// import ReactMarkdown from 'react-markdown';
|
||||
// import { Minimize2, Maximize2, ArrowsMaximize, X } from 'lucide-react';
|
||||
// import { Tooltip } from 'antd';
|
||||
|
||||
export const TruncatableText = memo(
|
||||
({
|
||||
content,
|
||||
@ -48,14 +64,17 @@ export const TruncatableText = memo(
|
||||
className = "",
|
||||
jsonThreshold = 1000,
|
||||
textThreshold = 500,
|
||||
showFullscreen = true,
|
||||
}: {
|
||||
content: string;
|
||||
isJson?: boolean;
|
||||
className?: string;
|
||||
jsonThreshold?: number;
|
||||
textThreshold?: number;
|
||||
showFullscreen?: boolean;
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const threshold = isJson ? jsonThreshold : textThreshold;
|
||||
const shouldTruncate = content.length > threshold;
|
||||
|
||||
@ -72,7 +91,7 @@ export const TruncatableText = memo(
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`
|
||||
transition-[max-height,opacity] duration-500 ease-in-out
|
||||
transition-[max-height,opacity] overflow-auto scroll duration-500 ease-in-out
|
||||
${
|
||||
shouldTruncate && !isExpanded
|
||||
? "max-h-[300px]"
|
||||
@ -81,32 +100,71 @@ export const TruncatableText = memo(
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{/* {displayContent} */}
|
||||
<ReactMarkdown>{displayContent}</ReactMarkdown>
|
||||
{shouldTruncate && !isExpanded && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-secondary/20 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-secondary to-transparent opacity-20" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldTruncate && (
|
||||
<div className="mt-2 flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-3 py-1.5
|
||||
rounded bg-secondary/80
|
||||
text-xs font-medium
|
||||
transition-all duration-300
|
||||
hover:text-accent
|
||||
hover:scale-105
|
||||
z-10
|
||||
`}
|
||||
aria-label={isExpanded ? "less" : "more"}
|
||||
<div className="mt-2 flex items-center justify-end gap-2">
|
||||
<Tooltip title={isExpanded ? "Show less" : "Show more"}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpand}
|
||||
className="inline-flex items-center justify-center p-2 rounded bg-secondary text-primary hover:text-accent hover:scale-105 transition-all duration-300 z-10"
|
||||
aria-label={isExpanded ? "Show less" : "Show more"}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp size={18} />
|
||||
) : (
|
||||
<ChevronDown size={18} />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{showFullscreen && (
|
||||
<Tooltip title="Fullscreen">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
className="inline-flex items-center justify-center p-2 rounded bg-secondary text-primary hover:text-accent hover:scale-105 transition-all duration-300 z-10"
|
||||
aria-label="Toggle fullscreen"
|
||||
>
|
||||
<Maximize2 size={18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFullscreen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center"
|
||||
onClick={() => setIsFullscreen(false)}
|
||||
>
|
||||
<div
|
||||
className="relative bg-secondary w-full h-full md:w-4/5 md:h-4/5 md:rounded-lg p-8 overflow-auto"
|
||||
style={{ opacity: 0.95 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>{isExpanded ? "Show less" : "Show more"}</span>
|
||||
{isExpanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
||||
</button>
|
||||
<Tooltip title="Close">
|
||||
<button
|
||||
onClick={() => setIsFullscreen(false)}
|
||||
className="absolute top-4 right-4 p-2 rounded-full bg-black/50 hover:bg-black/70 text-primary transition-colors"
|
||||
aria-label="Close fullscreen view"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className="mt-8 text-base text-primary">
|
||||
{isJson ? (
|
||||
<pre className="whitespace-pre-wrap">{content}</pre>
|
||||
) : (
|
||||
<ReactMarkdown>{content}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -828,7 +828,7 @@
|
||||
"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.",
|
||||
"label": "Google Search Tool",
|
||||
"config": {
|
||||
"source_code": "async def google_search(\n query: str,\n num_results: int = 5,\n include_snippets: bool = True,\n include_content: bool = True,\n content_max_length: Optional[int] = 15000,\n language: str = \"en\",\n country: Optional[str] = None,\n safe_search: bool = True,\n) -> List[Dict[str, str]]:\n \"\"\"\n Perform a Google search using the Custom Search API and optionally fetch webpage content.\n\n Args:\n query: Search query string\n num_results: Number of results to return (max 10)\n include_snippets: Include result snippets in output\n include_content: Include full webpage content in markdown format\n content_max_length: Maximum length of webpage content (if included)\n language: Language code for search results (e.g., en, es, fr)\n country: Optional country code for search results (e.g., us, uk)\n safe_search: Enable safe search filtering\n\n Returns:\n List[Dict[str, str]]: List of search results, each containing:\n - title: Result title\n - link: Result URL\n - snippet: Result description (if include_snippets=True)\n - content: Webpage content in markdown (if include_content=True)\n \"\"\"\n api_key = os.getenv(\"GOOGLE_API_KEY\")\n cse_id = os.getenv(\"GOOGLE_CSE_ID\")\n\n if not api_key or not cse_id:\n raise ValueError(\"Missing required environment variables. Please set GOOGLE_API_KEY and GOOGLE_CSE_ID.\")\n\n num_results = min(max(1, num_results), 10)\n\n async def fetch_page_content(url: str, max_length: Optional[int] = 50000) -> str:\n \"\"\"Helper function to fetch and convert webpage content to markdown\"\"\"\n headers = {\"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"}\n\n try:\n async with httpx.AsyncClient() as client:\n response = await client.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n\n soup = BeautifulSoup(response.text, \"html.parser\")\n\n # Remove script and style elements\n for script in soup([\"script\", \"style\"]):\n script.decompose()\n\n # Convert relative URLs to absolute\n for tag in soup.find_all([\"a\", \"img\"]):\n if tag.get(\"href\"):\n tag[\"href\"] = urljoin(url, tag[\"href\"])\n if tag.get(\"src\"):\n tag[\"src\"] = urljoin(url, tag[\"src\"])\n\n h2t = html2text.HTML2Text()\n h2t.body_width = 0\n h2t.ignore_images = False\n h2t.ignore_emphasis = False\n h2t.ignore_links = False\n h2t.ignore_tables = False\n\n markdown = h2t.handle(str(soup))\n\n if max_length and len(markdown) > max_length:\n markdown = markdown[:max_length] + \"\\n...(truncated)\"\n\n return markdown.strip()\n\n except Exception as e:\n return f\"Error fetching content: {str(e)}\"\n\n params = {\n \"key\": api_key,\n \"cx\": cse_id,\n \"q\": query,\n \"num\": num_results,\n \"hl\": language,\n \"safe\": \"active\" if safe_search else \"off\",\n }\n\n if country:\n params[\"gl\"] = country\n\n try:\n async with httpx.AsyncClient() as client:\n response = await client.get(\"https://www.googleapis.com/customsearch/v1\", params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n\n results = []\n if \"items\" in data:\n for item in data[\"items\"]:\n result = {\"title\": item.get(\"title\", \"\"), \"link\": item.get(\"link\", \"\")}\n if include_snippets:\n result[\"snippet\"] = item.get(\"snippet\", \"\")\n\n if include_content:\n result[\"content\"] = await fetch_page_content(result[\"link\"], max_length=content_max_length)\n\n results.append(result)\n\n return results\n\n except httpx.RequestError as e:\n raise ValueError(f\"Failed to perform search: {str(e)}\") from e\n except KeyError as e:\n raise ValueError(f\"Invalid API response format: {str(e)}\") from e\n except Exception as e:\n raise ValueError(f\"Error during search: {str(e)}\") from e\n",
|
||||
"source_code": "async def google_search(\n query: str,\n num_results: int = 3,\n include_snippets: bool = True,\n include_content: bool = True,\n content_max_length: Optional[int] = 15000,\n language: str = \"en\",\n country: Optional[str] = None,\n safe_search: bool = True,\n) -> List[Dict[str, str]]:\n \"\"\"\n Perform a Google search using the Custom Search API and optionally fetch webpage content.\n\n Args:\n query: Search query string\n num_results: Number of results to return (max 10)\n include_snippets: Include result snippets in output\n include_content: Include full webpage content in markdown format\n content_max_length: Maximum length of webpage content (if included)\n language: Language code for search results (e.g., en, es, fr)\n country: Optional country code for search results (e.g., us, uk)\n safe_search: Enable safe search filtering\n\n Returns:\n List[Dict[str, str]]: List of search results, each containing:\n - title: Result title\n - link: Result URL\n - snippet: Result description (if include_snippets=True)\n - content: Webpage content in markdown (if include_content=True)\n \"\"\"\n api_key = os.getenv(\"GOOGLE_API_KEY\")\n cse_id = os.getenv(\"GOOGLE_CSE_ID\")\n\n if not api_key or not cse_id:\n raise ValueError(\"Missing required environment variables. Please set GOOGLE_API_KEY and GOOGLE_CSE_ID.\")\n\n num_results = min(max(1, num_results), 10)\n\n async def fetch_page_content(url: str, max_length: Optional[int] = 50000) -> str:\n \"\"\"Helper function to fetch and convert webpage content to markdown\"\"\"\n headers = {\"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"}\n\n try:\n async with httpx.AsyncClient() as client:\n response = await client.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n\n soup = BeautifulSoup(response.text, \"html.parser\")\n\n # Remove script and style elements\n for script in soup([\"script\", \"style\"]):\n script.decompose()\n\n # Convert relative URLs to absolute\n for tag in soup.find_all([\"a\", \"img\"]):\n if tag.get(\"href\"):\n tag[\"href\"] = urljoin(url, tag[\"href\"])\n if tag.get(\"src\"):\n tag[\"src\"] = urljoin(url, tag[\"src\"])\n\n h2t = html2text.HTML2Text()\n h2t.body_width = 0\n h2t.ignore_images = False\n h2t.ignore_emphasis = False\n h2t.ignore_links = False\n h2t.ignore_tables = False\n\n markdown = h2t.handle(str(soup))\n\n if max_length and len(markdown) > max_length:\n markdown = markdown[:max_length] + \"\\n...(truncated)\"\n\n return markdown.strip()\n\n except Exception as e:\n return f\"Error fetching content: {str(e)}\"\n\n params = {\n \"key\": api_key,\n \"cx\": cse_id,\n \"q\": query,\n \"num\": num_results,\n \"hl\": language,\n \"safe\": \"active\" if safe_search else \"off\",\n }\n\n if country:\n params[\"gl\"] = country\n\n try:\n async with httpx.AsyncClient() as client:\n response = await client.get(\"https://www.googleapis.com/customsearch/v1\", params=params, timeout=10)\n response.raise_for_status()\n data = response.json()\n\n results = []\n if \"items\" in data:\n for item in data[\"items\"]:\n result = {\"title\": item.get(\"title\", \"\"), \"link\": item.get(\"link\", \"\")}\n if include_snippets:\n result[\"snippet\"] = item.get(\"snippet\", \"\")\n\n if include_content:\n result[\"content\"] = await fetch_page_content(result[\"link\"], max_length=content_max_length)\n\n results.append(result)\n\n return results\n\n except httpx.RequestError as e:\n raise ValueError(f\"Failed to perform search: {str(e)}\") from e\n except KeyError as e:\n raise ValueError(f\"Invalid API response format: {str(e)}\") from e\n except Exception as e:\n raise ValueError(f\"Error during search: {str(e)}\") from e\n",
|
||||
"name": "google_search",
|
||||
"description": "\n Perform Google searches using the Custom Search API with optional webpage content fetching.\n Requires GOOGLE_API_KEY and GOOGLE_CSE_ID environment variables to be set.\n ",
|
||||
"global_imports": [
|
||||
|
||||
@ -151,7 +151,7 @@ export const useGalleryStore = create<GalleryStore>()(
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "gallery-storage-v3",
|
||||
name: "gallery-storage-v4",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -195,23 +195,6 @@ const createNode = (
|
||||
};
|
||||
}
|
||||
|
||||
// if (type === "task") {
|
||||
// return {
|
||||
// id,
|
||||
// type: "agentNode",
|
||||
// position: { x: 0, y: 0 },
|
||||
// data: {
|
||||
// type: "task",
|
||||
// label: "Task",
|
||||
// description: run?.task.content || "",
|
||||
// isActive: false,
|
||||
// status: null,
|
||||
// reason: null,
|
||||
// draggable: false,
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
return {
|
||||
id,
|
||||
type: "agentNode",
|
||||
@ -567,7 +550,7 @@ const AgentFlow: React.FC<AgentFlowProps> = ({ teamConfig, run }) => {
|
||||
<ReactFlow {...reactFlowProps}>
|
||||
{settings.showGrid && <Background />}
|
||||
{settings.showMiniMap && <MiniMap />}
|
||||
<div className="absolute top-0 right-0 z-50">
|
||||
<div className="absolute top-0 right-0 z-10">
|
||||
<AgentFlowToolbar {...toolbarProps} />
|
||||
</div>
|
||||
</ReactFlow>
|
||||
@ -0,0 +1,187 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Terminal, Maximize2, X } from "lucide-react";
|
||||
import { TruncatableText } from "../../atoms";
|
||||
import { Tooltip } from "antd";
|
||||
|
||||
interface LLMLogEvent {
|
||||
type: "LLMCall";
|
||||
messages: {
|
||||
content: string;
|
||||
role: string;
|
||||
name?: string;
|
||||
}[];
|
||||
response: {
|
||||
id: string;
|
||||
choices: {
|
||||
message: {
|
||||
content: string;
|
||||
role: string;
|
||||
};
|
||||
}[];
|
||||
usage: {
|
||||
completion_tokens: number;
|
||||
prompt_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
model: string;
|
||||
};
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
agent_id: string;
|
||||
}
|
||||
|
||||
interface LLMLogRendererProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const formatTokens = (tokens: number) => {
|
||||
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : tokens;
|
||||
};
|
||||
|
||||
const FullLogView = ({
|
||||
event,
|
||||
onClose,
|
||||
}: {
|
||||
event: LLMLogEvent;
|
||||
onClose: () => void;
|
||||
}) => (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center transition-opacity duration-300"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="relative bg-primary w-full h-full md:w-4/5 md:h-4/5 md:rounded-lg p-8 overflow-auto"
|
||||
style={{ opacity: 0.95 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Tooltip title="Close">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-2 rounded-full bg-black/50 hover:bg-black/70 text-primary transition-colors"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Terminal size={20} className="text-accent" />
|
||||
<h3 className="text-lg font-medium">LLM Call Details</h3>
|
||||
<h4 className="text-sm text-secondary">
|
||||
{event.agent_id.split("/")[0]} • {event.response.model} •{" "}
|
||||
{formatTokens(event.response.usage.total_tokens)} tokens
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Messages</h4>
|
||||
{event.messages.map((msg, idx) => (
|
||||
<div key={idx} className="p-4 bg-tertiary rounded-lg">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-xs font-medium uppercase text-secondary">
|
||||
{msg.role} {msg.name && `(${msg.name})`}
|
||||
</span>
|
||||
</div>
|
||||
<TruncatableText
|
||||
content={msg.content}
|
||||
textThreshold={1000}
|
||||
showFullscreen={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Response</h4>
|
||||
<div className="p-4 bg-tertiary rounded-lg">
|
||||
<TruncatableText
|
||||
content={event.response.choices[0]?.message.content}
|
||||
textThreshold={1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||
<div className="p-3 bg-tertiary rounded-lg">
|
||||
<div className="text-xs text-secondary mb-1">Model</div>
|
||||
<div className="font-medium">{event.response.model}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-tertiary rounded-lg">
|
||||
<div className="text-xs text-secondary mb-1">Prompt Tokens</div>
|
||||
<div className="font-medium">
|
||||
{event.response.usage.prompt_tokens}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-tertiary rounded-lg">
|
||||
<div className="text-xs text-secondary mb-1">Completion Tokens</div>
|
||||
<div className="font-medium">
|
||||
{event.response.usage.completion_tokens}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-tertiary rounded-lg">
|
||||
<div className="text-xs text-secondary mb-1">Total Tokens</div>
|
||||
<div className="font-medium">
|
||||
{event.response.usage.total_tokens}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LLMLogRenderer: React.FC<LLMLogRendererProps> = ({ content }) => {
|
||||
const [showFullLog, setShowFullLog] = useState(false);
|
||||
|
||||
const parsedContent = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(content) as LLMLogEvent;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse LLM log content:", e);
|
||||
return null;
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
if (!parsedContent) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-red-500 p-2 bg-red-500/10 rounded">
|
||||
<Terminal size={16} />
|
||||
<span>Invalid log format</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { messages, response, agent_id } = parsedContent;
|
||||
const agentName = messages[0]?.name || "Agent";
|
||||
const totalTokens = response.usage.total_tokens;
|
||||
const shortAgentId = agent_id ? `${agent_id.split("/")[0]}` : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 py-2 bg-secondary/20 rounded-lg text-sm text-secondary hover:text-primary transition-colors group">
|
||||
<Terminal size={14} className="text-accent" />
|
||||
<span className="flex-1">
|
||||
{shortAgentId ? `${shortAgentId}` : ""} • {response.model} •{" "}
|
||||
{formatTokens(totalTokens)} tokens
|
||||
</span>
|
||||
<Tooltip title="View details">
|
||||
<button
|
||||
onClick={() => setShowFullLog(true)}
|
||||
className="p-1 mr-1 hover:bg-secondary rounded-md transition-colors"
|
||||
>
|
||||
<Maximize2 size={14} className="group-hover:text-accent" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{showFullLog && (
|
||||
<FullLogView
|
||||
event={parsedContent}
|
||||
onClose={() => setShowFullLog(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LLMLogRenderer;
|
||||
@ -1,5 +1,12 @@
|
||||
import React, { useState, memo } from "react";
|
||||
import { User, Bot, Maximize2, Minimize2, DraftingCompass } from "lucide-react";
|
||||
import {
|
||||
User,
|
||||
Bot,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
DraftingCompass,
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AgentMessageConfig,
|
||||
FunctionCall,
|
||||
@ -7,6 +14,7 @@ import {
|
||||
ImageContent,
|
||||
} from "../../../types/datamodel";
|
||||
import { ClickableImage, TruncatableText } from "../../atoms";
|
||||
import LLMLogRenderer from "./logrenderer";
|
||||
|
||||
const TEXT_THRESHOLD = 400;
|
||||
const JSON_THRESHOLD = 800;
|
||||
@ -143,9 +151,14 @@ export const RenderMessage: React.FC<MessageProps> = ({
|
||||
if (!message) return null;
|
||||
const isUser = messageUtils.isUser(message.source);
|
||||
const content = message.content;
|
||||
const isLLMEventMessage = message.source === "llm_call_event";
|
||||
|
||||
return (
|
||||
<div className={`relative group ${!isLast ? "mb-2" : ""} ${className}`}>
|
||||
<div
|
||||
className={`relative group ${!isLast ? "mb-2" : ""} ${className} ${
|
||||
isLLMEventMessage ? "border-accent" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-start gap-2 p-2 rounded
|
||||
@ -160,7 +173,13 @@ export const RenderMessage: React.FC<MessageProps> = ({
|
||||
${isUser ? "text-accent" : "text-primary"}
|
||||
`}
|
||||
>
|
||||
{isUser ? <User size={14} /> : <Bot size={14} />}
|
||||
{isUser ? (
|
||||
<User size={14} />
|
||||
) : message.source == "llm_call_event" ? (
|
||||
<Brain size={14} />
|
||||
) : (
|
||||
<Bot size={14} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
@ -177,6 +196,8 @@ export const RenderMessage: React.FC<MessageProps> = ({
|
||||
<RenderMultiModal content={content} />
|
||||
) : messageUtils.isFunctionExecutionResult(content) ? (
|
||||
<RenderToolResult content={content} />
|
||||
) : message.source === "llm_call_event" ? (
|
||||
<LLMLogRenderer content={String(content)} />
|
||||
) : (
|
||||
<TruncatableText
|
||||
content={String(content)}
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import {
|
||||
StopCircle,
|
||||
MessageSquare,
|
||||
@ -6,12 +6,10 @@ import {
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
TriangleAlertIcon,
|
||||
GroupIcon,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Bot,
|
||||
PanelRightClose,
|
||||
PanelLeftOpen,
|
||||
PanelRightOpen,
|
||||
} from "lucide-react";
|
||||
import { Run, Message, TeamConfig, Component } from "../../../types/datamodel";
|
||||
@ -19,11 +17,8 @@ import AgentFlow from "./agentflow/agentflow";
|
||||
import { RenderMessage } from "./rendermessage";
|
||||
import InputRequestView from "./inputrequest";
|
||||
import { Tooltip } from "antd";
|
||||
import {
|
||||
getRelativeTimeString,
|
||||
LoadingDots,
|
||||
TruncatableText,
|
||||
} from "../../atoms";
|
||||
import { getRelativeTimeString, LoadingDots } from "../../atoms";
|
||||
import { useSettingsStore } from "../../settings/store";
|
||||
|
||||
interface RunViewProps {
|
||||
run: Run;
|
||||
@ -33,6 +28,23 @@ interface RunViewProps {
|
||||
isFirstRun?: boolean;
|
||||
}
|
||||
|
||||
export const getAgentMessages = (messages: Message[]): Message[] => {
|
||||
return messages.filter((msg) => msg.config.source !== "llm_call_event");
|
||||
};
|
||||
|
||||
export const getLastMeaningfulMessage = (
|
||||
messages: Message[]
|
||||
): Message | undefined => {
|
||||
return messages
|
||||
.filter((msg) => msg.config.source !== "llm_call_event")
|
||||
.slice(-1)[0];
|
||||
};
|
||||
|
||||
// Type guard for message arrays
|
||||
export const isAgentMessage = (message: Message): boolean => {
|
||||
return message.config.source !== "llm_call_event";
|
||||
};
|
||||
|
||||
const RunView: React.FC<RunViewProps> = ({
|
||||
run,
|
||||
onInputResponse,
|
||||
@ -45,6 +57,18 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
const isActive = run.status === "active" || run.status === "awaiting_input";
|
||||
const [isFlowVisible, setIsFlowVisible] = useState(true);
|
||||
|
||||
const showLLMEvents = useSettingsStore(
|
||||
(state) => state.playground.showLLMEvents
|
||||
);
|
||||
console.log("showLLMEvents", showLLMEvents);
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
if (showLLMEvents) {
|
||||
return run.messages;
|
||||
}
|
||||
return run.messages.filter((msg) => msg.config.source !== "llm_call_event");
|
||||
}, [run.messages, showLLMEvents]);
|
||||
|
||||
// Replace existing scroll effect with this simpler one
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
@ -56,7 +80,7 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
}
|
||||
}, 450);
|
||||
}, [run.messages]); // Only depend on messages changing
|
||||
|
||||
// console.log("run", run);
|
||||
const calculateThreadTokens = (messages: Message[]) => {
|
||||
// console.log("messages", messages);
|
||||
return messages.reduce((total, msg) => {
|
||||
@ -123,7 +147,7 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
};
|
||||
|
||||
const lastResultMessage = run.team_result?.task_result.messages.slice(-1)[0];
|
||||
const lastMessage = run.messages.slice(-1)[0];
|
||||
const lastMessage = getLastMeaningfulMessage(visibleMessages);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 mr-2 ">
|
||||
@ -202,19 +226,7 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
</div>
|
||||
|
||||
{lastMessage ? (
|
||||
// <TruncatableText
|
||||
// key={"_" + run.id}
|
||||
// textThreshold={700}
|
||||
// content={
|
||||
// run.messages[run.messages.length - 1]?.config?.content +
|
||||
// ""
|
||||
// }
|
||||
// className="break-all"
|
||||
// />
|
||||
<RenderMessage
|
||||
message={run.messages[run.messages.length - 1]?.config}
|
||||
isLast={true}
|
||||
/>
|
||||
<RenderMessage message={lastMessage.config} isLast={true} />
|
||||
) : (
|
||||
<>
|
||||
{lastResultMessage && (
|
||||
@ -228,7 +240,7 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
|
||||
{/* Thread Section */}
|
||||
<div className="">
|
||||
{run.messages.length > 0 && (
|
||||
{visibleMessages.length > 0 && (
|
||||
<div className="mt-2 pl-4 border-secondary rounded-b border-l-2 border-secondary/30">
|
||||
<div className="flex pt-2">
|
||||
<div className="flex-1">
|
||||
@ -262,8 +274,8 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-secondary">
|
||||
{calculateThreadTokens(run.messages)} tokens |{" "}
|
||||
{run.messages.length} messages
|
||||
{calculateThreadTokens(visibleMessages)} tokens |{" "}
|
||||
{visibleMessages.length} messages
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -290,14 +302,14 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
{" "}
|
||||
<span className=" inline-block h-6"></span>{" "}
|
||||
</div>
|
||||
{run.messages.map((msg, idx) => (
|
||||
{visibleMessages.map((msg, idx) => (
|
||||
<div
|
||||
key={"message_id" + idx + run.id}
|
||||
className=" mr-2"
|
||||
>
|
||||
<RenderMessage
|
||||
message={msg.config}
|
||||
isLast={idx === run.messages.length - 1}
|
||||
isLast={idx === visibleMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -322,7 +334,7 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
{/* Agent Flow Visualization */}
|
||||
{isFlowVisible && (
|
||||
<div className="bg-tertiary flex-1 rounded mt-2 relative">
|
||||
<div className="z-50 absolute left-2 top-2 p-2 hover:opacity-100 opacity-80">
|
||||
<div className="z-10 absolute left-2 top-2 p-2 hover:opacity-100 opacity-80">
|
||||
<Tooltip title="Hide message flow">
|
||||
<button
|
||||
onClick={() => setIsFlowVisible(false)}
|
||||
@ -333,7 +345,13 @@ const RunView: React.FC<RunViewProps> = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
{teamConfig && (
|
||||
<AgentFlow teamConfig={teamConfig} run={run} />
|
||||
<AgentFlow
|
||||
teamConfig={teamConfig}
|
||||
run={{
|
||||
...run,
|
||||
messages: getAgentMessages(visibleMessages),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -0,0 +1,163 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ChevronRight, RotateCcw } 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";
|
||||
|
||||
interface SettingToggleProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
const SettingToggle: React.FC<SettingToggleProps> = ({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
}) => (
|
||||
<div className="flex justify-between items-start p-4 hover:bg-secondary/5 rounded-lg transition-colors">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-medium">{label}</label>
|
||||
{description && (
|
||||
<span className="text-sm text-secondary">{description}</span>
|
||||
)}
|
||||
</div>
|
||||
<Switch defaultValue={checked} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const SectionHeader: React.FC<SectionHeaderProps> = ({
|
||||
title,
|
||||
icon: Icon,
|
||||
onReset,
|
||||
}) => (
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-accent" size={20} />
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
</div>
|
||||
<Tooltip title="Reset section settings">
|
||||
<Button
|
||||
icon={<RotateCcw className="w-4 h-4" />}
|
||||
onClick={onReset}
|
||||
type="text"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SettingsManager: React.FC = () => {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = localStorage.getItem("settingsSidebar");
|
||||
return stored !== null ? JSON.parse(stored) : true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const {
|
||||
playground,
|
||||
updatePlaygroundSettings,
|
||||
resetPlaygroundSettings,
|
||||
resetAllSettings,
|
||||
} = useSettingsStore();
|
||||
|
||||
const sections: SettingsSection[] = [
|
||||
{
|
||||
id: "playground",
|
||||
title: "Playground",
|
||||
icon: MessagesSquare,
|
||||
content: () => (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Playground"
|
||||
icon={MessagesSquare}
|
||||
onReset={resetPlaygroundSettings}
|
||||
/>
|
||||
<div className="space-y-2 rounded-xl border border-secondary">
|
||||
<SettingToggle
|
||||
checked={playground.showLLMEvents}
|
||||
onChange={(checked) =>
|
||||
updatePlaygroundSettings({ showLLMEvents: checked })
|
||||
}
|
||||
label={"Show LLM Events"}
|
||||
description="Display detailed LLM call logs in the message thread"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const [currentSection, setCurrentSection] = useState<SettingsSection>(
|
||||
sections[0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("settingsSidebar", JSON.stringify(isSidebarOpen));
|
||||
}
|
||||
}, [isSidebarOpen]);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full">
|
||||
<div
|
||||
className={`absolute left-0 top-0 h-full transition-all duration-200 ease-in-out ${
|
||||
isSidebarOpen ? "w-64" : "w-12"
|
||||
}`}
|
||||
>
|
||||
<SettingsSidebar
|
||||
isOpen={isSidebarOpen}
|
||||
sections={sections}
|
||||
currentSection={currentSection}
|
||||
onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
onSelectSection={setCurrentSection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex-1 transition-all max-w-5xl -mr-6 duration-200 ${
|
||||
isSidebarOpen ? "ml-64" : "ml-12"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4 pt-2">
|
||||
<div className="flex items-center gap-2 mb-4 text-sm">
|
||||
<span className="text-primary font-medium">Settings</span>
|
||||
<ChevronRight className="w-4 h-4 text-secondary" />
|
||||
<span className="text-secondary">{currentSection.title}</span>
|
||||
</div>
|
||||
|
||||
<currentSection.content />
|
||||
|
||||
<div className="mt-12 pt-6 border-t border-secondary flex justify-between items-center">
|
||||
<p className="text-xs text-secondary">
|
||||
Settings are automatically saved and synced across browser
|
||||
sessions
|
||||
</p>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<RotateCcw className="w-4 h-4 mr-1" />}
|
||||
onClick={resetAllSettings}
|
||||
>
|
||||
Reset All Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsManager;
|
||||
@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
import { SettingsSection } from "./types";
|
||||
|
||||
interface SettingsSidebarProps {
|
||||
isOpen: boolean;
|
||||
sections: SettingsSection[];
|
||||
currentSection: SettingsSection;
|
||||
onToggle: () => void;
|
||||
onSelectSection: (section: SettingsSection) => void;
|
||||
}
|
||||
|
||||
export const SettingsSidebar: React.FC<SettingsSidebarProps> = ({
|
||||
isOpen,
|
||||
sections,
|
||||
currentSection,
|
||||
onToggle,
|
||||
onSelectSection,
|
||||
}) => {
|
||||
// Render collapsed state
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<div className="h-full border-r border-secondary">
|
||||
<div className="p-2 -ml-2">
|
||||
<Tooltip title={`Settings (${sections.length})`}>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-2 rounded-md hover:bg-secondary hover:text-accent text-secondary transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50"
|
||||
>
|
||||
<PanelLeftOpen strokeWidth={1.5} className="h-6 w-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full border-r border-secondary">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pt-0 p-4 pl-2 pr-2 border-b border-secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary font-medium">Settings</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded">
|
||||
{sections.length}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip title="Close Sidebar">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-2 rounded-md hover:bg-secondary hover:text-accent text-secondary transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50"
|
||||
>
|
||||
<PanelLeftClose strokeWidth={1.5} className="h-6 w-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto h-[calc(100%-64px)]">
|
||||
{sections.map((section) => (
|
||||
<div key={section.id} className="relative">
|
||||
<div
|
||||
className={`absolute top-1 left-0.5 z-50 h-[calc(100%-8px)] w-1 bg-opacity-80 rounded
|
||||
${
|
||||
currentSection.id === section.id ? "bg-accent" : "bg-tertiary"
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`group ml-1 flex flex-col p-3 rounded-l cursor-pointer hover:bg-secondary
|
||||
${
|
||||
currentSection.id === section.id
|
||||
? "border-accent bg-secondary"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
onClick={() => onSelectSection(section)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<section.icon className="w-4 h-4" />
|
||||
<span className="text-sm">{section.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,121 @@
|
||||
//settings/store.tsx
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface PlaygroundSettings {
|
||||
showLLMEvents: boolean;
|
||||
// Future playground settings
|
||||
expandedMessagesByDefault?: boolean;
|
||||
showAgentFlowByDefault?: boolean;
|
||||
}
|
||||
|
||||
interface TeamBuilderSettings {
|
||||
// Future teambuilder settings
|
||||
showAdvancedOptions?: boolean;
|
||||
defaultAgentLayout?: "grid" | "list";
|
||||
}
|
||||
|
||||
interface GallerySettings {
|
||||
// Future gallery settings
|
||||
viewMode?: "grid" | "list";
|
||||
sortBy?: "date" | "popularity";
|
||||
}
|
||||
|
||||
interface SettingsState {
|
||||
playground: PlaygroundSettings;
|
||||
teamBuilder: TeamBuilderSettings;
|
||||
gallery: GallerySettings;
|
||||
// Actions to update settings
|
||||
updatePlaygroundSettings: (settings: Partial<PlaygroundSettings>) => void;
|
||||
updateTeamBuilderSettings: (settings: Partial<TeamBuilderSettings>) => void;
|
||||
updateGallerySettings: (settings: Partial<GallerySettings>) => void;
|
||||
// Reset functions
|
||||
resetPlaygroundSettings: () => void;
|
||||
resetTeamBuilderSettings: () => void;
|
||||
resetGallerySettings: () => void;
|
||||
resetAllSettings: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PLAYGROUND_SETTINGS: PlaygroundSettings = {
|
||||
showLLMEvents: true, // Default to hiding LLM events
|
||||
};
|
||||
|
||||
const DEFAULT_TEAMBUILDER_SETTINGS: TeamBuilderSettings = {
|
||||
showAdvancedOptions: false,
|
||||
defaultAgentLayout: "grid",
|
||||
};
|
||||
|
||||
const DEFAULT_GALLERY_SETTINGS: GallerySettings = {
|
||||
viewMode: "grid",
|
||||
sortBy: "date",
|
||||
};
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
// Initial state
|
||||
playground: DEFAULT_PLAYGROUND_SETTINGS,
|
||||
teamBuilder: DEFAULT_TEAMBUILDER_SETTINGS,
|
||||
gallery: DEFAULT_GALLERY_SETTINGS,
|
||||
|
||||
// Update functions
|
||||
updatePlaygroundSettings: (settings) =>
|
||||
set((state) => ({
|
||||
playground: { ...state.playground, ...settings },
|
||||
})),
|
||||
|
||||
updateTeamBuilderSettings: (settings) =>
|
||||
set((state) => ({
|
||||
teamBuilder: { ...state.teamBuilder, ...settings },
|
||||
})),
|
||||
|
||||
updateGallerySettings: (settings) =>
|
||||
set((state) => ({
|
||||
gallery: { ...state.gallery, ...settings },
|
||||
})),
|
||||
|
||||
// Reset functions
|
||||
resetPlaygroundSettings: () =>
|
||||
set((state) => ({
|
||||
playground: DEFAULT_PLAYGROUND_SETTINGS,
|
||||
})),
|
||||
|
||||
resetTeamBuilderSettings: () =>
|
||||
set((state) => ({
|
||||
teamBuilder: DEFAULT_TEAMBUILDER_SETTINGS,
|
||||
})),
|
||||
|
||||
resetGallerySettings: () =>
|
||||
set((state) => ({
|
||||
gallery: DEFAULT_GALLERY_SETTINGS,
|
||||
})),
|
||||
|
||||
resetAllSettings: () =>
|
||||
set({
|
||||
playground: DEFAULT_PLAYGROUND_SETTINGS,
|
||||
teamBuilder: DEFAULT_TEAMBUILDER_SETTINGS,
|
||||
gallery: DEFAULT_GALLERY_SETTINGS,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "ags-app-settings",
|
||||
partialize: (state) => ({
|
||||
playground: state.playground,
|
||||
teamBuilder: state.teamBuilder,
|
||||
gallery: state.gallery,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Example usage:
|
||||
/*
|
||||
import { useSettingsStore } from './stores/settings';
|
||||
|
||||
// In a component:
|
||||
const { showLLMEvents } = useSettingsStore((state) => state.playground);
|
||||
const updatePlaygroundSettings = useSettingsStore((state) => state.updatePlaygroundSettings);
|
||||
|
||||
// Toggle LLM events
|
||||
updatePlaygroundSettings({ showLLMEvents: !showLLMEvents });
|
||||
*/
|
||||
@ -0,0 +1,8 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
export interface SettingsSection {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
content: () => JSX.Element;
|
||||
}
|
||||
@ -41,7 +41,7 @@ import { MonacoEditor } from "../../monaco";
|
||||
import { NodeEditor } from "./node-editor/node-editor";
|
||||
import debounce from "lodash.debounce";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { sessionAPI } from "../../session/api";
|
||||
import { sessionAPI } from "../../playground/api";
|
||||
import TestDrawer from "./testdrawer";
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
@ -179,7 +179,6 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
console.log("Saving team configuration", component);
|
||||
const teamData: Partial<Team> = team
|
||||
? {
|
||||
...team,
|
||||
@ -288,7 +287,7 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
||||
};
|
||||
|
||||
const handleTestDrawerClose = () => {
|
||||
console.log("TestDrawer closed");
|
||||
// console.log("TestDrawer closed");
|
||||
setTestDrawerVisible(false);
|
||||
};
|
||||
|
||||
|
||||
@ -2,9 +2,9 @@ import React, { useContext, useEffect, useState } from "react";
|
||||
|
||||
import { Drawer, Button, message, Checkbox } from "antd";
|
||||
import { Team, Session } from "../../../types/datamodel";
|
||||
import ChatView from "../../session/chat/chat";
|
||||
import ChatView from "../../playground/chat/chat";
|
||||
import { appContext } from "../../../../hooks/provider";
|
||||
import { sessionAPI } from "../../session/api";
|
||||
import { sessionAPI } from "../../playground/api";
|
||||
|
||||
interface TestDrawerProps {
|
||||
isVisble: boolean;
|
||||
|
||||
@ -3,11 +3,9 @@ import { Button, Tooltip } from "antd";
|
||||
import {
|
||||
Bot,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Calendar,
|
||||
Copy,
|
||||
GalleryHorizontalEnd,
|
||||
InfoIcon,
|
||||
@ -15,7 +13,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { Team } from "../../types/datamodel";
|
||||
import { getRelativeTimeString } from "../atoms";
|
||||
import { defaultTeam } from "./types";
|
||||
import { useGalleryStore } from "../gallery/store";
|
||||
|
||||
interface TeamSidebarProps {
|
||||
@ -43,8 +40,12 @@ export const TeamSidebar: React.FC<TeamSidebarProps> = ({
|
||||
}) => {
|
||||
const defaultGallery = useGalleryStore((state) => state.getDefaultGallery());
|
||||
const createTeam = () => {
|
||||
const newTeam = Object.assign({}, defaultTeam);
|
||||
newTeam.component.label = "new_team_" + new Date().getTime();
|
||||
const newTeam = Object.assign(
|
||||
{},
|
||||
{ component: defaultGallery?.items.teams[0] }
|
||||
);
|
||||
newTeam.component.label =
|
||||
"default_team" + new Date().getTime().toString().slice(0, 2);
|
||||
onCreateTeam(newTeam);
|
||||
};
|
||||
|
||||
|
||||
@ -15,85 +15,3 @@ export interface TeamListProps {
|
||||
onDelete: (teamId: number) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const defaultTeamConfig: Component<TeamConfig> = {
|
||||
provider: "autogen_agentchat.teams.RoundRobinGroupChat",
|
||||
component_type: "team",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
description:
|
||||
"A team of agents that chat with users in a round-robin fashion.",
|
||||
label: "General Team",
|
||||
config: {
|
||||
participants: [
|
||||
{
|
||||
provider: "autogen_agentchat.agents.AssistantAgent",
|
||||
component_type: "agent",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
config: {
|
||||
name: "weather_agent",
|
||||
model_client: {
|
||||
provider: "autogen_ext.models.openai.OpenAIChatCompletionClient",
|
||||
component_type: "model",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
config: { model: "gpt-4o-mini" },
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
provider: "autogen_core.tools.FunctionTool",
|
||||
component_type: "tool",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
config: {
|
||||
source_code:
|
||||
'async def get_weather(city: str) -> str:\n return f"The weather in {city} is 73 degrees and Sunny."\n',
|
||||
name: "get_weather",
|
||||
description: "",
|
||||
global_imports: [],
|
||||
has_cancellation_support: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
handoffs: [],
|
||||
description:
|
||||
"An agent that provides assistance with ability to use tools.",
|
||||
system_message:
|
||||
"You are a helpful AI assistant. Solve tasks using your tools. Reply with TERMINATE when the task has been completed.",
|
||||
reflect_on_tool_use: false,
|
||||
tool_call_summary_format: "{result}",
|
||||
},
|
||||
},
|
||||
],
|
||||
termination_condition: {
|
||||
provider: "autogen_agentchat.base.OrTerminationCondition",
|
||||
component_type: "termination",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
config: {
|
||||
conditions: [
|
||||
{
|
||||
provider: "autogen_agentchat.conditions.MaxMessageTermination",
|
||||
component_type: "termination",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
config: { max_messages: 10 },
|
||||
},
|
||||
{
|
||||
provider: "autogen_agentchat.conditions.TextMentionTermination",
|
||||
component_type: "termination",
|
||||
version: 1,
|
||||
component_version: 1,
|
||||
config: { text: "TERMINATE" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
max_turns: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultTeam: Team = {
|
||||
component: defaultTeamConfig,
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import Layout from "../components/layout";
|
||||
import { graphql } from "gatsby";
|
||||
import ChatView from "../components/views/session/chat/chat";
|
||||
import { SessionManager } from "../components/views/session/manager";
|
||||
import ChatView from "../components/views/playground/chat/chat";
|
||||
import { SessionManager } from "../components/views/playground/manager";
|
||||
|
||||
// markup
|
||||
const IndexPage = ({ data }: any) => {
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
import * as React from "react";
|
||||
import Layout from "../components/layout";
|
||||
import { graphql } from "gatsby";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import { SettingsManager } from "../components/views/settings/manager";
|
||||
|
||||
// markup
|
||||
const SettingsPage = ({ data }: any) => {
|
||||
return (
|
||||
<Layout meta={data.site.siteMetadata} title="Home" link={"/build"}>
|
||||
<Layout meta={data.site.siteMetadata} title="Home" link={"/settings"}>
|
||||
<main style={{ height: "100%" }} className=" h-full ">
|
||||
<div className="mb-2"> Settings</div>
|
||||
<div className="p-2 mt-4 bg-tertiary rounded ">
|
||||
<TriangleAlertIcon
|
||||
className="w-5 h-5 text-primary inline-block -mt-1"
|
||||
strokeWidth={1.5}
|
||||
/>{" "}
|
||||
Work in progress ..
|
||||
</div>
|
||||
<SettingsManager />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user