Merge branch 'main' into enhance/add-login-close-btn

This commit is contained in:
Wendong-Fan 2025-11-06 03:37:06 +08:00
commit 8082532c71
23 changed files with 177 additions and 54 deletions

View file

@ -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)

View file

@ -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):

View file

@ -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")

View file

@ -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")

59
backend/app/router.py Normal file
View file

@ -0,0 +1,59 @@
"""
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
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": 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)}")

View file

@ -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")

View file

@ -1 +0,0 @@
# Utils package

View file

@ -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`

View file

@ -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

View file

@ -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",

View file

@ -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"

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Before After
Before After

View file

@ -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>

View file

@ -38,7 +38,25 @@ 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 &&
activeTaskId &&
chatState.tasks[activeTaskId] &&
(chatState.tasks[activeTaskId].activeAsk ||
// Check if this user message follows an 'ask' message in the message sequence
(() => {
const messages = chatState.tasks[activeTaskId].messages;
const userMessageIndex = messages.findIndex((m: any) => m.id === queryGroup.userMessage.id);
if (userMessageIndex > 0) {
// Check the previous message - if it's an agent message with step 'ask', this is a human-reply
const prevMessage = messages[userMessageIndex - 1];
return prevMessage?.role === 'agent' && prevMessage?.step === 'ask';
}
return false;
})());
const isLastUserQuery = !queryGroup.taskMessage &&
!isHumanReply &&
activeTaskId &&
chatState.tasks[activeTaskId] &&
queryGroup.userMessage &&

View file

@ -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);
}

View file

@ -145,9 +145,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`, {
@ -162,11 +165,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
@ -176,8 +183,8 @@ function HeaderWin() {
const newTaskId = chatStore.create();
chatStore.setActiveTaskId(newTaskId);
// Navigate to home
navigate("/");
// Navigate to home with replace to force refresh
navigate("/", { replace: true });
toast.success(t("layout.project-ended-successfully"), {
closeButton: true,

View file

@ -1 +1,3 @@
from . import traceroot_wrapper
__all__ = ['traceroot_wrapper']

View file

@ -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
@ -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']