Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Merge pull request #21 from huggingface/feature/web-frontend
Browse files- .gitignore +1 -0
- Dockerfile +59 -0
- README.md +14 -0
- agent/core/agent_loop.py +4 -2
- agent/core/tools.py +7 -1
- agent/prompts/system_prompt.yaml +1 -1
- agent/tools/jobs_tool.py +61 -9
- agent/tools/plan_tool.py +16 -4
- backend/__init__.py +1 -0
- backend/main.py +79 -0
- backend/models.py +76 -0
- backend/routes/__init__.py +1 -0
- backend/routes/agent.py +151 -0
- backend/routes/auth.py +148 -0
- backend/session_manager.py +276 -0
- backend/websocket.py +72 -0
- configs/main_agent_config.json +1 -1
- eval/.amp_batch_solve.py.swp +0 -0
- frontend/eslint.config.js +28 -0
- frontend/index.html +16 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +38 -0
- frontend/public/vite.svg +3 -0
- frontend/src/App.tsx +12 -0
- frontend/src/components/ApprovalModal/ApprovalModal.tsx +208 -0
- frontend/src/components/Chat/ApprovalFlow.tsx +490 -0
- frontend/src/components/Chat/ChatInput.tsx +126 -0
- frontend/src/components/Chat/MessageBubble.tsx +178 -0
- frontend/src/components/Chat/MessageList.tsx +100 -0
- frontend/src/components/CodePanel/CodePanel.tsx +204 -0
- frontend/src/components/Layout/AppLayout.tsx +284 -0
- frontend/src/components/SessionSidebar/SessionSidebar.tsx +248 -0
- frontend/src/hooks/useAgentWebSocket.ts +405 -0
- frontend/src/main.tsx +15 -0
- frontend/src/store/agentStore.ts +209 -0
- frontend/src/store/layoutStore.ts +23 -0
- frontend/src/store/sessionStore.ts +78 -0
- frontend/src/theme.ts +158 -0
- frontend/src/types/agent.ts +65 -0
- frontend/src/types/events.ts +80 -0
- frontend/src/utils/logProcessor.ts +81 -0
- frontend/src/vite-env.d.ts +1 -0
- frontend/tsconfig.json +25 -0
- frontend/vite.config.ts +29 -0
- pyproject.toml +5 -0
- uv.lock +190 -0
.gitignore
CHANGED
|
@@ -68,3 +68,4 @@ models/
|
|
| 68 |
checkpoint-*/
|
| 69 |
runs/
|
| 70 |
wandb/
|
|
|
|
|
|
| 68 |
checkpoint-*/
|
| 69 |
runs/
|
| 70 |
wandb/
|
| 71 |
+
frontend/tsconfig.tsbuildinfo
|
Dockerfile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build frontend
|
| 2 |
+
FROM node:20-alpine AS frontend-builder
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ ./
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Stage 2: Production
|
| 10 |
+
FROM python:3.12-slim
|
| 11 |
+
|
| 12 |
+
# Install uv directly from official image
|
| 13 |
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 14 |
+
|
| 15 |
+
# Create user with UID 1000 (required for HF Spaces)
|
| 16 |
+
RUN useradd -m -u 1000 user
|
| 17 |
+
|
| 18 |
+
WORKDIR /app
|
| 19 |
+
|
| 20 |
+
# Install system dependencies
|
| 21 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 22 |
+
git \
|
| 23 |
+
curl \
|
| 24 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 25 |
+
|
| 26 |
+
# Copy dependency files
|
| 27 |
+
COPY pyproject.toml uv.lock ./
|
| 28 |
+
|
| 29 |
+
# Install dependencies into /app/.venv
|
| 30 |
+
# Use --frozen to ensure exact versions from uv.lock
|
| 31 |
+
RUN uv sync --extra agent --no-dev --frozen
|
| 32 |
+
|
| 33 |
+
# Copy application code
|
| 34 |
+
COPY agent/ ./agent/
|
| 35 |
+
COPY backend/ ./backend/
|
| 36 |
+
COPY configs/ ./configs/
|
| 37 |
+
|
| 38 |
+
# Copy built frontend
|
| 39 |
+
COPY --from=frontend-builder /app/frontend/dist ./static/
|
| 40 |
+
|
| 41 |
+
# Create directories and set ownership
|
| 42 |
+
RUN mkdir -p /app/session_logs && \
|
| 43 |
+
chown -R user:user /app
|
| 44 |
+
|
| 45 |
+
# Switch to non-root user
|
| 46 |
+
USER user
|
| 47 |
+
|
| 48 |
+
# Set environment
|
| 49 |
+
ENV HOME=/home/user \
|
| 50 |
+
PYTHONUNBUFFERED=1 \
|
| 51 |
+
PYTHONPATH=/app \
|
| 52 |
+
PATH="/app/.venv/bin:$PATH"
|
| 53 |
+
|
| 54 |
+
# Expose port
|
| 55 |
+
EXPOSE 7860
|
| 56 |
+
|
| 57 |
+
# Run the application from backend directory
|
| 58 |
+
WORKDIR /app/backend
|
| 59 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# HF Agent
|
| 2 |
|
| 3 |
An MLE agent CLI with MCP (Model Context Protocol) integration and built-in tool support.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: HF Agent
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
hf_oauth: true
|
| 9 |
+
hf_oauth_scopes:
|
| 10 |
+
- read-repos
|
| 11 |
+
- write-repos
|
| 12 |
+
- inference-api
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
# HF Agent
|
| 16 |
|
| 17 |
An MLE agent CLI with MCP (Model Context Protocol) integration and built-in tool support.
|
agent/core/agent_loop.py
CHANGED
|
@@ -203,7 +203,7 @@ class Handlers:
|
|
| 203 |
)
|
| 204 |
|
| 205 |
output, success = await session.tool_router.call_tool(
|
| 206 |
-
tool_name, tool_args
|
| 207 |
)
|
| 208 |
|
| 209 |
# Add tool result to history
|
|
@@ -376,7 +376,9 @@ class Handlers:
|
|
| 376 |
)
|
| 377 |
)
|
| 378 |
|
| 379 |
-
output, success = await session.tool_router.call_tool(
|
|
|
|
|
|
|
| 380 |
|
| 381 |
return (tc, tool_name, output, success)
|
| 382 |
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
output, success = await session.tool_router.call_tool(
|
| 206 |
+
tool_name, tool_args, session=session
|
| 207 |
)
|
| 208 |
|
| 209 |
# Add tool result to history
|
|
|
|
| 376 |
)
|
| 377 |
)
|
| 378 |
|
| 379 |
+
output, success = await session.tool_router.call_tool(
|
| 380 |
+
tool_name, tool_args, session=session
|
| 381 |
+
)
|
| 382 |
|
| 383 |
return (tc, tool_name, output, success)
|
| 384 |
|
agent/core/tools.py
CHANGED
|
@@ -219,7 +219,7 @@ class ToolRouter:
|
|
| 219 |
|
| 220 |
@observe(name="call_tool")
|
| 221 |
async def call_tool(
|
| 222 |
-
self, tool_name: str, arguments: dict[str, Any]
|
| 223 |
) -> tuple[str, bool]:
|
| 224 |
"""
|
| 225 |
Call a tool and return (output_string, success_bool).
|
|
@@ -230,6 +230,12 @@ class ToolRouter:
|
|
| 230 |
# Check if this is a built-in tool with a handler
|
| 231 |
tool = self.tools.get(tool_name)
|
| 232 |
if tool and tool.handler:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
return await tool.handler(arguments)
|
| 234 |
|
| 235 |
# Otherwise, use MCP client
|
|
|
|
| 219 |
|
| 220 |
@observe(name="call_tool")
|
| 221 |
async def call_tool(
|
| 222 |
+
self, tool_name: str, arguments: dict[str, Any], session: Any = None
|
| 223 |
) -> tuple[str, bool]:
|
| 224 |
"""
|
| 225 |
Call a tool and return (output_string, success_bool).
|
|
|
|
| 230 |
# Check if this is a built-in tool with a handler
|
| 231 |
tool = self.tools.get(tool_name)
|
| 232 |
if tool and tool.handler:
|
| 233 |
+
import inspect
|
| 234 |
+
|
| 235 |
+
# Check if handler accepts session argument
|
| 236 |
+
sig = inspect.signature(tool.handler)
|
| 237 |
+
if "session" in sig.parameters:
|
| 238 |
+
return await tool.handler(arguments, session=session)
|
| 239 |
return await tool.handler(arguments)
|
| 240 |
|
| 241 |
# Otherwise, use MCP client
|
agent/prompts/system_prompt.yaml
CHANGED
|
@@ -77,7 +77,7 @@ system_prompt: |
|
|
| 77 |
|
| 78 |
- Be concise and direct.
|
| 79 |
- Don't flatter the user.
|
| 80 |
-
-
|
| 81 |
- If you are limited in a task, offer alternatives.
|
| 82 |
- Don't thank the user when he provides results.
|
| 83 |
- Explain what you're doing for non-trivial operations.
|
|
|
|
| 77 |
|
| 78 |
- Be concise and direct.
|
| 79 |
- Don't flatter the user.
|
| 80 |
+
- Never use emojis nor exclamation points.
|
| 81 |
- If you are limited in a task, offer alternatives.
|
| 82 |
- Don't thank the user when he provides results.
|
| 83 |
- Explain what you're doing for non-trivial operations.
|
agent/tools/jobs_tool.py
CHANGED
|
@@ -9,12 +9,13 @@ import base64
|
|
| 9 |
import http.client
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
-
from typing import Any, Dict, Literal, Optional
|
| 13 |
|
| 14 |
import httpx
|
| 15 |
from huggingface_hub import HfApi
|
| 16 |
from huggingface_hub.utils import HfHubHTTPError
|
| 17 |
|
|
|
|
| 18 |
from agent.tools.types import ToolResult
|
| 19 |
from agent.tools.utilities import (
|
| 20 |
format_job_details,
|
|
@@ -269,9 +270,15 @@ def _scheduled_job_info_to_dict(scheduled_job_info) -> Dict[str, Any]:
|
|
| 269 |
class HfJobsTool:
|
| 270 |
"""Tool for managing Hugging Face compute jobs using huggingface-hub library"""
|
| 271 |
|
| 272 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
self.api = HfApi(token=hf_token)
|
| 274 |
self.namespace = namespace
|
|
|
|
| 275 |
|
| 276 |
async def execute(self, params: Dict[str, Any]) -> ToolResult:
|
| 277 |
"""Execute the specified operation"""
|
|
@@ -360,15 +367,48 @@ class HfJobsTool:
|
|
| 360 |
|
| 361 |
for _ in range(max_retries):
|
| 362 |
try:
|
| 363 |
-
#
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
-
|
| 367 |
-
|
| 368 |
print("\t" + log_line)
|
|
|
|
|
|
|
| 369 |
all_logs.append(log_line)
|
| 370 |
|
| 371 |
-
# If we get here, streaming completed normally
|
|
|
|
|
|
|
| 372 |
break
|
| 373 |
|
| 374 |
except (
|
|
@@ -963,10 +1003,22 @@ HF_JOBS_TOOL_SPEC = {
|
|
| 963 |
}
|
| 964 |
|
| 965 |
|
| 966 |
-
async def hf_jobs_handler(
|
|
|
|
|
|
|
| 967 |
"""Handler for agent tool router"""
|
| 968 |
try:
|
| 969 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
result = await tool.execute(arguments)
|
| 971 |
return result["formatted"], not result.get("isError", False)
|
| 972 |
except Exception as e:
|
|
|
|
| 9 |
import http.client
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
+
from typing import Any, Dict, Literal, Optional, Callable, Awaitable
|
| 13 |
|
| 14 |
import httpx
|
| 15 |
from huggingface_hub import HfApi
|
| 16 |
from huggingface_hub.utils import HfHubHTTPError
|
| 17 |
|
| 18 |
+
from agent.core.session import Event
|
| 19 |
from agent.tools.types import ToolResult
|
| 20 |
from agent.tools.utilities import (
|
| 21 |
format_job_details,
|
|
|
|
| 270 |
class HfJobsTool:
|
| 271 |
"""Tool for managing Hugging Face compute jobs using huggingface-hub library"""
|
| 272 |
|
| 273 |
+
def __init__(
|
| 274 |
+
self,
|
| 275 |
+
hf_token: Optional[str] = None,
|
| 276 |
+
namespace: Optional[str] = None,
|
| 277 |
+
log_callback: Optional[Callable[[str], Awaitable[None]]] = None,
|
| 278 |
+
):
|
| 279 |
self.api = HfApi(token=hf_token)
|
| 280 |
self.namespace = namespace
|
| 281 |
+
self.log_callback = log_callback
|
| 282 |
|
| 283 |
async def execute(self, params: Dict[str, Any]) -> ToolResult:
|
| 284 |
"""Execute the specified operation"""
|
|
|
|
| 367 |
|
| 368 |
for _ in range(max_retries):
|
| 369 |
try:
|
| 370 |
+
# Use a queue to bridge sync generator to async consumer
|
| 371 |
+
queue = asyncio.Queue()
|
| 372 |
+
loop = asyncio.get_running_loop()
|
| 373 |
+
|
| 374 |
+
def log_producer():
|
| 375 |
+
try:
|
| 376 |
+
# fetch_job_logs is a blocking sync generator
|
| 377 |
+
logs_gen = self.api.fetch_job_logs(job_id=job_id, namespace=namespace)
|
| 378 |
+
for line in logs_gen:
|
| 379 |
+
# Push line to queue thread-safely
|
| 380 |
+
loop.call_soon_threadsafe(queue.put_nowait, line)
|
| 381 |
+
# Signal EOF
|
| 382 |
+
loop.call_soon_threadsafe(queue.put_nowait, None)
|
| 383 |
+
except Exception as e:
|
| 384 |
+
# Signal error
|
| 385 |
+
loop.call_soon_threadsafe(queue.put_nowait, e)
|
| 386 |
+
|
| 387 |
+
# Start producer in a background thread so it doesn't block the event loop
|
| 388 |
+
producer_future = loop.run_in_executor(None, log_producer)
|
| 389 |
+
|
| 390 |
+
# Consume logs from the queue as they arrive
|
| 391 |
+
while True:
|
| 392 |
+
item = await queue.get()
|
| 393 |
+
|
| 394 |
+
# EOF sentinel
|
| 395 |
+
if item is None:
|
| 396 |
+
break
|
| 397 |
+
|
| 398 |
+
# Error occurred in producer
|
| 399 |
+
if isinstance(item, Exception):
|
| 400 |
+
raise item
|
| 401 |
|
| 402 |
+
# Process log line
|
| 403 |
+
log_line = item
|
| 404 |
print("\t" + log_line)
|
| 405 |
+
if self.log_callback:
|
| 406 |
+
await self.log_callback(log_line)
|
| 407 |
all_logs.append(log_line)
|
| 408 |
|
| 409 |
+
# If we get here, streaming completed normally (EOF received)
|
| 410 |
+
# Wait for thread to cleanup (should be done)
|
| 411 |
+
await producer_future
|
| 412 |
break
|
| 413 |
|
| 414 |
except (
|
|
|
|
| 1003 |
}
|
| 1004 |
|
| 1005 |
|
| 1006 |
+
async def hf_jobs_handler(
|
| 1007 |
+
arguments: Dict[str, Any], session: Any = None
|
| 1008 |
+
) -> tuple[str, bool]:
|
| 1009 |
"""Handler for agent tool router"""
|
| 1010 |
try:
|
| 1011 |
+
|
| 1012 |
+
async def log_callback(log: str):
|
| 1013 |
+
if session:
|
| 1014 |
+
await session.send_event(
|
| 1015 |
+
Event(event_type="tool_log", data={"tool": "hf_jobs", "log": log})
|
| 1016 |
+
)
|
| 1017 |
+
|
| 1018 |
+
tool = HfJobsTool(
|
| 1019 |
+
namespace=os.environ.get("HF_NAMESPACE", ""),
|
| 1020 |
+
log_callback=log_callback if session else None,
|
| 1021 |
+
)
|
| 1022 |
result = await tool.execute(arguments)
|
| 1023 |
return result["formatted"], not result.get("isError", False)
|
| 1024 |
except Exception as e:
|
agent/tools/plan_tool.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from typing import Any, Dict, List
|
| 2 |
|
|
|
|
| 3 |
from agent.utils.terminal_display import format_plan_tool_output
|
| 4 |
|
| 5 |
from .types import ToolResult
|
|
@@ -11,8 +12,8 @@ _current_plan: List[Dict[str, str]] = []
|
|
| 11 |
class PlanTool:
|
| 12 |
"""Tool for managing a list of todos with status tracking."""
|
| 13 |
|
| 14 |
-
def __init__(self):
|
| 15 |
-
|
| 16 |
|
| 17 |
async def execute(self, params: Dict[str, Any]) -> ToolResult:
|
| 18 |
"""
|
|
@@ -56,6 +57,15 @@ class PlanTool:
|
|
| 56 |
# Store the raw todos structure in memory
|
| 57 |
_current_plan = todos
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
# Format only for display using terminal_display utility
|
| 60 |
formatted_output = format_plan_tool_output(todos)
|
| 61 |
|
|
@@ -120,7 +130,9 @@ PLAN_TOOL_SPEC = {
|
|
| 120 |
}
|
| 121 |
|
| 122 |
|
| 123 |
-
async def plan_tool_handler(
|
| 124 |
-
|
|
|
|
|
|
|
| 125 |
result = await tool.execute(arguments)
|
| 126 |
return result["formatted"], not result.get("isError", False)
|
|
|
|
| 1 |
from typing import Any, Dict, List
|
| 2 |
|
| 3 |
+
from agent.core.session import Event
|
| 4 |
from agent.utils.terminal_display import format_plan_tool_output
|
| 5 |
|
| 6 |
from .types import ToolResult
|
|
|
|
| 12 |
class PlanTool:
|
| 13 |
"""Tool for managing a list of todos with status tracking."""
|
| 14 |
|
| 15 |
+
def __init__(self, session: Any = None):
|
| 16 |
+
self.session = session
|
| 17 |
|
| 18 |
async def execute(self, params: Dict[str, Any]) -> ToolResult:
|
| 19 |
"""
|
|
|
|
| 57 |
# Store the raw todos structure in memory
|
| 58 |
_current_plan = todos
|
| 59 |
|
| 60 |
+
# Emit plan update event if session is available
|
| 61 |
+
if self.session:
|
| 62 |
+
await self.session.send_event(
|
| 63 |
+
Event(
|
| 64 |
+
event_type="plan_update",
|
| 65 |
+
data={"plan": todos},
|
| 66 |
+
)
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
# Format only for display using terminal_display utility
|
| 70 |
formatted_output = format_plan_tool_output(todos)
|
| 71 |
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
|
| 133 |
+
async def plan_tool_handler(
|
| 134 |
+
arguments: Dict[str, Any], session: Any = None
|
| 135 |
+
) -> tuple[str, bool]:
|
| 136 |
+
tool = PlanTool(session=session)
|
| 137 |
result = await tool.execute(arguments)
|
| 138 |
return result["formatted"], not result.get("isError", False)
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Backend package for HF Agent web interface
|
backend/main.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI application for HF Agent web interface."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from contextlib import asynccontextmanager
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from fastapi.staticfiles import StaticFiles
|
| 11 |
+
|
| 12 |
+
from routes.agent import router as agent_router
|
| 13 |
+
from routes.auth import router as auth_router
|
| 14 |
+
|
| 15 |
+
# Configure logging
|
| 16 |
+
logging.basicConfig(
|
| 17 |
+
level=logging.INFO,
|
| 18 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 19 |
+
)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@asynccontextmanager
|
| 24 |
+
async def lifespan(app: FastAPI):
|
| 25 |
+
"""Application lifespan handler."""
|
| 26 |
+
logger.info("Starting HF Agent backend...")
|
| 27 |
+
yield
|
| 28 |
+
logger.info("Shutting down HF Agent backend...")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
app = FastAPI(
|
| 32 |
+
title="HF Agent",
|
| 33 |
+
description="ML Engineering Assistant API",
|
| 34 |
+
version="1.0.0",
|
| 35 |
+
lifespan=lifespan,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# CORS middleware for development
|
| 39 |
+
app.add_middleware(
|
| 40 |
+
CORSMiddleware,
|
| 41 |
+
allow_origins=[
|
| 42 |
+
"http://localhost:5173", # Vite dev server
|
| 43 |
+
"http://localhost:3000",
|
| 44 |
+
"http://127.0.0.1:5173",
|
| 45 |
+
"http://127.0.0.1:3000",
|
| 46 |
+
],
|
| 47 |
+
allow_credentials=True,
|
| 48 |
+
allow_methods=["*"],
|
| 49 |
+
allow_headers=["*"],
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Include routers
|
| 53 |
+
app.include_router(agent_router)
|
| 54 |
+
app.include_router(auth_router)
|
| 55 |
+
|
| 56 |
+
# Serve static files (frontend build) in production
|
| 57 |
+
static_path = Path(__file__).parent.parent / "static"
|
| 58 |
+
if static_path.exists():
|
| 59 |
+
app.mount("/", StaticFiles(directory=str(static_path), html=True), name="static")
|
| 60 |
+
logger.info(f"Serving static files from {static_path}")
|
| 61 |
+
else:
|
| 62 |
+
logger.info("No static directory found, running in API-only mode")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@app.get("/api")
|
| 66 |
+
async def api_root():
|
| 67 |
+
"""API root endpoint."""
|
| 68 |
+
return {
|
| 69 |
+
"name": "HF Agent API",
|
| 70 |
+
"version": "1.0.0",
|
| 71 |
+
"docs": "/docs",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
import uvicorn
|
| 77 |
+
|
| 78 |
+
port = int(os.environ.get("PORT", 7860))
|
| 79 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
backend/models.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic models for API requests and responses."""
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class OpType(str, Enum):
|
| 10 |
+
"""Operation types matching agent/core/agent_loop.py."""
|
| 11 |
+
|
| 12 |
+
USER_INPUT = "user_input"
|
| 13 |
+
EXEC_APPROVAL = "exec_approval"
|
| 14 |
+
INTERRUPT = "interrupt"
|
| 15 |
+
UNDO = "undo"
|
| 16 |
+
COMPACT = "compact"
|
| 17 |
+
SHUTDOWN = "shutdown"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Operation(BaseModel):
|
| 21 |
+
"""Operation to be submitted to the agent."""
|
| 22 |
+
|
| 23 |
+
op_type: OpType
|
| 24 |
+
data: dict[str, Any] | None = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Submission(BaseModel):
|
| 28 |
+
"""Submission wrapper with ID and operation."""
|
| 29 |
+
|
| 30 |
+
id: str
|
| 31 |
+
operation: Operation
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ToolApproval(BaseModel):
|
| 35 |
+
"""Approval decision for a single tool call."""
|
| 36 |
+
|
| 37 |
+
tool_call_id: str
|
| 38 |
+
approved: bool
|
| 39 |
+
feedback: str | None = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ApprovalRequest(BaseModel):
|
| 43 |
+
"""Request to approve/reject tool calls."""
|
| 44 |
+
|
| 45 |
+
session_id: str
|
| 46 |
+
approvals: list[ToolApproval]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class SubmitRequest(BaseModel):
|
| 50 |
+
"""Request to submit user input."""
|
| 51 |
+
|
| 52 |
+
session_id: str
|
| 53 |
+
text: str
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class SessionResponse(BaseModel):
|
| 57 |
+
"""Response when creating a new session."""
|
| 58 |
+
|
| 59 |
+
session_id: str
|
| 60 |
+
ready: bool = True
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class SessionInfo(BaseModel):
|
| 64 |
+
"""Session metadata."""
|
| 65 |
+
|
| 66 |
+
session_id: str
|
| 67 |
+
created_at: str
|
| 68 |
+
is_active: bool
|
| 69 |
+
message_count: int
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class HealthResponse(BaseModel):
|
| 73 |
+
"""Health check response."""
|
| 74 |
+
|
| 75 |
+
status: str = "ok"
|
| 76 |
+
active_sessions: int = 0
|
backend/routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Routes package
|
backend/routes/agent.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agent API routes - WebSocket and REST endpoints."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
| 6 |
+
|
| 7 |
+
from models import (
|
| 8 |
+
ApprovalRequest,
|
| 9 |
+
HealthResponse,
|
| 10 |
+
SessionInfo,
|
| 11 |
+
SessionResponse,
|
| 12 |
+
SubmitRequest,
|
| 13 |
+
)
|
| 14 |
+
from session_manager import session_manager
|
| 15 |
+
from websocket import manager as ws_manager
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
router = APIRouter(prefix="/api", tags=["agent"])
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@router.get("/health", response_model=HealthResponse)
|
| 23 |
+
async def health_check() -> HealthResponse:
|
| 24 |
+
"""Health check endpoint."""
|
| 25 |
+
return HealthResponse(
|
| 26 |
+
status="ok", active_sessions=session_manager.active_session_count
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@router.post("/session", response_model=SessionResponse)
|
| 31 |
+
async def create_session() -> SessionResponse:
|
| 32 |
+
"""Create a new agent session."""
|
| 33 |
+
session_id = await session_manager.create_session()
|
| 34 |
+
return SessionResponse(session_id=session_id, ready=True)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get("/session/{session_id}", response_model=SessionInfo)
|
| 38 |
+
async def get_session(session_id: str) -> SessionInfo:
|
| 39 |
+
"""Get session information."""
|
| 40 |
+
info = session_manager.get_session_info(session_id)
|
| 41 |
+
if not info:
|
| 42 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 43 |
+
return SessionInfo(**info)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@router.get("/sessions", response_model=list[SessionInfo])
|
| 47 |
+
async def list_sessions() -> list[SessionInfo]:
|
| 48 |
+
"""List all sessions."""
|
| 49 |
+
sessions = session_manager.list_sessions()
|
| 50 |
+
return [SessionInfo(**s) for s in sessions]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@router.delete("/session/{session_id}")
|
| 54 |
+
async def delete_session(session_id: str) -> dict:
|
| 55 |
+
"""Delete a session."""
|
| 56 |
+
success = await session_manager.delete_session(session_id)
|
| 57 |
+
if not success:
|
| 58 |
+
raise HTTPException(status_code=404, detail="Session not found")
|
| 59 |
+
return {"status": "deleted", "session_id": session_id}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@router.post("/submit")
|
| 63 |
+
async def submit_input(request: SubmitRequest) -> dict:
|
| 64 |
+
"""Submit user input to a session."""
|
| 65 |
+
success = await session_manager.submit_user_input(request.session_id, request.text)
|
| 66 |
+
if not success:
|
| 67 |
+
raise HTTPException(status_code=404, detail="Session not found or inactive")
|
| 68 |
+
return {"status": "submitted", "session_id": request.session_id}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@router.post("/approve")
|
| 72 |
+
async def submit_approval(request: ApprovalRequest) -> dict:
|
| 73 |
+
"""Submit tool approvals to a session."""
|
| 74 |
+
approvals = [
|
| 75 |
+
{
|
| 76 |
+
"tool_call_id": a.tool_call_id,
|
| 77 |
+
"approved": a.approved,
|
| 78 |
+
"feedback": a.feedback,
|
| 79 |
+
}
|
| 80 |
+
for a in request.approvals
|
| 81 |
+
]
|
| 82 |
+
success = await session_manager.submit_approval(request.session_id, approvals)
|
| 83 |
+
if not success:
|
| 84 |
+
raise HTTPException(status_code=404, detail="Session not found or inactive")
|
| 85 |
+
return {"status": "submitted", "session_id": request.session_id}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@router.post("/interrupt/{session_id}")
|
| 89 |
+
async def interrupt_session(session_id: str) -> dict:
|
| 90 |
+
"""Interrupt the current operation in a session."""
|
| 91 |
+
success = await session_manager.interrupt(session_id)
|
| 92 |
+
if not success:
|
| 93 |
+
raise HTTPException(status_code=404, detail="Session not found or inactive")
|
| 94 |
+
return {"status": "interrupted", "session_id": session_id}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@router.post("/undo/{session_id}")
|
| 98 |
+
async def undo_session(session_id: str) -> dict:
|
| 99 |
+
"""Undo the last turn in a session."""
|
| 100 |
+
success = await session_manager.undo(session_id)
|
| 101 |
+
if not success:
|
| 102 |
+
raise HTTPException(status_code=404, detail="Session not found or inactive")
|
| 103 |
+
return {"status": "undo_requested", "session_id": session_id}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@router.post("/compact/{session_id}")
|
| 107 |
+
async def compact_session(session_id: str) -> dict:
|
| 108 |
+
"""Compact the context in a session."""
|
| 109 |
+
success = await session_manager.compact(session_id)
|
| 110 |
+
if not success:
|
| 111 |
+
raise HTTPException(status_code=404, detail="Session not found or inactive")
|
| 112 |
+
return {"status": "compact_requested", "session_id": session_id}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@router.post("/shutdown/{session_id}")
|
| 116 |
+
async def shutdown_session(session_id: str) -> dict:
|
| 117 |
+
"""Shutdown a session."""
|
| 118 |
+
success = await session_manager.shutdown_session(session_id)
|
| 119 |
+
if not success:
|
| 120 |
+
raise HTTPException(status_code=404, detail="Session not found or inactive")
|
| 121 |
+
return {"status": "shutdown_requested", "session_id": session_id}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@router.websocket("/ws/{session_id}")
|
| 125 |
+
async def websocket_endpoint(websocket: WebSocket, session_id: str) -> None:
|
| 126 |
+
"""WebSocket endpoint for real-time events."""
|
| 127 |
+
logger.info(f"WebSocket connection request for session {session_id}")
|
| 128 |
+
# Verify session exists
|
| 129 |
+
info = session_manager.get_session_info(session_id)
|
| 130 |
+
if not info:
|
| 131 |
+
logger.warning(f"WebSocket connection rejected: Session {session_id} not found")
|
| 132 |
+
await websocket.close(code=4004, reason="Session not found")
|
| 133 |
+
return
|
| 134 |
+
|
| 135 |
+
await ws_manager.connect(websocket, session_id)
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
while True:
|
| 139 |
+
# Keep connection alive, handle ping/pong
|
| 140 |
+
data = await websocket.receive_json()
|
| 141 |
+
|
| 142 |
+
# Handle client messages (e.g., ping)
|
| 143 |
+
if data.get("type") == "ping":
|
| 144 |
+
await websocket.send_json({"type": "pong"})
|
| 145 |
+
|
| 146 |
+
except WebSocketDisconnect:
|
| 147 |
+
logger.info(f"WebSocket disconnected for session {session_id}")
|
| 148 |
+
except Exception as e:
|
| 149 |
+
logger.error(f"WebSocket error for session {session_id}: {e}")
|
| 150 |
+
finally:
|
| 151 |
+
ws_manager.disconnect(session_id)
|
backend/routes/auth.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Authentication routes for HF OAuth."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import secrets
|
| 5 |
+
from urllib.parse import urlencode
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
from fastapi import APIRouter, HTTPException, Request
|
| 9 |
+
from fastapi.responses import RedirectResponse
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
| 12 |
+
|
| 13 |
+
# OAuth configuration from environment
|
| 14 |
+
OAUTH_CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID", "")
|
| 15 |
+
OAUTH_CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
|
| 16 |
+
OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
|
| 17 |
+
|
| 18 |
+
# In-memory session store (replace with proper session management in production)
|
| 19 |
+
oauth_states: dict[str, dict] = {}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_redirect_uri(request: Request) -> str:
|
| 23 |
+
"""Get the OAuth callback redirect URI."""
|
| 24 |
+
# In HF Spaces, use the SPACE_HOST if available
|
| 25 |
+
space_host = os.environ.get("SPACE_HOST")
|
| 26 |
+
if space_host:
|
| 27 |
+
return f"https://{space_host}/auth/callback"
|
| 28 |
+
# Otherwise construct from request
|
| 29 |
+
return str(request.url_for("oauth_callback"))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@router.get("/login")
|
| 33 |
+
async def oauth_login(request: Request) -> RedirectResponse:
|
| 34 |
+
"""Initiate OAuth login flow."""
|
| 35 |
+
if not OAUTH_CLIENT_ID:
|
| 36 |
+
raise HTTPException(
|
| 37 |
+
status_code=500,
|
| 38 |
+
detail="OAuth not configured. Set OAUTH_CLIENT_ID environment variable.",
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Generate state for CSRF protection
|
| 42 |
+
state = secrets.token_urlsafe(32)
|
| 43 |
+
oauth_states[state] = {"redirect_uri": get_redirect_uri(request)}
|
| 44 |
+
|
| 45 |
+
# Build authorization URL
|
| 46 |
+
params = {
|
| 47 |
+
"client_id": OAUTH_CLIENT_ID,
|
| 48 |
+
"redirect_uri": get_redirect_uri(request),
|
| 49 |
+
"scope": "openid profile",
|
| 50 |
+
"response_type": "code",
|
| 51 |
+
"state": state,
|
| 52 |
+
}
|
| 53 |
+
auth_url = f"{OPENID_PROVIDER_URL}/oauth/authorize?{urlencode(params)}"
|
| 54 |
+
|
| 55 |
+
return RedirectResponse(url=auth_url)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@router.get("/callback")
|
| 59 |
+
async def oauth_callback(
|
| 60 |
+
request: Request, code: str = "", state: str = ""
|
| 61 |
+
) -> RedirectResponse:
|
| 62 |
+
"""Handle OAuth callback."""
|
| 63 |
+
# Verify state
|
| 64 |
+
if state not in oauth_states:
|
| 65 |
+
raise HTTPException(status_code=400, detail="Invalid state parameter")
|
| 66 |
+
|
| 67 |
+
stored_state = oauth_states.pop(state)
|
| 68 |
+
redirect_uri = stored_state["redirect_uri"]
|
| 69 |
+
|
| 70 |
+
if not code:
|
| 71 |
+
raise HTTPException(status_code=400, detail="No authorization code provided")
|
| 72 |
+
|
| 73 |
+
# Exchange code for token
|
| 74 |
+
token_url = f"{OPENID_PROVIDER_URL}/oauth/token"
|
| 75 |
+
async with httpx.AsyncClient() as client:
|
| 76 |
+
try:
|
| 77 |
+
response = await client.post(
|
| 78 |
+
token_url,
|
| 79 |
+
data={
|
| 80 |
+
"grant_type": "authorization_code",
|
| 81 |
+
"code": code,
|
| 82 |
+
"redirect_uri": redirect_uri,
|
| 83 |
+
"client_id": OAUTH_CLIENT_ID,
|
| 84 |
+
"client_secret": OAUTH_CLIENT_SECRET,
|
| 85 |
+
},
|
| 86 |
+
)
|
| 87 |
+
response.raise_for_status()
|
| 88 |
+
token_data = response.json()
|
| 89 |
+
except httpx.HTTPError as e:
|
| 90 |
+
raise HTTPException(status_code=500, detail=f"Token exchange failed: {e}")
|
| 91 |
+
|
| 92 |
+
# Get user info
|
| 93 |
+
access_token = token_data.get("access_token")
|
| 94 |
+
if access_token:
|
| 95 |
+
async with httpx.AsyncClient() as client:
|
| 96 |
+
try:
|
| 97 |
+
userinfo_response = await client.get(
|
| 98 |
+
f"{OPENID_PROVIDER_URL}/oauth/userinfo",
|
| 99 |
+
headers={"Authorization": f"Bearer {access_token}"},
|
| 100 |
+
)
|
| 101 |
+
userinfo_response.raise_for_status()
|
| 102 |
+
user_info = userinfo_response.json()
|
| 103 |
+
except httpx.HTTPError:
|
| 104 |
+
user_info = {}
|
| 105 |
+
else:
|
| 106 |
+
user_info = {}
|
| 107 |
+
|
| 108 |
+
# For now, redirect to home with token in query params
|
| 109 |
+
# In production, use secure cookies or session storage
|
| 110 |
+
redirect_params = {
|
| 111 |
+
"access_token": access_token,
|
| 112 |
+
"username": user_info.get("preferred_username", ""),
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return RedirectResponse(url=f"/?{urlencode(redirect_params)}")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@router.get("/logout")
|
| 119 |
+
async def logout() -> RedirectResponse:
|
| 120 |
+
"""Log out the user."""
|
| 121 |
+
return RedirectResponse(url="/")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@router.get("/me")
|
| 125 |
+
async def get_current_user(request: Request) -> dict:
|
| 126 |
+
"""Get current user info from Authorization header."""
|
| 127 |
+
auth_header = request.headers.get("Authorization", "")
|
| 128 |
+
if not auth_header.startswith("Bearer "):
|
| 129 |
+
return {"authenticated": False}
|
| 130 |
+
|
| 131 |
+
token = auth_header.split(" ")[1]
|
| 132 |
+
|
| 133 |
+
async with httpx.AsyncClient() as client:
|
| 134 |
+
try:
|
| 135 |
+
response = await client.get(
|
| 136 |
+
f"{OPENID_PROVIDER_URL}/oauth/userinfo",
|
| 137 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 138 |
+
)
|
| 139 |
+
response.raise_for_status()
|
| 140 |
+
user_info = response.json()
|
| 141 |
+
return {
|
| 142 |
+
"authenticated": True,
|
| 143 |
+
"username": user_info.get("preferred_username"),
|
| 144 |
+
"name": user_info.get("name"),
|
| 145 |
+
"picture": user_info.get("picture"),
|
| 146 |
+
}
|
| 147 |
+
except httpx.HTTPError:
|
| 148 |
+
return {"authenticated": False}
|
backend/session_manager.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Session manager for handling multiple concurrent agent sessions."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import uuid
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Optional
|
| 10 |
+
|
| 11 |
+
from websocket import manager as ws_manager
|
| 12 |
+
|
| 13 |
+
from agent.config import load_config
|
| 14 |
+
from agent.core.agent_loop import process_submission
|
| 15 |
+
from agent.core.session import Event, OpType, Session
|
| 16 |
+
from agent.core.tools import ToolRouter
|
| 17 |
+
|
| 18 |
+
# Get project root (parent of backend directory)
|
| 19 |
+
PROJECT_ROOT = Path(__file__).parent.parent
|
| 20 |
+
DEFAULT_CONFIG_PATH = str(PROJECT_ROOT / "configs" / "main_agent_config.json")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# These dataclasses match agent/main.py structure
|
| 24 |
+
@dataclass
|
| 25 |
+
class Operation:
|
| 26 |
+
"""Operation to be executed by the agent."""
|
| 27 |
+
|
| 28 |
+
op_type: OpType
|
| 29 |
+
data: Optional[dict[str, Any]] = None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class Submission:
|
| 34 |
+
"""Submission to the agent loop."""
|
| 35 |
+
|
| 36 |
+
id: str
|
| 37 |
+
operation: Operation
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
logger = logging.getLogger(__name__)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclass
|
| 44 |
+
class AgentSession:
|
| 45 |
+
"""Wrapper for an agent session with its associated resources."""
|
| 46 |
+
|
| 47 |
+
session_id: str
|
| 48 |
+
session: Session
|
| 49 |
+
tool_router: ToolRouter
|
| 50 |
+
submission_queue: asyncio.Queue
|
| 51 |
+
task: asyncio.Task | None = None
|
| 52 |
+
created_at: datetime = field(default_factory=datetime.utcnow)
|
| 53 |
+
is_active: bool = True
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class SessionManager:
|
| 57 |
+
"""Manages multiple concurrent agent sessions."""
|
| 58 |
+
|
| 59 |
+
def __init__(self, config_path: str | None = None) -> None:
|
| 60 |
+
self.config = load_config(config_path or DEFAULT_CONFIG_PATH)
|
| 61 |
+
self.sessions: dict[str, AgentSession] = {}
|
| 62 |
+
self._lock = asyncio.Lock()
|
| 63 |
+
|
| 64 |
+
async def create_session(self) -> str:
|
| 65 |
+
"""Create a new agent session and return its ID."""
|
| 66 |
+
session_id = str(uuid.uuid4())
|
| 67 |
+
|
| 68 |
+
# Create queues for this session
|
| 69 |
+
submission_queue: asyncio.Queue = asyncio.Queue()
|
| 70 |
+
event_queue: asyncio.Queue = asyncio.Queue()
|
| 71 |
+
|
| 72 |
+
# Create tool router
|
| 73 |
+
tool_router = ToolRouter(self.config.mcpServers)
|
| 74 |
+
|
| 75 |
+
# Create the agent session
|
| 76 |
+
session = Session(event_queue, config=self.config, tool_router=tool_router)
|
| 77 |
+
|
| 78 |
+
# Create wrapper
|
| 79 |
+
agent_session = AgentSession(
|
| 80 |
+
session_id=session_id,
|
| 81 |
+
session=session,
|
| 82 |
+
tool_router=tool_router,
|
| 83 |
+
submission_queue=submission_queue,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
async with self._lock:
|
| 87 |
+
self.sessions[session_id] = agent_session
|
| 88 |
+
|
| 89 |
+
# Start the agent loop task
|
| 90 |
+
task = asyncio.create_task(
|
| 91 |
+
self._run_session(session_id, submission_queue, event_queue, tool_router)
|
| 92 |
+
)
|
| 93 |
+
agent_session.task = task
|
| 94 |
+
|
| 95 |
+
logger.info(f"Created session {session_id}")
|
| 96 |
+
return session_id
|
| 97 |
+
|
| 98 |
+
async def _run_session(
|
| 99 |
+
self,
|
| 100 |
+
session_id: str,
|
| 101 |
+
submission_queue: asyncio.Queue,
|
| 102 |
+
event_queue: asyncio.Queue,
|
| 103 |
+
tool_router: ToolRouter,
|
| 104 |
+
) -> None:
|
| 105 |
+
"""Run the agent loop for a session and forward events to WebSocket."""
|
| 106 |
+
agent_session = self.sessions.get(session_id)
|
| 107 |
+
if not agent_session:
|
| 108 |
+
logger.error(f"Session {session_id} not found")
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
session = agent_session.session
|
| 112 |
+
|
| 113 |
+
# Start event forwarder task
|
| 114 |
+
event_forwarder = asyncio.create_task(
|
| 115 |
+
self._forward_events(session_id, event_queue)
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
async with tool_router:
|
| 120 |
+
# Send ready event
|
| 121 |
+
await session.send_event(
|
| 122 |
+
Event(event_type="ready", data={"message": "Agent initialized"})
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
while session.is_running:
|
| 126 |
+
try:
|
| 127 |
+
# Wait for submission with timeout to allow checking is_running
|
| 128 |
+
submission = await asyncio.wait_for(
|
| 129 |
+
submission_queue.get(), timeout=1.0
|
| 130 |
+
)
|
| 131 |
+
should_continue = await process_submission(session, submission)
|
| 132 |
+
if not should_continue:
|
| 133 |
+
break
|
| 134 |
+
except asyncio.TimeoutError:
|
| 135 |
+
continue
|
| 136 |
+
except asyncio.CancelledError:
|
| 137 |
+
logger.info(f"Session {session_id} cancelled")
|
| 138 |
+
break
|
| 139 |
+
except Exception as e:
|
| 140 |
+
logger.error(f"Error in session {session_id}: {e}")
|
| 141 |
+
await session.send_event(
|
| 142 |
+
Event(event_type="error", data={"error": str(e)})
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
finally:
|
| 146 |
+
event_forwarder.cancel()
|
| 147 |
+
try:
|
| 148 |
+
await event_forwarder
|
| 149 |
+
except asyncio.CancelledError:
|
| 150 |
+
pass
|
| 151 |
+
|
| 152 |
+
async with self._lock:
|
| 153 |
+
if session_id in self.sessions:
|
| 154 |
+
self.sessions[session_id].is_active = False
|
| 155 |
+
|
| 156 |
+
logger.info(f"Session {session_id} ended")
|
| 157 |
+
|
| 158 |
+
async def _forward_events(
|
| 159 |
+
self, session_id: str, event_queue: asyncio.Queue
|
| 160 |
+
) -> None:
|
| 161 |
+
"""Forward events from the agent to the WebSocket."""
|
| 162 |
+
while True:
|
| 163 |
+
try:
|
| 164 |
+
event: Event = await event_queue.get()
|
| 165 |
+
await ws_manager.send_event(session_id, event.event_type, event.data)
|
| 166 |
+
except asyncio.CancelledError:
|
| 167 |
+
break
|
| 168 |
+
except Exception as e:
|
| 169 |
+
logger.error(f"Error forwarding event for {session_id}: {e}")
|
| 170 |
+
|
| 171 |
+
async def submit(self, session_id: str, operation: Operation) -> bool:
|
| 172 |
+
"""Submit an operation to a session."""
|
| 173 |
+
async with self._lock:
|
| 174 |
+
agent_session = self.sessions.get(session_id)
|
| 175 |
+
|
| 176 |
+
if not agent_session or not agent_session.is_active:
|
| 177 |
+
logger.warning(f"Session {session_id} not found or inactive")
|
| 178 |
+
return False
|
| 179 |
+
|
| 180 |
+
submission = Submission(id=f"sub_{uuid.uuid4().hex[:8]}", operation=operation)
|
| 181 |
+
await agent_session.submission_queue.put(submission)
|
| 182 |
+
return True
|
| 183 |
+
|
| 184 |
+
async def submit_user_input(self, session_id: str, text: str) -> bool:
|
| 185 |
+
"""Submit user input to a session."""
|
| 186 |
+
operation = Operation(op_type=OpType.USER_INPUT, data={"text": text})
|
| 187 |
+
return await self.submit(session_id, operation)
|
| 188 |
+
|
| 189 |
+
async def submit_approval(
|
| 190 |
+
self, session_id: str, approvals: list[dict[str, Any]]
|
| 191 |
+
) -> bool:
|
| 192 |
+
"""Submit tool approvals to a session."""
|
| 193 |
+
operation = Operation(
|
| 194 |
+
op_type=OpType.EXEC_APPROVAL, data={"approvals": approvals}
|
| 195 |
+
)
|
| 196 |
+
return await self.submit(session_id, operation)
|
| 197 |
+
|
| 198 |
+
async def interrupt(self, session_id: str) -> bool:
|
| 199 |
+
"""Interrupt a session."""
|
| 200 |
+
operation = Operation(op_type=OpType.INTERRUPT)
|
| 201 |
+
return await self.submit(session_id, operation)
|
| 202 |
+
|
| 203 |
+
async def undo(self, session_id: str) -> bool:
|
| 204 |
+
"""Undo last turn in a session."""
|
| 205 |
+
operation = Operation(op_type=OpType.UNDO)
|
| 206 |
+
return await self.submit(session_id, operation)
|
| 207 |
+
|
| 208 |
+
async def compact(self, session_id: str) -> bool:
|
| 209 |
+
"""Compact context in a session."""
|
| 210 |
+
operation = Operation(op_type=OpType.COMPACT)
|
| 211 |
+
return await self.submit(session_id, operation)
|
| 212 |
+
|
| 213 |
+
async def shutdown_session(self, session_id: str) -> bool:
|
| 214 |
+
"""Shutdown a specific session."""
|
| 215 |
+
operation = Operation(op_type=OpType.SHUTDOWN)
|
| 216 |
+
success = await self.submit(session_id, operation)
|
| 217 |
+
|
| 218 |
+
if success:
|
| 219 |
+
async with self._lock:
|
| 220 |
+
agent_session = self.sessions.get(session_id)
|
| 221 |
+
if agent_session and agent_session.task:
|
| 222 |
+
# Wait for task to complete
|
| 223 |
+
try:
|
| 224 |
+
await asyncio.wait_for(agent_session.task, timeout=5.0)
|
| 225 |
+
except asyncio.TimeoutError:
|
| 226 |
+
agent_session.task.cancel()
|
| 227 |
+
|
| 228 |
+
return success
|
| 229 |
+
|
| 230 |
+
async def delete_session(self, session_id: str) -> bool:
|
| 231 |
+
"""Delete a session entirely."""
|
| 232 |
+
async with self._lock:
|
| 233 |
+
agent_session = self.sessions.pop(session_id, None)
|
| 234 |
+
|
| 235 |
+
if not agent_session:
|
| 236 |
+
return False
|
| 237 |
+
|
| 238 |
+
# Cancel the task if running
|
| 239 |
+
if agent_session.task and not agent_session.task.done():
|
| 240 |
+
agent_session.task.cancel()
|
| 241 |
+
try:
|
| 242 |
+
await agent_session.task
|
| 243 |
+
except asyncio.CancelledError:
|
| 244 |
+
pass
|
| 245 |
+
|
| 246 |
+
return True
|
| 247 |
+
|
| 248 |
+
def get_session_info(self, session_id: str) -> dict[str, Any] | None:
|
| 249 |
+
"""Get information about a session."""
|
| 250 |
+
agent_session = self.sessions.get(session_id)
|
| 251 |
+
if not agent_session:
|
| 252 |
+
return None
|
| 253 |
+
|
| 254 |
+
return {
|
| 255 |
+
"session_id": session_id,
|
| 256 |
+
"created_at": agent_session.created_at.isoformat(),
|
| 257 |
+
"is_active": agent_session.is_active,
|
| 258 |
+
"message_count": len(agent_session.session.context_manager.items),
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
def list_sessions(self) -> list[dict[str, Any]]:
|
| 262 |
+
"""List all sessions."""
|
| 263 |
+
return [
|
| 264 |
+
self.get_session_info(sid)
|
| 265 |
+
for sid in self.sessions
|
| 266 |
+
if self.get_session_info(sid)
|
| 267 |
+
]
|
| 268 |
+
|
| 269 |
+
@property
|
| 270 |
+
def active_session_count(self) -> int:
|
| 271 |
+
"""Get count of active sessions."""
|
| 272 |
+
return sum(1 for s in self.sessions.values() if s.is_active)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
# Global session manager instance
|
| 276 |
+
session_manager = SessionManager()
|
backend/websocket.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""WebSocket connection manager for real-time communication."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from fastapi import WebSocket
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ConnectionManager:
|
| 13 |
+
"""Manages WebSocket connections for multiple sessions."""
|
| 14 |
+
|
| 15 |
+
def __init__(self) -> None:
|
| 16 |
+
# session_id -> WebSocket
|
| 17 |
+
self.active_connections: dict[str, WebSocket] = {}
|
| 18 |
+
# session_id -> asyncio.Queue for outgoing messages
|
| 19 |
+
self.message_queues: dict[str, asyncio.Queue] = {}
|
| 20 |
+
|
| 21 |
+
async def connect(self, websocket: WebSocket, session_id: str) -> None:
|
| 22 |
+
"""Accept a WebSocket connection and register it."""
|
| 23 |
+
logger.info(f"Attempting to accept WebSocket for session {session_id}")
|
| 24 |
+
await websocket.accept()
|
| 25 |
+
self.active_connections[session_id] = websocket
|
| 26 |
+
self.message_queues[session_id] = asyncio.Queue()
|
| 27 |
+
logger.info(f"WebSocket connected and registered for session {session_id}")
|
| 28 |
+
|
| 29 |
+
def disconnect(self, session_id: str) -> None:
|
| 30 |
+
"""Remove a WebSocket connection."""
|
| 31 |
+
if session_id in self.active_connections:
|
| 32 |
+
del self.active_connections[session_id]
|
| 33 |
+
if session_id in self.message_queues:
|
| 34 |
+
del self.message_queues[session_id]
|
| 35 |
+
logger.info(f"WebSocket disconnected for session {session_id}")
|
| 36 |
+
|
| 37 |
+
async def send_event(
|
| 38 |
+
self, session_id: str, event_type: str, data: dict[str, Any] | None = None
|
| 39 |
+
) -> None:
|
| 40 |
+
"""Send an event to a specific session's WebSocket."""
|
| 41 |
+
if session_id not in self.active_connections:
|
| 42 |
+
logger.warning(f"No active connection for session {session_id}")
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
message = {"event_type": event_type}
|
| 46 |
+
if data is not None:
|
| 47 |
+
message["data"] = data
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
await self.active_connections[session_id].send_json(message)
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logger.error(f"Error sending to session {session_id}: {e}")
|
| 53 |
+
self.disconnect(session_id)
|
| 54 |
+
|
| 55 |
+
async def broadcast(
|
| 56 |
+
self, event_type: str, data: dict[str, Any] | None = None
|
| 57 |
+
) -> None:
|
| 58 |
+
"""Broadcast an event to all connected sessions."""
|
| 59 |
+
for session_id in list(self.active_connections.keys()):
|
| 60 |
+
await self.send_event(session_id, event_type, data)
|
| 61 |
+
|
| 62 |
+
def is_connected(self, session_id: str) -> bool:
|
| 63 |
+
"""Check if a session has an active WebSocket connection."""
|
| 64 |
+
return session_id in self.active_connections
|
| 65 |
+
|
| 66 |
+
def get_queue(self, session_id: str) -> asyncio.Queue | None:
|
| 67 |
+
"""Get the message queue for a session."""
|
| 68 |
+
return self.message_queues.get(session_id)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# Global connection manager instance
|
| 72 |
+
manager = ConnectionManager()
|
configs/main_agent_config.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
"save_sessions": true,
|
| 4 |
"session_dataset_repo": "akseljoonas/hf-agent-sessions",
|
| 5 |
"yolo_mode": false,
|
| 6 |
-
"confirm_cpu_jobs":
|
| 7 |
"auto_file_upload": false,
|
| 8 |
"mcpServers": {
|
| 9 |
"hf-mcp-server": {
|
|
|
|
| 3 |
"save_sessions": true,
|
| 4 |
"session_dataset_repo": "akseljoonas/hf-agent-sessions",
|
| 5 |
"yolo_mode": false,
|
| 6 |
+
"confirm_cpu_jobs": true,
|
| 7 |
"auto_file_upload": false,
|
| 8 |
"mcpServers": {
|
| 9 |
"hf-mcp-server": {
|
eval/.amp_batch_solve.py.swp
DELETED
|
Binary file (12.3 kB)
|
|
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
|
| 7 |
+
export default tseslint.config(
|
| 8 |
+
{ ignores: ['dist'] },
|
| 9 |
+
{
|
| 10 |
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
languageOptions: {
|
| 13 |
+
ecmaVersion: 2020,
|
| 14 |
+
globals: globals.browser,
|
| 15 |
+
},
|
| 16 |
+
plugins: {
|
| 17 |
+
'react-hooks': reactHooks,
|
| 18 |
+
'react-refresh': reactRefresh,
|
| 19 |
+
},
|
| 20 |
+
rules: {
|
| 21 |
+
...reactHooks.configs.recommended.rules,
|
| 22 |
+
'react-refresh/only-export-components': [
|
| 23 |
+
'warn',
|
| 24 |
+
{ allowConstantExport: true },
|
| 25 |
+
],
|
| 26 |
+
},
|
| 27 |
+
},
|
| 28 |
+
)
|
frontend/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/png" href="/hf-log-only-white.png" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>HF Agent</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
| 11 |
+
</head>
|
| 12 |
+
<body style="margin: 0; padding: 0; background-color: #0D1117; color: #E6EDF3;">
|
| 13 |
+
<div id="root"></div>
|
| 14 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 15 |
+
</body>
|
| 16 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hf-agent-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@emotion/react": "^11.13.0",
|
| 14 |
+
"@emotion/styled": "^11.13.0",
|
| 15 |
+
"@mui/icons-material": "^6.1.0",
|
| 16 |
+
"@mui/material": "^6.1.0",
|
| 17 |
+
"react": "^18.3.1",
|
| 18 |
+
"react-dom": "^18.3.1",
|
| 19 |
+
"react-markdown": "^9.0.1",
|
| 20 |
+
"react-syntax-highlighter": "^16.1.0",
|
| 21 |
+
"remark-gfm": "^4.0.1",
|
| 22 |
+
"zustand": "^5.0.0"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@eslint/js": "^9.13.0",
|
| 26 |
+
"@types/react": "^18.3.12",
|
| 27 |
+
"@types/react-dom": "^18.3.1",
|
| 28 |
+
"@types/react-syntax-highlighter": "^15.5.13",
|
| 29 |
+
"@vitejs/plugin-react": "^4.3.3",
|
| 30 |
+
"eslint": "^9.13.0",
|
| 31 |
+
"eslint-plugin-react-hooks": "^5.0.0",
|
| 32 |
+
"eslint-plugin-react-refresh": "^0.4.13",
|
| 33 |
+
"globals": "^15.11.0",
|
| 34 |
+
"typescript": "~5.6.2",
|
| 35 |
+
"typescript-eslint": "^8.10.0",
|
| 36 |
+
"vite": "^5.4.10"
|
| 37 |
+
}
|
| 38 |
+
}
|
frontend/public/vite.svg
ADDED
|
|
frontend/src/App.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Box } from '@mui/material';
|
| 2 |
+
import AppLayout from '@/components/Layout/AppLayout';
|
| 3 |
+
|
| 4 |
+
function App() {
|
| 5 |
+
return (
|
| 6 |
+
<Box sx={{ height: '100vh', display: 'flex' }}>
|
| 7 |
+
<AppLayout />
|
| 8 |
+
</Box>
|
| 9 |
+
);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default App;
|
frontend/src/components/ApprovalModal/ApprovalModal.tsx
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Dialog,
|
| 4 |
+
DialogTitle,
|
| 5 |
+
DialogContent,
|
| 6 |
+
DialogActions,
|
| 7 |
+
Button,
|
| 8 |
+
Box,
|
| 9 |
+
Typography,
|
| 10 |
+
Checkbox,
|
| 11 |
+
FormControlLabel,
|
| 12 |
+
Accordion,
|
| 13 |
+
AccordionSummary,
|
| 14 |
+
AccordionDetails,
|
| 15 |
+
TextField,
|
| 16 |
+
Chip,
|
| 17 |
+
} from '@mui/material';
|
| 18 |
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
| 19 |
+
import WarningIcon from '@mui/icons-material/Warning';
|
| 20 |
+
import { useAgentStore } from '@/store/agentStore';
|
| 21 |
+
|
| 22 |
+
interface ApprovalModalProps {
|
| 23 |
+
sessionId: string | null;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
interface ApprovalState {
|
| 27 |
+
[toolCallId: string]: {
|
| 28 |
+
approved: boolean;
|
| 29 |
+
feedback: string;
|
| 30 |
+
};
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export default function ApprovalModal({ sessionId }: ApprovalModalProps) {
|
| 34 |
+
const { pendingApprovals, setPendingApprovals } = useAgentStore();
|
| 35 |
+
const [approvalState, setApprovalState] = useState<ApprovalState>({});
|
| 36 |
+
|
| 37 |
+
const isOpen = pendingApprovals !== null && pendingApprovals.tools.length > 0;
|
| 38 |
+
|
| 39 |
+
const handleApprovalChange = useCallback(
|
| 40 |
+
(toolCallId: string, approved: boolean) => {
|
| 41 |
+
setApprovalState((prev) => ({
|
| 42 |
+
...prev,
|
| 43 |
+
[toolCallId]: {
|
| 44 |
+
...prev[toolCallId],
|
| 45 |
+
approved,
|
| 46 |
+
feedback: prev[toolCallId]?.feedback || '',
|
| 47 |
+
},
|
| 48 |
+
}));
|
| 49 |
+
},
|
| 50 |
+
[]
|
| 51 |
+
);
|
| 52 |
+
|
| 53 |
+
const handleFeedbackChange = useCallback(
|
| 54 |
+
(toolCallId: string, feedback: string) => {
|
| 55 |
+
setApprovalState((prev) => ({
|
| 56 |
+
...prev,
|
| 57 |
+
[toolCallId]: {
|
| 58 |
+
...prev[toolCallId],
|
| 59 |
+
feedback,
|
| 60 |
+
},
|
| 61 |
+
}));
|
| 62 |
+
},
|
| 63 |
+
[]
|
| 64 |
+
);
|
| 65 |
+
|
| 66 |
+
const handleSubmit = useCallback(async () => {
|
| 67 |
+
if (!sessionId || !pendingApprovals) return;
|
| 68 |
+
|
| 69 |
+
const approvals = pendingApprovals.tools.map((tool) => ({
|
| 70 |
+
tool_call_id: tool.tool_call_id,
|
| 71 |
+
approved: approvalState[tool.tool_call_id]?.approved ?? false,
|
| 72 |
+
feedback: approvalState[tool.tool_call_id]?.feedback || null,
|
| 73 |
+
}));
|
| 74 |
+
|
| 75 |
+
try {
|
| 76 |
+
await fetch('/api/approve', {
|
| 77 |
+
method: 'POST',
|
| 78 |
+
headers: { 'Content-Type': 'application/json' },
|
| 79 |
+
body: JSON.stringify({
|
| 80 |
+
session_id: sessionId,
|
| 81 |
+
approvals,
|
| 82 |
+
}),
|
| 83 |
+
});
|
| 84 |
+
setPendingApprovals(null);
|
| 85 |
+
setApprovalState({});
|
| 86 |
+
} catch (e) {
|
| 87 |
+
console.error('Approval submission failed:', e);
|
| 88 |
+
}
|
| 89 |
+
}, [sessionId, pendingApprovals, approvalState, setPendingApprovals]);
|
| 90 |
+
|
| 91 |
+
const handleApproveAll = useCallback(() => {
|
| 92 |
+
if (!pendingApprovals) return;
|
| 93 |
+
const newState: ApprovalState = {};
|
| 94 |
+
pendingApprovals.tools.forEach((tool) => {
|
| 95 |
+
newState[tool.tool_call_id] = { approved: true, feedback: '' };
|
| 96 |
+
});
|
| 97 |
+
setApprovalState(newState);
|
| 98 |
+
}, [pendingApprovals]);
|
| 99 |
+
|
| 100 |
+
const handleRejectAll = useCallback(() => {
|
| 101 |
+
if (!pendingApprovals) return;
|
| 102 |
+
const newState: ApprovalState = {};
|
| 103 |
+
pendingApprovals.tools.forEach((tool) => {
|
| 104 |
+
newState[tool.tool_call_id] = { approved: false, feedback: '' };
|
| 105 |
+
});
|
| 106 |
+
setApprovalState(newState);
|
| 107 |
+
}, [pendingApprovals]);
|
| 108 |
+
|
| 109 |
+
if (!isOpen || !pendingApprovals) return null;
|
| 110 |
+
|
| 111 |
+
const approvedCount = Object.values(approvalState).filter((s) => s.approved).length;
|
| 112 |
+
|
| 113 |
+
return (
|
| 114 |
+
<Dialog
|
| 115 |
+
open={isOpen}
|
| 116 |
+
maxWidth="md"
|
| 117 |
+
fullWidth
|
| 118 |
+
PaperProps={{
|
| 119 |
+
sx: { bgcolor: 'background.paper' },
|
| 120 |
+
}}
|
| 121 |
+
>
|
| 122 |
+
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
| 123 |
+
<WarningIcon color="warning" />
|
| 124 |
+
Approval Required
|
| 125 |
+
<Chip
|
| 126 |
+
label={`${pendingApprovals.count} tool${pendingApprovals.count > 1 ? 's' : ''}`}
|
| 127 |
+
size="small"
|
| 128 |
+
sx={{ ml: 1 }}
|
| 129 |
+
/>
|
| 130 |
+
</DialogTitle>
|
| 131 |
+
<DialogContent dividers>
|
| 132 |
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
| 133 |
+
The following tool calls require your approval before execution:
|
| 134 |
+
</Typography>
|
| 135 |
+
{pendingApprovals.tools.map((tool, index) => (
|
| 136 |
+
<Accordion key={tool.tool_call_id} defaultExpanded={index === 0}>
|
| 137 |
+
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
| 138 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
|
| 139 |
+
<FormControlLabel
|
| 140 |
+
control={
|
| 141 |
+
<Checkbox
|
| 142 |
+
checked={approvalState[tool.tool_call_id]?.approved ?? false}
|
| 143 |
+
onChange={(e) => {
|
| 144 |
+
e.stopPropagation();
|
| 145 |
+
handleApprovalChange(tool.tool_call_id, e.target.checked);
|
| 146 |
+
}}
|
| 147 |
+
onClick={(e) => e.stopPropagation()}
|
| 148 |
+
/>
|
| 149 |
+
}
|
| 150 |
+
label=""
|
| 151 |
+
sx={{ m: 0 }}
|
| 152 |
+
/>
|
| 153 |
+
<Chip label={tool.tool} size="small" color="primary" variant="outlined" />
|
| 154 |
+
<Typography variant="body2" color="text.secondary" sx={{ ml: 'auto' }}>
|
| 155 |
+
{approvalState[tool.tool_call_id]?.approved ? 'Approved' : 'Pending'}
|
| 156 |
+
</Typography>
|
| 157 |
+
</Box>
|
| 158 |
+
</AccordionSummary>
|
| 159 |
+
<AccordionDetails>
|
| 160 |
+
<Typography variant="subtitle2" gutterBottom>
|
| 161 |
+
Arguments:
|
| 162 |
+
</Typography>
|
| 163 |
+
<Box
|
| 164 |
+
component="pre"
|
| 165 |
+
sx={{
|
| 166 |
+
bgcolor: 'background.default',
|
| 167 |
+
p: 1.5,
|
| 168 |
+
borderRadius: 1,
|
| 169 |
+
overflow: 'auto',
|
| 170 |
+
fontSize: '0.8rem',
|
| 171 |
+
maxHeight: 200,
|
| 172 |
+
}}
|
| 173 |
+
>
|
| 174 |
+
{JSON.stringify(tool.arguments, null, 2)}
|
| 175 |
+
</Box>
|
| 176 |
+
{!approvalState[tool.tool_call_id]?.approved && (
|
| 177 |
+
<TextField
|
| 178 |
+
fullWidth
|
| 179 |
+
size="small"
|
| 180 |
+
label="Feedback (optional)"
|
| 181 |
+
placeholder="Explain why you're rejecting this..."
|
| 182 |
+
value={approvalState[tool.tool_call_id]?.feedback || ''}
|
| 183 |
+
onChange={(e) => handleFeedbackChange(tool.tool_call_id, e.target.value)}
|
| 184 |
+
sx={{ mt: 2 }}
|
| 185 |
+
/>
|
| 186 |
+
)}
|
| 187 |
+
</AccordionDetails>
|
| 188 |
+
</Accordion>
|
| 189 |
+
))}
|
| 190 |
+
</DialogContent>
|
| 191 |
+
<DialogActions sx={{ px: 3, py: 2 }}>
|
| 192 |
+
<Button onClick={handleRejectAll} color="error" variant="outlined">
|
| 193 |
+
Reject All
|
| 194 |
+
</Button>
|
| 195 |
+
<Button onClick={handleApproveAll} color="success" variant="outlined">
|
| 196 |
+
Approve All
|
| 197 |
+
</Button>
|
| 198 |
+
<Box sx={{ flex: 1 }} />
|
| 199 |
+
<Typography variant="body2" color="text.secondary" sx={{ mr: 2 }}>
|
| 200 |
+
{approvedCount} of {pendingApprovals.count} approved
|
| 201 |
+
</Typography>
|
| 202 |
+
<Button onClick={handleSubmit} variant="contained" color="primary">
|
| 203 |
+
Submit
|
| 204 |
+
</Button>
|
| 205 |
+
</DialogActions>
|
| 206 |
+
</Dialog>
|
| 207 |
+
);
|
| 208 |
+
}
|
frontend/src/components/Chat/ApprovalFlow.tsx
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback, useEffect } from 'react';
|
| 2 |
+
import { Box, Typography, Button, TextField, IconButton, Link } from '@mui/material';
|
| 3 |
+
import SendIcon from '@mui/icons-material/Send';
|
| 4 |
+
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
| 5 |
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
| 6 |
+
import CancelIcon from '@mui/icons-material/Cancel';
|
| 7 |
+
import LaunchIcon from '@mui/icons-material/Launch';
|
| 8 |
+
import { useAgentStore } from '@/store/agentStore';
|
| 9 |
+
import { useLayoutStore } from '@/store/layoutStore';
|
| 10 |
+
import { useSessionStore } from '@/store/sessionStore';
|
| 11 |
+
import type { Message, ToolApproval } from '@/types/agent';
|
| 12 |
+
|
| 13 |
+
interface ApprovalFlowProps {
|
| 14 |
+
message: Message;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default function ApprovalFlow({ message }: ApprovalFlowProps) {
|
| 18 |
+
const { setPanelContent, setPanelTab, setActivePanelTab, clearPanelTabs, updateMessage } = useAgentStore();
|
| 19 |
+
const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
|
| 20 |
+
const { activeSessionId } = useSessionStore();
|
| 21 |
+
const [currentIndex, setCurrentIndex] = useState(0);
|
| 22 |
+
const [feedback, setFeedback] = useState('');
|
| 23 |
+
const [decisions, setDecisions] = useState<ToolApproval[]>([]);
|
| 24 |
+
|
| 25 |
+
const approvalData = message.approval;
|
| 26 |
+
|
| 27 |
+
if (!approvalData) return null;
|
| 28 |
+
|
| 29 |
+
const { batch, status } = approvalData;
|
| 30 |
+
|
| 31 |
+
// Parse toolOutput to extract job info (URL, status, logs)
|
| 32 |
+
let logsContent = '';
|
| 33 |
+
let showLogsButton = false;
|
| 34 |
+
let jobUrl = '';
|
| 35 |
+
let jobId = '';
|
| 36 |
+
let jobStatus = '';
|
| 37 |
+
let jobFailed = false;
|
| 38 |
+
|
| 39 |
+
if (message.toolOutput) {
|
| 40 |
+
// Extract job URL: **View at:** https://...
|
| 41 |
+
const urlMatch = message.toolOutput.match(/\*\*View at:\*\*\s*(https:\/\/[^\s\n]+)/);
|
| 42 |
+
if (urlMatch) {
|
| 43 |
+
jobUrl = urlMatch[1];
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Extract job ID: **Job ID:** ...
|
| 47 |
+
const idMatch = message.toolOutput.match(/\*\*Job ID:\*\*\s*([^\s\n]+)/);
|
| 48 |
+
if (idMatch) {
|
| 49 |
+
jobId = idMatch[1];
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Extract job status: **Final Status:** ...
|
| 53 |
+
const statusMatch = message.toolOutput.match(/\*\*Final Status:\*\*\s*([^\n]+)/);
|
| 54 |
+
if (statusMatch) {
|
| 55 |
+
jobStatus = statusMatch[1].trim();
|
| 56 |
+
jobFailed = jobStatus.toLowerCase().includes('error') || jobStatus.toLowerCase().includes('failed');
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Extract logs
|
| 60 |
+
if (message.toolOutput.includes('**Logs:**')) {
|
| 61 |
+
const parts = message.toolOutput.split('**Logs:**');
|
| 62 |
+
if (parts.length > 1) {
|
| 63 |
+
const logsPart = parts[1].trim();
|
| 64 |
+
const codeBlockMatch = logsPart.match(/```([\s\S]*?)```/);
|
| 65 |
+
if (codeBlockMatch) {
|
| 66 |
+
logsContent = codeBlockMatch[1].trim();
|
| 67 |
+
showLogsButton = true;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Sync right panel with current tool
|
| 74 |
+
useEffect(() => {
|
| 75 |
+
if (!batch || currentIndex >= batch.tools.length) return;
|
| 76 |
+
|
| 77 |
+
// Only auto-open panel if pending
|
| 78 |
+
if (status !== 'pending') return;
|
| 79 |
+
|
| 80 |
+
const tool = batch.tools[currentIndex];
|
| 81 |
+
const args = tool.arguments as any;
|
| 82 |
+
|
| 83 |
+
if (tool.tool === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
|
| 84 |
+
setPanelContent({
|
| 85 |
+
title: 'Compute Job Script',
|
| 86 |
+
content: args.script,
|
| 87 |
+
language: 'python',
|
| 88 |
+
parameters: args
|
| 89 |
+
});
|
| 90 |
+
// Don't auto-open if already resolved
|
| 91 |
+
} else if (tool.tool === 'hf_repo_files' && args.operation === 'upload' && args.content) {
|
| 92 |
+
setPanelContent({
|
| 93 |
+
title: `File Upload: ${args.path || 'unnamed'}`,
|
| 94 |
+
content: args.content,
|
| 95 |
+
parameters: args
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
}, [currentIndex, batch, status, setPanelContent]);
|
| 99 |
+
|
| 100 |
+
const handleResolve = useCallback(async (approved: boolean) => {
|
| 101 |
+
if (!batch || !activeSessionId) return;
|
| 102 |
+
|
| 103 |
+
const currentTool = batch.tools[currentIndex];
|
| 104 |
+
const newDecisions = [
|
| 105 |
+
...decisions,
|
| 106 |
+
{
|
| 107 |
+
tool_call_id: currentTool.tool_call_id,
|
| 108 |
+
approved,
|
| 109 |
+
feedback: approved ? null : feedback || 'Rejected by user',
|
| 110 |
+
},
|
| 111 |
+
];
|
| 112 |
+
|
| 113 |
+
if (currentIndex < batch.tools.length - 1) {
|
| 114 |
+
setDecisions(newDecisions);
|
| 115 |
+
setCurrentIndex(currentIndex + 1);
|
| 116 |
+
setFeedback('');
|
| 117 |
+
} else {
|
| 118 |
+
// All tools in batch resolved
|
| 119 |
+
try {
|
| 120 |
+
await fetch('/api/approve', {
|
| 121 |
+
method: 'POST',
|
| 122 |
+
headers: { 'Content-Type': 'application/json' },
|
| 123 |
+
body: JSON.stringify({
|
| 124 |
+
session_id: activeSessionId,
|
| 125 |
+
approvals: newDecisions,
|
| 126 |
+
}),
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
// Update message status
|
| 130 |
+
updateMessage(activeSessionId, message.id, {
|
| 131 |
+
approval: {
|
| 132 |
+
...approvalData!,
|
| 133 |
+
status: approved ? 'approved' : 'rejected',
|
| 134 |
+
decisions: newDecisions
|
| 135 |
+
}
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
} catch (e) {
|
| 139 |
+
console.error('Approval submission failed:', e);
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
}, [activeSessionId, message.id, batch, currentIndex, feedback, decisions, approvalData, updateMessage]);
|
| 143 |
+
|
| 144 |
+
if (!batch || currentIndex >= batch.tools.length) return null;
|
| 145 |
+
|
| 146 |
+
const currentTool = batch.tools[currentIndex];
|
| 147 |
+
|
| 148 |
+
// Check if script contains push_to_hub or upload_file
|
| 149 |
+
const args = currentTool.arguments as any;
|
| 150 |
+
const containsPushToHub = currentTool.tool === 'hf_jobs' && args.script && (args.script.includes('push_to_hub') || args.script.includes('upload_file'));
|
| 151 |
+
|
| 152 |
+
const getToolDescription = (toolName: string, args: any) => {
|
| 153 |
+
if (toolName === 'hf_jobs') {
|
| 154 |
+
return (
|
| 155 |
+
<Box sx={{ flex: 1 }}>
|
| 156 |
+
<Typography variant="body2" sx={{ color: 'var(--muted-text)' }}>
|
| 157 |
+
The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>hf_jobs</Box> on{' '}
|
| 158 |
+
<Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.hardware_flavor || 'default'}</Box> with a timeout of{' '}
|
| 159 |
+
<Box component="span" sx={{ fontWeight: 500, color: 'var(--text)' }}>{args.timeout || '30m'}</Box>
|
| 160 |
+
</Typography>
|
| 161 |
+
</Box>
|
| 162 |
+
);
|
| 163 |
+
}
|
| 164 |
+
return (
|
| 165 |
+
<Typography variant="body2" sx={{ color: 'var(--muted-text)', flex: 1 }}>
|
| 166 |
+
The agent wants to execute <Box component="span" sx={{ color: 'var(--accent-yellow)', fontWeight: 500 }}>{toolName}</Box>
|
| 167 |
+
</Typography>
|
| 168 |
+
);
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
const showCode = () => {
|
| 172 |
+
const args = currentTool.arguments as any;
|
| 173 |
+
if (currentTool.tool === 'hf_jobs' && args.script) {
|
| 174 |
+
// Clear existing tabs and set up script tab (and logs if available)
|
| 175 |
+
clearPanelTabs();
|
| 176 |
+
setPanelTab({
|
| 177 |
+
id: 'script',
|
| 178 |
+
title: 'Script',
|
| 179 |
+
content: args.script,
|
| 180 |
+
language: 'python',
|
| 181 |
+
parameters: args
|
| 182 |
+
});
|
| 183 |
+
// If logs are available (job completed), also add logs tab
|
| 184 |
+
if (logsContent) {
|
| 185 |
+
setPanelTab({
|
| 186 |
+
id: 'logs',
|
| 187 |
+
title: 'Logs',
|
| 188 |
+
content: logsContent,
|
| 189 |
+
language: 'text'
|
| 190 |
+
});
|
| 191 |
+
}
|
| 192 |
+
setActivePanelTab('script');
|
| 193 |
+
setRightPanelOpen(true);
|
| 194 |
+
setLeftSidebarOpen(false);
|
| 195 |
+
} else {
|
| 196 |
+
setPanelContent({
|
| 197 |
+
title: `Tool: ${currentTool.tool}`,
|
| 198 |
+
content: JSON.stringify(args, null, 2),
|
| 199 |
+
language: 'json',
|
| 200 |
+
parameters: args
|
| 201 |
+
});
|
| 202 |
+
setRightPanelOpen(true);
|
| 203 |
+
setLeftSidebarOpen(false);
|
| 204 |
+
}
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
const handleViewLogs = (e: React.MouseEvent) => {
|
| 208 |
+
e.stopPropagation();
|
| 209 |
+
const args = currentTool.arguments as any;
|
| 210 |
+
// Set up both tabs so user can switch between script and logs
|
| 211 |
+
clearPanelTabs();
|
| 212 |
+
if (currentTool.tool === 'hf_jobs' && args.script) {
|
| 213 |
+
setPanelTab({
|
| 214 |
+
id: 'script',
|
| 215 |
+
title: 'Script',
|
| 216 |
+
content: args.script,
|
| 217 |
+
language: 'python',
|
| 218 |
+
parameters: args
|
| 219 |
+
});
|
| 220 |
+
}
|
| 221 |
+
setPanelTab({
|
| 222 |
+
id: 'logs',
|
| 223 |
+
title: 'Logs',
|
| 224 |
+
content: logsContent,
|
| 225 |
+
language: 'text'
|
| 226 |
+
});
|
| 227 |
+
setActivePanelTab('logs');
|
| 228 |
+
setRightPanelOpen(true);
|
| 229 |
+
setLeftSidebarOpen(false);
|
| 230 |
+
};
|
| 231 |
+
|
| 232 |
+
return (
|
| 233 |
+
<Box
|
| 234 |
+
className="action-card"
|
| 235 |
+
sx={{
|
| 236 |
+
width: '100%',
|
| 237 |
+
padding: '18px',
|
| 238 |
+
borderRadius: 'var(--radius-md)',
|
| 239 |
+
background: 'linear-gradient(180deg, rgba(255,255,255,0.015), transparent)',
|
| 240 |
+
border: '1px solid rgba(255,255,255,0.03)',
|
| 241 |
+
display: 'flex',
|
| 242 |
+
flexDirection: 'column',
|
| 243 |
+
gap: '12px',
|
| 244 |
+
opacity: status !== 'pending' && !showLogsButton ? 0.8 : 1
|
| 245 |
+
}}
|
| 246 |
+
>
|
| 247 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
| 248 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'var(--text)' }}>
|
| 249 |
+
{status === 'pending' ? 'Approval Required' : status === 'approved' ? 'Approved' : 'Rejected'}
|
| 250 |
+
</Typography>
|
| 251 |
+
<Typography variant="caption" sx={{ color: 'var(--muted-text)' }}>
|
| 252 |
+
({currentIndex + 1}/{batch.count})
|
| 253 |
+
</Typography>
|
| 254 |
+
{status === 'approved' && <CheckCircleIcon sx={{ fontSize: 18, color: 'var(--accent-green)' }} />}
|
| 255 |
+
{status === 'rejected' && <CancelIcon sx={{ fontSize: 18, color: 'var(--accent-red)' }} />}
|
| 256 |
+
</Box>
|
| 257 |
+
|
| 258 |
+
<Box
|
| 259 |
+
onClick={showCode}
|
| 260 |
+
sx={{
|
| 261 |
+
display: 'flex',
|
| 262 |
+
alignItems: 'center',
|
| 263 |
+
gap: 1,
|
| 264 |
+
cursor: 'pointer',
|
| 265 |
+
p: 1.5,
|
| 266 |
+
borderRadius: '8px',
|
| 267 |
+
bgcolor: 'rgba(0,0,0,0.2)',
|
| 268 |
+
border: '1px solid rgba(255,255,255,0.05)',
|
| 269 |
+
transition: 'all 0.2s',
|
| 270 |
+
'&:hover': {
|
| 271 |
+
bgcolor: 'rgba(255,255,255,0.03)',
|
| 272 |
+
borderColor: 'var(--accent-primary)',
|
| 273 |
+
}
|
| 274 |
+
}}
|
| 275 |
+
>
|
| 276 |
+
{getToolDescription(currentTool.tool, currentTool.arguments)}
|
| 277 |
+
<OpenInNewIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.7 }} />
|
| 278 |
+
</Box>
|
| 279 |
+
|
| 280 |
+
{currentTool.tool === 'hf_jobs' && (
|
| 281 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
| 282 |
+
{/* Show specific job URL if available (after execution), otherwise generic link */}
|
| 283 |
+
{jobUrl ? (
|
| 284 |
+
<Link
|
| 285 |
+
href={jobUrl}
|
| 286 |
+
target="_blank"
|
| 287 |
+
rel="noopener noreferrer"
|
| 288 |
+
sx={{
|
| 289 |
+
display: 'flex',
|
| 290 |
+
alignItems: 'center',
|
| 291 |
+
gap: 0.5,
|
| 292 |
+
color: 'var(--accent-primary)',
|
| 293 |
+
fontSize: '0.8rem',
|
| 294 |
+
textDecoration: 'none',
|
| 295 |
+
opacity: 0.9,
|
| 296 |
+
'&:hover': {
|
| 297 |
+
opacity: 1,
|
| 298 |
+
textDecoration: 'underline',
|
| 299 |
+
}
|
| 300 |
+
}}
|
| 301 |
+
>
|
| 302 |
+
<LaunchIcon sx={{ fontSize: 14 }} />
|
| 303 |
+
View job{jobId ? ` (${jobId.substring(0, 8)}...)` : ''} on Hugging Face
|
| 304 |
+
</Link>
|
| 305 |
+
) : (
|
| 306 |
+
<Link
|
| 307 |
+
href="https://huggingface.co/settings/jobs"
|
| 308 |
+
target="_blank"
|
| 309 |
+
rel="noopener noreferrer"
|
| 310 |
+
sx={{
|
| 311 |
+
display: 'flex',
|
| 312 |
+
alignItems: 'center',
|
| 313 |
+
gap: 0.5,
|
| 314 |
+
color: 'var(--muted-text)',
|
| 315 |
+
fontSize: '0.8rem',
|
| 316 |
+
textDecoration: 'none',
|
| 317 |
+
opacity: 0.7,
|
| 318 |
+
'&:hover': {
|
| 319 |
+
opacity: 1,
|
| 320 |
+
textDecoration: 'underline',
|
| 321 |
+
}
|
| 322 |
+
}}
|
| 323 |
+
>
|
| 324 |
+
<LaunchIcon sx={{ fontSize: 14 }} />
|
| 325 |
+
View all jobs on Hugging Face
|
| 326 |
+
</Link>
|
| 327 |
+
)}
|
| 328 |
+
|
| 329 |
+
{/* Show job status if available */}
|
| 330 |
+
{jobStatus && (
|
| 331 |
+
<Typography
|
| 332 |
+
variant="caption"
|
| 333 |
+
sx={{
|
| 334 |
+
color: jobFailed ? 'var(--accent-red)' : 'var(--accent-green)',
|
| 335 |
+
fontSize: '0.75rem',
|
| 336 |
+
fontWeight: 500,
|
| 337 |
+
}}
|
| 338 |
+
>
|
| 339 |
+
Status: {jobStatus}
|
| 340 |
+
</Typography>
|
| 341 |
+
)}
|
| 342 |
+
</Box>
|
| 343 |
+
)}
|
| 344 |
+
|
| 345 |
+
{containsPushToHub && (
|
| 346 |
+
<Typography variant="caption" sx={{ color: 'var(--accent-green)', fontSize: '0.75rem', opacity: 0.8, px: 0.5 }}>
|
| 347 |
+
We've detected the result will be pushed to hub.
|
| 348 |
+
</Typography>
|
| 349 |
+
)}
|
| 350 |
+
|
| 351 |
+
{/* Show script/logs buttons for completed jobs */}
|
| 352 |
+
{status !== 'pending' && currentTool.tool === 'hf_jobs' && (args.script || showLogsButton) && (
|
| 353 |
+
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
| 354 |
+
{args.script && (
|
| 355 |
+
<Button
|
| 356 |
+
variant="outlined"
|
| 357 |
+
size="small"
|
| 358 |
+
startIcon={<OpenInNewIcon />}
|
| 359 |
+
onClick={showCode}
|
| 360 |
+
sx={{
|
| 361 |
+
textTransform: 'none',
|
| 362 |
+
borderColor: 'rgba(255,255,255,0.1)',
|
| 363 |
+
color: 'var(--muted-text)',
|
| 364 |
+
fontSize: '0.75rem',
|
| 365 |
+
py: 0.5,
|
| 366 |
+
'&:hover': {
|
| 367 |
+
borderColor: 'var(--accent-primary)',
|
| 368 |
+
color: 'var(--accent-primary)',
|
| 369 |
+
bgcolor: 'rgba(255,255,255,0.03)'
|
| 370 |
+
}
|
| 371 |
+
}}
|
| 372 |
+
>
|
| 373 |
+
View Script
|
| 374 |
+
</Button>
|
| 375 |
+
)}
|
| 376 |
+
{showLogsButton && (
|
| 377 |
+
<Button
|
| 378 |
+
variant="outlined"
|
| 379 |
+
size="small"
|
| 380 |
+
startIcon={<OpenInNewIcon />}
|
| 381 |
+
onClick={handleViewLogs}
|
| 382 |
+
sx={{
|
| 383 |
+
textTransform: 'none',
|
| 384 |
+
borderColor: 'rgba(255,255,255,0.1)',
|
| 385 |
+
color: 'var(--accent-primary)',
|
| 386 |
+
fontSize: '0.75rem',
|
| 387 |
+
py: 0.5,
|
| 388 |
+
'&:hover': {
|
| 389 |
+
borderColor: 'var(--accent-primary)',
|
| 390 |
+
bgcolor: 'rgba(255,255,255,0.03)'
|
| 391 |
+
}
|
| 392 |
+
}}
|
| 393 |
+
>
|
| 394 |
+
View Logs
|
| 395 |
+
</Button>
|
| 396 |
+
)}
|
| 397 |
+
</Box>
|
| 398 |
+
)}
|
| 399 |
+
|
| 400 |
+
{status === 'pending' && (
|
| 401 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 402 |
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
| 403 |
+
<TextField
|
| 404 |
+
fullWidth
|
| 405 |
+
size="small"
|
| 406 |
+
placeholder="Feedback (optional)"
|
| 407 |
+
value={feedback}
|
| 408 |
+
onChange={(e) => setFeedback(e.target.value)}
|
| 409 |
+
variant="outlined"
|
| 410 |
+
sx={{
|
| 411 |
+
'& .MuiOutlinedInput-root': {
|
| 412 |
+
bgcolor: 'rgba(0,0,0,0.2)',
|
| 413 |
+
fontFamily: 'inherit',
|
| 414 |
+
fontSize: '0.9rem'
|
| 415 |
+
}
|
| 416 |
+
}}
|
| 417 |
+
/>
|
| 418 |
+
<IconButton
|
| 419 |
+
onClick={() => handleResolve(false)}
|
| 420 |
+
disabled={!feedback}
|
| 421 |
+
title="Reject with feedback"
|
| 422 |
+
sx={{
|
| 423 |
+
color: 'var(--accent-red)',
|
| 424 |
+
border: '1px solid rgba(255,255,255,0.05)',
|
| 425 |
+
borderRadius: '8px',
|
| 426 |
+
width: 40,
|
| 427 |
+
height: 40,
|
| 428 |
+
'&:hover': {
|
| 429 |
+
bgcolor: 'rgba(224, 90, 79, 0.1)',
|
| 430 |
+
borderColor: 'var(--accent-red)',
|
| 431 |
+
},
|
| 432 |
+
'&.Mui-disabled': {
|
| 433 |
+
color: 'rgba(255,255,255,0.1)',
|
| 434 |
+
borderColor: 'rgba(255,255,255,0.02)'
|
| 435 |
+
}
|
| 436 |
+
}}
|
| 437 |
+
>
|
| 438 |
+
<SendIcon fontSize="small" />
|
| 439 |
+
</IconButton>
|
| 440 |
+
</Box>
|
| 441 |
+
|
| 442 |
+
<Box className="action-buttons" sx={{ display: 'flex', gap: '10px' }}>
|
| 443 |
+
<Button
|
| 444 |
+
className="btn-reject"
|
| 445 |
+
onClick={() => handleResolve(false)}
|
| 446 |
+
sx={{
|
| 447 |
+
flex: 1,
|
| 448 |
+
background: 'transparent',
|
| 449 |
+
border: '1px solid rgba(255,255,255,0.05)',
|
| 450 |
+
color: 'var(--accent-red)',
|
| 451 |
+
padding: '10px 14px',
|
| 452 |
+
borderRadius: '10px',
|
| 453 |
+
'&:hover': {
|
| 454 |
+
bgcolor: 'rgba(224, 90, 79, 0.05)',
|
| 455 |
+
borderColor: 'var(--accent-red)',
|
| 456 |
+
}
|
| 457 |
+
}}
|
| 458 |
+
>
|
| 459 |
+
Reject
|
| 460 |
+
</Button>
|
| 461 |
+
<Button
|
| 462 |
+
className="btn-approve"
|
| 463 |
+
onClick={() => handleResolve(true)}
|
| 464 |
+
sx={{
|
| 465 |
+
flex: 1,
|
| 466 |
+
background: 'transparent',
|
| 467 |
+
border: '1px solid rgba(255,255,255,0.05)',
|
| 468 |
+
color: 'var(--accent-green)',
|
| 469 |
+
padding: '10px 14px',
|
| 470 |
+
borderRadius: '10px',
|
| 471 |
+
'&:hover': {
|
| 472 |
+
bgcolor: 'rgba(47, 204, 113, 0.05)',
|
| 473 |
+
borderColor: 'var(--accent-green)',
|
| 474 |
+
}
|
| 475 |
+
}}
|
| 476 |
+
>
|
| 477 |
+
Approve
|
| 478 |
+
</Button>
|
| 479 |
+
</Box>
|
| 480 |
+
</Box>
|
| 481 |
+
)}
|
| 482 |
+
|
| 483 |
+
{status === 'rejected' && decisions.some(d => d.feedback) && (
|
| 484 |
+
<Typography variant="body2" sx={{ color: 'var(--accent-red)', mt: 1 }}>
|
| 485 |
+
Feedback: {decisions.find(d => d.feedback)?.feedback}
|
| 486 |
+
</Typography>
|
| 487 |
+
)}
|
| 488 |
+
</Box>
|
| 489 |
+
);
|
| 490 |
+
}
|
frontend/src/components/Chat/ChatInput.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback, KeyboardEvent } from 'react';
|
| 2 |
+
import { Box, TextField, IconButton, CircularProgress, Typography } from '@mui/material';
|
| 3 |
+
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
| 4 |
+
|
| 5 |
+
interface ChatInputProps {
|
| 6 |
+
onSend: (text: string) => void;
|
| 7 |
+
disabled?: boolean;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function ChatInput({ onSend, disabled = false }: ChatInputProps) {
|
| 11 |
+
const [input, setInput] = useState('');
|
| 12 |
+
|
| 13 |
+
const handleSend = useCallback(() => {
|
| 14 |
+
if (input.trim() && !disabled) {
|
| 15 |
+
onSend(input);
|
| 16 |
+
setInput('');
|
| 17 |
+
}
|
| 18 |
+
}, [input, disabled, onSend]);
|
| 19 |
+
|
| 20 |
+
const handleKeyDown = useCallback(
|
| 21 |
+
(e: KeyboardEvent<HTMLDivElement>) => {
|
| 22 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 23 |
+
e.preventDefault();
|
| 24 |
+
handleSend();
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
[handleSend]
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<Box
|
| 32 |
+
sx={{
|
| 33 |
+
pb: 4,
|
| 34 |
+
pt: 2,
|
| 35 |
+
position: 'relative',
|
| 36 |
+
zIndex: 10,
|
| 37 |
+
}}
|
| 38 |
+
>
|
| 39 |
+
<Box sx={{ maxWidth: '880px', mx: 'auto', width: '100%', px: 2 }}>
|
| 40 |
+
<Box
|
| 41 |
+
className="composer"
|
| 42 |
+
sx={{
|
| 43 |
+
display: 'flex',
|
| 44 |
+
gap: '10px',
|
| 45 |
+
alignItems: 'flex-start',
|
| 46 |
+
bgcolor: 'rgba(255,255,255,0.01)',
|
| 47 |
+
borderRadius: 'var(--radius-md)',
|
| 48 |
+
p: '12px',
|
| 49 |
+
border: '1px solid rgba(255,255,255,0.03)',
|
| 50 |
+
transition: 'box-shadow 0.2s ease, border-color 0.2s ease',
|
| 51 |
+
'&:focus-within': {
|
| 52 |
+
borderColor: 'var(--accent-yellow)',
|
| 53 |
+
boxShadow: 'var(--focus)',
|
| 54 |
+
}
|
| 55 |
+
}}
|
| 56 |
+
>
|
| 57 |
+
<TextField
|
| 58 |
+
fullWidth
|
| 59 |
+
multiline
|
| 60 |
+
maxRows={6}
|
| 61 |
+
value={input}
|
| 62 |
+
onChange={(e) => setInput(e.target.value)}
|
| 63 |
+
onKeyDown={handleKeyDown}
|
| 64 |
+
placeholder="Ask anything..."
|
| 65 |
+
disabled={disabled}
|
| 66 |
+
variant="standard"
|
| 67 |
+
InputProps={{
|
| 68 |
+
disableUnderline: true,
|
| 69 |
+
sx: {
|
| 70 |
+
color: 'var(--text)',
|
| 71 |
+
fontSize: '15px',
|
| 72 |
+
fontFamily: 'inherit',
|
| 73 |
+
padding: 0,
|
| 74 |
+
lineHeight: 1.5,
|
| 75 |
+
minHeight: '56px',
|
| 76 |
+
alignItems: 'flex-start',
|
| 77 |
+
}
|
| 78 |
+
}}
|
| 79 |
+
sx={{
|
| 80 |
+
flex: 1,
|
| 81 |
+
'& .MuiInputBase-root': {
|
| 82 |
+
p: 0,
|
| 83 |
+
backgroundColor: 'transparent',
|
| 84 |
+
},
|
| 85 |
+
'& textarea': {
|
| 86 |
+
resize: 'none',
|
| 87 |
+
padding: '0 !important',
|
| 88 |
+
}
|
| 89 |
+
}}
|
| 90 |
+
/>
|
| 91 |
+
<IconButton
|
| 92 |
+
onClick={handleSend}
|
| 93 |
+
disabled={disabled || !input.trim()}
|
| 94 |
+
sx={{
|
| 95 |
+
mt: 1,
|
| 96 |
+
p: 1,
|
| 97 |
+
borderRadius: '10px',
|
| 98 |
+
color: 'var(--muted-text)',
|
| 99 |
+
transition: 'all 0.2s',
|
| 100 |
+
'&:hover': {
|
| 101 |
+
color: 'var(--accent-yellow)',
|
| 102 |
+
bgcolor: 'rgba(255,255,255,0.05)',
|
| 103 |
+
},
|
| 104 |
+
'&.Mui-disabled': {
|
| 105 |
+
opacity: 0.3,
|
| 106 |
+
},
|
| 107 |
+
}}
|
| 108 |
+
>
|
| 109 |
+
{disabled ? <CircularProgress size={20} color="inherit" /> : <ArrowUpwardIcon fontSize="small" />}
|
| 110 |
+
</IconButton>
|
| 111 |
+
</Box>
|
| 112 |
+
|
| 113 |
+
{/* Powered By Badge */}
|
| 114 |
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', mt: 1.5, gap: 0.8, opacity: 0.5 }}>
|
| 115 |
+
<Typography variant="caption" sx={{ fontSize: '10px', color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
| 116 |
+
powered by
|
| 117 |
+
</Typography>
|
| 118 |
+
<img src="/claude-logo.png" alt="Claude" style={{ height: '12px', objectFit: 'contain' }} />
|
| 119 |
+
<Typography variant="caption" sx={{ fontSize: '10px', color: 'var(--text)', fontWeight: 600, letterSpacing: '0.02em' }}>
|
| 120 |
+
claude-opus-4-5-20251101
|
| 121 |
+
</Typography>
|
| 122 |
+
</Box>
|
| 123 |
+
</Box>
|
| 124 |
+
</Box>
|
| 125 |
+
);
|
| 126 |
+
}
|
frontend/src/components/Chat/MessageBubble.tsx
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Box, Paper, Typography, Chip } from '@mui/material';
|
| 2 |
+
import ReactMarkdown from 'react-markdown';
|
| 3 |
+
import remarkGfm from 'remark-gfm';
|
| 4 |
+
import ApprovalFlow from './ApprovalFlow';
|
| 5 |
+
import type { Message } from '@/types/agent';
|
| 6 |
+
|
| 7 |
+
interface MessageBubbleProps {
|
| 8 |
+
message: Message;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function MessageBubble({ message }: MessageBubbleProps) {
|
| 12 |
+
const isUser = message.role === 'user';
|
| 13 |
+
const isTool = message.role === 'tool';
|
| 14 |
+
const isAssistant = message.role === 'assistant';
|
| 15 |
+
|
| 16 |
+
if (message.approval) {
|
| 17 |
+
return (
|
| 18 |
+
<Box sx={{ width: '100%', maxWidth: '880px', mx: 'auto', my: 2 }}>
|
| 19 |
+
<ApprovalFlow message={message} />
|
| 20 |
+
</Box>
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<Box
|
| 26 |
+
sx={{
|
| 27 |
+
display: 'flex',
|
| 28 |
+
justifyContent: isUser ? 'flex-end' : 'flex-start',
|
| 29 |
+
width: '100%',
|
| 30 |
+
maxWidth: '880px',
|
| 31 |
+
mx: 'auto',
|
| 32 |
+
}}
|
| 33 |
+
>
|
| 34 |
+
<Paper
|
| 35 |
+
elevation={0}
|
| 36 |
+
className={`message ${isUser ? 'user' : isAssistant ? 'assistant' : ''}`}
|
| 37 |
+
sx={{
|
| 38 |
+
p: '14px 18px',
|
| 39 |
+
margin: '10px 0',
|
| 40 |
+
width: isTool ? '100%' : 'auto',
|
| 41 |
+
maxWidth: '100%',
|
| 42 |
+
borderRadius: 'var(--radius-lg)',
|
| 43 |
+
borderTopLeftRadius: isAssistant ? '6px' : undefined,
|
| 44 |
+
lineHeight: 1.45,
|
| 45 |
+
boxShadow: 'var(--shadow-1)',
|
| 46 |
+
border: '1px solid rgba(255,255,255,0.03)',
|
| 47 |
+
background: 'linear-gradient(180deg, rgba(255,255,255,0.015), transparent)',
|
| 48 |
+
}}
|
| 49 |
+
>
|
| 50 |
+
{isTool && (
|
| 51 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
| 52 |
+
<Typography variant="caption" color="text.secondary">
|
| 53 |
+
Tool
|
| 54 |
+
</Typography>
|
| 55 |
+
{message.toolName && (
|
| 56 |
+
<Chip
|
| 57 |
+
label={message.toolName}
|
| 58 |
+
size="small"
|
| 59 |
+
variant="outlined"
|
| 60 |
+
sx={{ ml: 1, height: 20, fontSize: '0.7rem' }}
|
| 61 |
+
/>
|
| 62 |
+
)}
|
| 63 |
+
</Box>
|
| 64 |
+
)}
|
| 65 |
+
|
| 66 |
+
<Box
|
| 67 |
+
sx={{
|
| 68 |
+
'& p': { m: 0, color: isUser ? 'var(--text)' : 'var(--text)' }, // User might want different text color? Defaults to --text
|
| 69 |
+
'& pre': {
|
| 70 |
+
bgcolor: 'rgba(0,0,0,0.5)',
|
| 71 |
+
p: 1.5,
|
| 72 |
+
borderRadius: 1,
|
| 73 |
+
overflow: 'auto',
|
| 74 |
+
fontSize: '0.85rem',
|
| 75 |
+
border: '1px solid rgba(255,255,255,0.05)',
|
| 76 |
+
},
|
| 77 |
+
'& code': {
|
| 78 |
+
bgcolor: 'rgba(255,255,255,0.05)',
|
| 79 |
+
px: 0.5,
|
| 80 |
+
py: 0.25,
|
| 81 |
+
borderRadius: 0.5,
|
| 82 |
+
fontSize: '0.85rem',
|
| 83 |
+
fontFamily: '"JetBrains Mono", monospace',
|
| 84 |
+
},
|
| 85 |
+
'& pre code': {
|
| 86 |
+
bgcolor: 'transparent',
|
| 87 |
+
p: 0,
|
| 88 |
+
},
|
| 89 |
+
'& a': {
|
| 90 |
+
color: 'var(--accent-yellow)',
|
| 91 |
+
textDecoration: 'none',
|
| 92 |
+
'&:hover': {
|
| 93 |
+
textDecoration: 'underline',
|
| 94 |
+
},
|
| 95 |
+
},
|
| 96 |
+
'& ul, & ol': {
|
| 97 |
+
pl: 2,
|
| 98 |
+
my: 1,
|
| 99 |
+
},
|
| 100 |
+
'& table': {
|
| 101 |
+
borderCollapse: 'collapse',
|
| 102 |
+
width: '100%',
|
| 103 |
+
my: 2,
|
| 104 |
+
fontSize: '0.875rem',
|
| 105 |
+
},
|
| 106 |
+
'& th': {
|
| 107 |
+
borderBottom: '1px solid',
|
| 108 |
+
borderColor: 'rgba(255,255,255,0.1)',
|
| 109 |
+
textAlign: 'left',
|
| 110 |
+
p: 1,
|
| 111 |
+
bgcolor: 'rgba(255,255,255,0.02)',
|
| 112 |
+
},
|
| 113 |
+
'& td': {
|
| 114 |
+
borderBottom: '1px solid',
|
| 115 |
+
borderColor: 'rgba(255,255,255,0.05)',
|
| 116 |
+
p: 1,
|
| 117 |
+
},
|
| 118 |
+
}}
|
| 119 |
+
>
|
| 120 |
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
|
| 121 |
+
</Box>
|
| 122 |
+
|
| 123 |
+
{/* Persisted Trace Logs - Now at the bottom */}
|
| 124 |
+
{message.trace && message.trace.length > 0 && (
|
| 125 |
+
<Box
|
| 126 |
+
sx={{
|
| 127 |
+
bgcolor: 'rgba(0,0,0,0.3)',
|
| 128 |
+
borderRadius: 1,
|
| 129 |
+
p: 1.5,
|
| 130 |
+
border: 1,
|
| 131 |
+
borderColor: 'rgba(255,255,255,0.05)',
|
| 132 |
+
width: '100%',
|
| 133 |
+
mt: 2,
|
| 134 |
+
}}
|
| 135 |
+
>
|
| 136 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
| 137 |
+
{message.trace.map((log) => {
|
| 138 |
+
// Extract tool name from text "Agent is executing {toolName}..."
|
| 139 |
+
const match = log.text.match(/Agent is executing (.+)\.\.\./);
|
| 140 |
+
const toolName = match ? match[1] : log.tool;
|
| 141 |
+
|
| 142 |
+
return (
|
| 143 |
+
<Typography
|
| 144 |
+
key={log.id}
|
| 145 |
+
variant="caption"
|
| 146 |
+
component="div"
|
| 147 |
+
sx={{
|
| 148 |
+
color: 'var(--muted-text)',
|
| 149 |
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
|
| 150 |
+
fontSize: '0.75rem',
|
| 151 |
+
display: 'flex',
|
| 152 |
+
alignItems: 'center',
|
| 153 |
+
gap: 0.5,
|
| 154 |
+
}}
|
| 155 |
+
>
|
| 156 |
+
<span style={{ color: log.completed ? '#FDB022' : 'inherit' }}>*</span>
|
| 157 |
+
<span>Agent is executing </span>
|
| 158 |
+
<span style={{
|
| 159 |
+
fontWeight: 600,
|
| 160 |
+
color: 'rgba(255, 255, 255, 0.9)',
|
| 161 |
+
}}>
|
| 162 |
+
{toolName}
|
| 163 |
+
</span>
|
| 164 |
+
<span>...</span>
|
| 165 |
+
</Typography>
|
| 166 |
+
);
|
| 167 |
+
})}
|
| 168 |
+
</Box>
|
| 169 |
+
</Box>
|
| 170 |
+
)}
|
| 171 |
+
|
| 172 |
+
<Typography className="meta" variant="caption" sx={{ display: 'block', textAlign: 'right', mt: 1, fontSize: '11px', opacity: 0.5 }}>
|
| 173 |
+
{new Date(message.timestamp).toLocaleTimeString()}
|
| 174 |
+
</Typography>
|
| 175 |
+
</Paper>
|
| 176 |
+
</Box>
|
| 177 |
+
);
|
| 178 |
+
}
|
frontend/src/components/Chat/MessageList.tsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from 'react';
|
| 2 |
+
import { Box, Typography } from '@mui/material';
|
| 3 |
+
import { useSessionStore } from '@/store/sessionStore';
|
| 4 |
+
import MessageBubble from './MessageBubble';
|
| 5 |
+
import type { Message } from '@/types/agent';
|
| 6 |
+
|
| 7 |
+
interface MessageListProps {
|
| 8 |
+
messages: Message[];
|
| 9 |
+
isProcessing: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const TechnicalIndicator = () => (
|
| 13 |
+
<Box
|
| 14 |
+
component="span"
|
| 15 |
+
sx={{
|
| 16 |
+
color: 'primary.main',
|
| 17 |
+
fontFamily: 'monospace',
|
| 18 |
+
fontWeight: 'bold',
|
| 19 |
+
fontSize: '1.2rem',
|
| 20 |
+
lineHeight: 0,
|
| 21 |
+
display: 'inline-block',
|
| 22 |
+
verticalAlign: 'middle',
|
| 23 |
+
width: '1em',
|
| 24 |
+
letterSpacing: '-3px',
|
| 25 |
+
transform: 'scale(0.6) translateY(-2px)',
|
| 26 |
+
'&::after': {
|
| 27 |
+
content: '""',
|
| 28 |
+
animation: 'dots 2s steps(4, end) infinite',
|
| 29 |
+
},
|
| 30 |
+
'@keyframes dots': {
|
| 31 |
+
'0%': { content: '""' },
|
| 32 |
+
'25%': { content: '"."' },
|
| 33 |
+
'50%': { content: '".."' },
|
| 34 |
+
'75%, 100%': { content: '"..."' },
|
| 35 |
+
},
|
| 36 |
+
}}
|
| 37 |
+
/>
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
export default function MessageList({ messages, isProcessing }: MessageListProps) {
|
| 41 |
+
const bottomRef = useRef<HTMLDivElement>(null);
|
| 42 |
+
const { activeSessionId } = useSessionStore();
|
| 43 |
+
|
| 44 |
+
// Auto-scroll to bottom when new messages arrive
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 47 |
+
}, [messages, isProcessing]);
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<Box
|
| 51 |
+
sx={{
|
| 52 |
+
flex: 1,
|
| 53 |
+
overflow: 'auto',
|
| 54 |
+
p: 2,
|
| 55 |
+
display: 'flex',
|
| 56 |
+
flexDirection: 'column',
|
| 57 |
+
}}
|
| 58 |
+
>
|
| 59 |
+
<Box sx={{ maxWidth: 'md', mx: 'auto', width: '100%', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 60 |
+
{messages.length === 0 && !isProcessing ? (
|
| 61 |
+
<Box
|
| 62 |
+
sx={{
|
| 63 |
+
flex: 1,
|
| 64 |
+
display: 'flex',
|
| 65 |
+
alignItems: 'center',
|
| 66 |
+
justifyContent: 'center',
|
| 67 |
+
py: 8,
|
| 68 |
+
}}
|
| 69 |
+
>
|
| 70 |
+
<Typography color="text.secondary" sx={{ fontFamily: 'monospace' }}>
|
| 71 |
+
Awaiting input…
|
| 72 |
+
</Typography>
|
| 73 |
+
</Box>
|
| 74 |
+
) : (
|
| 75 |
+
messages.map((message) => (
|
| 76 |
+
<MessageBubble key={message.id} message={message} />
|
| 77 |
+
))
|
| 78 |
+
)}
|
| 79 |
+
|
| 80 |
+
{isProcessing && (
|
| 81 |
+
<Box sx={{ width: '100%', mb: 2 }}>
|
| 82 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1, px: 0.5 }}>
|
| 83 |
+
<Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
| 84 |
+
Thinking
|
| 85 |
+
</Typography>
|
| 86 |
+
<TechnicalIndicator />
|
| 87 |
+
</Box>
|
| 88 |
+
</Box>
|
| 89 |
+
)}
|
| 90 |
+
|
| 91 |
+
{activeSessionId && (
|
| 92 |
+
// ApprovalFlow is now handled within messages
|
| 93 |
+
null
|
| 94 |
+
)}
|
| 95 |
+
|
| 96 |
+
<div ref={bottomRef} />
|
| 97 |
+
</Box>
|
| 98 |
+
</Box>
|
| 99 |
+
);
|
| 100 |
+
}
|
frontend/src/components/CodePanel/CodePanel.tsx
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useEffect, useMemo } from 'react';
|
| 2 |
+
import { Box, Typography, IconButton, Tabs, Tab } from '@mui/material';
|
| 3 |
+
import CloseIcon from '@mui/icons-material/Close';
|
| 4 |
+
import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
|
| 5 |
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
| 6 |
+
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
|
| 7 |
+
import CodeIcon from '@mui/icons-material/Code';
|
| 8 |
+
import TerminalIcon from '@mui/icons-material/Terminal';
|
| 9 |
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
| 10 |
+
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
| 11 |
+
import { useAgentStore } from '@/store/agentStore';
|
| 12 |
+
import { useLayoutStore } from '@/store/layoutStore';
|
| 13 |
+
import { processLogs } from '@/utils/logProcessor';
|
| 14 |
+
|
| 15 |
+
export default function CodePanel() {
|
| 16 |
+
const { panelContent, panelTabs, activePanelTab, setActivePanelTab, plan } = useAgentStore();
|
| 17 |
+
const { setRightPanelOpen } = useLayoutStore();
|
| 18 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 19 |
+
|
| 20 |
+
// Get the active tab content, or fall back to panelContent for backwards compatibility
|
| 21 |
+
const activeTab = panelTabs.find(t => t.id === activePanelTab);
|
| 22 |
+
const currentContent = activeTab || panelContent;
|
| 23 |
+
|
| 24 |
+
const displayContent = useMemo(() => {
|
| 25 |
+
if (!currentContent?.content) return '';
|
| 26 |
+
// Apply log processing only for text/logs, not for code/json
|
| 27 |
+
if (!currentContent.language || currentContent.language === 'text') {
|
| 28 |
+
return processLogs(currentContent.content);
|
| 29 |
+
}
|
| 30 |
+
return currentContent.content;
|
| 31 |
+
}, [currentContent?.content, currentContent?.language]);
|
| 32 |
+
|
| 33 |
+
useEffect(() => {
|
| 34 |
+
// Auto-scroll only for logs tab
|
| 35 |
+
if (scrollRef.current && activePanelTab === 'logs') {
|
| 36 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 37 |
+
}
|
| 38 |
+
}, [displayContent, activePanelTab]);
|
| 39 |
+
|
| 40 |
+
const hasTabs = panelTabs.length > 0;
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'var(--panel)' }}>
|
| 44 |
+
{/* Header - Fixed 60px to align */}
|
| 45 |
+
<Box sx={{
|
| 46 |
+
height: '60px',
|
| 47 |
+
display: 'flex',
|
| 48 |
+
alignItems: 'center',
|
| 49 |
+
justifyContent: 'space-between',
|
| 50 |
+
px: 2,
|
| 51 |
+
borderBottom: '1px solid rgba(255,255,255,0.03)'
|
| 52 |
+
}}>
|
| 53 |
+
{hasTabs ? (
|
| 54 |
+
<Tabs
|
| 55 |
+
value={activePanelTab || panelTabs[0]?.id}
|
| 56 |
+
onChange={(_, newValue) => setActivePanelTab(newValue)}
|
| 57 |
+
sx={{
|
| 58 |
+
minHeight: 36,
|
| 59 |
+
'& .MuiTabs-indicator': {
|
| 60 |
+
backgroundColor: 'var(--accent-primary)',
|
| 61 |
+
},
|
| 62 |
+
'& .MuiTab-root': {
|
| 63 |
+
minHeight: 36,
|
| 64 |
+
minWidth: 'auto',
|
| 65 |
+
px: 2,
|
| 66 |
+
py: 0.5,
|
| 67 |
+
fontSize: '0.75rem',
|
| 68 |
+
fontWeight: 600,
|
| 69 |
+
textTransform: 'uppercase',
|
| 70 |
+
letterSpacing: '0.05em',
|
| 71 |
+
color: 'var(--muted-text)',
|
| 72 |
+
'&.Mui-selected': {
|
| 73 |
+
color: 'var(--text)',
|
| 74 |
+
},
|
| 75 |
+
},
|
| 76 |
+
}}
|
| 77 |
+
>
|
| 78 |
+
{panelTabs.map((tab) => (
|
| 79 |
+
<Tab
|
| 80 |
+
key={tab.id}
|
| 81 |
+
value={tab.id}
|
| 82 |
+
label={tab.title}
|
| 83 |
+
icon={tab.id === 'script' ? <CodeIcon sx={{ fontSize: 16 }} /> : <TerminalIcon sx={{ fontSize: 16 }} />}
|
| 84 |
+
iconPosition="start"
|
| 85 |
+
/>
|
| 86 |
+
))}
|
| 87 |
+
</Tabs>
|
| 88 |
+
) : (
|
| 89 |
+
<Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
| 90 |
+
{currentContent?.title || 'Code Panel'}
|
| 91 |
+
</Typography>
|
| 92 |
+
)}
|
| 93 |
+
<IconButton size="small" onClick={() => setRightPanelOpen(false)} sx={{ color: 'var(--muted-text)' }}>
|
| 94 |
+
<CloseIcon fontSize="small" />
|
| 95 |
+
</IconButton>
|
| 96 |
+
</Box>
|
| 97 |
+
|
| 98 |
+
{/* Main Content Area */}
|
| 99 |
+
<Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
| 100 |
+
{!currentContent ? (
|
| 101 |
+
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}>
|
| 102 |
+
<Typography variant="body2" color="text.secondary" sx={{ opacity: 0.5 }}>
|
| 103 |
+
NO DATA LOADED
|
| 104 |
+
</Typography>
|
| 105 |
+
</Box>
|
| 106 |
+
) : (
|
| 107 |
+
<Box sx={{ flex: 1, overflow: 'hidden', p: 2 }}>
|
| 108 |
+
<Box
|
| 109 |
+
ref={scrollRef}
|
| 110 |
+
className="code-panel"
|
| 111 |
+
sx={{
|
| 112 |
+
background: '#0A0B0C',
|
| 113 |
+
borderRadius: 'var(--radius-md)',
|
| 114 |
+
padding: '18px',
|
| 115 |
+
border: '1px solid rgba(255,255,255,0.03)',
|
| 116 |
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace',
|
| 117 |
+
fontSize: '13px',
|
| 118 |
+
lineHeight: 1.55,
|
| 119 |
+
height: '100%',
|
| 120 |
+
overflow: 'auto',
|
| 121 |
+
}}
|
| 122 |
+
>
|
| 123 |
+
{currentContent.content ? (
|
| 124 |
+
currentContent.language === 'python' ? (
|
| 125 |
+
<SyntaxHighlighter
|
| 126 |
+
language="python"
|
| 127 |
+
style={vscDarkPlus}
|
| 128 |
+
customStyle={{
|
| 129 |
+
margin: 0,
|
| 130 |
+
padding: 0,
|
| 131 |
+
background: 'transparent',
|
| 132 |
+
fontSize: '13px',
|
| 133 |
+
fontFamily: 'inherit',
|
| 134 |
+
}}
|
| 135 |
+
wrapLines={true}
|
| 136 |
+
wrapLongLines={true}
|
| 137 |
+
>
|
| 138 |
+
{displayContent}
|
| 139 |
+
</SyntaxHighlighter>
|
| 140 |
+
) : (
|
| 141 |
+
<Box component="pre" sx={{
|
| 142 |
+
m: 0,
|
| 143 |
+
fontFamily: 'inherit',
|
| 144 |
+
color: 'var(--text)',
|
| 145 |
+
whiteSpace: 'pre-wrap',
|
| 146 |
+
wordBreak: 'break-all'
|
| 147 |
+
}}>
|
| 148 |
+
<code>{displayContent}</code>
|
| 149 |
+
</Box>
|
| 150 |
+
)
|
| 151 |
+
) : (
|
| 152 |
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
|
| 153 |
+
<Typography variant="caption">
|
| 154 |
+
NO CONTENT TO DISPLAY
|
| 155 |
+
</Typography>
|
| 156 |
+
</Box>
|
| 157 |
+
)}
|
| 158 |
+
</Box>
|
| 159 |
+
</Box>
|
| 160 |
+
)}
|
| 161 |
+
</Box>
|
| 162 |
+
|
| 163 |
+
{/* Plan Display at Bottom */}
|
| 164 |
+
{plan && plan.length > 0 && (
|
| 165 |
+
<Box sx={{
|
| 166 |
+
borderTop: '1px solid rgba(255,255,255,0.03)',
|
| 167 |
+
bgcolor: 'rgba(0,0,0,0.2)',
|
| 168 |
+
maxHeight: '30%',
|
| 169 |
+
display: 'flex',
|
| 170 |
+
flexDirection: 'column'
|
| 171 |
+
}}>
|
| 172 |
+
<Box sx={{ p: 1.5, borderBottom: '1px solid rgba(255,255,255,0.03)', display: 'flex', alignItems: 'center', gap: 1 }}>
|
| 173 |
+
<Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
| 174 |
+
CURRENT PLAN
|
| 175 |
+
</Typography>
|
| 176 |
+
</Box>
|
| 177 |
+
<Box sx={{ p: 2, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
| 178 |
+
{plan.map((item) => (
|
| 179 |
+
<Box key={item.id} sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
|
| 180 |
+
<Box sx={{ mt: 0.2 }}>
|
| 181 |
+
{item.status === 'completed' && <CheckCircleIcon sx={{ fontSize: 16, color: 'var(--accent-green)' }} />}
|
| 182 |
+
{item.status === 'in_progress' && <PlayCircleOutlineIcon sx={{ fontSize: 16, color: 'var(--accent-yellow)' }} />}
|
| 183 |
+
{item.status === 'pending' && <RadioButtonUncheckedIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.5 }} />}
|
| 184 |
+
</Box>
|
| 185 |
+
<Typography
|
| 186 |
+
variant="body2"
|
| 187 |
+
sx={{
|
| 188 |
+
fontSize: '13px',
|
| 189 |
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace',
|
| 190 |
+
color: item.status === 'completed' ? 'var(--muted-text)' : 'var(--text)',
|
| 191 |
+
textDecoration: item.status === 'completed' ? 'line-through' : 'none',
|
| 192 |
+
opacity: item.status === 'pending' ? 0.7 : 1
|
| 193 |
+
}}
|
| 194 |
+
>
|
| 195 |
+
{item.content}
|
| 196 |
+
</Typography>
|
| 197 |
+
</Box>
|
| 198 |
+
))}
|
| 199 |
+
</Box>
|
| 200 |
+
</Box>
|
| 201 |
+
)}
|
| 202 |
+
</Box>
|
| 203 |
+
);
|
| 204 |
+
}
|
frontend/src/components/Layout/AppLayout.tsx
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useRef, useEffect } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Box,
|
| 4 |
+
Drawer,
|
| 5 |
+
Typography,
|
| 6 |
+
IconButton,
|
| 7 |
+
} from '@mui/material';
|
| 8 |
+
import MenuIcon from '@mui/icons-material/Menu';
|
| 9 |
+
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
| 10 |
+
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
| 11 |
+
|
| 12 |
+
import { useSessionStore } from '@/store/sessionStore';
|
| 13 |
+
import { useAgentStore } from '@/store/agentStore';
|
| 14 |
+
import { useLayoutStore } from '@/store/layoutStore';
|
| 15 |
+
import { useAgentWebSocket } from '@/hooks/useAgentWebSocket';
|
| 16 |
+
import SessionSidebar from '@/components/SessionSidebar/SessionSidebar';
|
| 17 |
+
import CodePanel from '@/components/CodePanel/CodePanel';
|
| 18 |
+
import ChatInput from '@/components/Chat/ChatInput';
|
| 19 |
+
import MessageList from '@/components/Chat/MessageList';
|
| 20 |
+
import type { Message } from '@/types/agent';
|
| 21 |
+
|
| 22 |
+
const DRAWER_WIDTH = 260;
|
| 23 |
+
|
| 24 |
+
export default function AppLayout() {
|
| 25 |
+
const { activeSessionId } = useSessionStore();
|
| 26 |
+
const { isConnected, isProcessing, getMessages, addMessage } = useAgentStore();
|
| 27 |
+
const {
|
| 28 |
+
isLeftSidebarOpen,
|
| 29 |
+
isRightPanelOpen,
|
| 30 |
+
rightPanelWidth,
|
| 31 |
+
setRightPanelWidth,
|
| 32 |
+
toggleLeftSidebar,
|
| 33 |
+
toggleRightPanel
|
| 34 |
+
} = useLayoutStore();
|
| 35 |
+
|
| 36 |
+
const isResizing = useRef(false);
|
| 37 |
+
|
| 38 |
+
const startResizing = useCallback((e: React.MouseEvent) => {
|
| 39 |
+
e.preventDefault();
|
| 40 |
+
isResizing.current = true;
|
| 41 |
+
document.addEventListener('mousemove', handleMouseMove);
|
| 42 |
+
document.addEventListener('mouseup', stopResizing);
|
| 43 |
+
document.body.style.cursor = 'col-resize';
|
| 44 |
+
}, []);
|
| 45 |
+
|
| 46 |
+
const stopResizing = useCallback(() => {
|
| 47 |
+
isResizing.current = false;
|
| 48 |
+
document.removeEventListener('mousemove', handleMouseMove);
|
| 49 |
+
document.removeEventListener('mouseup', stopResizing);
|
| 50 |
+
document.body.style.cursor = 'default';
|
| 51 |
+
}, []);
|
| 52 |
+
|
| 53 |
+
const handleMouseMove = useCallback((e: MouseEvent) => {
|
| 54 |
+
if (!isResizing.current) return;
|
| 55 |
+
const newWidth = window.innerWidth - e.clientX;
|
| 56 |
+
const maxWidth = window.innerWidth * 0.8;
|
| 57 |
+
const minWidth = 300;
|
| 58 |
+
if (newWidth > minWidth && newWidth < maxWidth) {
|
| 59 |
+
setRightPanelWidth(newWidth);
|
| 60 |
+
}
|
| 61 |
+
}, [setRightPanelWidth]);
|
| 62 |
+
|
| 63 |
+
useEffect(() => {
|
| 64 |
+
return () => {
|
| 65 |
+
document.removeEventListener('mousemove', handleMouseMove);
|
| 66 |
+
document.removeEventListener('mouseup', stopResizing);
|
| 67 |
+
};
|
| 68 |
+
}, [handleMouseMove, stopResizing]);
|
| 69 |
+
|
| 70 |
+
const messages = activeSessionId ? getMessages(activeSessionId) : [];
|
| 71 |
+
|
| 72 |
+
useAgentWebSocket({
|
| 73 |
+
sessionId: activeSessionId,
|
| 74 |
+
onReady: () => console.log('Agent ready'),
|
| 75 |
+
onError: (error) => console.error('Agent error:', error),
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
const handleSendMessage = useCallback(
|
| 79 |
+
async (text: string) => {
|
| 80 |
+
if (!activeSessionId || !text.trim()) return;
|
| 81 |
+
|
| 82 |
+
const userMsg: Message = {
|
| 83 |
+
id: `user_${Date.now()}`,
|
| 84 |
+
role: 'user',
|
| 85 |
+
content: text.trim(),
|
| 86 |
+
timestamp: new Date().toISOString(),
|
| 87 |
+
};
|
| 88 |
+
addMessage(activeSessionId, userMsg);
|
| 89 |
+
|
| 90 |
+
try {
|
| 91 |
+
await fetch('/api/submit', {
|
| 92 |
+
method: 'POST',
|
| 93 |
+
headers: { 'Content-Type': 'application/json' },
|
| 94 |
+
body: JSON.stringify({
|
| 95 |
+
session_id: activeSessionId,
|
| 96 |
+
text: text.trim(),
|
| 97 |
+
}),
|
| 98 |
+
});
|
| 99 |
+
} catch (e) {
|
| 100 |
+
console.error('Send failed:', e);
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
[activeSessionId, addMessage]
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
return (
|
| 107 |
+
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
|
| 108 |
+
{/* Left Sidebar Drawer */}
|
| 109 |
+
<Box
|
| 110 |
+
component="nav"
|
| 111 |
+
sx={{
|
| 112 |
+
width: { md: isLeftSidebarOpen ? DRAWER_WIDTH : 0 },
|
| 113 |
+
flexShrink: { md: 0 },
|
| 114 |
+
transition: isResizing.current ? 'none' : 'width 0.2s',
|
| 115 |
+
overflow: 'hidden',
|
| 116 |
+
}}
|
| 117 |
+
>
|
| 118 |
+
<Drawer
|
| 119 |
+
variant="persistent"
|
| 120 |
+
sx={{
|
| 121 |
+
display: { xs: 'none', md: 'block' },
|
| 122 |
+
'& .MuiDrawer-paper': {
|
| 123 |
+
boxSizing: 'border-box',
|
| 124 |
+
width: DRAWER_WIDTH,
|
| 125 |
+
borderRight: '1px solid',
|
| 126 |
+
borderColor: 'divider',
|
| 127 |
+
top: 0,
|
| 128 |
+
height: '100%',
|
| 129 |
+
bgcolor: 'var(--panel)', // Ensure correct background matches sidebar
|
| 130 |
+
},
|
| 131 |
+
}}
|
| 132 |
+
open={isLeftSidebarOpen}
|
| 133 |
+
>
|
| 134 |
+
<SessionSidebar />
|
| 135 |
+
</Drawer>
|
| 136 |
+
</Box>
|
| 137 |
+
|
| 138 |
+
{/* Main Content Area */}
|
| 139 |
+
<Box
|
| 140 |
+
sx={{
|
| 141 |
+
flexGrow: 1,
|
| 142 |
+
height: '100%',
|
| 143 |
+
display: 'flex',
|
| 144 |
+
flexDirection: 'column',
|
| 145 |
+
transition: isResizing.current ? 'none' : 'width 0.2s',
|
| 146 |
+
position: 'relative',
|
| 147 |
+
overflow: 'hidden',
|
| 148 |
+
}}
|
| 149 |
+
>
|
| 150 |
+
{/* Top Header Bar (Fixed) */}
|
| 151 |
+
<Box sx={{
|
| 152 |
+
height: '60px',
|
| 153 |
+
px: 1,
|
| 154 |
+
display: 'flex',
|
| 155 |
+
alignItems: 'center',
|
| 156 |
+
borderBottom: 1,
|
| 157 |
+
borderColor: 'divider',
|
| 158 |
+
bgcolor: 'background.default',
|
| 159 |
+
zIndex: 1200,
|
| 160 |
+
}}>
|
| 161 |
+
<IconButton onClick={toggleLeftSidebar} size="small">
|
| 162 |
+
{isLeftSidebarOpen ? <ChevronLeftIcon /> : <MenuIcon />}
|
| 163 |
+
</IconButton>
|
| 164 |
+
|
| 165 |
+
<Box sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
|
| 166 |
+
<img
|
| 167 |
+
src="/hf-logo-white.png"
|
| 168 |
+
alt="Hugging Face"
|
| 169 |
+
style={{ height: '40px', objectFit: 'contain' }}
|
| 170 |
+
/>
|
| 171 |
+
</Box>
|
| 172 |
+
|
| 173 |
+
<IconButton
|
| 174 |
+
onClick={toggleRightPanel}
|
| 175 |
+
size="small"
|
| 176 |
+
sx={{ visibility: isRightPanelOpen ? 'hidden' : 'visible' }}
|
| 177 |
+
>
|
| 178 |
+
<MenuIcon />
|
| 179 |
+
</IconButton>
|
| 180 |
+
</Box>
|
| 181 |
+
|
| 182 |
+
<Box
|
| 183 |
+
component="main"
|
| 184 |
+
className="chat-pane"
|
| 185 |
+
sx={{
|
| 186 |
+
flexGrow: 1,
|
| 187 |
+
display: 'flex',
|
| 188 |
+
flexDirection: 'column',
|
| 189 |
+
overflow: 'hidden',
|
| 190 |
+
background: 'linear-gradient(180deg, var(--bg), var(--panel))',
|
| 191 |
+
padding: '24px',
|
| 192 |
+
}}
|
| 193 |
+
>
|
| 194 |
+
{activeSessionId ? (
|
| 195 |
+
<>
|
| 196 |
+
<MessageList messages={messages} isProcessing={isProcessing} />
|
| 197 |
+
<ChatInput
|
| 198 |
+
onSend={handleSendMessage}
|
| 199 |
+
disabled={isProcessing || !isConnected}
|
| 200 |
+
/>
|
| 201 |
+
</>
|
| 202 |
+
) : (
|
| 203 |
+
<Box
|
| 204 |
+
sx={{
|
| 205 |
+
flex: 1,
|
| 206 |
+
display: 'flex',
|
| 207 |
+
alignItems: 'center',
|
| 208 |
+
justifyContent: 'center',
|
| 209 |
+
flexDirection: 'column',
|
| 210 |
+
gap: 2,
|
| 211 |
+
}}
|
| 212 |
+
>
|
| 213 |
+
<Typography variant="h5" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
|
| 214 |
+
NO SESSION SELECTED
|
| 215 |
+
</Typography>
|
| 216 |
+
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: 'monospace' }}>
|
| 217 |
+
Initialize a session via the sidebar
|
| 218 |
+
</Typography>
|
| 219 |
+
</Box>
|
| 220 |
+
)}
|
| 221 |
+
</Box>
|
| 222 |
+
</Box>
|
| 223 |
+
|
| 224 |
+
{/* Resize Handle */}
|
| 225 |
+
{isRightPanelOpen && (
|
| 226 |
+
<Box
|
| 227 |
+
onMouseDown={startResizing}
|
| 228 |
+
sx={{
|
| 229 |
+
width: '4px',
|
| 230 |
+
cursor: 'col-resize',
|
| 231 |
+
bgcolor: 'divider',
|
| 232 |
+
display: 'flex',
|
| 233 |
+
alignItems: 'center',
|
| 234 |
+
justifyContent: 'center',
|
| 235 |
+
transition: 'background-color 0.2s',
|
| 236 |
+
zIndex: 1300,
|
| 237 |
+
overflow: 'hidden',
|
| 238 |
+
'&:hover': {
|
| 239 |
+
bgcolor: 'primary.main',
|
| 240 |
+
},
|
| 241 |
+
}}
|
| 242 |
+
>
|
| 243 |
+
<DragIndicatorIcon
|
| 244 |
+
sx={{
|
| 245 |
+
fontSize: '0.8rem',
|
| 246 |
+
color: 'text.secondary',
|
| 247 |
+
pointerEvents: 'none',
|
| 248 |
+
}}
|
| 249 |
+
/>
|
| 250 |
+
</Box>
|
| 251 |
+
)}
|
| 252 |
+
|
| 253 |
+
{/* Right Panel Drawer */}
|
| 254 |
+
<Box
|
| 255 |
+
component="nav"
|
| 256 |
+
sx={{
|
| 257 |
+
width: { md: isRightPanelOpen ? rightPanelWidth : 0 },
|
| 258 |
+
flexShrink: { md: 0 },
|
| 259 |
+
transition: isResizing.current ? 'none' : 'width 0.2s',
|
| 260 |
+
overflow: 'hidden',
|
| 261 |
+
}}
|
| 262 |
+
>
|
| 263 |
+
<Drawer
|
| 264 |
+
anchor="right"
|
| 265 |
+
variant="persistent"
|
| 266 |
+
sx={{
|
| 267 |
+
display: { xs: 'none', md: 'block' },
|
| 268 |
+
'& .MuiDrawer-paper': {
|
| 269 |
+
boxSizing: 'border-box',
|
| 270 |
+
width: rightPanelWidth,
|
| 271 |
+
borderLeft: 'none',
|
| 272 |
+
top: 0,
|
| 273 |
+
height: '100%',
|
| 274 |
+
bgcolor: 'var(--panel)',
|
| 275 |
+
},
|
| 276 |
+
}}
|
| 277 |
+
open={isRightPanelOpen}
|
| 278 |
+
>
|
| 279 |
+
<CodePanel />
|
| 280 |
+
</Drawer>
|
| 281 |
+
</Box>
|
| 282 |
+
</Box>
|
| 283 |
+
);
|
| 284 |
+
}
|
frontend/src/components/SessionSidebar/SessionSidebar.tsx
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Box,
|
| 4 |
+
List,
|
| 5 |
+
ListItem,
|
| 6 |
+
IconButton,
|
| 7 |
+
Typography,
|
| 8 |
+
Button,
|
| 9 |
+
Tooltip,
|
| 10 |
+
} from '@mui/material';
|
| 11 |
+
import DeleteIcon from '@mui/icons-material/Delete';
|
| 12 |
+
import UndoIcon from '@mui/icons-material/Undo';
|
| 13 |
+
import { useSessionStore } from '@/store/sessionStore';
|
| 14 |
+
import { useAgentStore } from '@/store/agentStore';
|
| 15 |
+
|
| 16 |
+
interface SessionSidebarProps {
|
| 17 |
+
onClose?: () => void;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const StatusDiode = ({ connected }: { connected: boolean }) => (
|
| 21 |
+
<Box
|
| 22 |
+
sx={{
|
| 23 |
+
width: 10,
|
| 24 |
+
height: 10,
|
| 25 |
+
borderRadius: '50%',
|
| 26 |
+
bgcolor: connected ? 'var(--accent-green)' : 'var(--accent-red)', // Use green/red for connection status
|
| 27 |
+
boxShadow: connected ? '0 0 6px rgba(47, 204, 113, 0.4)' : 'none',
|
| 28 |
+
transition: 'all 0.3s ease',
|
| 29 |
+
}}
|
| 30 |
+
/>
|
| 31 |
+
);
|
| 32 |
+
|
| 33 |
+
const RunningIndicator = () => (
|
| 34 |
+
<Box
|
| 35 |
+
className="running-indicator"
|
| 36 |
+
sx={{
|
| 37 |
+
width: 10,
|
| 38 |
+
height: 10,
|
| 39 |
+
borderRadius: '50%',
|
| 40 |
+
bgcolor: 'var(--accent-yellow)',
|
| 41 |
+
boxShadow: '0 0 6px rgba(199,165,0,0.18)',
|
| 42 |
+
}}
|
| 43 |
+
/>
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
export default function SessionSidebar({ onClose }: SessionSidebarProps) {
|
| 47 |
+
const { sessions, activeSessionId, createSession, deleteSession, switchSession } =
|
| 48 |
+
useSessionStore();
|
| 49 |
+
const { clearMessages, isConnected, isProcessing, setPlan, setPanelContent } = useAgentStore();
|
| 50 |
+
|
| 51 |
+
const handleNewSession = useCallback(async () => {
|
| 52 |
+
try {
|
| 53 |
+
const response = await fetch('/api/session', { method: 'POST' });
|
| 54 |
+
const data = await response.json();
|
| 55 |
+
createSession(data.session_id);
|
| 56 |
+
// Clear plan and code panel for new session
|
| 57 |
+
setPlan([]);
|
| 58 |
+
setPanelContent(null);
|
| 59 |
+
onClose?.();
|
| 60 |
+
} catch (e) {
|
| 61 |
+
console.error('Failed to create session:', e);
|
| 62 |
+
}
|
| 63 |
+
}, [createSession, setPlan, setPanelContent, onClose]);
|
| 64 |
+
|
| 65 |
+
const handleDeleteSession = useCallback(
|
| 66 |
+
async (sessionId: string, e: React.MouseEvent) => {
|
| 67 |
+
e.stopPropagation();
|
| 68 |
+
try {
|
| 69 |
+
await fetch(`/api/session/${sessionId}`, { method: 'DELETE' });
|
| 70 |
+
deleteSession(sessionId);
|
| 71 |
+
clearMessages(sessionId);
|
| 72 |
+
} catch (e) {
|
| 73 |
+
console.error('Failed to delete session:', e);
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
[deleteSession, clearMessages]
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
const handleSelectSession = useCallback(
|
| 80 |
+
(sessionId: string) => {
|
| 81 |
+
switchSession(sessionId);
|
| 82 |
+
// Clear plan and code panel when switching sessions
|
| 83 |
+
setPlan([]);
|
| 84 |
+
setPanelContent(null);
|
| 85 |
+
onClose?.();
|
| 86 |
+
},
|
| 87 |
+
[switchSession, setPlan, setPanelContent, onClose]
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
const handleUndo = useCallback(async () => {
|
| 91 |
+
if (!activeSessionId) return;
|
| 92 |
+
try {
|
| 93 |
+
await fetch(`/api/undo/${activeSessionId}`, { method: 'POST' });
|
| 94 |
+
} catch (e) {
|
| 95 |
+
console.error('Undo failed:', e);
|
| 96 |
+
}
|
| 97 |
+
}, [activeSessionId]);
|
| 98 |
+
|
| 99 |
+
const formatTime = (dateString: string) => {
|
| 100 |
+
return new Date(dateString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<Box className="sidebar" sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'var(--panel)' }}>
|
| 105 |
+
{/* Header - Aligned with AppLayout 60px */}
|
| 106 |
+
<Box sx={{
|
| 107 |
+
height: '60px',
|
| 108 |
+
display: 'flex',
|
| 109 |
+
alignItems: 'center',
|
| 110 |
+
px: 2,
|
| 111 |
+
borderBottom: '1px solid rgba(255,255,255,0.03)'
|
| 112 |
+
}}>
|
| 113 |
+
<Box className="brand-logo" sx={{ display: 'flex' }}>
|
| 114 |
+
<img
|
| 115 |
+
src="/hf-log-only-white.png"
|
| 116 |
+
alt="HF Agent"
|
| 117 |
+
style={{ height: '24px', objectFit: 'contain' }}
|
| 118 |
+
/>
|
| 119 |
+
</Box>
|
| 120 |
+
</Box>
|
| 121 |
+
|
| 122 |
+
{/* Content */}
|
| 123 |
+
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', p: 2, overflow: 'hidden' }}>
|
| 124 |
+
{/* System Info / Status */}
|
| 125 |
+
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
| 126 |
+
<StatusDiode connected={isConnected} />
|
| 127 |
+
<Typography variant="caption" sx={{ color: 'var(--muted-text)', fontFamily: 'inherit' }}>
|
| 128 |
+
{isConnected ? 'System Online' : 'Disconnected'}
|
| 129 |
+
</Typography>
|
| 130 |
+
</Box>
|
| 131 |
+
|
| 132 |
+
<Button
|
| 133 |
+
fullWidth
|
| 134 |
+
className="create-session"
|
| 135 |
+
onClick={handleNewSession}
|
| 136 |
+
sx={{
|
| 137 |
+
display: 'inline-flex',
|
| 138 |
+
alignItems: 'center',
|
| 139 |
+
justifyContent: 'flex-start',
|
| 140 |
+
gap: '10px',
|
| 141 |
+
padding: '10px 14px',
|
| 142 |
+
borderRadius: 'var(--radius-md)',
|
| 143 |
+
border: '1px solid rgba(255,255,255,0.06)',
|
| 144 |
+
bgcolor: 'transparent',
|
| 145 |
+
color: 'var(--text)',
|
| 146 |
+
fontWeight: 600,
|
| 147 |
+
textTransform: 'none',
|
| 148 |
+
mb: 3,
|
| 149 |
+
'&:hover': {
|
| 150 |
+
bgcolor: 'rgba(255,255,255,0.02)',
|
| 151 |
+
border: '1px solid rgba(255,255,255,0.1)',
|
| 152 |
+
},
|
| 153 |
+
'&::before': {
|
| 154 |
+
content: '""',
|
| 155 |
+
width: '4px',
|
| 156 |
+
height: '20px',
|
| 157 |
+
background: 'linear-gradient(180deg, var(--accent-yellow), rgba(199,165,0,0.9))',
|
| 158 |
+
borderRadius: '4px',
|
| 159 |
+
}
|
| 160 |
+
}}
|
| 161 |
+
>
|
| 162 |
+
New Session
|
| 163 |
+
</Button>
|
| 164 |
+
|
| 165 |
+
{/* Session List */}
|
| 166 |
+
<Box sx={{ flex: 1, overflow: 'auto', mx: -1, px: 1 }}>
|
| 167 |
+
<List disablePadding sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
| 168 |
+
{[...sessions].reverse().map((session, index) => {
|
| 169 |
+
const sessionNumber = sessions.length - index;
|
| 170 |
+
const isSelected = session.id === activeSessionId;
|
| 171 |
+
return (
|
| 172 |
+
<ListItem
|
| 173 |
+
key={session.id}
|
| 174 |
+
disablePadding
|
| 175 |
+
className="session-item"
|
| 176 |
+
onClick={() => handleSelectSession(session.id)}
|
| 177 |
+
sx={{
|
| 178 |
+
display: 'flex',
|
| 179 |
+
alignItems: 'center',
|
| 180 |
+
gap: '12px',
|
| 181 |
+
padding: '10px',
|
| 182 |
+
borderRadius: 'var(--radius-md)',
|
| 183 |
+
bgcolor: isSelected ? 'rgba(255,255,255,0.05)' : 'transparent',
|
| 184 |
+
cursor: 'pointer',
|
| 185 |
+
transition: 'background 0.18s ease, transform 0.08s ease',
|
| 186 |
+
'&:hover': {
|
| 187 |
+
bgcolor: 'rgba(255,255,255,0.02)',
|
| 188 |
+
transform: 'translateY(-1px)',
|
| 189 |
+
},
|
| 190 |
+
'& .delete-btn': {
|
| 191 |
+
opacity: 0,
|
| 192 |
+
transition: 'opacity 0.2s',
|
| 193 |
+
},
|
| 194 |
+
'&:hover .delete-btn': {
|
| 195 |
+
opacity: 1,
|
| 196 |
+
}
|
| 197 |
+
}}
|
| 198 |
+
>
|
| 199 |
+
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
| 200 |
+
<Typography variant="body2" sx={{ fontWeight: 500, color: 'var(--text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
| 201 |
+
Session {String(sessionNumber).padStart(2, '0')}
|
| 202 |
+
</Typography>
|
| 203 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
|
| 204 |
+
{session.isActive && <RunningIndicator />}
|
| 205 |
+
<Typography className="time" variant="caption" sx={{ fontSize: '12px', color: 'var(--muted-text)' }}>
|
| 206 |
+
{formatTime(session.createdAt)}
|
| 207 |
+
</Typography>
|
| 208 |
+
</Box>
|
| 209 |
+
</Box>
|
| 210 |
+
|
| 211 |
+
<IconButton
|
| 212 |
+
className="delete-btn"
|
| 213 |
+
size="small"
|
| 214 |
+
onClick={(e) => handleDeleteSession(session.id, e)}
|
| 215 |
+
sx={{ color: 'var(--muted-text)', '&:hover': { color: 'var(--accent-red)' } }}
|
| 216 |
+
>
|
| 217 |
+
<DeleteIcon fontSize="small" />
|
| 218 |
+
</IconButton>
|
| 219 |
+
</ListItem>
|
| 220 |
+
);
|
| 221 |
+
})}
|
| 222 |
+
</List>
|
| 223 |
+
</Box>
|
| 224 |
+
</Box>
|
| 225 |
+
|
| 226 |
+
{/* Footer */}
|
| 227 |
+
<Box sx={{ p: 2, borderTop: '1px solid rgba(255,255,255,0.03)' }}>
|
| 228 |
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
| 229 |
+
<Typography variant="caption" className="small-note" sx={{ fontSize: '12px', color: 'var(--muted-text)' }}>
|
| 230 |
+
{sessions.length} active
|
| 231 |
+
</Typography>
|
| 232 |
+
<Tooltip title="Undo last turn">
|
| 233 |
+
<span>
|
| 234 |
+
<IconButton
|
| 235 |
+
onClick={handleUndo}
|
| 236 |
+
disabled={!activeSessionId || isProcessing}
|
| 237 |
+
size="small"
|
| 238 |
+
sx={{ color: 'var(--muted-text)', '&:hover': { color: 'var(--text)' } }}
|
| 239 |
+
>
|
| 240 |
+
<UndoIcon fontSize="small" />
|
| 241 |
+
</IconButton>
|
| 242 |
+
</span>
|
| 243 |
+
</Tooltip>
|
| 244 |
+
</Box>
|
| 245 |
+
</Box>
|
| 246 |
+
</Box>
|
| 247 |
+
);
|
| 248 |
+
}
|
frontend/src/hooks/useAgentWebSocket.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useRef } from 'react';
|
| 2 |
+
import { useAgentStore } from '@/store/agentStore';
|
| 3 |
+
import { useSessionStore } from '@/store/sessionStore';
|
| 4 |
+
import { useLayoutStore } from '@/store/layoutStore';
|
| 5 |
+
import type { AgentEvent } from '@/types/events';
|
| 6 |
+
import type { Message, TraceLog } from '@/types/agent';
|
| 7 |
+
|
| 8 |
+
const WS_RECONNECT_DELAY = 1000;
|
| 9 |
+
const WS_MAX_RECONNECT_DELAY = 30000;
|
| 10 |
+
|
| 11 |
+
interface UseAgentWebSocketOptions {
|
| 12 |
+
sessionId: string | null;
|
| 13 |
+
onReady?: () => void;
|
| 14 |
+
onError?: (error: string) => void;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function useAgentWebSocket({
|
| 18 |
+
sessionId,
|
| 19 |
+
onReady,
|
| 20 |
+
onError,
|
| 21 |
+
}: UseAgentWebSocketOptions) {
|
| 22 |
+
const wsRef = useRef<WebSocket | null>(null);
|
| 23 |
+
const reconnectTimeoutRef = useRef<number | null>(null);
|
| 24 |
+
const reconnectDelayRef = useRef(WS_RECONNECT_DELAY);
|
| 25 |
+
|
| 26 |
+
const {
|
| 27 |
+
addMessage,
|
| 28 |
+
updateMessage,
|
| 29 |
+
setProcessing,
|
| 30 |
+
setConnected,
|
| 31 |
+
setPendingApprovals,
|
| 32 |
+
setError,
|
| 33 |
+
addTraceLog,
|
| 34 |
+
updateTraceLog,
|
| 35 |
+
clearTraceLogs,
|
| 36 |
+
setPanelContent,
|
| 37 |
+
setPanelTab,
|
| 38 |
+
setActivePanelTab,
|
| 39 |
+
clearPanelTabs,
|
| 40 |
+
setPlan,
|
| 41 |
+
setCurrentTurnMessageId,
|
| 42 |
+
updateCurrentTurnTrace,
|
| 43 |
+
} = useAgentStore();
|
| 44 |
+
|
| 45 |
+
const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore();
|
| 46 |
+
|
| 47 |
+
const { setSessionActive } = useSessionStore();
|
| 48 |
+
|
| 49 |
+
const handleEvent = useCallback(
|
| 50 |
+
(event: AgentEvent) => {
|
| 51 |
+
if (!sessionId) return;
|
| 52 |
+
|
| 53 |
+
switch (event.event_type) {
|
| 54 |
+
case 'ready':
|
| 55 |
+
setConnected(true);
|
| 56 |
+
setProcessing(false);
|
| 57 |
+
setSessionActive(sessionId, true);
|
| 58 |
+
onReady?.();
|
| 59 |
+
break;
|
| 60 |
+
|
| 61 |
+
case 'processing':
|
| 62 |
+
setProcessing(true);
|
| 63 |
+
clearTraceLogs();
|
| 64 |
+
// Don't clear panel tabs here - they should persist during approval flow
|
| 65 |
+
// Tabs will be cleared when a new tool_call sets up new content
|
| 66 |
+
setCurrentTurnMessageId(null); // Start a new turn
|
| 67 |
+
break;
|
| 68 |
+
|
| 69 |
+
case 'assistant_message': {
|
| 70 |
+
const content = (event.data?.content as string) || '';
|
| 71 |
+
const currentTrace = useAgentStore.getState().traceLogs;
|
| 72 |
+
const currentTurnMsgId = useAgentStore.getState().currentTurnMessageId;
|
| 73 |
+
|
| 74 |
+
if (currentTurnMsgId) {
|
| 75 |
+
// Update existing message - append content and update trace
|
| 76 |
+
const messages = useAgentStore.getState().getMessages(sessionId);
|
| 77 |
+
const existingMsg = messages.find(m => m.id === currentTurnMsgId);
|
| 78 |
+
|
| 79 |
+
if (existingMsg) {
|
| 80 |
+
const newContent = existingMsg.content ? existingMsg.content + '\n\n' + content : content;
|
| 81 |
+
updateMessage(sessionId, currentTurnMsgId, {
|
| 82 |
+
content: newContent,
|
| 83 |
+
trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
} else {
|
| 87 |
+
// Create new message
|
| 88 |
+
const messageId = `msg_${Date.now()}`;
|
| 89 |
+
const message: Message = {
|
| 90 |
+
id: messageId,
|
| 91 |
+
role: 'assistant',
|
| 92 |
+
content,
|
| 93 |
+
timestamp: new Date().toISOString(),
|
| 94 |
+
trace: currentTrace.length > 0 ? [...currentTrace] : undefined,
|
| 95 |
+
};
|
| 96 |
+
addMessage(sessionId, message);
|
| 97 |
+
setCurrentTurnMessageId(messageId);
|
| 98 |
+
}
|
| 99 |
+
break;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
case 'tool_call': {
|
| 103 |
+
const toolName = (event.data?.tool as string) || 'unknown';
|
| 104 |
+
const args = (event.data?.arguments as Record<string, any>) || {};
|
| 105 |
+
|
| 106 |
+
// Don't display plan_tool in trace logs (it shows up elsewhere in the UI)
|
| 107 |
+
if (toolName !== 'plan_tool') {
|
| 108 |
+
const log: TraceLog = {
|
| 109 |
+
id: `tool_${Date.now()}`,
|
| 110 |
+
type: 'call',
|
| 111 |
+
text: `Agent is executing ${toolName}...`,
|
| 112 |
+
tool: toolName,
|
| 113 |
+
timestamp: new Date().toISOString(),
|
| 114 |
+
completed: false,
|
| 115 |
+
};
|
| 116 |
+
addTraceLog(log);
|
| 117 |
+
// Update the current turn message's trace in real-time
|
| 118 |
+
updateCurrentTurnTrace(sessionId);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Auto-expand Right Panel for specific tools
|
| 122 |
+
if (toolName === 'hf_jobs' && (args.operation === 'run' || args.operation === 'scheduled run') && args.script) {
|
| 123 |
+
// Clear any existing tabs from previous jobs before setting new script
|
| 124 |
+
clearPanelTabs();
|
| 125 |
+
// Use tab system for jobs - add script tab immediately
|
| 126 |
+
setPanelTab({
|
| 127 |
+
id: 'script',
|
| 128 |
+
title: 'Script',
|
| 129 |
+
content: args.script,
|
| 130 |
+
language: 'python',
|
| 131 |
+
parameters: args
|
| 132 |
+
});
|
| 133 |
+
setActivePanelTab('script');
|
| 134 |
+
setRightPanelOpen(true);
|
| 135 |
+
setLeftSidebarOpen(false);
|
| 136 |
+
} else if (toolName === 'hf_repo_files' && args.operation === 'upload' && args.content) {
|
| 137 |
+
setPanelContent({
|
| 138 |
+
title: `File Upload: ${args.path || 'unnamed'}`,
|
| 139 |
+
content: args.content,
|
| 140 |
+
parameters: args,
|
| 141 |
+
language: args.path?.endsWith('.py') ? 'python' : undefined
|
| 142 |
+
});
|
| 143 |
+
setRightPanelOpen(true);
|
| 144 |
+
setLeftSidebarOpen(false);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
console.log('Tool call:', toolName, args);
|
| 148 |
+
break;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
case 'tool_output': {
|
| 152 |
+
const toolName = (event.data?.tool as string) || 'unknown';
|
| 153 |
+
const output = (event.data?.output as string) || '';
|
| 154 |
+
const success = event.data?.success as boolean;
|
| 155 |
+
|
| 156 |
+
// Mark the corresponding trace log as completed
|
| 157 |
+
updateTraceLog(toolName, { completed: true });
|
| 158 |
+
// Update the current turn message's trace in real-time
|
| 159 |
+
updateCurrentTurnTrace(sessionId);
|
| 160 |
+
|
| 161 |
+
// Special handling for hf_jobs - update the approval message with output
|
| 162 |
+
if (toolName === 'hf_jobs') {
|
| 163 |
+
const messages = useAgentStore.getState().getMessages(sessionId);
|
| 164 |
+
const lastApprovalMsg = [...messages].reverse().find(m => m.approval);
|
| 165 |
+
|
| 166 |
+
if (lastApprovalMsg) {
|
| 167 |
+
const currentOutput = lastApprovalMsg.toolOutput || '';
|
| 168 |
+
const newOutput = currentOutput ? currentOutput + '\n\n' + output : output;
|
| 169 |
+
|
| 170 |
+
useAgentStore.getState().updateMessage(sessionId, lastApprovalMsg.id, {
|
| 171 |
+
toolOutput: newOutput
|
| 172 |
+
});
|
| 173 |
+
console.log('Updated approval message with tool output:', toolName);
|
| 174 |
+
} else {
|
| 175 |
+
console.warn('Received hf_jobs output but no approval message found to update.');
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Don't create message bubbles for tool outputs - they only show in trace logs
|
| 180 |
+
console.log('Tool output:', toolName, success);
|
| 181 |
+
break;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
case 'tool_log': {
|
| 185 |
+
const toolName = (event.data?.tool as string) || 'unknown';
|
| 186 |
+
const log = (event.data?.log as string) || '';
|
| 187 |
+
|
| 188 |
+
if (toolName === 'hf_jobs') {
|
| 189 |
+
const currentTabs = useAgentStore.getState().panelTabs;
|
| 190 |
+
const logsTab = currentTabs.find(t => t.id === 'logs');
|
| 191 |
+
|
| 192 |
+
// Append to existing logs tab or create new one
|
| 193 |
+
const newContent = logsTab
|
| 194 |
+
? logsTab.content + '\n' + log
|
| 195 |
+
: '--- Job execution started ---\n' + log;
|
| 196 |
+
|
| 197 |
+
setPanelTab({
|
| 198 |
+
id: 'logs',
|
| 199 |
+
title: 'Logs',
|
| 200 |
+
content: newContent,
|
| 201 |
+
language: 'text'
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
+
// Auto-switch to logs tab when logs start streaming
|
| 205 |
+
setActivePanelTab('logs');
|
| 206 |
+
|
| 207 |
+
if (!useLayoutStore.getState().isRightPanelOpen) {
|
| 208 |
+
setRightPanelOpen(true);
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
break;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
case 'plan_update': {
|
| 215 |
+
const plan = (event.data?.plan as any[]) || [];
|
| 216 |
+
setPlan(plan);
|
| 217 |
+
if (!useLayoutStore.getState().isRightPanelOpen) {
|
| 218 |
+
setRightPanelOpen(true);
|
| 219 |
+
}
|
| 220 |
+
break;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
case 'approval_required': {
|
| 224 |
+
const tools = event.data?.tools as Array<{
|
| 225 |
+
tool: string;
|
| 226 |
+
arguments: Record<string, unknown>;
|
| 227 |
+
tool_call_id: string;
|
| 228 |
+
}>;
|
| 229 |
+
const count = (event.data?.count as number) || 0;
|
| 230 |
+
|
| 231 |
+
// Create a persistent message for the approval request
|
| 232 |
+
const message: Message = {
|
| 233 |
+
id: `msg_approval_${Date.now()}`,
|
| 234 |
+
role: 'assistant',
|
| 235 |
+
content: '', // Content is handled by the approval UI
|
| 236 |
+
timestamp: new Date().toISOString(),
|
| 237 |
+
approval: {
|
| 238 |
+
status: 'pending',
|
| 239 |
+
batch: { tools, count }
|
| 240 |
+
}
|
| 241 |
+
};
|
| 242 |
+
addMessage(sessionId, message);
|
| 243 |
+
|
| 244 |
+
// Clear currentTurnMessageId so subsequent assistant_message events create a new message below the approval
|
| 245 |
+
setCurrentTurnMessageId(null);
|
| 246 |
+
|
| 247 |
+
// We don't set pendingApprovals in the global store anymore as the message handles the UI
|
| 248 |
+
setPendingApprovals(null);
|
| 249 |
+
setProcessing(false);
|
| 250 |
+
break;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
case 'turn_complete':
|
| 254 |
+
setProcessing(false);
|
| 255 |
+
setCurrentTurnMessageId(null); // Clear the current turn
|
| 256 |
+
break;
|
| 257 |
+
|
| 258 |
+
case 'compacted': {
|
| 259 |
+
const oldTokens = event.data?.old_tokens as number;
|
| 260 |
+
const newTokens = event.data?.new_tokens as number;
|
| 261 |
+
console.log(`Context compacted: ${oldTokens} -> ${newTokens} tokens`);
|
| 262 |
+
break;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
case 'error': {
|
| 266 |
+
const errorMsg = (event.data?.error as string) || 'Unknown error';
|
| 267 |
+
setError(errorMsg);
|
| 268 |
+
setProcessing(false);
|
| 269 |
+
onError?.(errorMsg);
|
| 270 |
+
break;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
case 'shutdown':
|
| 274 |
+
setConnected(false);
|
| 275 |
+
setProcessing(false);
|
| 276 |
+
break;
|
| 277 |
+
|
| 278 |
+
case 'interrupted':
|
| 279 |
+
setProcessing(false);
|
| 280 |
+
break;
|
| 281 |
+
|
| 282 |
+
case 'undo_complete':
|
| 283 |
+
// Could remove last messages from store
|
| 284 |
+
break;
|
| 285 |
+
|
| 286 |
+
default:
|
| 287 |
+
console.log('Unknown event:', event);
|
| 288 |
+
}
|
| 289 |
+
},
|
| 290 |
+
// Zustand setters are stable, so we don't need them in deps
|
| 291 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 292 |
+
[sessionId, onReady, onError]
|
| 293 |
+
);
|
| 294 |
+
|
| 295 |
+
const connect = useCallback(() => {
|
| 296 |
+
if (!sessionId) return;
|
| 297 |
+
|
| 298 |
+
// Don't connect if already connected or connecting
|
| 299 |
+
if (wsRef.current?.readyState === WebSocket.OPEN ||
|
| 300 |
+
wsRef.current?.readyState === WebSocket.CONNECTING) {
|
| 301 |
+
return;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
// Connect directly to backend (Vite doesn't proxy WebSockets)
|
| 305 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 306 |
+
// In development, connect directly to backend port 7860
|
| 307 |
+
// In production, use the same host
|
| 308 |
+
const isDev = import.meta.env.DEV;
|
| 309 |
+
const host = isDev ? '127.0.0.1:7860' : window.location.host;
|
| 310 |
+
const wsUrl = `${protocol}//${host}/api/ws/${sessionId}`;
|
| 311 |
+
|
| 312 |
+
console.log('Connecting to WebSocket:', wsUrl);
|
| 313 |
+
const ws = new WebSocket(wsUrl);
|
| 314 |
+
|
| 315 |
+
ws.onopen = () => {
|
| 316 |
+
console.log('WebSocket connected');
|
| 317 |
+
setConnected(true);
|
| 318 |
+
reconnectDelayRef.current = WS_RECONNECT_DELAY;
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
ws.onmessage = (event) => {
|
| 322 |
+
try {
|
| 323 |
+
const data = JSON.parse(event.data) as AgentEvent;
|
| 324 |
+
handleEvent(data);
|
| 325 |
+
} catch (e) {
|
| 326 |
+
console.error('Failed to parse WebSocket message:', e);
|
| 327 |
+
}
|
| 328 |
+
};
|
| 329 |
+
|
| 330 |
+
ws.onerror = (error) => {
|
| 331 |
+
console.error('WebSocket error:', error);
|
| 332 |
+
};
|
| 333 |
+
|
| 334 |
+
ws.onclose = (event) => {
|
| 335 |
+
console.log('WebSocket closed', event.code, event.reason);
|
| 336 |
+
setConnected(false);
|
| 337 |
+
|
| 338 |
+
// Only reconnect if it wasn't a normal closure and session still exists
|
| 339 |
+
if (event.code !== 1000 && sessionId) {
|
| 340 |
+
// Attempt to reconnect with exponential backoff
|
| 341 |
+
if (reconnectTimeoutRef.current) {
|
| 342 |
+
clearTimeout(reconnectTimeoutRef.current);
|
| 343 |
+
}
|
| 344 |
+
reconnectTimeoutRef.current = window.setTimeout(() => {
|
| 345 |
+
reconnectDelayRef.current = Math.min(
|
| 346 |
+
reconnectDelayRef.current * 2,
|
| 347 |
+
WS_MAX_RECONNECT_DELAY
|
| 348 |
+
);
|
| 349 |
+
connect();
|
| 350 |
+
}, reconnectDelayRef.current);
|
| 351 |
+
}
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
wsRef.current = ws;
|
| 355 |
+
}, [sessionId, handleEvent]);
|
| 356 |
+
|
| 357 |
+
const disconnect = useCallback(() => {
|
| 358 |
+
if (reconnectTimeoutRef.current) {
|
| 359 |
+
clearTimeout(reconnectTimeoutRef.current);
|
| 360 |
+
reconnectTimeoutRef.current = null;
|
| 361 |
+
}
|
| 362 |
+
if (wsRef.current) {
|
| 363 |
+
wsRef.current.close();
|
| 364 |
+
wsRef.current = null;
|
| 365 |
+
}
|
| 366 |
+
setConnected(false);
|
| 367 |
+
}, []);
|
| 368 |
+
|
| 369 |
+
const sendPing = useCallback(() => {
|
| 370 |
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
| 371 |
+
wsRef.current.send(JSON.stringify({ type: 'ping' }));
|
| 372 |
+
}
|
| 373 |
+
}, []);
|
| 374 |
+
|
| 375 |
+
// Connect when sessionId changes (with a small delay to ensure session is ready)
|
| 376 |
+
useEffect(() => {
|
| 377 |
+
if (!sessionId) {
|
| 378 |
+
disconnect();
|
| 379 |
+
return;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// Small delay to ensure session is fully created on backend
|
| 383 |
+
const timeoutId = setTimeout(() => {
|
| 384 |
+
connect();
|
| 385 |
+
}, 100);
|
| 386 |
+
|
| 387 |
+
return () => {
|
| 388 |
+
clearTimeout(timeoutId);
|
| 389 |
+
disconnect();
|
| 390 |
+
};
|
| 391 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 392 |
+
}, [sessionId]);
|
| 393 |
+
|
| 394 |
+
// Heartbeat
|
| 395 |
+
useEffect(() => {
|
| 396 |
+
const interval = setInterval(sendPing, 30000);
|
| 397 |
+
return () => clearInterval(interval);
|
| 398 |
+
}, [sendPing]);
|
| 399 |
+
|
| 400 |
+
return {
|
| 401 |
+
isConnected: wsRef.current?.readyState === WebSocket.OPEN,
|
| 402 |
+
connect,
|
| 403 |
+
disconnect,
|
| 404 |
+
};
|
| 405 |
+
}
|
frontend/src/main.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react';
|
| 2 |
+
import { createRoot } from 'react-dom/client';
|
| 3 |
+
import { ThemeProvider } from '@mui/material/styles';
|
| 4 |
+
import CssBaseline from '@mui/material/CssBaseline';
|
| 5 |
+
import App from './App';
|
| 6 |
+
import theme from './theme';
|
| 7 |
+
|
| 8 |
+
createRoot(document.getElementById('root')!).render(
|
| 9 |
+
<StrictMode>
|
| 10 |
+
<ThemeProvider theme={theme}>
|
| 11 |
+
<CssBaseline />
|
| 12 |
+
<App />
|
| 13 |
+
</ThemeProvider>
|
| 14 |
+
</StrictMode>
|
| 15 |
+
);
|
frontend/src/store/agentStore.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import type { Message, ApprovalBatch, User, TraceLog } from '@/types/agent';
|
| 3 |
+
|
| 4 |
+
export interface PlanItem {
|
| 5 |
+
id: string;
|
| 6 |
+
content: string;
|
| 7 |
+
status: 'pending' | 'in_progress' | 'completed';
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
interface PanelTab {
|
| 11 |
+
id: string;
|
| 12 |
+
title: string;
|
| 13 |
+
content: string;
|
| 14 |
+
language?: string;
|
| 15 |
+
parameters?: any;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface AgentStore {
|
| 19 |
+
// State per session (keyed by session ID)
|
| 20 |
+
messagesBySession: Record<string, Message[]>;
|
| 21 |
+
isProcessing: boolean;
|
| 22 |
+
isConnected: boolean;
|
| 23 |
+
pendingApprovals: ApprovalBatch | null;
|
| 24 |
+
user: User | null;
|
| 25 |
+
error: string | null;
|
| 26 |
+
traceLogs: TraceLog[];
|
| 27 |
+
panelContent: { title: string; content: string; language?: string; parameters?: any } | null;
|
| 28 |
+
panelTabs: PanelTab[];
|
| 29 |
+
activePanelTab: string | null;
|
| 30 |
+
plan: PlanItem[];
|
| 31 |
+
currentTurnMessageId: string | null; // Track the current turn's assistant message
|
| 32 |
+
|
| 33 |
+
// Actions
|
| 34 |
+
addMessage: (sessionId: string, message: Message) => void;
|
| 35 |
+
updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => void;
|
| 36 |
+
clearMessages: (sessionId: string) => void;
|
| 37 |
+
setProcessing: (isProcessing: boolean) => void;
|
| 38 |
+
setConnected: (isConnected: boolean) => void;
|
| 39 |
+
setPendingApprovals: (approvals: ApprovalBatch | null) => void;
|
| 40 |
+
setUser: (user: User | null) => void;
|
| 41 |
+
setError: (error: string | null) => void;
|
| 42 |
+
getMessages: (sessionId: string) => Message[];
|
| 43 |
+
addTraceLog: (log: TraceLog) => void;
|
| 44 |
+
updateTraceLog: (toolName: string, updates: Partial<TraceLog>) => void;
|
| 45 |
+
clearTraceLogs: () => void;
|
| 46 |
+
setPanelContent: (content: { title: string; content: string; language?: string; parameters?: any } | null) => void;
|
| 47 |
+
setPanelTab: (tab: PanelTab) => void;
|
| 48 |
+
setActivePanelTab: (tabId: string) => void;
|
| 49 |
+
clearPanelTabs: () => void;
|
| 50 |
+
setPlan: (plan: PlanItem[]) => void;
|
| 51 |
+
setCurrentTurnMessageId: (id: string | null) => void;
|
| 52 |
+
updateCurrentTurnTrace: (sessionId: string) => void;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export const useAgentStore = create<AgentStore>((set, get) => ({
|
| 56 |
+
messagesBySession: {},
|
| 57 |
+
isProcessing: false,
|
| 58 |
+
isConnected: false,
|
| 59 |
+
pendingApprovals: null,
|
| 60 |
+
user: null,
|
| 61 |
+
error: null,
|
| 62 |
+
traceLogs: [],
|
| 63 |
+
panelContent: null,
|
| 64 |
+
panelTabs: [],
|
| 65 |
+
activePanelTab: null,
|
| 66 |
+
plan: [],
|
| 67 |
+
currentTurnMessageId: null,
|
| 68 |
+
|
| 69 |
+
addMessage: (sessionId: string, message: Message) => {
|
| 70 |
+
set((state) => {
|
| 71 |
+
const currentMessages = state.messagesBySession[sessionId] || [];
|
| 72 |
+
return {
|
| 73 |
+
messagesBySession: {
|
| 74 |
+
...state.messagesBySession,
|
| 75 |
+
[sessionId]: [...currentMessages, message],
|
| 76 |
+
},
|
| 77 |
+
};
|
| 78 |
+
});
|
| 79 |
+
},
|
| 80 |
+
|
| 81 |
+
updateMessage: (sessionId: string, messageId: string, updates: Partial<Message>) => {
|
| 82 |
+
set((state) => {
|
| 83 |
+
const currentMessages = state.messagesBySession[sessionId] || [];
|
| 84 |
+
const updatedMessages = currentMessages.map((msg) =>
|
| 85 |
+
msg.id === messageId ? { ...msg, ...updates } : msg
|
| 86 |
+
);
|
| 87 |
+
return {
|
| 88 |
+
messagesBySession: {
|
| 89 |
+
...state.messagesBySession,
|
| 90 |
+
[sessionId]: updatedMessages,
|
| 91 |
+
},
|
| 92 |
+
};
|
| 93 |
+
});
|
| 94 |
+
},
|
| 95 |
+
|
| 96 |
+
clearMessages: (sessionId: string) => {
|
| 97 |
+
set((state) => ({
|
| 98 |
+
messagesBySession: {
|
| 99 |
+
...state.messagesBySession,
|
| 100 |
+
[sessionId]: [],
|
| 101 |
+
},
|
| 102 |
+
}));
|
| 103 |
+
},
|
| 104 |
+
|
| 105 |
+
setProcessing: (isProcessing: boolean) => {
|
| 106 |
+
set({ isProcessing });
|
| 107 |
+
},
|
| 108 |
+
|
| 109 |
+
setConnected: (isConnected: boolean) => {
|
| 110 |
+
set({ isConnected });
|
| 111 |
+
},
|
| 112 |
+
|
| 113 |
+
setPendingApprovals: (approvals: ApprovalBatch | null) => {
|
| 114 |
+
set({ pendingApprovals: approvals });
|
| 115 |
+
},
|
| 116 |
+
|
| 117 |
+
setUser: (user: User | null) => {
|
| 118 |
+
set({ user });
|
| 119 |
+
},
|
| 120 |
+
|
| 121 |
+
setError: (error: string | null) => {
|
| 122 |
+
set({ error });
|
| 123 |
+
},
|
| 124 |
+
|
| 125 |
+
getMessages: (sessionId: string) => {
|
| 126 |
+
return get().messagesBySession[sessionId] || [];
|
| 127 |
+
},
|
| 128 |
+
|
| 129 |
+
addTraceLog: (log: TraceLog) => {
|
| 130 |
+
set((state) => ({
|
| 131 |
+
traceLogs: [...state.traceLogs, log],
|
| 132 |
+
}));
|
| 133 |
+
},
|
| 134 |
+
|
| 135 |
+
updateTraceLog: (toolName: string, updates: Partial<TraceLog>) => {
|
| 136 |
+
set((state) => {
|
| 137 |
+
// Find the last trace log with this tool name and update it
|
| 138 |
+
const traceLogs = [...state.traceLogs];
|
| 139 |
+
for (let i = traceLogs.length - 1; i >= 0; i--) {
|
| 140 |
+
if (traceLogs[i].tool === toolName && traceLogs[i].type === 'call') {
|
| 141 |
+
traceLogs[i] = { ...traceLogs[i], ...updates };
|
| 142 |
+
break;
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
return { traceLogs };
|
| 146 |
+
});
|
| 147 |
+
},
|
| 148 |
+
|
| 149 |
+
clearTraceLogs: () => {
|
| 150 |
+
set({ traceLogs: [] });
|
| 151 |
+
},
|
| 152 |
+
|
| 153 |
+
setPanelContent: (content) => {
|
| 154 |
+
set({ panelContent: content });
|
| 155 |
+
},
|
| 156 |
+
|
| 157 |
+
setPanelTab: (tab: PanelTab) => {
|
| 158 |
+
set((state) => {
|
| 159 |
+
const existingIndex = state.panelTabs.findIndex(t => t.id === tab.id);
|
| 160 |
+
let newTabs: PanelTab[];
|
| 161 |
+
if (existingIndex >= 0) {
|
| 162 |
+
// Update existing tab
|
| 163 |
+
newTabs = [...state.panelTabs];
|
| 164 |
+
newTabs[existingIndex] = tab;
|
| 165 |
+
} else {
|
| 166 |
+
// Add new tab
|
| 167 |
+
newTabs = [...state.panelTabs, tab];
|
| 168 |
+
}
|
| 169 |
+
return {
|
| 170 |
+
panelTabs: newTabs,
|
| 171 |
+
activePanelTab: state.activePanelTab || tab.id, // Auto-select first tab
|
| 172 |
+
};
|
| 173 |
+
});
|
| 174 |
+
},
|
| 175 |
+
|
| 176 |
+
setActivePanelTab: (tabId: string) => {
|
| 177 |
+
set({ activePanelTab: tabId });
|
| 178 |
+
},
|
| 179 |
+
|
| 180 |
+
clearPanelTabs: () => {
|
| 181 |
+
set({ panelTabs: [], activePanelTab: null });
|
| 182 |
+
},
|
| 183 |
+
|
| 184 |
+
setPlan: (plan: PlanItem[]) => {
|
| 185 |
+
set({ plan });
|
| 186 |
+
},
|
| 187 |
+
|
| 188 |
+
setCurrentTurnMessageId: (id: string | null) => {
|
| 189 |
+
set({ currentTurnMessageId: id });
|
| 190 |
+
},
|
| 191 |
+
|
| 192 |
+
updateCurrentTurnTrace: (sessionId: string) => {
|
| 193 |
+
const state = get();
|
| 194 |
+
if (state.currentTurnMessageId) {
|
| 195 |
+
const currentMessages = state.messagesBySession[sessionId] || [];
|
| 196 |
+
const updatedMessages = currentMessages.map((msg) =>
|
| 197 |
+
msg.id === state.currentTurnMessageId
|
| 198 |
+
? { ...msg, trace: state.traceLogs.length > 0 ? [...state.traceLogs] : undefined }
|
| 199 |
+
: msg
|
| 200 |
+
);
|
| 201 |
+
set({
|
| 202 |
+
messagesBySession: {
|
| 203 |
+
...state.messagesBySession,
|
| 204 |
+
[sessionId]: updatedMessages,
|
| 205 |
+
},
|
| 206 |
+
});
|
| 207 |
+
}
|
| 208 |
+
},
|
| 209 |
+
}));
|
frontend/src/store/layoutStore.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
|
| 3 |
+
interface LayoutStore {
|
| 4 |
+
isLeftSidebarOpen: boolean;
|
| 5 |
+
isRightPanelOpen: boolean;
|
| 6 |
+
rightPanelWidth: number;
|
| 7 |
+
setLeftSidebarOpen: (open: boolean) => void;
|
| 8 |
+
setRightPanelOpen: (open: boolean) => void;
|
| 9 |
+
setRightPanelWidth: (width: number) => void;
|
| 10 |
+
toggleLeftSidebar: () => void;
|
| 11 |
+
toggleRightPanel: () => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const useLayoutStore = create<LayoutStore>((set) => ({
|
| 15 |
+
isLeftSidebarOpen: true,
|
| 16 |
+
isRightPanelOpen: false,
|
| 17 |
+
rightPanelWidth: 450,
|
| 18 |
+
setLeftSidebarOpen: (open) => set({ isLeftSidebarOpen: open }),
|
| 19 |
+
setRightPanelOpen: (open) => set({ isRightPanelOpen: open }),
|
| 20 |
+
setRightPanelWidth: (width) => set({ rightPanelWidth: width }),
|
| 21 |
+
toggleLeftSidebar: () => set((state) => ({ isLeftSidebarOpen: !state.isLeftSidebarOpen })),
|
| 22 |
+
toggleRightPanel: () => set((state) => ({ isRightPanelOpen: !state.isRightPanelOpen })),
|
| 23 |
+
}));
|
frontend/src/store/sessionStore.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import { persist } from 'zustand/middleware';
|
| 3 |
+
import type { SessionMeta } from '@/types/agent';
|
| 4 |
+
|
| 5 |
+
interface SessionStore {
|
| 6 |
+
sessions: SessionMeta[];
|
| 7 |
+
activeSessionId: string | null;
|
| 8 |
+
|
| 9 |
+
// Actions
|
| 10 |
+
createSession: (id: string) => void;
|
| 11 |
+
deleteSession: (id: string) => void;
|
| 12 |
+
switchSession: (id: string) => void;
|
| 13 |
+
updateSessionTitle: (id: string, title: string) => void;
|
| 14 |
+
setSessionActive: (id: string, isActive: boolean) => void;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export const useSessionStore = create<SessionStore>()(
|
| 18 |
+
persist(
|
| 19 |
+
(set, get) => ({
|
| 20 |
+
sessions: [],
|
| 21 |
+
activeSessionId: null,
|
| 22 |
+
|
| 23 |
+
createSession: (id: string) => {
|
| 24 |
+
const newSession: SessionMeta = {
|
| 25 |
+
id,
|
| 26 |
+
title: `Chat ${get().sessions.length + 1}`,
|
| 27 |
+
createdAt: new Date().toISOString(),
|
| 28 |
+
isActive: true,
|
| 29 |
+
};
|
| 30 |
+
set((state) => ({
|
| 31 |
+
sessions: [...state.sessions, newSession],
|
| 32 |
+
activeSessionId: id,
|
| 33 |
+
}));
|
| 34 |
+
},
|
| 35 |
+
|
| 36 |
+
deleteSession: (id: string) => {
|
| 37 |
+
set((state) => {
|
| 38 |
+
const newSessions = state.sessions.filter((s) => s.id !== id);
|
| 39 |
+
const newActiveId =
|
| 40 |
+
state.activeSessionId === id
|
| 41 |
+
? newSessions[newSessions.length - 1]?.id || null
|
| 42 |
+
: state.activeSessionId;
|
| 43 |
+
return {
|
| 44 |
+
sessions: newSessions,
|
| 45 |
+
activeSessionId: newActiveId,
|
| 46 |
+
};
|
| 47 |
+
});
|
| 48 |
+
},
|
| 49 |
+
|
| 50 |
+
switchSession: (id: string) => {
|
| 51 |
+
set({ activeSessionId: id });
|
| 52 |
+
},
|
| 53 |
+
|
| 54 |
+
updateSessionTitle: (id: string, title: string) => {
|
| 55 |
+
set((state) => ({
|
| 56 |
+
sessions: state.sessions.map((s) =>
|
| 57 |
+
s.id === id ? { ...s, title } : s
|
| 58 |
+
),
|
| 59 |
+
}));
|
| 60 |
+
},
|
| 61 |
+
|
| 62 |
+
setSessionActive: (id: string, isActive: boolean) => {
|
| 63 |
+
set((state) => ({
|
| 64 |
+
sessions: state.sessions.map((s) =>
|
| 65 |
+
s.id === id ? { ...s, isActive } : s
|
| 66 |
+
),
|
| 67 |
+
}));
|
| 68 |
+
},
|
| 69 |
+
}),
|
| 70 |
+
{
|
| 71 |
+
name: 'hf-agent-sessions',
|
| 72 |
+
partialize: (state) => ({
|
| 73 |
+
sessions: state.sessions,
|
| 74 |
+
activeSessionId: state.activeSessionId,
|
| 75 |
+
}),
|
| 76 |
+
}
|
| 77 |
+
)
|
| 78 |
+
);
|
frontend/src/theme.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createTheme } from '@mui/material/styles';
|
| 2 |
+
|
| 3 |
+
const theme = createTheme({
|
| 4 |
+
palette: {
|
| 5 |
+
mode: 'dark',
|
| 6 |
+
primary: {
|
| 7 |
+
main: '#C7A500', // --accent-yellow
|
| 8 |
+
},
|
| 9 |
+
secondary: {
|
| 10 |
+
main: '#FF9D00',
|
| 11 |
+
},
|
| 12 |
+
background: {
|
| 13 |
+
default: '#0B0D10', // --bg
|
| 14 |
+
paper: '#0F1316', // --panel
|
| 15 |
+
},
|
| 16 |
+
text: {
|
| 17 |
+
primary: '#E6EEF8', // --text
|
| 18 |
+
secondary: '#98A0AA', // --muted-text
|
| 19 |
+
},
|
| 20 |
+
divider: 'rgba(255,255,255,0.03)',
|
| 21 |
+
success: {
|
| 22 |
+
main: '#2FCC71', // --accent-green
|
| 23 |
+
},
|
| 24 |
+
error: {
|
| 25 |
+
main: '#E05A4F', // --accent-red
|
| 26 |
+
},
|
| 27 |
+
warning: {
|
| 28 |
+
main: '#C7A500',
|
| 29 |
+
},
|
| 30 |
+
info: {
|
| 31 |
+
main: '#58A6FF',
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
typography: {
|
| 35 |
+
fontFamily: 'Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
|
| 36 |
+
fontSize: 15,
|
| 37 |
+
h1: { fontWeight: 600, color: '#E6EEF8' },
|
| 38 |
+
h2: { fontWeight: 600, color: '#E6EEF8' },
|
| 39 |
+
h3: { fontWeight: 600, color: '#E6EEF8' },
|
| 40 |
+
h4: { fontWeight: 600, color: '#E6EEF8' },
|
| 41 |
+
h5: { fontWeight: 600, color: '#E6EEF8' },
|
| 42 |
+
h6: { fontWeight: 600, color: '#E6EEF8' },
|
| 43 |
+
body1: { fontSize: '15px', color: '#E6EEF8' },
|
| 44 |
+
body2: { fontSize: '0.875rem', color: '#98A0AA' },
|
| 45 |
+
button: {
|
| 46 |
+
fontFamily: 'Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
|
| 47 |
+
textTransform: 'none',
|
| 48 |
+
fontWeight: 600,
|
| 49 |
+
},
|
| 50 |
+
},
|
| 51 |
+
components: {
|
| 52 |
+
MuiCssBaseline: {
|
| 53 |
+
styleOverrides: {
|
| 54 |
+
':root': {
|
| 55 |
+
'--bg': '#0B0D10',
|
| 56 |
+
'--panel': '#0F1316',
|
| 57 |
+
'--surface': '#121416',
|
| 58 |
+
'--text': '#E6EEF8',
|
| 59 |
+
'--muted-text': '#98A0AA',
|
| 60 |
+
'--accent-yellow': '#C7A500',
|
| 61 |
+
'--accent-yellow-weak': 'rgba(199,165,0,0.08)',
|
| 62 |
+
'--accent-green': '#2FCC71',
|
| 63 |
+
'--accent-red': '#E05A4F',
|
| 64 |
+
'--shadow-1': '0 6px 18px rgba(2,6,12,0.55)',
|
| 65 |
+
'--radius-lg': '20px',
|
| 66 |
+
'--radius-md': '12px',
|
| 67 |
+
'--focus': '0 0 0 3px rgba(199,165,0,0.12)',
|
| 68 |
+
},
|
| 69 |
+
body: {
|
| 70 |
+
background: 'linear-gradient(180deg, var(--bg), #090B0D)',
|
| 71 |
+
color: 'var(--text)',
|
| 72 |
+
scrollbarWidth: 'thin',
|
| 73 |
+
'&::-webkit-scrollbar': {
|
| 74 |
+
width: '8px',
|
| 75 |
+
height: '8px',
|
| 76 |
+
},
|
| 77 |
+
'&::-webkit-scrollbar-thumb': {
|
| 78 |
+
backgroundColor: '#30363D',
|
| 79 |
+
borderRadius: '2px',
|
| 80 |
+
},
|
| 81 |
+
'&::-webkit-scrollbar-track': {
|
| 82 |
+
backgroundColor: 'transparent',
|
| 83 |
+
},
|
| 84 |
+
},
|
| 85 |
+
'code, pre': {
|
| 86 |
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace',
|
| 87 |
+
},
|
| 88 |
+
'.brand-logo': {
|
| 89 |
+
position: 'relative',
|
| 90 |
+
padding: '6px',
|
| 91 |
+
borderRadius: '8px',
|
| 92 |
+
'&::after': {
|
| 93 |
+
content: '""',
|
| 94 |
+
position: 'absolute',
|
| 95 |
+
inset: '-6px',
|
| 96 |
+
borderRadius: '10px',
|
| 97 |
+
background: 'var(--accent-yellow-weak)',
|
| 98 |
+
zIndex: -1,
|
| 99 |
+
pointerEvents: 'none',
|
| 100 |
+
},
|
| 101 |
+
},
|
| 102 |
+
},
|
| 103 |
+
},
|
| 104 |
+
MuiButton: {
|
| 105 |
+
styleOverrides: {
|
| 106 |
+
root: {
|
| 107 |
+
borderRadius: '10px',
|
| 108 |
+
fontWeight: 600,
|
| 109 |
+
transition: 'transform 0.06s ease, background 0.12s ease, box-shadow 0.12s ease',
|
| 110 |
+
'&:hover': {
|
| 111 |
+
transform: 'translateY(-1px)',
|
| 112 |
+
},
|
| 113 |
+
},
|
| 114 |
+
},
|
| 115 |
+
},
|
| 116 |
+
MuiPaper: {
|
| 117 |
+
styleOverrides: {
|
| 118 |
+
root: {
|
| 119 |
+
backgroundImage: 'none',
|
| 120 |
+
backgroundColor: 'transparent', // Default to transparent for gradients
|
| 121 |
+
},
|
| 122 |
+
},
|
| 123 |
+
},
|
| 124 |
+
MuiDrawer: {
|
| 125 |
+
styleOverrides: {
|
| 126 |
+
paper: {
|
| 127 |
+
backgroundColor: 'var(--panel)',
|
| 128 |
+
borderRight: '1px solid rgba(255,255,255,0.03)',
|
| 129 |
+
},
|
| 130 |
+
},
|
| 131 |
+
},
|
| 132 |
+
MuiTextField: {
|
| 133 |
+
styleOverrides: {
|
| 134 |
+
root: {
|
| 135 |
+
'& .MuiOutlinedInput-root': {
|
| 136 |
+
borderRadius: 'var(--radius-md)',
|
| 137 |
+
'& fieldset': {
|
| 138 |
+
borderColor: 'rgba(255,255,255,0.03)',
|
| 139 |
+
},
|
| 140 |
+
'&:hover fieldset': {
|
| 141 |
+
borderColor: 'rgba(255,255,255,0.1)',
|
| 142 |
+
},
|
| 143 |
+
'&.Mui-focused fieldset': {
|
| 144 |
+
borderColor: 'var(--accent-yellow)',
|
| 145 |
+
borderWidth: '1px',
|
| 146 |
+
boxShadow: 'var(--focus)',
|
| 147 |
+
},
|
| 148 |
+
},
|
| 149 |
+
},
|
| 150 |
+
},
|
| 151 |
+
},
|
| 152 |
+
},
|
| 153 |
+
shape: {
|
| 154 |
+
borderRadius: 12,
|
| 155 |
+
},
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
export default theme;
|
frontend/src/types/agent.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Agent-related types
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export interface SessionMeta {
|
| 6 |
+
id: string;
|
| 7 |
+
title: string;
|
| 8 |
+
createdAt: string;
|
| 9 |
+
isActive: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface Message {
|
| 13 |
+
id: string;
|
| 14 |
+
role: 'user' | 'assistant' | 'tool';
|
| 15 |
+
content: string;
|
| 16 |
+
timestamp: string;
|
| 17 |
+
toolName?: string;
|
| 18 |
+
tool_call_id?: string;
|
| 19 |
+
trace?: TraceLog[];
|
| 20 |
+
approval?: {
|
| 21 |
+
status: 'pending' | 'approved' | 'rejected';
|
| 22 |
+
batch: ApprovalBatch;
|
| 23 |
+
decisions?: ToolApproval[];
|
| 24 |
+
};
|
| 25 |
+
toolOutput?: string;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export interface ToolCall {
|
| 29 |
+
id: string;
|
| 30 |
+
tool: string;
|
| 31 |
+
arguments: Record<string, unknown>;
|
| 32 |
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
| 33 |
+
output?: string;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export interface ToolApproval {
|
| 37 |
+
tool_call_id: string;
|
| 38 |
+
approved: boolean;
|
| 39 |
+
feedback?: string | null;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export interface ApprovalBatch {
|
| 43 |
+
tools: Array<{
|
| 44 |
+
tool: string;
|
| 45 |
+
arguments: Record<string, unknown>;
|
| 46 |
+
tool_call_id: string;
|
| 47 |
+
}>;
|
| 48 |
+
count: number;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export interface TraceLog {
|
| 52 |
+
id: string;
|
| 53 |
+
type: 'call' | 'output';
|
| 54 |
+
text: string;
|
| 55 |
+
tool: string;
|
| 56 |
+
timestamp: string;
|
| 57 |
+
completed?: boolean;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export interface User {
|
| 61 |
+
authenticated: boolean;
|
| 62 |
+
username?: string;
|
| 63 |
+
name?: string;
|
| 64 |
+
picture?: string;
|
| 65 |
+
}
|
frontend/src/types/events.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Event types from the agent backend
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export type EventType =
|
| 6 |
+
| 'ready'
|
| 7 |
+
| 'processing'
|
| 8 |
+
| 'assistant_message'
|
| 9 |
+
| 'tool_call'
|
| 10 |
+
| 'tool_output'
|
| 11 |
+
| 'tool_log'
|
| 12 |
+
| 'approval_required'
|
| 13 |
+
| 'turn_complete'
|
| 14 |
+
| 'compacted'
|
| 15 |
+
| 'error'
|
| 16 |
+
| 'shutdown'
|
| 17 |
+
| 'interrupted'
|
| 18 |
+
| 'undo_complete'
|
| 19 |
+
| 'plan_update';
|
| 20 |
+
|
| 21 |
+
export interface AgentEvent {
|
| 22 |
+
event_type: EventType;
|
| 23 |
+
data?: Record<string, unknown>;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export interface ReadyEventData {
|
| 27 |
+
message: string;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export interface ProcessingEventData {
|
| 31 |
+
message: string;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface AssistantMessageEventData {
|
| 35 |
+
content: string;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export interface ToolCallEventData {
|
| 39 |
+
tool: string;
|
| 40 |
+
arguments: Record<string, unknown>;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export interface ToolOutputEventData {
|
| 44 |
+
tool: string;
|
| 45 |
+
output: string;
|
| 46 |
+
success: boolean;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export interface ToolLogEventData {
|
| 50 |
+
tool: string;
|
| 51 |
+
log: string;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export interface PlanUpdateEventData {
|
| 55 |
+
plan: Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed' }>;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export interface ApprovalRequiredEventData {
|
| 59 |
+
tools: ApprovalToolItem[];
|
| 60 |
+
count: number;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export interface ApprovalToolItem {
|
| 64 |
+
tool: string;
|
| 65 |
+
arguments: Record<string, unknown>;
|
| 66 |
+
tool_call_id: string;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
export interface TurnCompleteEventData {
|
| 70 |
+
history_size: number;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface CompactedEventData {
|
| 74 |
+
old_tokens: number;
|
| 75 |
+
new_tokens: number;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export interface ErrorEventData {
|
| 79 |
+
error: string;
|
| 80 |
+
}
|
frontend/src/utils/logProcessor.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function processLogs(logs: string): string {
|
| 2 |
+
if (!logs) return '';
|
| 3 |
+
|
| 4 |
+
// 1. Handle \r (Carriage Return) for progress bars
|
| 5 |
+
const rawLines = logs.split('\n');
|
| 6 |
+
const processedLines: string[] = [];
|
| 7 |
+
|
| 8 |
+
for (const rawLine of rawLines) {
|
| 9 |
+
// Remove potential trailing \r from \r\n split
|
| 10 |
+
let line = rawLine;
|
| 11 |
+
if (line.endsWith('\r')) {
|
| 12 |
+
line = line.slice(0, -1);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
if (line.includes('\r')) {
|
| 16 |
+
const segments = line.split('\r');
|
| 17 |
+
// Find the last non-empty segment
|
| 18 |
+
// Iterate backwards
|
| 19 |
+
let found = false;
|
| 20 |
+
for (let i = segments.length - 1; i >= 0; i--) {
|
| 21 |
+
if (segments[i].length > 0) {
|
| 22 |
+
processedLines.push(segments[i]);
|
| 23 |
+
found = true;
|
| 24 |
+
break;
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
if (!found) {
|
| 28 |
+
// If all segments were empty, push empty string (or skip?)
|
| 29 |
+
processedLines.push("");
|
| 30 |
+
}
|
| 31 |
+
} else {
|
| 32 |
+
processedLines.push(line);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 2. Compaction (Downloading & TQDM)
|
| 37 |
+
const finalLines: string[] = [];
|
| 38 |
+
|
| 39 |
+
// Regex for "Downloading <package>" or "Downloaded <package>"
|
| 40 |
+
const downloadPattern = /^(Downloading|Downloaded)\s+/;
|
| 41 |
+
|
| 42 |
+
// Regex for TQDM-like progress bars
|
| 43 |
+
// Examples:
|
| 44 |
+
// "100%|██████████| 10/10 [00:01<00:00, 8.00it/s]"
|
| 45 |
+
// " 20%|## | ..."
|
| 46 |
+
// "Downloading: 10%"
|
| 47 |
+
const tqdmPattern = /^\s*\d+%\|.*\||^\s*\d+%\s+/;
|
| 48 |
+
|
| 49 |
+
for (let i = 0; i < processedLines.length; i++) {
|
| 50 |
+
const line = processedLines[i];
|
| 51 |
+
|
| 52 |
+
// Check for Download pattern
|
| 53 |
+
if (downloadPattern.test(line)) {
|
| 54 |
+
// Look ahead for consecutive download lines
|
| 55 |
+
let nextIsDownload = false;
|
| 56 |
+
if (i + 1 < processedLines.length) {
|
| 57 |
+
nextIsDownload = downloadPattern.test(processedLines[i + 1]);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (nextIsDownload) {
|
| 61 |
+
continue; // Skip this line
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
// Check for TQDM pattern
|
| 65 |
+
else if (tqdmPattern.test(line)) {
|
| 66 |
+
// Look ahead for consecutive TQDM lines
|
| 67 |
+
let nextIsTqdm = false;
|
| 68 |
+
if (i + 1 < processedLines.length) {
|
| 69 |
+
nextIsTqdm = tqdmPattern.test(processedLines[i + 1]);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
if (nextIsTqdm) {
|
| 73 |
+
continue; // Skip this line
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
finalLines.push(line);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return finalLines.join('\n');
|
| 81 |
+
}
|
frontend/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"moduleResolution": "bundler",
|
| 9 |
+
"allowImportingTsExtensions": true,
|
| 10 |
+
"isolatedModules": true,
|
| 11 |
+
"moduleDetection": "force",
|
| 12 |
+
"noEmit": true,
|
| 13 |
+
"jsx": "react-jsx",
|
| 14 |
+
"strict": true,
|
| 15 |
+
"noUnusedLocals": true,
|
| 16 |
+
"noUnusedParameters": true,
|
| 17 |
+
"noFallthroughCasesInSwitch": true,
|
| 18 |
+
"noUncheckedSideEffectImports": true,
|
| 19 |
+
"baseUrl": ".",
|
| 20 |
+
"paths": {
|
| 21 |
+
"@/*": ["src/*"]
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"include": ["src"]
|
| 25 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import path from 'path'
|
| 4 |
+
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
resolve: {
|
| 8 |
+
alias: {
|
| 9 |
+
'@': path.resolve(__dirname, './src'),
|
| 10 |
+
},
|
| 11 |
+
},
|
| 12 |
+
server: {
|
| 13 |
+
port: 5173,
|
| 14 |
+
proxy: {
|
| 15 |
+
'/api': {
|
| 16 |
+
target: 'http://localhost:7860',
|
| 17 |
+
changeOrigin: true,
|
| 18 |
+
},
|
| 19 |
+
'/auth': {
|
| 20 |
+
target: 'http://localhost:7860',
|
| 21 |
+
changeOrigin: true,
|
| 22 |
+
},
|
| 23 |
+
},
|
| 24 |
+
},
|
| 25 |
+
build: {
|
| 26 |
+
outDir: 'dist',
|
| 27 |
+
sourcemap: false,
|
| 28 |
+
},
|
| 29 |
+
})
|
pyproject.toml
CHANGED
|
@@ -25,6 +25,11 @@ agent = [
|
|
| 25 |
"nbformat>=5.10.4",
|
| 26 |
"datasets>=4.3.0", # For session logging to HF datasets
|
| 27 |
"whoosh>=2.7.4",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
]
|
| 29 |
|
| 30 |
# Evaluation/benchmarking dependencies
|
|
|
|
| 25 |
"nbformat>=5.10.4",
|
| 26 |
"datasets>=4.3.0", # For session logging to HF datasets
|
| 27 |
"whoosh>=2.7.4",
|
| 28 |
+
# Web backend dependencies
|
| 29 |
+
"fastapi>=0.115.0",
|
| 30 |
+
"uvicorn[standard]>=0.32.0",
|
| 31 |
+
"httpx>=0.27.0",
|
| 32 |
+
"websockets>=13.0",
|
| 33 |
]
|
| 34 |
|
| 35 |
# Evaluation/benchmarking dependencies
|
uv.lock
CHANGED
|
@@ -169,6 +169,15 @@ wheels = [
|
|
| 169 |
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
| 170 |
]
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
[[package]]
|
| 173 |
name = "annotated-types"
|
| 174 |
version = "0.7.0"
|
|
@@ -638,6 +647,21 @@ wheels = [
|
|
| 638 |
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
|
| 639 |
]
|
| 640 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
[[package]]
|
| 642 |
name = "fastjsonschema"
|
| 643 |
version = "2.21.2"
|
|
@@ -910,7 +934,9 @@ dependencies = [
|
|
| 910 |
[package.optional-dependencies]
|
| 911 |
agent = [
|
| 912 |
{ name = "datasets" },
|
|
|
|
| 913 |
{ name = "fastmcp" },
|
|
|
|
| 914 |
{ name = "huggingface-hub" },
|
| 915 |
{ name = "litellm" },
|
| 916 |
{ name = "lmnr" },
|
|
@@ -919,10 +945,15 @@ agent = [
|
|
| 919 |
{ name = "prompt-toolkit" },
|
| 920 |
{ name = "requests" },
|
| 921 |
{ name = "thefuzz" },
|
|
|
|
|
|
|
|
|
|
| 922 |
]
|
| 923 |
all = [
|
| 924 |
{ name = "datasets" },
|
|
|
|
| 925 |
{ name = "fastmcp" },
|
|
|
|
| 926 |
{ name = "huggingface-hub" },
|
| 927 |
{ name = "inspect-ai" },
|
| 928 |
{ name = "litellm" },
|
|
@@ -935,6 +966,9 @@ all = [
|
|
| 935 |
{ name = "requests" },
|
| 936 |
{ name = "tenacity" },
|
| 937 |
{ name = "thefuzz" },
|
|
|
|
|
|
|
|
|
|
| 938 |
]
|
| 939 |
dev = [
|
| 940 |
{ name = "pytest" },
|
|
@@ -951,8 +985,10 @@ requires-dist = [
|
|
| 951 |
{ name = "datasets", specifier = ">=4.4.1" },
|
| 952 |
{ name = "datasets", marker = "extra == 'agent'", specifier = ">=4.3.0" },
|
| 953 |
{ name = "datasets", marker = "extra == 'eval'", specifier = ">=4.3.0" },
|
|
|
|
| 954 |
{ name = "fastmcp", marker = "extra == 'agent'", specifier = ">=2.4.0" },
|
| 955 |
{ name = "hf-agent", extras = ["agent", "eval", "dev"], marker = "extra == 'all'" },
|
|
|
|
| 956 |
{ name = "huggingface-hub", marker = "extra == 'agent'", specifier = ">=1.0.1" },
|
| 957 |
{ name = "inspect-ai", marker = "extra == 'eval'", specifier = ">=0.3.149" },
|
| 958 |
{ name = "litellm", marker = "extra == 'agent'", specifier = ">=1.0.0" },
|
|
@@ -967,6 +1003,9 @@ requires-dist = [
|
|
| 967 |
{ name = "requests", marker = "extra == 'agent'", specifier = ">=2.32.5" },
|
| 968 |
{ name = "tenacity", marker = "extra == 'eval'", specifier = ">=8.0.0" },
|
| 969 |
{ name = "thefuzz", marker = "extra == 'agent'", specifier = ">=0.22.1" },
|
|
|
|
|
|
|
|
|
|
| 970 |
]
|
| 971 |
provides-extras = ["agent", "eval", "dev", "all"]
|
| 972 |
|
|
@@ -1012,6 +1051,35 @@ wheels = [
|
|
| 1012 |
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
| 1013 |
]
|
| 1014 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1015 |
[[package]]
|
| 1016 |
name = "httpx"
|
| 1017 |
version = "0.28.1"
|
|
@@ -3525,6 +3593,119 @@ wheels = [
|
|
| 3525 |
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
|
| 3526 |
]
|
| 3527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3528 |
[[package]]
|
| 3529 |
name = "wcwidth"
|
| 3530 |
version = "0.2.14"
|
|
@@ -3574,6 +3755,15 @@ wheels = [
|
|
| 3574 |
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
| 3575 |
]
|
| 3576 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3577 |
[[package]]
|
| 3578 |
name = "wrapt"
|
| 3579 |
version = "1.17.3"
|
|
|
|
| 169 |
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
| 170 |
]
|
| 171 |
|
| 172 |
+
[[package]]
|
| 173 |
+
name = "annotated-doc"
|
| 174 |
+
version = "0.0.4"
|
| 175 |
+
source = { registry = "https://pypi.org/simple" }
|
| 176 |
+
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
| 177 |
+
wheels = [
|
| 178 |
+
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
[[package]]
|
| 182 |
name = "annotated-types"
|
| 183 |
version = "0.7.0"
|
|
|
|
| 647 |
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
|
| 648 |
]
|
| 649 |
|
| 650 |
+
[[package]]
|
| 651 |
+
name = "fastapi"
|
| 652 |
+
version = "0.128.0"
|
| 653 |
+
source = { registry = "https://pypi.org/simple" }
|
| 654 |
+
dependencies = [
|
| 655 |
+
{ name = "annotated-doc" },
|
| 656 |
+
{ name = "pydantic" },
|
| 657 |
+
{ name = "starlette" },
|
| 658 |
+
{ name = "typing-extensions" },
|
| 659 |
+
]
|
| 660 |
+
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
|
| 661 |
+
wheels = [
|
| 662 |
+
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
| 663 |
+
]
|
| 664 |
+
|
| 665 |
[[package]]
|
| 666 |
name = "fastjsonschema"
|
| 667 |
version = "2.21.2"
|
|
|
|
| 934 |
[package.optional-dependencies]
|
| 935 |
agent = [
|
| 936 |
{ name = "datasets" },
|
| 937 |
+
{ name = "fastapi" },
|
| 938 |
{ name = "fastmcp" },
|
| 939 |
+
{ name = "httpx" },
|
| 940 |
{ name = "huggingface-hub" },
|
| 941 |
{ name = "litellm" },
|
| 942 |
{ name = "lmnr" },
|
|
|
|
| 945 |
{ name = "prompt-toolkit" },
|
| 946 |
{ name = "requests" },
|
| 947 |
{ name = "thefuzz" },
|
| 948 |
+
{ name = "uvicorn", extra = ["standard"] },
|
| 949 |
+
{ name = "websockets" },
|
| 950 |
+
{ name = "whoosh" },
|
| 951 |
]
|
| 952 |
all = [
|
| 953 |
{ name = "datasets" },
|
| 954 |
+
{ name = "fastapi" },
|
| 955 |
{ name = "fastmcp" },
|
| 956 |
+
{ name = "httpx" },
|
| 957 |
{ name = "huggingface-hub" },
|
| 958 |
{ name = "inspect-ai" },
|
| 959 |
{ name = "litellm" },
|
|
|
|
| 966 |
{ name = "requests" },
|
| 967 |
{ name = "tenacity" },
|
| 968 |
{ name = "thefuzz" },
|
| 969 |
+
{ name = "uvicorn", extra = ["standard"] },
|
| 970 |
+
{ name = "websockets" },
|
| 971 |
+
{ name = "whoosh" },
|
| 972 |
]
|
| 973 |
dev = [
|
| 974 |
{ name = "pytest" },
|
|
|
|
| 985 |
{ name = "datasets", specifier = ">=4.4.1" },
|
| 986 |
{ name = "datasets", marker = "extra == 'agent'", specifier = ">=4.3.0" },
|
| 987 |
{ name = "datasets", marker = "extra == 'eval'", specifier = ">=4.3.0" },
|
| 988 |
+
{ name = "fastapi", marker = "extra == 'agent'", specifier = ">=0.115.0" },
|
| 989 |
{ name = "fastmcp", marker = "extra == 'agent'", specifier = ">=2.4.0" },
|
| 990 |
{ name = "hf-agent", extras = ["agent", "eval", "dev"], marker = "extra == 'all'" },
|
| 991 |
+
{ name = "httpx", marker = "extra == 'agent'", specifier = ">=0.27.0" },
|
| 992 |
{ name = "huggingface-hub", marker = "extra == 'agent'", specifier = ">=1.0.1" },
|
| 993 |
{ name = "inspect-ai", marker = "extra == 'eval'", specifier = ">=0.3.149" },
|
| 994 |
{ name = "litellm", marker = "extra == 'agent'", specifier = ">=1.0.0" },
|
|
|
|
| 1003 |
{ name = "requests", marker = "extra == 'agent'", specifier = ">=2.32.5" },
|
| 1004 |
{ name = "tenacity", marker = "extra == 'eval'", specifier = ">=8.0.0" },
|
| 1005 |
{ name = "thefuzz", marker = "extra == 'agent'", specifier = ">=0.22.1" },
|
| 1006 |
+
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'agent'", specifier = ">=0.32.0" },
|
| 1007 |
+
{ name = "websockets", marker = "extra == 'agent'", specifier = ">=13.0" },
|
| 1008 |
+
{ name = "whoosh", marker = "extra == 'agent'", specifier = ">=2.7.4" },
|
| 1009 |
]
|
| 1010 |
provides-extras = ["agent", "eval", "dev", "all"]
|
| 1011 |
|
|
|
|
| 1051 |
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
| 1052 |
]
|
| 1053 |
|
| 1054 |
+
[[package]]
|
| 1055 |
+
name = "httptools"
|
| 1056 |
+
version = "0.7.1"
|
| 1057 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1058 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
| 1059 |
+
wheels = [
|
| 1060 |
+
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
|
| 1061 |
+
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
|
| 1062 |
+
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
|
| 1063 |
+
{ url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" },
|
| 1064 |
+
{ url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" },
|
| 1065 |
+
{ url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" },
|
| 1066 |
+
{ url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" },
|
| 1067 |
+
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
|
| 1068 |
+
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
|
| 1069 |
+
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
|
| 1070 |
+
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
|
| 1071 |
+
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
|
| 1072 |
+
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
|
| 1073 |
+
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
|
| 1074 |
+
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
| 1075 |
+
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
| 1076 |
+
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
| 1077 |
+
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" },
|
| 1078 |
+
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" },
|
| 1079 |
+
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" },
|
| 1080 |
+
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
| 1081 |
+
]
|
| 1082 |
+
|
| 1083 |
[[package]]
|
| 1084 |
name = "httpx"
|
| 1085 |
version = "0.28.1"
|
|
|
|
| 3593 |
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
|
| 3594 |
]
|
| 3595 |
|
| 3596 |
+
[package.optional-dependencies]
|
| 3597 |
+
standard = [
|
| 3598 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 3599 |
+
{ name = "httptools" },
|
| 3600 |
+
{ name = "python-dotenv" },
|
| 3601 |
+
{ name = "pyyaml" },
|
| 3602 |
+
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
| 3603 |
+
{ name = "watchfiles" },
|
| 3604 |
+
{ name = "websockets" },
|
| 3605 |
+
]
|
| 3606 |
+
|
| 3607 |
+
[[package]]
|
| 3608 |
+
name = "uvloop"
|
| 3609 |
+
version = "0.22.1"
|
| 3610 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3611 |
+
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
| 3612 |
+
wheels = [
|
| 3613 |
+
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
|
| 3614 |
+
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
|
| 3615 |
+
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
|
| 3616 |
+
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
|
| 3617 |
+
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
|
| 3618 |
+
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
|
| 3619 |
+
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
| 3620 |
+
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
| 3621 |
+
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
| 3622 |
+
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
| 3623 |
+
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
| 3624 |
+
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
| 3625 |
+
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
| 3626 |
+
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
| 3627 |
+
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
| 3628 |
+
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
|
| 3629 |
+
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
|
| 3630 |
+
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
|
| 3631 |
+
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
|
| 3632 |
+
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
|
| 3633 |
+
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
|
| 3634 |
+
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
|
| 3635 |
+
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
|
| 3636 |
+
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
| 3637 |
+
]
|
| 3638 |
+
|
| 3639 |
+
[[package]]
|
| 3640 |
+
name = "watchfiles"
|
| 3641 |
+
version = "1.1.1"
|
| 3642 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3643 |
+
dependencies = [
|
| 3644 |
+
{ name = "anyio" },
|
| 3645 |
+
]
|
| 3646 |
+
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
| 3647 |
+
wheels = [
|
| 3648 |
+
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
|
| 3649 |
+
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
|
| 3650 |
+
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
|
| 3651 |
+
{ url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
|
| 3652 |
+
{ url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
|
| 3653 |
+
{ url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
|
| 3654 |
+
{ url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
|
| 3655 |
+
{ url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
|
| 3656 |
+
{ url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
|
| 3657 |
+
{ url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
|
| 3658 |
+
{ url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
|
| 3659 |
+
{ url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
|
| 3660 |
+
{ url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
|
| 3661 |
+
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
| 3662 |
+
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
| 3663 |
+
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
| 3664 |
+
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
| 3665 |
+
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
| 3666 |
+
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
| 3667 |
+
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
| 3668 |
+
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
| 3669 |
+
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
| 3670 |
+
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
| 3671 |
+
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
| 3672 |
+
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
| 3673 |
+
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
| 3674 |
+
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
| 3675 |
+
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
| 3676 |
+
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
| 3677 |
+
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
| 3678 |
+
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
| 3679 |
+
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
| 3680 |
+
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
| 3681 |
+
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
| 3682 |
+
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
| 3683 |
+
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
| 3684 |
+
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
| 3685 |
+
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
| 3686 |
+
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
| 3687 |
+
{ url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
|
| 3688 |
+
{ url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
|
| 3689 |
+
{ url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
|
| 3690 |
+
{ url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
|
| 3691 |
+
{ url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
|
| 3692 |
+
{ url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
|
| 3693 |
+
{ url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
|
| 3694 |
+
{ url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
|
| 3695 |
+
{ url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
|
| 3696 |
+
{ url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
|
| 3697 |
+
{ url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
|
| 3698 |
+
{ url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
|
| 3699 |
+
{ url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
|
| 3700 |
+
{ url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
|
| 3701 |
+
{ url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
|
| 3702 |
+
{ url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
|
| 3703 |
+
{ url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
|
| 3704 |
+
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
| 3705 |
+
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
| 3706 |
+
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
| 3707 |
+
]
|
| 3708 |
+
|
| 3709 |
[[package]]
|
| 3710 |
name = "wcwidth"
|
| 3711 |
version = "0.2.14"
|
|
|
|
| 3755 |
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
| 3756 |
]
|
| 3757 |
|
| 3758 |
+
[[package]]
|
| 3759 |
+
name = "whoosh"
|
| 3760 |
+
version = "2.7.4"
|
| 3761 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3762 |
+
sdist = { url = "https://files.pythonhosted.org/packages/25/2b/6beed2107b148edc1321da0d489afc4617b9ed317ef7b72d4993cad9b684/Whoosh-2.7.4.tar.gz", hash = "sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83", size = 968741, upload-time = "2016-04-04T01:19:32.327Z" }
|
| 3763 |
+
wheels = [
|
| 3764 |
+
{ url = "https://files.pythonhosted.org/packages/ba/19/24d0f1f454a2c1eb689ca28d2f178db81e5024f42d82729a4ff6771155cf/Whoosh-2.7.4-py2.py3-none-any.whl", hash = "sha256:aa39c3c3426e3fd107dcb4bde64ca1e276a65a889d9085a6e4b54ba82420a852", size = 468790, upload-time = "2016-04-04T01:19:40.379Z" },
|
| 3765 |
+
]
|
| 3766 |
+
|
| 3767 |
[[package]]
|
| 3768 |
name = "wrapt"
|
| 3769 |
version = "1.17.3"
|