Merge branch 'main' into enhance/add-login-close-btn
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
59
backend/app/router.py
Normal 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)}")
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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']
|
||||
|
|
|
|||