Merge branch 'main' into chatbox-ux
|
|
@ -27,7 +27,7 @@ from app.utils.workforce import Workforce
|
|||
from camel.tasks.task import Task
|
||||
|
||||
|
||||
router = APIRouter(tags=["chat"])
|
||||
router = APIRouter()
|
||||
|
||||
# Create traceroot logger for chat controller
|
||||
chat_logger = traceroot.get_logger('chat_controller')
|
||||
|
|
@ -50,7 +50,7 @@ async def post(data: Chat, request: Request):
|
|||
os.environ["CAMEL_MODEL_LOG_ENABLED"] = "true"
|
||||
|
||||
email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", data.email.split("@")[0]).strip(".")
|
||||
camel_log = Path.home() / ".eigent" / email_sanitized / ("task_" + data.project_id) / "camel_logs"
|
||||
camel_log = Path.home() / ".eigent" / email_sanitized / ("project_" + data.project_id) / ("task_" + data.task_id) / "camel_logs"
|
||||
camel_log.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
os.environ["CAMEL_LOG_DIR"] = str(camel_log)
|
||||
|
|
|
|||
16
backend/app/controller/health_controller.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(tags=["Health"])
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
service: str
|
||||
|
||||
|
||||
@router.get("/health", name="health check", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""Health check endpoint for verifying backend is ready to accept requests."""
|
||||
return HealthResponse(status="ok", service="eigent")
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ from utils import traceroot_wrapper as traceroot
|
|||
logger = traceroot.get_logger("model_controller")
|
||||
|
||||
|
||||
router = APIRouter(tags=["model"])
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ValidateModelRequest(BaseModel):
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from utils import traceroot_wrapper as traceroot
|
|||
logger = traceroot.get_logger("task_controller")
|
||||
|
||||
|
||||
router = APIRouter(tags=["task"])
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/task/{id}/start", name="start task")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
|
|||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
logger = traceroot.get_logger("tool_controller")
|
||||
router = APIRouter(tags=["task"])
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/install/tool/{tool}", name="install tool")
|
||||
|
|
|
|||
64
backend/app/router.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
Centralized router registration for the Eigent API.
|
||||
All routers are explicitly registered here for better visibility and maintainability.
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from app.controller import chat_controller, model_controller, task_controller, tool_controller, health_controller
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
logger = traceroot.get_logger("router")
|
||||
|
||||
|
||||
def register_routers(app: FastAPI, prefix: str = "") -> None:
|
||||
"""
|
||||
Register all API routers with their respective prefixes and tags.
|
||||
|
||||
This replaces the auto-discovery mechanism for better:
|
||||
- Visibility: See all routes in one place
|
||||
- Maintainability: Easy to add/remove routes
|
||||
- Debugging: Clear registration order and configuration
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
prefix: Optional global prefix for all routes (e.g., "/api")
|
||||
"""
|
||||
routers_config = [
|
||||
{
|
||||
"router": health_controller.router,
|
||||
"tags": ["Health"],
|
||||
"description": "Health check endpoint for service readiness"
|
||||
},
|
||||
{
|
||||
"router": chat_controller.router,
|
||||
"tags": ["chat"],
|
||||
"description": "Chat session management, improvements, and human interactions"
|
||||
},
|
||||
{
|
||||
"router": model_controller.router,
|
||||
"tags": ["model"],
|
||||
"description": "Model validation and configuration"
|
||||
},
|
||||
{
|
||||
"router": task_controller.router,
|
||||
"tags": ["task"],
|
||||
"description": "Task lifecycle management (start, stop, update, control)"
|
||||
},
|
||||
{
|
||||
"router": tool_controller.router,
|
||||
"tags": ["tool"],
|
||||
"description": "Tool installation and management"
|
||||
},
|
||||
]
|
||||
|
||||
for config in routers_config:
|
||||
app.include_router(
|
||||
config["router"],
|
||||
prefix=prefix,
|
||||
tags=config["tags"]
|
||||
)
|
||||
route_count = len(config["router"].routes)
|
||||
logger.info(
|
||||
f"Registered {config['tags'][0]} router: {route_count} routes - {config['description']}"
|
||||
)
|
||||
|
||||
logger.info(f"Total routers registered: {len(routers_config)}")
|
||||
|
|
@ -20,7 +20,9 @@ if traceroot.is_enabled():
|
|||
connect_fastapi(api)
|
||||
|
||||
# 2) Now safe to import modules that use traceroot.get_logger() at import-time
|
||||
from app.component.environment import auto_include_routers, env
|
||||
from app.component.environment import env
|
||||
from app.router import register_routers
|
||||
|
||||
|
||||
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||
|
||||
|
|
@ -33,7 +35,7 @@ app_logger.info(f"Environment: {os.environ.get('ENVIRONMENT', 'development')}")
|
|||
|
||||
prefix = env("url_prefix", "")
|
||||
app_logger.info(f"Loading routers with prefix: '{prefix}'")
|
||||
auto_include_routers(api, prefix, "app/controller")
|
||||
register_routers(api, prefix)
|
||||
app_logger.info("All routers loaded successfully")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
# Utils package
|
||||
|
|
@ -4,6 +4,7 @@ import log from 'electron-log'
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import * as net from "net";
|
||||
import * as http from "http";
|
||||
import { ipcMain, BrowserWindow, app } from 'electron'
|
||||
import { promisify } from 'util'
|
||||
import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
|
||||
|
|
@ -195,21 +196,77 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
|
||||
|
||||
let started = false;
|
||||
let healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
const startTimeout = setTimeout(() => {
|
||||
if (!started) {
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error('Backend failed to start within timeout'));
|
||||
}
|
||||
}, 30000); // 30 second timeout
|
||||
|
||||
// Helper function to poll health endpoint
|
||||
const pollHealthEndpoint = (): void => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20; // 5 seconds total (20 * 250ms)
|
||||
const intervalMs = 250;
|
||||
|
||||
healthCheckInterval = setInterval(() => {
|
||||
attempts++;
|
||||
const healthUrl = `http://127.0.0.1:${port}/health`;
|
||||
|
||||
const req = http.get(healthUrl, { timeout: 1000 }, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
log.info(`Backend health check passed after ${attempts} attempts`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
resolve(node_process);
|
||||
} else {
|
||||
// Non-200 status (e.g., 404), continue polling unless max attempts reached
|
||||
if (attempts >= maxAttempts) {
|
||||
log.error(`Backend health check failed after ${attempts} attempts with status ${res.statusCode}`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error(`Backend health check failed: HTTP ${res.statusCode}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
// Connection error - backend might not be ready yet, continue polling
|
||||
if (attempts >= maxAttempts) {
|
||||
log.error(`Backend health check failed after ${attempts} attempts: unable to connect`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error('Backend health check failed: unable to connect'));
|
||||
}
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
if (attempts >= maxAttempts) {
|
||||
log.error(`Backend health check timed out after ${attempts} attempts`);
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error('Backend health check timed out'));
|
||||
}
|
||||
});
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
node_process.stdout.on('data', (data) => {
|
||||
displayFilteredLogs(data);
|
||||
// check output content, judge if start success
|
||||
if (!started && data.toString().includes("Uvicorn running on")) {
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
resolve(node_process);
|
||||
log.info('Uvicorn startup detected, starting health check polling...');
|
||||
pollHealthEndpoint();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -217,9 +274,8 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
displayFilteredLogs(data);
|
||||
|
||||
if (!started && data.toString().includes("Uvicorn running on")) {
|
||||
started = true;
|
||||
clearTimeout(startTimeout);
|
||||
resolve(node_process);
|
||||
log.info('Uvicorn startup detected (stderr), starting health check polling...');
|
||||
pollHealthEndpoint();
|
||||
}
|
||||
|
||||
// Check for port binding errors
|
||||
|
|
@ -227,6 +283,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
data.toString().includes("bind() failed")) {
|
||||
started = true; // Prevent multiple rejections
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
node_process.kill();
|
||||
reject(new Error(`Port ${port} is already in use`));
|
||||
}
|
||||
|
|
@ -234,6 +291,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
|
||||
node_process.on('close', (code) => {
|
||||
clearTimeout(startTimeout);
|
||||
if (healthCheckInterval) clearInterval(healthCheckInterval);
|
||||
if (!started) {
|
||||
reject(new Error(`fastapi exited with code ${code}`));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ RUN apt-get update && apt-get install -y \
|
|||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy dependency files first
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY server/pyproject.toml server/uv.lock ./
|
||||
|
||||
# Install the project's dependencies
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
|
|
@ -29,7 +29,11 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||
|
||||
# Then, add the rest of the project source code and install it
|
||||
# Installing separately from its dependencies allows optimal layer caching
|
||||
COPY . /app
|
||||
COPY server/ /app
|
||||
|
||||
# Copy the utils directory from the parent project
|
||||
COPY utils /app/utils
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --no-dev
|
||||
|
||||
|
|
@ -45,7 +49,7 @@ RUN apt-get update && apt-get install -y curl netcat-openbsd && rm -rf /var/lib/
|
|||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Copy and make the start script executable
|
||||
COPY start.sh /app/start.sh
|
||||
COPY server/start.sh /app/start.sh
|
||||
RUN sed -i 's/\r$//' /app/start.sh && chmod +x /app/start.sh
|
||||
|
||||
# Reset the entrypoint, don't invoke `uv`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
from logging.config import fileConfig
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
# Add project root to Python path to import shared utils
|
||||
_project_root = pathlib.Path(__file__).parent.parent.parent
|
||||
if str(_project_root) not in sys.path:
|
||||
sys.path.insert(0, str(_project_root))
|
||||
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from alembic import context
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from fastapi_pagination import Page
|
|||
from fastapi_pagination.ext.sqlmodel import paginate
|
||||
from app.model.chat.chat_history import ChatHistoryOut, ChatHistoryIn, ChatHistory, ChatHistoryUpdate
|
||||
from fastapi_babel import _
|
||||
from sqlmodel import Session, select, desc
|
||||
from sqlmodel import Session, select, desc, case
|
||||
from app.component.auth import Auth, auth_must
|
||||
from app.component.database import session
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
|
@ -38,7 +38,19 @@ def create_chat_history(data: ChatHistoryIn, session: Session = Depends(session)
|
|||
def list_chat_history(session: Session = Depends(session), auth: Auth = Depends(auth_must)) -> Page[ChatHistoryOut]:
|
||||
"""List chat histories for current user."""
|
||||
user_id = auth.user.id
|
||||
stmt = select(ChatHistory).where(ChatHistory.user_id == user_id).order_by(desc(ChatHistory.created_at))
|
||||
|
||||
# Order by created_at descending, but fallback to id descending for old records without timestamps
|
||||
# This ensures newer records with timestamps come first, followed by old records ordered by id
|
||||
stmt = (
|
||||
select(ChatHistory)
|
||||
.where(ChatHistory.user_id == user_id)
|
||||
.order_by(
|
||||
desc(case((ChatHistory.created_at.is_(None), 0), else_=1)), # Non-null created_at first
|
||||
desc(ChatHistory.created_at), # Then by created_at descending
|
||||
desc(ChatHistory.id) # Finally by id descending for records with same/null created_at
|
||||
)
|
||||
)
|
||||
|
||||
result = paginate(session, stmt)
|
||||
total = result.total if hasattr(result, 'total') else 0
|
||||
logger.debug("Chat histories listed", extra={"user_id": user_id, "total": total})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from sqlalchemy import Float, Integer
|
|||
from sqlmodel import Field, SmallInteger, Column, JSON, String
|
||||
from typing import Optional
|
||||
from enum import IntEnum
|
||||
from datetime import datetime
|
||||
from sqlalchemy_utils import ChoiceType
|
||||
from app.model.abstract.model import AbstractModel, DefaultTimes
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
|
@ -13,6 +14,16 @@ class ChatStatus(IntEnum):
|
|||
|
||||
|
||||
class ChatHistory(AbstractModel, DefaultTimes, table=True):
|
||||
"""
|
||||
Chat history model with timestamp tracking.
|
||||
|
||||
Inherits from DefaultTimes which provides:
|
||||
- created_at: timestamp when record is created (auto-populated)
|
||||
- updated_at: timestamp when record is last modified (auto-updated)
|
||||
- deleted_at: timestamp for soft deletion (nullable)
|
||||
|
||||
For legacy records without timestamps, sorting falls back to id ordering.
|
||||
"""
|
||||
id: int = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(index=True)
|
||||
task_id: str = Field(index=True, unique=True)
|
||||
|
|
@ -70,13 +81,22 @@ class ChatHistoryOut(BaseModel):
|
|||
summary: str | None = None
|
||||
tokens: int
|
||||
status: int
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def fill_project_id_from_task_id(self):
|
||||
"""fill by task_id when project_id is None"""
|
||||
"""Fill project_id from task_id when project_id is None"""
|
||||
if self.project_id is None:
|
||||
self.project_id = self.task_id
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def handle_legacy_timestamps(self):
|
||||
"""Handle legacy records that might not have timestamp fields"""
|
||||
# For old records without timestamps, we rely on database-level defaults
|
||||
# The sorting in the controller will handle ordering appropriately
|
||||
return self
|
||||
|
||||
|
||||
class ChatHistoryUpdate(BaseModel):
|
||||
|
|
|
|||
|
|
@ -124,10 +124,10 @@ class ConfigInfo:
|
|||
"env_vars": [],
|
||||
"toolkit": "google_drive_mcp_toolkit",
|
||||
},
|
||||
# ConfigGroup.GOOGLE_GMAIL_MCP.value: {
|
||||
# "env_vars": [],
|
||||
# "toolkit": "google_gmail_mcp_toolkit",
|
||||
# },
|
||||
ConfigGroup.GOOGLE_GMAIL_MCP.value: {
|
||||
"env_vars": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GOOGLE_REFRESH_TOKEN"],
|
||||
"toolkit": "google_gmail_native_toolkit",
|
||||
},
|
||||
ConfigGroup.IMAGE_ANALYSIS.value: {
|
||||
"env_vars": [],
|
||||
"toolkit": "image_analysis_toolkit",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ConfigGroup(str, Enum):
|
|||
GITHUB = "Github"
|
||||
GOOGLE_CALENDAR = "Google Calendar"
|
||||
GOOGLE_DRIVE_MCP = "Google Drive MCP"
|
||||
GOOGLE_GMAIL_MCP = "Google Gmail MCP"
|
||||
GOOGLE_GMAIL_MCP = "Google Gmail"
|
||||
IMAGE_ANALYSIS = "Image Analysis"
|
||||
MCP_SEARCH = "MCP Search"
|
||||
PPTX = "PPTX"
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ services:
|
|||
# FastAPI Application
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
context: ..
|
||||
dockerfile: server/Dockerfile
|
||||
args:
|
||||
database_url: postgresql://postgres:123456@postgres:5432/eigent
|
||||
container_name: eigent_api
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 159 KiB |
|
|
@ -1,7 +1,6 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogContentSection,
|
||||
DialogFooter,
|
||||
|
|
@ -11,12 +10,10 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Bot,
|
||||
CircleAlert,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
ChevronLeft,
|
||||
ArrowRight,
|
||||
Edit,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import ToolSelect from "./ToolSelect";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
|
@ -25,7 +22,6 @@ import githubIcon from "@/assets/github.svg";
|
|||
import { fetchPost } from "@/api/http";
|
||||
import { useAuthStore, useWorkerList } from "@/store/authStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TooltipSimple } from "../ui/tooltip";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
|
||||
interface EnvValue {
|
||||
|
|
@ -68,6 +64,7 @@ export function AddWorker({
|
|||
const [showEnvConfig, setShowEnvConfig] = useState(false);
|
||||
const [activeMcp, setActiveMcp] = useState<McpItem | null>(null);
|
||||
const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({});
|
||||
const [secretVisible, setSecretVisible] = useState<{ [key: string]: boolean }>({});
|
||||
const toolSelectRef = useRef<{
|
||||
installMcp: (id: number, env?: any, activeMcp?: any) => Promise<void>;
|
||||
} | null>(null);
|
||||
|
|
@ -86,6 +83,7 @@ export function AddWorker({
|
|||
console.log(mcp);
|
||||
if (mcp?.install_command?.env) {
|
||||
const initialValues: { [key: string]: EnvValue } = {};
|
||||
const initialVisibility: { [key: string]: boolean } = {};
|
||||
for(const key of Object.keys(mcp.install_command.env)) {
|
||||
initialValues[key] = {
|
||||
value: "",
|
||||
|
|
@ -95,8 +93,10 @@ export function AddWorker({
|
|||
?.replace(/{{/g, "")
|
||||
?.replace(/}}/g, "") || "",
|
||||
};
|
||||
initialVisibility[key] = false;
|
||||
}
|
||||
setEnvValues(initialValues);
|
||||
setSecretVisible(initialVisibility);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -136,12 +136,14 @@ export function AddWorker({
|
|||
// clean status
|
||||
setActiveMcp(null);
|
||||
setEnvValues({});
|
||||
setSecretVisible({});
|
||||
};
|
||||
|
||||
const handleCloseMcpEnvSetting = () => {
|
||||
setShowEnvConfig(false);
|
||||
setActiveMcp(null);
|
||||
setEnvValues({});
|
||||
setSecretVisible({});
|
||||
};
|
||||
|
||||
const handleShowEnvConfig = (mcp: McpItem) => {
|
||||
|
|
@ -150,6 +152,11 @@ export function AddWorker({
|
|||
setShowEnvConfig(true);
|
||||
};
|
||||
|
||||
const isSensitiveKey = (key: string) => /token|key|secret|password|id/i.test(key);
|
||||
const toggleSecretVisibility = (key: string) => {
|
||||
setSecretVisible((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const handleSelectedToolsChange = (tools: McpItem[]) => {
|
||||
setSelectedTools(tools);
|
||||
};
|
||||
|
|
@ -161,6 +168,7 @@ export function AddWorker({
|
|||
setShowEnvConfig(false);
|
||||
setActiveMcp(null);
|
||||
setEnvValues({});
|
||||
setSecretVisible({});
|
||||
setNameError("");
|
||||
};
|
||||
|
||||
|
|
@ -204,9 +212,11 @@ export function AddWorker({
|
|||
}
|
||||
});
|
||||
console.log("mcpLocal.mcpServers", mcpLocal.mcpServers);
|
||||
for(const key of Object.keys(mcpLocal.mcpServers)) {
|
||||
if (!mcpList.includes(key)) {
|
||||
delete mcpLocal.mcpServers[key];
|
||||
if (mcpLocal.mcpServers && typeof mcpLocal.mcpServers === 'object') {
|
||||
for(const key of Object.keys(mcpLocal.mcpServers)) {
|
||||
if (!mcpList.includes(key)) {
|
||||
delete mcpLocal.mcpServers[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (edit) {
|
||||
|
|
@ -319,7 +329,12 @@ export function AddWorker({
|
|||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent size="sm" className="p-0 gap-0">
|
||||
<DialogContent
|
||||
size="sm"
|
||||
className="p-0 gap-0"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader
|
||||
title={showEnvConfig ? t("workforce.configure-mcp-server") : t("workforce.add-your-agent")}
|
||||
tooltip={t("layout.configure-your-mcp-worker-node-here")}
|
||||
|
|
@ -364,18 +379,24 @@ export function AddWorker({
|
|||
{Object.keys(activeMcp?.install_command?.env || {}).map(
|
||||
(key) => (
|
||||
<div key={key}>
|
||||
<div className="text-text-body text-sm leading-normal font-bold">
|
||||
{key}*
|
||||
</div>
|
||||
<Input
|
||||
placeholder=""
|
||||
className="h-7 rounded-sm border border-solid border-input-border-default bg-input-bg-default !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 resize-none"
|
||||
size="default"
|
||||
title={key}
|
||||
required
|
||||
placeholder={envValues[key]?.tip || `Enter ${key}`}
|
||||
type={isSensitiveKey(key) && !secretVisible[key] ? "password" : "text"}
|
||||
value={envValues[key]?.value || ""}
|
||||
onChange={(e) => updateEnvValue(key, e.target.value)}
|
||||
note={envValues[key]?.tip}
|
||||
backIcon={isSensitiveKey(key) ? (
|
||||
secretVisible[key] ? (
|
||||
<EyeOff size={16} className="text-button-transparent-icon-disabled" />
|
||||
) : (
|
||||
<Eye size={16} className="text-button-transparent-icon-disabled" />
|
||||
)
|
||||
) : undefined}
|
||||
onBackIconClick={isSensitiveKey(key) ? () => toggleSecretVisibility(key) : undefined}
|
||||
/>
|
||||
<div className="text-input-label-default text-xs leading-normal">
|
||||
{envValues[key]?.tip}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
|
@ -392,7 +413,6 @@ export function AddWorker({
|
|||
cancelButtonVariant="ghost"
|
||||
confirmButtonVariant="primary"
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</DialogFooter>
|
||||
{/* hidden but keep rendering ToolSelect component */}
|
||||
<div style={{ display: "none" }}>
|
||||
|
|
@ -425,11 +445,6 @@ export function AddWorker({
|
|||
}}
|
||||
state={nameError ? "error" : "default"}
|
||||
note={nameError || ""}
|
||||
backIcon={<RefreshCw size={16} className="text-button-transparent-icon-disabled" />}
|
||||
onBackIconClick={() => {
|
||||
// Handle refresh/regenerate logic here
|
||||
console.log("Refresh agent name");
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -152,10 +152,18 @@ function groupMessagesByQuery(messages: any[]) {
|
|||
otherMessages: []
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Other messages (assistant responses, etc.)
|
||||
} else {
|
||||
// Other messages (assistant responses, errors, etc.)
|
||||
if (currentGroup) {
|
||||
currentGroup.otherMessages.push(message);
|
||||
} else {
|
||||
// If there is no current user group yet (e.g., the first message is from agent/error),
|
||||
// create an anonymous group to ensure the message is rendered.
|
||||
currentGroup = {
|
||||
queryId: `orphan-${message.id}`,
|
||||
userMessage: null,
|
||||
otherMessages: [message]
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
// Show task if this query group has a task message OR if it's the most recent user query during splitting
|
||||
// During splitting phase (no to_sub_tasks yet), show task for the most recent query only
|
||||
// Exclude human-reply scenarios (when user is replying to an activeAsk)
|
||||
const isHumanReply = queryGroup.userMessage &&
|
||||
const isHumanReply = queryGroup.userMessage &&
|
||||
activeTaskId &&
|
||||
chatState.tasks[activeTaskId] &&
|
||||
(chatState.tasks[activeTaskId].activeAsk ||
|
||||
(chatState.tasks[activeTaskId].activeAsk ||
|
||||
// Check if this user message follows an 'ask' message in the message sequence
|
||||
(() => {
|
||||
const messages = chatState.tasks[activeTaskId].messages;
|
||||
|
|
@ -164,7 +164,9 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
>
|
||||
<UserMessageCard
|
||||
id={queryGroup.userMessage.id}
|
||||
role={queryGroup.userMessage.role}
|
||||
content={queryGroup.userMessage.content}
|
||||
onTyping={() => {}}
|
||||
attaches={queryGroup.userMessage.attaches}
|
||||
/>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export default function HistorySidebar() {
|
|||
try {
|
||||
//TODO(file): rename endpoint to use project_id
|
||||
//TODO(history): make sure to sync to projectId when updating endpoint
|
||||
await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id);
|
||||
await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id, history.project_id ?? undefined);
|
||||
} catch (error) {
|
||||
console.warn("Local file cleanup failed:", error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,9 +154,12 @@ function HeaderWin() {
|
|||
return;
|
||||
}
|
||||
|
||||
const projectId = projectStore.activeProjectId;
|
||||
const historyId = projectId ? projectStore.getHistoryId(projectId) : null;
|
||||
|
||||
try {
|
||||
const task = chatStore.tasks[taskId];
|
||||
|
||||
|
||||
// Stop the task if it's running
|
||||
if (task && task.status === 'running') {
|
||||
await fetchPut(`/task/${taskId}/take-control`, {
|
||||
|
|
@ -171,11 +174,15 @@ function HeaderWin() {
|
|||
console.log("Task may not exist on backend:", error);
|
||||
}
|
||||
|
||||
// Delete from history
|
||||
try {
|
||||
await proxyFetchDelete(`/api/chat/history/${taskId}`);
|
||||
} catch (error) {
|
||||
console.log("Task may not exist in history:", error);
|
||||
// Delete from history using historyId
|
||||
if (historyId) {
|
||||
try {
|
||||
await proxyFetchDelete(`/api/chat/history/${historyId}`);
|
||||
} catch (error) {
|
||||
console.log("History may not exist:", error);
|
||||
}
|
||||
} else {
|
||||
console.warn("No historyId found for project, skipping history deletion");
|
||||
}
|
||||
|
||||
// Remove from local store
|
||||
|
|
@ -185,8 +192,8 @@ function HeaderWin() {
|
|||
// This ensures we start fresh without any residual state
|
||||
projectStore.createProject("new project");
|
||||
|
||||
// Navigate to home
|
||||
navigate("/");
|
||||
// Navigate to home with replace to force refresh
|
||||
navigate("/", { replace: true });
|
||||
|
||||
toast.success(t("layout.project-ended-successfully"), {
|
||||
closeButton: true,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { Plus } from "lucide-react";
|
|||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUser } from "@stackframe/react";
|
||||
import { hasStackKeys } from "@/lib";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { MenuToggleGroup, MenuToggleItem } from "@/components/MenuButton/MenuButton";
|
||||
import Project from "@/pages/Dashboard/Project";
|
||||
|
|
@ -37,10 +35,8 @@ export default function Home() {
|
|||
const [activeTab, setActiveTab] = useState<"projects" | "workers" | "trigger" | "settings" | "mcp_tools">(tabParam || "projects");
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const HAS_STACK_KEYS = hasStackKeys();
|
||||
const stackUser = HAS_STACK_KEYS ? useUser({ or: 'anonymous-if-exists' }) : null;
|
||||
const { username, email } = useAuthStore();
|
||||
const displayName = stackUser?.displayName ?? stackUser?.primaryEmail ?? username ?? email ?? "";
|
||||
const displayName = username ?? email ?? "";
|
||||
|
||||
// Sync activeTab with URL changes
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
from . import traceroot_wrapper
|
||||
|
||||
__all__ = ['traceroot_wrapper']
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
from pathlib import Path
|
||||
from typing import Callable
|
||||
import logging
|
||||
import traceroot
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Try to import traceroot, but handle gracefully if not available
|
||||
try:
|
||||
import traceroot
|
||||
TRACEROOT_AVAILABLE = True
|
||||
except ImportError:
|
||||
TRACEROOT_AVAILABLE = False
|
||||
traceroot = None
|
||||
|
||||
# Auto-detect module name based on caller's path
|
||||
def _get_module_name():
|
||||
"""Automatically detect if this is being called from backend or server."""
|
||||
|
|
@ -26,7 +33,7 @@ env_path = Path(__file__).resolve().parents[1] / '.env'
|
|||
|
||||
load_dotenv(env_path)
|
||||
|
||||
if traceroot.init():
|
||||
if TRACEROOT_AVAILABLE and traceroot.init():
|
||||
from traceroot.logger import get_logger as _get_traceroot_logger
|
||||
|
||||
trace = traceroot.trace
|
||||
|
|
@ -42,7 +49,7 @@ if traceroot.init():
|
|||
# Log successful initialization
|
||||
module_name = _get_module_name()
|
||||
_init_logger = _get_traceroot_logger("traceroot_wrapper")
|
||||
_init_logger.info("TraceRoot initialized successfully", extra={"backend": "traceroot", "module": module_name})
|
||||
_init_logger.info("TraceRoot initialized successfully", extra={"backend": "traceroot", "service_module": module_name})
|
||||
else:
|
||||
# No-op implementations when TraceRoot is not configured
|
||||
def trace(*args, **kwargs):
|
||||
|
|
@ -69,7 +76,10 @@ else:
|
|||
|
||||
# Log fallback mode
|
||||
_fallback_logger = logging.getLogger("traceroot_wrapper")
|
||||
_fallback_logger.warning("TraceRoot not initialized - using Python logging as fallback")
|
||||
if TRACEROOT_AVAILABLE:
|
||||
_fallback_logger.warning("TraceRoot available but not initialized - using Python logging as fallback")
|
||||
else:
|
||||
_fallback_logger.warning("TraceRoot not available - using Python logging as fallback")
|
||||
|
||||
|
||||
__all__ = ['trace', 'get_logger', 'is_enabled']
|
||||
|
|
|
|||