mirror of
https://github.com/microsoft/autogen.git
synced 2025-12-02 10:00:03 +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,
|
GalleryComponents,
|
||||||
GalleryItems,
|
GalleryItems,
|
||||||
GalleryMetadata,
|
GalleryMetadata,
|
||||||
|
LLMCallEventMessage,
|
||||||
MessageConfig,
|
MessageConfig,
|
||||||
MessageMeta,
|
MessageMeta,
|
||||||
Response,
|
Response,
|
||||||
@ -22,4 +23,5 @@ __all__ = [
|
|||||||
"TeamResult",
|
"TeamResult",
|
||||||
"Response",
|
"Response",
|
||||||
"SocketMessage",
|
"SocketMessage",
|
||||||
|
"LLMCallEventMessage",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from datetime import datetime
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from autogen_agentchat.base import TaskResult
|
from autogen_agentchat.base import TaskResult
|
||||||
|
from autogen_agentchat.messages import BaseChatMessage
|
||||||
from autogen_core import ComponentModel
|
from autogen_core import ComponentModel
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@ -18,6 +19,11 @@ class TeamResult(BaseModel):
|
|||||||
duration: float
|
duration: float
|
||||||
|
|
||||||
|
|
||||||
|
class LLMCallEventMessage(BaseChatMessage):
|
||||||
|
source: str = "llm_call_event"
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
class MessageMeta(BaseModel):
|
class MessageMeta(BaseModel):
|
||||||
task: Optional[str] = None
|
task: Optional[str] = None
|
||||||
task_result: Optional[TaskResult] = 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(
|
builder.add_team(
|
||||||
websurfer_team.dump_component(),
|
websurfer_team.dump_component(),
|
||||||
label="Web Agent Team (Operator)",
|
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(
|
builder.add_tool(
|
||||||
@ -347,7 +347,7 @@ Read the above conversation. Then select the next role from {participants} to pl
|
|||||||
builder.add_team(
|
builder.add_team(
|
||||||
deep_research_team.dump_component(),
|
deep_research_team.dump_component(),
|
||||||
label="Deep Research Team",
|
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()
|
return builder.build()
|
||||||
|
|||||||
@ -12,10 +12,10 @@ from bs4 import BeautifulSoup
|
|||||||
|
|
||||||
async def bing_search(
|
async def bing_search(
|
||||||
query: str,
|
query: str,
|
||||||
num_results: int = 5,
|
num_results: int = 3,
|
||||||
include_snippets: bool = True,
|
include_snippets: bool = True,
|
||||||
include_content: bool = True,
|
include_content: bool = True,
|
||||||
content_max_length: Optional[int] = 15000,
|
content_max_length: Optional[int] = 10000,
|
||||||
language: str = "en",
|
language: str = "en",
|
||||||
country: Optional[str] = None,
|
country: Optional[str] = None,
|
||||||
safe_search: str = "moderate",
|
safe_search: str = "moderate",
|
||||||
|
|||||||
@ -11,10 +11,10 @@ from bs4 import BeautifulSoup
|
|||||||
|
|
||||||
async def google_search(
|
async def google_search(
|
||||||
query: str,
|
query: str,
|
||||||
num_results: int = 5,
|
num_results: int = 3,
|
||||||
include_snippets: bool = True,
|
include_snippets: bool = True,
|
||||||
include_content: bool = True,
|
include_content: bool = True,
|
||||||
content_max_length: Optional[int] = 15000,
|
content_max_length: Optional[int] = 10000,
|
||||||
language: str = "en",
|
language: str = "en",
|
||||||
country: Optional[str] = None,
|
country: Optional[str] = None,
|
||||||
safe_search: bool = True,
|
safe_search: bool = True,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@ -8,13 +9,26 @@ import aiofiles
|
|||||||
import yaml
|
import yaml
|
||||||
from autogen_agentchat.base import TaskResult, Team
|
from autogen_agentchat.base import TaskResult, Team
|
||||||
from autogen_agentchat.messages import AgentEvent, ChatMessage
|
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__)
|
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:
|
class TeamManager:
|
||||||
"""Manages team operations including loading configs and running teams"""
|
"""Manages team operations including loading configs and running teams"""
|
||||||
|
|
||||||
@ -35,14 +49,7 @@ class TeamManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def load_from_directory(directory: Union[str, Path]) -> List[dict]:
|
async def load_from_directory(directory: Union[str, Path]) -> List[dict]:
|
||||||
"""Load all team configurations from a directory
|
"""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
|
|
||||||
"""
|
|
||||||
directory = Path(directory)
|
directory = Path(directory)
|
||||||
configs = []
|
configs = []
|
||||||
valid_extensions = {".json", ".yaml", ".yml"}
|
valid_extensions = {".json", ".yaml", ".yml"}
|
||||||
@ -61,7 +68,6 @@ class TeamManager:
|
|||||||
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
|
||||||
) -> Component:
|
) -> Component:
|
||||||
"""Create team instance from config"""
|
"""Create team instance from config"""
|
||||||
# Handle different input types
|
|
||||||
if isinstance(team_config, (str, Path)):
|
if isinstance(team_config, (str, Path)):
|
||||||
config = await self.load_from_file(team_config)
|
config = await self.load_from_file(team_config)
|
||||||
elif isinstance(team_config, dict):
|
elif isinstance(team_config, dict):
|
||||||
@ -69,14 +75,12 @@ class TeamManager:
|
|||||||
else:
|
else:
|
||||||
config = team_config.model_dump()
|
config = team_config.model_dump()
|
||||||
|
|
||||||
# Use Component.load_component directly
|
|
||||||
team = Team.load_component(config)
|
team = Team.load_component(config)
|
||||||
|
|
||||||
for agent in team._participants:
|
for agent in team._participants:
|
||||||
if hasattr(agent, "input_func"):
|
if hasattr(agent, "input_func"):
|
||||||
agent.input_func = input_func
|
agent.input_func = input_func
|
||||||
|
|
||||||
# TBD - set input function
|
|
||||||
return team
|
return team
|
||||||
|
|
||||||
async def run_stream(
|
async def run_stream(
|
||||||
@ -85,11 +89,17 @@ class TeamManager:
|
|||||||
team_config: Union[str, Path, dict, ComponentModel],
|
team_config: Union[str, Path, dict, ComponentModel],
|
||||||
input_func: Optional[Callable] = None,
|
input_func: Optional[Callable] = None,
|
||||||
cancellation_token: Optional[CancellationToken] = 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"""
|
"""Stream team execution results"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
team = None
|
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:
|
try:
|
||||||
team = await self._create_team(team_config, input_func)
|
team = await self._create_team(team_config, input_func)
|
||||||
|
|
||||||
@ -102,7 +112,15 @@ class TeamManager:
|
|||||||
else:
|
else:
|
||||||
yield message
|
yield message
|
||||||
|
|
||||||
|
# Check for any LLM events
|
||||||
|
while not llm_event_logger.events.empty():
|
||||||
|
event = await llm_event_logger.events.get()
|
||||||
|
yield event
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
# Cleanup - remove our handler
|
||||||
|
logger.handlers.remove(llm_event_logger)
|
||||||
|
|
||||||
# Ensure cleanup happens
|
# Ensure cleanup happens
|
||||||
if team and hasattr(team, "_participants"):
|
if team and hasattr(team, "_participants"):
|
||||||
for agent in team._participants:
|
for agent in team._participants:
|
||||||
@ -127,7 +145,6 @@ class TeamManager:
|
|||||||
return TeamResult(task_result=result, usage="", duration=time.time() - start_time)
|
return TeamResult(task_result=result, usage="", duration=time.time() - start_time)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Ensure cleanup happens
|
|
||||||
if team and hasattr(team, "_participants"):
|
if team and hasattr(team, "_participants"):
|
||||||
for agent in team._participants:
|
for agent in team._participants:
|
||||||
if hasattr(agent, "close"):
|
if hasattr(agent, "close"):
|
||||||
|
|||||||
@ -15,11 +15,6 @@ from .deps import cleanup_managers, init_managers
|
|||||||
from .initialization import AppInitializer
|
from .initialization import AppInitializer
|
||||||
from .routes import runs, sessions, teams, ws
|
from .routes import runs, sessions, teams, ws
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
# logger = logging.getLogger(__name__)
|
|
||||||
# logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize application
|
# Initialize application
|
||||||
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
app_file_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
initializer = AppInitializer(settings, app_file_path)
|
initializer = AppInitializer(settings, app_file_path)
|
||||||
|
|||||||
@ -21,7 +21,7 @@ from autogen_core import Image as AGImage
|
|||||||
from fastapi import WebSocket, WebSocketDisconnect
|
from fastapi import WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from ...database import DatabaseManager
|
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
|
from ...teammanager import TeamManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -111,6 +111,7 @@ class WebSocketManager:
|
|||||||
HandoffMessage,
|
HandoffMessage,
|
||||||
ToolCallRequestEvent,
|
ToolCallRequestEvent,
|
||||||
ToolCallExecutionEvent,
|
ToolCallExecutionEvent,
|
||||||
|
LLMCallEventMessage,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
await self._save_message(run_id, message)
|
await self._save_message(run_id, message)
|
||||||
@ -328,7 +329,15 @@ class WebSocketManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
elif isinstance(
|
elif isinstance(
|
||||||
message, (TextMessage, StopMessage, HandoffMessage, ToolCallRequestEvent, ToolCallExecutionEvent)
|
message,
|
||||||
|
(
|
||||||
|
TextMessage,
|
||||||
|
StopMessage,
|
||||||
|
HandoffMessage,
|
||||||
|
ToolCallRequestEvent,
|
||||||
|
ToolCallExecutionEvent,
|
||||||
|
LLMCallEventMessage,
|
||||||
|
),
|
||||||
):
|
):
|
||||||
return {"type": "message", "data": message.model_dump()}
|
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" />
|
<Settings className="h-6 w-6 shrink-0 text-secondary group-hover:text-accent" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -248,7 +248,7 @@ const Sidebar = ({ link, meta, isMobile }: SidebarProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-full ">
|
<div className="w-full ">
|
||||||
<div className="hidden">
|
<div className="">
|
||||||
{" "}
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/settings"
|
to="/settings"
|
||||||
|
|||||||
@ -252,7 +252,13 @@ export interface SessionRuns {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WebSocketMessage {
|
export interface WebSocketMessage {
|
||||||
type: "message" | "result" | "completion" | "input_request" | "error";
|
type:
|
||||||
|
| "message"
|
||||||
|
| "result"
|
||||||
|
| "completion"
|
||||||
|
| "input_request"
|
||||||
|
| "error"
|
||||||
|
| "llm_call_event";
|
||||||
data?: AgentMessageConfig | TaskResult;
|
data?: AgentMessageConfig | TaskResult;
|
||||||
status?: RunStatus;
|
status?: RunStatus;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
import React, { memo, useState } from "react";
|
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 ReactMarkdown from "react-markdown";
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
|
||||||
export const LoadingIndicator = ({ size = 16 }: { size: number }) => (
|
export const LoadingIndicator = ({ size = 16 }: { size: number }) => (
|
||||||
<div className="inline-flex items-center gap-2 text-accent mr-2">
|
<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(
|
export const TruncatableText = memo(
|
||||||
({
|
({
|
||||||
content,
|
content,
|
||||||
@ -48,14 +64,17 @@ export const TruncatableText = memo(
|
|||||||
className = "",
|
className = "",
|
||||||
jsonThreshold = 1000,
|
jsonThreshold = 1000,
|
||||||
textThreshold = 500,
|
textThreshold = 500,
|
||||||
|
showFullscreen = true,
|
||||||
}: {
|
}: {
|
||||||
content: string;
|
content: string;
|
||||||
isJson?: boolean;
|
isJson?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
jsonThreshold?: number;
|
jsonThreshold?: number;
|
||||||
textThreshold?: number;
|
textThreshold?: number;
|
||||||
|
showFullscreen?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const threshold = isJson ? jsonThreshold : textThreshold;
|
const threshold = isJson ? jsonThreshold : textThreshold;
|
||||||
const shouldTruncate = content.length > threshold;
|
const shouldTruncate = content.length > threshold;
|
||||||
|
|
||||||
@ -72,7 +91,7 @@ export const TruncatableText = memo(
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
transition-[max-height,opacity] duration-500 ease-in-out
|
transition-[max-height,opacity] overflow-auto scroll duration-500 ease-in-out
|
||||||
${
|
${
|
||||||
shouldTruncate && !isExpanded
|
shouldTruncate && !isExpanded
|
||||||
? "max-h-[300px]"
|
? "max-h-[300px]"
|
||||||
@ -81,32 +100,71 @@ export const TruncatableText = memo(
|
|||||||
${className}
|
${className}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* {displayContent} */}
|
|
||||||
<ReactMarkdown>{displayContent}</ReactMarkdown>
|
<ReactMarkdown>{displayContent}</ReactMarkdown>
|
||||||
{shouldTruncate && !isExpanded && (
|
{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>
|
</div>
|
||||||
|
|
||||||
{shouldTruncate && (
|
{shouldTruncate && (
|
||||||
<div className="mt-2 flex items-center justify-end">
|
<div className="mt-2 flex items-center justify-end gap-2">
|
||||||
<button
|
<Tooltip title={isExpanded ? "Show less" : "Show more"}>
|
||||||
type="button"
|
<button
|
||||||
onClick={toggleExpand}
|
type="button"
|
||||||
className={`
|
onClick={toggleExpand}
|
||||||
inline-flex items-center gap-2 px-3 py-1.5
|
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"
|
||||||
rounded bg-secondary/80
|
aria-label={isExpanded ? "Show less" : "Show more"}
|
||||||
text-xs font-medium
|
>
|
||||||
transition-all duration-300
|
{isExpanded ? (
|
||||||
hover:text-accent
|
<ChevronUp size={18} />
|
||||||
hover:scale-105
|
) : (
|
||||||
z-10
|
<ChevronDown size={18} />
|
||||||
`}
|
)}
|
||||||
aria-label={isExpanded ? "less" : "more"}
|
</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>
|
<Tooltip title="Close">
|
||||||
{isExpanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
<button
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</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.",
|
"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",
|
"label": "Google Search Tool",
|
||||||
"config": {
|
"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",
|
"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 ",
|
"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": [
|
"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 {
|
return {
|
||||||
id,
|
id,
|
||||||
type: "agentNode",
|
type: "agentNode",
|
||||||
@ -567,7 +550,7 @@ const AgentFlow: React.FC<AgentFlowProps> = ({ teamConfig, run }) => {
|
|||||||
<ReactFlow {...reactFlowProps}>
|
<ReactFlow {...reactFlowProps}>
|
||||||
{settings.showGrid && <Background />}
|
{settings.showGrid && <Background />}
|
||||||
{settings.showMiniMap && <MiniMap />}
|
{settings.showMiniMap && <MiniMap />}
|
||||||
<div className="absolute top-0 right-0 z-50">
|
<div className="absolute top-0 right-0 z-10">
|
||||||
<AgentFlowToolbar {...toolbarProps} />
|
<AgentFlowToolbar {...toolbarProps} />
|
||||||
</div>
|
</div>
|
||||||
</ReactFlow>
|
</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 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 {
|
import {
|
||||||
AgentMessageConfig,
|
AgentMessageConfig,
|
||||||
FunctionCall,
|
FunctionCall,
|
||||||
@ -7,6 +14,7 @@ import {
|
|||||||
ImageContent,
|
ImageContent,
|
||||||
} from "../../../types/datamodel";
|
} from "../../../types/datamodel";
|
||||||
import { ClickableImage, TruncatableText } from "../../atoms";
|
import { ClickableImage, TruncatableText } from "../../atoms";
|
||||||
|
import LLMLogRenderer from "./logrenderer";
|
||||||
|
|
||||||
const TEXT_THRESHOLD = 400;
|
const TEXT_THRESHOLD = 400;
|
||||||
const JSON_THRESHOLD = 800;
|
const JSON_THRESHOLD = 800;
|
||||||
@ -143,9 +151,14 @@ export const RenderMessage: React.FC<MessageProps> = ({
|
|||||||
if (!message) return null;
|
if (!message) return null;
|
||||||
const isUser = messageUtils.isUser(message.source);
|
const isUser = messageUtils.isUser(message.source);
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
|
const isLLMEventMessage = message.source === "llm_call_event";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative group ${!isLast ? "mb-2" : ""} ${className}`}>
|
<div
|
||||||
|
className={`relative group ${!isLast ? "mb-2" : ""} ${className} ${
|
||||||
|
isLLMEventMessage ? "border-accent" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
flex items-start gap-2 p-2 rounded
|
flex items-start gap-2 p-2 rounded
|
||||||
@ -160,7 +173,13 @@ export const RenderMessage: React.FC<MessageProps> = ({
|
|||||||
${isUser ? "text-accent" : "text-primary"}
|
${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>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@ -177,6 +196,8 @@ export const RenderMessage: React.FC<MessageProps> = ({
|
|||||||
<RenderMultiModal content={content} />
|
<RenderMultiModal content={content} />
|
||||||
) : messageUtils.isFunctionExecutionResult(content) ? (
|
) : messageUtils.isFunctionExecutionResult(content) ? (
|
||||||
<RenderToolResult content={content} />
|
<RenderToolResult content={content} />
|
||||||
|
) : message.source === "llm_call_event" ? (
|
||||||
|
<LLMLogRenderer content={String(content)} />
|
||||||
) : (
|
) : (
|
||||||
<TruncatableText
|
<TruncatableText
|
||||||
content={String(content)}
|
content={String(content)}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
StopCircle,
|
StopCircle,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
@ -6,12 +6,10 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
GroupIcon,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Bot,
|
Bot,
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelLeftOpen,
|
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Run, Message, TeamConfig, Component } from "../../../types/datamodel";
|
import { Run, Message, TeamConfig, Component } from "../../../types/datamodel";
|
||||||
@ -19,11 +17,8 @@ import AgentFlow from "./agentflow/agentflow";
|
|||||||
import { RenderMessage } from "./rendermessage";
|
import { RenderMessage } from "./rendermessage";
|
||||||
import InputRequestView from "./inputrequest";
|
import InputRequestView from "./inputrequest";
|
||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import {
|
import { getRelativeTimeString, LoadingDots } from "../../atoms";
|
||||||
getRelativeTimeString,
|
import { useSettingsStore } from "../../settings/store";
|
||||||
LoadingDots,
|
|
||||||
TruncatableText,
|
|
||||||
} from "../../atoms";
|
|
||||||
|
|
||||||
interface RunViewProps {
|
interface RunViewProps {
|
||||||
run: Run;
|
run: Run;
|
||||||
@ -33,6 +28,23 @@ interface RunViewProps {
|
|||||||
isFirstRun?: boolean;
|
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> = ({
|
const RunView: React.FC<RunViewProps> = ({
|
||||||
run,
|
run,
|
||||||
onInputResponse,
|
onInputResponse,
|
||||||
@ -45,6 +57,18 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
const isActive = run.status === "active" || run.status === "awaiting_input";
|
const isActive = run.status === "active" || run.status === "awaiting_input";
|
||||||
const [isFlowVisible, setIsFlowVisible] = useState(true);
|
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
|
// Replace existing scroll effect with this simpler one
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -56,7 +80,7 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
}
|
}
|
||||||
}, 450);
|
}, 450);
|
||||||
}, [run.messages]); // Only depend on messages changing
|
}, [run.messages]); // Only depend on messages changing
|
||||||
|
// console.log("run", run);
|
||||||
const calculateThreadTokens = (messages: Message[]) => {
|
const calculateThreadTokens = (messages: Message[]) => {
|
||||||
// console.log("messages", messages);
|
// console.log("messages", messages);
|
||||||
return messages.reduce((total, msg) => {
|
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 lastResultMessage = run.team_result?.task_result.messages.slice(-1)[0];
|
||||||
const lastMessage = run.messages.slice(-1)[0];
|
const lastMessage = getLastMeaningfulMessage(visibleMessages);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 mr-2 ">
|
<div className="space-y-6 mr-2 ">
|
||||||
@ -202,19 +226,7 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lastMessage ? (
|
{lastMessage ? (
|
||||||
// <TruncatableText
|
<RenderMessage message={lastMessage.config} isLast={true} />
|
||||||
// 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}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{lastResultMessage && (
|
{lastResultMessage && (
|
||||||
@ -228,7 +240,7 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
|
|
||||||
{/* Thread Section */}
|
{/* Thread Section */}
|
||||||
<div className="">
|
<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="mt-2 pl-4 border-secondary rounded-b border-l-2 border-secondary/30">
|
||||||
<div className="flex pt-2">
|
<div className="flex pt-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -262,8 +274,8 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-secondary">
|
<div className="text-sm text-secondary">
|
||||||
{calculateThreadTokens(run.messages)} tokens |{" "}
|
{calculateThreadTokens(visibleMessages)} tokens |{" "}
|
||||||
{run.messages.length} messages
|
{visibleMessages.length} messages
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -290,14 +302,14 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
{" "}
|
{" "}
|
||||||
<span className=" inline-block h-6"></span>{" "}
|
<span className=" inline-block h-6"></span>{" "}
|
||||||
</div>
|
</div>
|
||||||
{run.messages.map((msg, idx) => (
|
{visibleMessages.map((msg, idx) => (
|
||||||
<div
|
<div
|
||||||
key={"message_id" + idx + run.id}
|
key={"message_id" + idx + run.id}
|
||||||
className=" mr-2"
|
className=" mr-2"
|
||||||
>
|
>
|
||||||
<RenderMessage
|
<RenderMessage
|
||||||
message={msg.config}
|
message={msg.config}
|
||||||
isLast={idx === run.messages.length - 1}
|
isLast={idx === visibleMessages.length - 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -322,7 +334,7 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
{/* Agent Flow Visualization */}
|
{/* Agent Flow Visualization */}
|
||||||
{isFlowVisible && (
|
{isFlowVisible && (
|
||||||
<div className="bg-tertiary flex-1 rounded mt-2 relative">
|
<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">
|
<Tooltip title="Hide message flow">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsFlowVisible(false)}
|
onClick={() => setIsFlowVisible(false)}
|
||||||
@ -333,7 +345,13 @@ const RunView: React.FC<RunViewProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{teamConfig && (
|
{teamConfig && (
|
||||||
<AgentFlow teamConfig={teamConfig} run={run} />
|
<AgentFlow
|
||||||
|
teamConfig={teamConfig}
|
||||||
|
run={{
|
||||||
|
...run,
|
||||||
|
messages: getAgentMessages(visibleMessages),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 { NodeEditor } from "./node-editor/node-editor";
|
||||||
import debounce from "lodash.debounce";
|
import debounce from "lodash.debounce";
|
||||||
import { appContext } from "../../../../hooks/provider";
|
import { appContext } from "../../../../hooks/provider";
|
||||||
import { sessionAPI } from "../../session/api";
|
import { sessionAPI } from "../../playground/api";
|
||||||
import TestDrawer from "./testdrawer";
|
import TestDrawer from "./testdrawer";
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
const { Sider, Content } = Layout;
|
||||||
@ -179,7 +179,6 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
console.log("Saving team configuration", component);
|
|
||||||
const teamData: Partial<Team> = team
|
const teamData: Partial<Team> = team
|
||||||
? {
|
? {
|
||||||
...team,
|
...team,
|
||||||
@ -288,7 +287,7 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTestDrawerClose = () => {
|
const handleTestDrawerClose = () => {
|
||||||
console.log("TestDrawer closed");
|
// console.log("TestDrawer closed");
|
||||||
setTestDrawerVisible(false);
|
setTestDrawerVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import React, { useContext, useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { Drawer, Button, message, Checkbox } from "antd";
|
import { Drawer, Button, message, Checkbox } from "antd";
|
||||||
import { Team, Session } from "../../../types/datamodel";
|
import { Team, Session } from "../../../types/datamodel";
|
||||||
import ChatView from "../../session/chat/chat";
|
import ChatView from "../../playground/chat/chat";
|
||||||
import { appContext } from "../../../../hooks/provider";
|
import { appContext } from "../../../../hooks/provider";
|
||||||
import { sessionAPI } from "../../session/api";
|
import { sessionAPI } from "../../playground/api";
|
||||||
|
|
||||||
interface TestDrawerProps {
|
interface TestDrawerProps {
|
||||||
isVisble: boolean;
|
isVisble: boolean;
|
||||||
|
|||||||
@ -3,11 +3,9 @@ import { Button, Tooltip } from "antd";
|
|||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
Plus,
|
Plus,
|
||||||
Edit,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
Calendar,
|
|
||||||
Copy,
|
Copy,
|
||||||
GalleryHorizontalEnd,
|
GalleryHorizontalEnd,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
@ -15,7 +13,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { Team } from "../../types/datamodel";
|
import type { Team } from "../../types/datamodel";
|
||||||
import { getRelativeTimeString } from "../atoms";
|
import { getRelativeTimeString } from "../atoms";
|
||||||
import { defaultTeam } from "./types";
|
|
||||||
import { useGalleryStore } from "../gallery/store";
|
import { useGalleryStore } from "../gallery/store";
|
||||||
|
|
||||||
interface TeamSidebarProps {
|
interface TeamSidebarProps {
|
||||||
@ -43,8 +40,12 @@ export const TeamSidebar: React.FC<TeamSidebarProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const defaultGallery = useGalleryStore((state) => state.getDefaultGallery());
|
const defaultGallery = useGalleryStore((state) => state.getDefaultGallery());
|
||||||
const createTeam = () => {
|
const createTeam = () => {
|
||||||
const newTeam = Object.assign({}, defaultTeam);
|
const newTeam = Object.assign(
|
||||||
newTeam.component.label = "new_team_" + new Date().getTime();
|
{},
|
||||||
|
{ component: defaultGallery?.items.teams[0] }
|
||||||
|
);
|
||||||
|
newTeam.component.label =
|
||||||
|
"default_team" + new Date().getTime().toString().slice(0, 2);
|
||||||
onCreateTeam(newTeam);
|
onCreateTeam(newTeam);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,85 +15,3 @@ export interface TeamListProps {
|
|||||||
onDelete: (teamId: number) => void;
|
onDelete: (teamId: number) => void;
|
||||||
isLoading?: boolean;
|
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 * as React from "react";
|
||||||
import Layout from "../components/layout";
|
import Layout from "../components/layout";
|
||||||
import { graphql } from "gatsby";
|
import { graphql } from "gatsby";
|
||||||
import ChatView from "../components/views/session/chat/chat";
|
import ChatView from "../components/views/playground/chat/chat";
|
||||||
import { SessionManager } from "../components/views/session/manager";
|
import { SessionManager } from "../components/views/playground/manager";
|
||||||
|
|
||||||
// markup
|
// markup
|
||||||
const IndexPage = ({ data }: any) => {
|
const IndexPage = ({ data }: any) => {
|
||||||
|
|||||||
@ -1,21 +1,14 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Layout from "../components/layout";
|
import Layout from "../components/layout";
|
||||||
import { graphql } from "gatsby";
|
import { graphql } from "gatsby";
|
||||||
import { TriangleAlertIcon } from "lucide-react";
|
import { SettingsManager } from "../components/views/settings/manager";
|
||||||
|
|
||||||
// markup
|
// markup
|
||||||
const SettingsPage = ({ data }: any) => {
|
const SettingsPage = ({ data }: any) => {
|
||||||
return (
|
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 ">
|
<main style={{ height: "100%" }} className=" h-full ">
|
||||||
<div className="mb-2"> Settings</div>
|
<SettingsManager />
|
||||||
<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>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user