diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index 73bfb2ee4..8fb4bcf1a 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -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) diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py deleted file mode 100644 index dd7ee44cc..000000000 --- a/backend/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Utils package diff --git a/server/Dockerfile b/server/Dockerfile index 17ff0e2d3..b09690365 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -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` diff --git a/server/alembic/env.py b/server/alembic/env.py index 4dccfeb6a..2e5cf7262 100644 --- a/server/alembic/env.py +++ b/server/alembic/env.py @@ -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 diff --git a/server/app/model/config/config.py b/server/app/model/config/config.py index 022a520d5..4677b4e82 100644 --- a/server/app/model/config/config.py +++ b/server/app/model/config/config.py @@ -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", diff --git a/server/app/type/config_group.py b/server/app/type/config_group.py index ba7b66050..df5103e64 100644 --- a/server/app/type/config_group.py +++ b/server/app/type/config_group.py @@ -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" diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 193ee5713..a4dbba26e 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -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 diff --git a/server/utils/__init__.py b/server/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/components/AddWorker/index.tsx b/src/components/AddWorker/index.tsx index 073b7adab..f8a053046 100644 --- a/src/components/AddWorker/index.tsx +++ b/src/components/AddWorker/index.tsx @@ -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(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; } | 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({ )} - + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > (
-
- {key}* -
updateEnvValue(key, e.target.value)} + note={envValues[key]?.tip} + backIcon={isSensitiveKey(key) ? ( + secretVisible[key] ? ( + + ) : ( + + ) + ) : undefined} + onBackIconClick={isSensitiveKey(key) ? () => toggleSecretVisibility(key) : undefined} /> -
- {envValues[key]?.tip} -
) )} @@ -392,7 +413,6 @@ export function AddWorker({ cancelButtonVariant="ghost" confirmButtonVariant="primary" > - {/* hidden but keep rendering ToolSelect component */}
@@ -425,11 +445,6 @@ export function AddWorker({ }} state={nameError ? "error" : "default"} note={nameError || ""} - backIcon={} - onBackIconClick={() => { - // Handle refresh/regenerate logic here - console.log("Refresh agent name"); - }} required />
diff --git a/src/components/ChatBox/UserQueryGroup.tsx b/src/components/ChatBox/UserQueryGroup.tsx index f82811b22..1022830a2 100644 --- a/src/components/ChatBox/UserQueryGroup.tsx +++ b/src/components/ChatBox/UserQueryGroup.tsx @@ -38,7 +38,25 @@ export const UserQueryGroup: React.FC = ({ // 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 && diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx index 26686915e..7a6b2223c 100644 --- a/src/components/HistorySidebar/index.tsx +++ b/src/components/HistorySidebar/index.tsx @@ -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); } diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index 0e6c4c89f..03e65ce4a 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -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, diff --git a/utils/__init__.py b/utils/__init__.py index 8b1378917..a93f712f3 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1 +1,3 @@ +from . import traceroot_wrapper +__all__ = ['traceroot_wrapper'] diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc index 664fc47bd..10d1d4a69 100644 Binary files a/utils/__pycache__/__init__.cpython-310.pyc and b/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc index 6d17bd26b..d695ee21d 100644 Binary files a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc and b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc differ diff --git a/utils/traceroot_wrapper.py b/utils/traceroot_wrapper.py index ac03e6c3a..5ad9c8e32 100644 --- a/utils/traceroot_wrapper.py +++ b/utils/traceroot_wrapper.py @@ -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']