diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d86716ef..8b5935e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: id: version attributes: label: What version of eigent are you using? - placeholder: E.g., 0.0.72 + placeholder: E.g., 0.0.73 validations: required: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..c66e115b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '42 22 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/backend/app/component/model_validation.py b/backend/app/component/model_validation.py index f1692a57..d734ab92 100644 --- a/backend/app/component/model_validation.py +++ b/backend/app/component/model_validation.py @@ -37,5 +37,6 @@ def create_agent( system_message="You are a helpful assistant that must use the tool get_website_content to get the content of a website.", model=model, tools=[get_website_content], + step_timeout=900, ) return agent diff --git a/backend/app/utils/agent.py b/backend/app/utils/agent.py index 69c8244f..b8a2e73e 100644 --- a/backend/app/utils/agent.py +++ b/backend/app/utils/agent.py @@ -107,6 +107,7 @@ class ListenChatAgent(ChatAgent): pause_event: asyncio.Event | None = None, prune_tool_calls_from_memory: bool = False, enable_snapshot_clean: bool = False, + step_timeout: float | None = 900, ) -> None: super().__init__( system_message=system_message, @@ -128,6 +129,7 @@ class ListenChatAgent(ChatAgent): pause_event=pause_event, prune_tool_calls_from_memory=prune_tool_calls_from_memory, enable_snapshot_clean=enable_snapshot_clean, + step_timeout=step_timeout, ) self.api_task_id = api_task_id self.agent_name = agent_name @@ -287,19 +289,25 @@ class ListenChatAgent(ChatAgent): if asyncio.iscoroutinefunction(tool.func): # For async functions, we need to use the async execution path return asyncio.run(self._aexecute_tool(tool_call_request)) - elif hasattr(tool.func, "__wrapped__"): - with set_process_task(self.process_task_id): - return super()._execute_tool(tool_call_request) - else: - args = tool_call_request.args - tool_call_id = tool_call_request.tool_call_id - try: - task_lock = get_task_lock(self.api_task_id) - toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit" - traceroot_logger.debug( - f"Agent {self.agent_name} executing tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}" - ) + # Handle all sync tools ourselves to maintain ContextVar context + args = tool_call_request.args + tool_call_id = tool_call_request.tool_call_id + + # Check if tool is wrapped by @listen_toolkit decorator + # If so, the decorator will handle activate/deactivate events + has_listen_decorator = hasattr(tool.func, "__wrapped__") + + try: + task_lock = get_task_lock(self.api_task_id) + + toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit" + traceroot_logger.debug( + f"Agent {self.agent_name} executing tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}" + ) + + # Only send activate event if tool is NOT wrapped by @listen_toolkit + if not has_listen_decorator: asyncio.create_task( task_lock.put_queue( ActionActivateToolkitData( @@ -313,29 +321,33 @@ class ListenChatAgent(ChatAgent): ) ) ) + # Set process_task context for all tool executions + with set_process_task(self.process_task_id): raw_result = tool(**args) - traceroot_logger.debug(f"Tool {func_name} executed successfully") - if self.mask_tool_output: - self._secure_result_store[tool_call_id] = raw_result - result = ( - "[The tool has been executed successfully, but the output" - " from the tool is masked. You can move forward]" - ) - mask_flag = True + traceroot_logger.debug(f"Tool {func_name} executed successfully") + if self.mask_tool_output: + self._secure_result_store[tool_call_id] = raw_result + result = ( + "[The tool has been executed successfully, but the output" + " from the tool is masked. You can move forward]" + ) + mask_flag = True + else: + result = raw_result + mask_flag = False + # Prepare result message with truncation + if isinstance(result, str): + result_msg = result + else: + result_str = repr(result) + MAX_RESULT_LENGTH = 500 + if len(result_str) > MAX_RESULT_LENGTH: + result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)" else: - result = raw_result - mask_flag = False - # Prepare result message with truncation - if isinstance(result, str): - result_msg = result - else: - result_str = repr(result) - MAX_RESULT_LENGTH = 500 - if len(result_str) > MAX_RESULT_LENGTH: - result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)" - else: - result_msg = result_str + result_msg = result_str + # Only send deactivate event if tool is NOT wrapped by @listen_toolkit + if not has_listen_decorator: asyncio.create_task( task_lock.put_queue( ActionDeactivateToolkitData( @@ -349,31 +361,40 @@ class ListenChatAgent(ChatAgent): ) ) ) - except Exception as e: - # Capture the error message to prevent framework crash - error_msg = f"Error executing tool '{func_name}': {e!s}" - result = f"Tool execution failed: {error_msg}" - mask_flag = False - traceroot_logger.error(f"Tool execution failed for {func_name}: {e}", exc_info=True) + except Exception as e: + # Capture the error message to prevent framework crash + error_msg = f"Error executing tool '{func_name}': {e!s}" + result = f"Tool execution failed: {error_msg}" + mask_flag = False + traceroot_logger.error(f"Tool execution failed for {func_name}: {e}", exc_info=True) - return self._record_tool_calling(func_name, args, result, tool_call_id, mask_output=mask_flag) + return self._record_tool_calling( + func_name, args, result, tool_call_id, + mask_output=mask_flag, + extra_content=tool_call_request.extra_content, + ) @traceroot.trace() async def _aexecute_tool(self, tool_call_request: ToolCallRequest) -> ToolCallingRecord: func_name = tool_call_request.tool_name tool: FunctionTool = self._internal_tools[func_name] - if hasattr(tool.func, "__wrapped__"): - with set_process_task(self.process_task_id): - return await super()._aexecute_tool(tool_call_request) - else: - args = tool_call_request.args - tool_call_id = tool_call_request.tool_call_id - task_lock = get_task_lock(self.api_task_id) - toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit" - traceroot_logger.info( - f"Agent {self.agent_name} executing async tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}" - ) + # Always handle tool execution ourselves to maintain ContextVar context + args = tool_call_request.args + tool_call_id = tool_call_request.tool_call_id + task_lock = get_task_lock(self.api_task_id) + + # Check if tool is wrapped by @listen_toolkit decorator + # If so, the decorator will handle activate/deactivate events + has_listen_decorator = hasattr(tool.func, "__wrapped__") + + toolkit_name = getattr(tool, "_toolkit_name") if hasattr(tool, "_toolkit_name") else "mcp_toolkit" + traceroot_logger.info( + f"Agent {self.agent_name} executing async tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}" + ) + + # Only send activate event if tool is NOT wrapped by @listen_toolkit + if not has_listen_decorator: await task_lock.put_queue( ActionActivateToolkitData( data={ @@ -385,7 +406,9 @@ class ListenChatAgent(ChatAgent): }, ) ) - try: + try: + # Set process_task context for all tool executions + with set_process_task(self.process_task_id): # Try different invocation paths in order of preference if hasattr(tool, "func") and hasattr(tool.func, "async_call"): # Case: FunctionTool wrapping an MCP tool @@ -404,29 +427,32 @@ class ListenChatAgent(ChatAgent): result = await tool(**args) else: - # Fallback: synchronous call + # Fallback: synchronous call - call directly in current context + # DO NOT use run_in_executor to preserve ContextVar result = tool(**args) # Handle case where synchronous call returns a coroutine if asyncio.iscoroutine(result): result = await result - except Exception as e: - # Capture the error message to prevent framework crash - error_msg = f"Error executing async tool '{func_name}': {e!s}" - result = {"error": error_msg} - traceroot_logger.error(f"Async tool execution failed for {func_name}: {e}", exc_info=True) + except Exception as e: + # Capture the error message to prevent framework crash + error_msg = f"Error executing async tool '{func_name}': {e!s}" + result = {"error": error_msg} + traceroot_logger.error(f"Async tool execution failed for {func_name}: {e}", exc_info=True) - # Prepare result message with truncation - if isinstance(result, str): - result_msg = result + # Prepare result message with truncation + if isinstance(result, str): + result_msg = result + else: + result_str = repr(result) + MAX_RESULT_LENGTH = 500 + if len(result_str) > MAX_RESULT_LENGTH: + result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)" else: - result_str = repr(result) - MAX_RESULT_LENGTH = 500 - if len(result_str) > MAX_RESULT_LENGTH: - result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)" - else: - result_msg = result_str + result_msg = result_str + # Only send deactivate event if tool is NOT wrapped by @listen_toolkit + if not has_listen_decorator: await task_lock.put_queue( ActionDeactivateToolkitData( data={ @@ -438,7 +464,10 @@ class ListenChatAgent(ChatAgent): }, ) ) - return self._record_tool_calling(func_name, args, result, tool_call_id) + return self._record_tool_calling( + func_name, args, result, tool_call_id, + extra_content=tool_call_request.extra_content, + ) @traceroot.trace() def clone(self, with_memory: bool = False) -> ChatAgent: @@ -468,6 +497,7 @@ class ListenChatAgent(ChatAgent): mask_tool_output=self.mask_tool_output, pause_event=self.pause_event, prune_tool_calls_from_memory=self.prune_tool_calls_from_memory, + step_timeout=self.step_timeout, ) new_agent.process_task_id = self.process_task_id @@ -746,7 +776,7 @@ def search_agent(options: Chat): "browser_enter", "browser_visit_page", "browser_scroll", - "browser_get_som_screenshot", + # "browser_get_som_screenshot", ], ) @@ -869,9 +899,6 @@ Your approach depends on available search tools: interactive elements, not the full page text. To see more content on long pages, Navigate with `browser_click`, `browser_back`, and `browser_forward`. Manage multiple pages with `browser_switch_tab`. -- **Analysis**: Use `browser_get_som_screenshot` to understand the page - layout and identify interactive elements. Since this is a heavy - operation, only use it when visual analysis is necessary. - **Interaction**: Use `browser_type` to fill out forms and `browser_enter` to submit or confirm search. diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index 7e98713b..eb52e8ad 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -27,6 +27,13 @@ def _safe_put_queue(task_lock, data): task = asyncio.create_task(task_lock.put_queue(data)) if hasattr(task_lock, "add_background_task"): task_lock.add_background_task(task) + # Add done callback to handle any exceptions and prevent warnings + def handle_task_result(t): + try: + t.result() # This will raise any exception that occurred + except Exception as e: + logger.error(f"[listen_toolkit] Background task failed: {e}") + task.add_done_callback(handle_task_result) except RuntimeError: # No running event loop, we need to handle this differently try: diff --git a/backend/app/utils/toolkit/file_write_toolkit.py b/backend/app/utils/toolkit/file_write_toolkit.py index 210ffc9c..375f9468 100644 --- a/backend/app/utils/toolkit/file_write_toolkit.py +++ b/backend/app/utils/toolkit/file_write_toolkit.py @@ -5,7 +5,7 @@ from camel.toolkits import FileToolkit as BaseFileToolkit from app.component.environment import env from app.service.task import process_task from app.service.task import ActionWriteFileData, Agents, get_task_lock -from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit +from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit, _safe_put_queue from app.utils.toolkit.abstract_toolkit import AbstractToolkit @@ -46,12 +46,15 @@ class FileToolkit(BaseFileToolkit, AbstractToolkit): res = super().write_to_file(title, content, filename, encoding, use_latex) if "Content successfully written to file: " in res: task_lock = get_task_lock(self.api_task_id) - asyncio.create_task( - task_lock.put_queue( - ActionWriteFileData( - process_task_id=process_task.get(), - data=res.replace("Content successfully written to file: ", ""), - ) + # Capture ContextVar value before creating async task + current_process_task_id = process_task.get("") + + # Use _safe_put_queue to handle both sync and async contexts + _safe_put_queue( + task_lock, + ActionWriteFileData( + process_task_id=current_process_task_id, + data=res.replace("Content successfully written to file: ", ""), ) ) return res diff --git a/backend/app/utils/toolkit/human_toolkit.py b/backend/app/utils/toolkit/human_toolkit.py index edd43a98..e76a45fb 100644 --- a/backend/app/utils/toolkit/human_toolkit.py +++ b/backend/app/utils/toolkit/human_toolkit.py @@ -107,12 +107,16 @@ class HumanToolkit(BaseToolkit, AbstractToolkit): print(message_attachment) logger.info(f"\nAgent Message:\n{message_title} {message_description} {message_attachment}") task_lock = get_task_lock(self.api_task_id) - asyncio.create_task( - task_lock.put_queue( - ActionNoticeData( - process_task_id=process_task.get(""), - data=f"{message_description}", - ) + # Capture ContextVar value before creating async task + current_process_task_id = process_task.get("") + + # Use _safe_put_queue to handle both sync and async contexts + from app.utils.listen.toolkit_listen import _safe_put_queue + _safe_put_queue( + task_lock, + ActionNoticeData( + process_task_id=current_process_task_id, + data=f"{message_description}", ) ) diff --git a/backend/app/utils/toolkit/pptx_toolkit.py b/backend/app/utils/toolkit/pptx_toolkit.py index 0e341d24..ae52c176 100644 --- a/backend/app/utils/toolkit/pptx_toolkit.py +++ b/backend/app/utils/toolkit/pptx_toolkit.py @@ -4,7 +4,7 @@ from camel.toolkits import PPTXToolkit as BasePPTXToolkit from app.component.environment import env from app.service.task import ActionWriteFileData, Agents, get_task_lock -from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit +from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit, _safe_put_queue from app.utils.toolkit.abstract_toolkit import AbstractToolkit from app.service.task import process_task @@ -39,7 +39,12 @@ class PPTXToolkit(BasePPTXToolkit, AbstractToolkit): res = super().create_presentation(content, filename, template) if "PowerPoint presentation successfully created" in res: task_lock = get_task_lock(self.api_task_id) - asyncio.create_task( - task_lock.put_queue(ActionWriteFileData(process_task_id=process_task.get(), data=str(file_path))) + # Capture ContextVar value before creating async task + current_process_task_id = process_task.get("") + + # Use _safe_put_queue to handle both sync and async contexts + _safe_put_queue( + task_lock, + ActionWriteFileData(process_task_id=current_process_task_id, data=str(file_path)) ) return res diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py index 4e6472c5..52ee19f3 100644 --- a/backend/app/utils/toolkit/terminal_toolkit.py +++ b/backend/app/utils/toolkit/terminal_toolkit.py @@ -134,6 +134,27 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): exc_info=True ) + def shell_exec(self, id: str, command: str, block: bool = True) -> str: + r"""Executes a shell command in blocking or non-blocking mode. + + Args: + id (str): A unique identifier for the command's session. + command (str): The shell command to execute. + block (bool, optional): Determines the execution mode. Defaults to True. + + Returns: + str: The output of the command execution. + """ + result = super().shell_exec(id, command, block) + + # If the command executed successfully but returned empty output, + # provide a clear success message to help the AI agent understand + # that the command completed without error. + if block and result == "": + return "Command executed successfully (no output)." + + return result + @classmethod def shutdown(cls): if cls._thread_pool: diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index c9793ec1..4833d1b2 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -6,6 +6,7 @@ from camel.societies.workforce.workforce import ( WorkforceState, DEFAULT_WORKER_POOL_SIZE, ) +from camel.societies.workforce.utils import FailureHandlingConfig from camel.societies.workforce.task_channel import TaskChannel from camel.societies.workforce.base import BaseNode from camel.societies.workforce.utils import TaskAssignResult @@ -58,6 +59,9 @@ class Workforce(BaseWorkforce): graceful_shutdown_timeout=graceful_shutdown_timeout, share_memory=share_memory, use_structured_output_handler=use_structured_output_handler, + failure_handling_config=FailureHandlingConfig( + enabled_strategies=["retry", "replan"], + ), ) logger.info(f"[WF-LIFECYCLE] ✅ Workforce.__init__ COMPLETED, id={id(self)}") @@ -217,8 +221,8 @@ class Workforce(BaseWorkforce): return subtasks async def _find_assignee(self, tasks: List[Task]) -> TaskAssignResult: - # Task assignment phase: send "waiting for execution" notification - # to the frontend, and send "start execution" notification when the + # Task assignment phase: send "waiting for execution" notification + # to the frontend, and send "start execution" notification when the # task actually begins execution assigned = await super()._find_assignee(tasks) @@ -238,6 +242,18 @@ class Workforce(BaseWorkforce): content = "" else: content = task_obj.content + + # Skip sending notification if this is a retry/replan for an already assigned task + # This prevents the frontend from showing "Reassigned" when a task is being retried + # with the same or different worker due to failure recovery + if task_obj and task_obj.assigned_worker_id: + logger.debug( + f"[WF] ASSIGN Skip notification for task {item.task_id}: " + f"already has assigned_worker_id={task_obj.assigned_worker_id}, " + f"new assignee={item.assignee_id} (retry/replan scenario)" + ) + continue + # Asynchronously send waiting notification task = asyncio.create_task( task_lock.put_queue( diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4b4dc445..30a01a15 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -3,9 +3,9 @@ name = "backend" version = "0.1.0" description = "Add your description here" readme = "README.md" -requires-python = "==3.10.16" +requires-python = ">=3.10,<3.11" dependencies = [ - "camel-ai[eigent]==0.2.80a3", + "camel-ai[eigent] @ git+https://github.com/camel-ai/camel.git@feature/workforce-configurable-fail-handling", "fastapi>=0.115.12", "fastapi-babel>=1.0.0", "uvicorn[standard]>=0.34.2", diff --git a/backend/uv.lock b/backend/uv.lock index 972efe55..74a5e62d 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = "==3.10.16" +requires-python = "==3.10.*" [[package]] name = "aiofiles" @@ -249,7 +249,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, - { name = "camel-ai", extras = ["eigent"], specifier = "==0.2.80a3" }, + { name = "camel-ai", extras = ["eigent"], git = "https://github.com/camel-ai/camel.git?rev=feature%2Fworkforce-configurable-fail-handling" }, { name = "debugpy", specifier = ">=1.8.17" }, { name = "fastapi", specifier = ">=0.115.12" }, { name = "fastapi-babel", specifier = ">=1.0.0" }, @@ -333,8 +333,8 @@ wheels = [ [[package]] name = "camel-ai" -version = "0.2.80a3" -source = { registry = "https://pypi.org/simple" } +version = "0.2.82.dev1" +source = { git = "https://github.com/camel-ai/camel.git?rev=feature%2Fworkforce-configurable-fail-handling#d84dbec44ecb26add2638857059e2f92d896d308" } dependencies = [ { name = "astor" }, { name = "colorama" }, @@ -349,10 +349,6 @@ dependencies = [ { name = "tiktoken" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/72/691a6126e062b5c5a24b7c5a116f690be1b50127a359c7d53d13435d27ef/camel_ai-0.2.80a3.tar.gz", hash = "sha256:edda7cb0466a63c4d8f92ae4ea2d11ee6946b69146336176346db3227de747d9", size = 1013184, upload-time = "2025-11-20T18:52:25.016Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/a1203a2fa8e432deb4e6a7740a4dff532b79e0129bbf2e60626fca8f4271/camel_ai-0.2.80a3-py3-none-any.whl", hash = "sha256:2f398e648edae57bf23372a9cc812c82bf64f007178cdaf7ba113f2fde6761a6", size = 1473469, upload-time = "2025-11-20T18:52:22.853Z" }, -] [package.optional-dependencies] eigent = [ diff --git a/docs/core/models/gemini.md b/docs/core/models/gemini.md new file mode 100644 index 00000000..76198777 --- /dev/null +++ b/docs/core/models/gemini.md @@ -0,0 +1,48 @@ +--- +title: "Gemini" +description: "This guide walks you through setting up your Google Gemini API key within Eigent to enable the Gemini model for your AI workforce." +--- + +### Prerequisites + +- **Get your API Key:** If you haven't already, generate a key at [Google AI Studio](https://aistudio.google.com/). +- **Copy the Key:** Keep your API key ready to paste. + +### Configuration Steps + +**1. Access Application Settings** + +- Launch Eigent and navigate to the **Home Page**. +- Click on the **Settings** tab (usually located in the sidebar or top navigation). + +![Gemini 1 Pn](/docs/images/gemini_1.png) + +**2. Locate Model Configuration** + +- In the Settings menu, find and select the **Models** section. +- Scroll down to the **Custom Model** area. +- Look for the **Gemini Config** card. +- + +![Gemini 2 Pn](/docs/images/gemini_2.png) + +**3. Enter API Details** Click on the Gemini Config card and fill in the following fields: + +- **API Key:** Paste the key you generated from Google AI Studio. +- **API Host:** Enter the appropriate API endpoint host (e.g., `generativelanguage.googleapis.com`). +- **Model Type:** Enter the specific model version you wish to use. + - _Example:_ `gemini-3-pro-preview` +- **Save:** Click the **Save** button to apply your changes. + +![Gemini 3 Pn](/docs/images/gemini_3.png) + +**4. Set as Default & Verify** + +- Once saved, the **"Set as Default"** button on the Gemini Config card will be selected/active. +- **You are ready to go.** Your Eigent agents can now utilize the Gemini model. + +![Gemini 4 Pn](/docs/images/gemini_4.png) + +--- + +> **Video Tutorial:** Prefer a visual guide? **Watch the full configuration video here** \ No newline at end of file diff --git a/docs/core/models.md b/docs/core/models/local-model.md similarity index 57% rename from docs/core/models.md rename to docs/core/models/local-model.md index 3a66a999..21024bee 100644 --- a/docs/core/models.md +++ b/docs/core/models/local-model.md @@ -1,33 +1,9 @@ --- -title: Models -description: Configure and deploy your preferred LLM models with Eigent. -icon: server +title: "Models (Local Model)" +description: "Configure and deploy your preferred LLM models with Eigent." +icon: "server" --- -Eigent supports flexible integration and deployment of top LLMs and multimodal models. You can follow the steps below to set up your preferred LLM models. - -1. Click Settings - -![click_settings](/docs/images/models_settings.png) - -2. Close Eigent Cloud Version - -![close_eigent](/docs/images/models_close.png) - -3. Configure your APIKEY and Model Type - -![configure_api](/docs/images/models_configure_models.png) - -4. Configure the Google Search toolkit - -![configure_searchtools](/docs/images/models_configure_tools.png) - -![configure_searchtoolsapi](/docs/images/models_configure_tools_key.png) - -You can refer to the following document for detailed information on how to configure **GOOGLE_API_KEY** and **SEARCH_ENGINE_ID :** https://developers.google.com/custom-search/v1/overview - -Now,start enjoying Eigent! - ## **Self-Host Model** 1. Configure your self-host model @@ -71,8 +47,7 @@ ollama pull qwen2.5:7b ![configure_searchtools](/docs/images/models_configure_tools.png) -![configure_searchtoolsapi](/docs/images/models_configure_tools_key.png) -You can refer to the following document for detailed information on how to configure **GOOGLE_API_KEY** and **SEARCH_ENGINE_ID :** https://developers.google.com/custom-search/v1/overview +configure_searchtoolsapi You can refer to the following document for detailed information on how to configure **GOOGLE_API_KEY** and **SEARCH_ENGINE_ID :** https://developers.google.com/custom-search/v1/overview ## **API KEY Reference** diff --git a/docs/docs.json b/docs/docs.json index 2c48f997..7da1ed62 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -47,24 +47,35 @@ "groups": [ { "group": "Get Started", + "icon": "rocket", "pages": [ "/get_started/welcome", - "/get_started/installation", + "/get_started/installation", "/get_started/quick_start" ] }, { "group": "Core", + "icon": "key", "pages": [ "/core/concepts", "/core/workforce", - "/core/models", + { + "group": "Models", + "icon": "brain", + "expanded": true, + "pages": [ + "/core/models/gemini", + "/core/models/local-model" + ] + }, "/core/tools", "/core/workers" ] }, { "group": "Troubleshooting", + "icon": "question-circle", "pages": [ "/troubleshooting/support", "/troubleshooting/bug" diff --git a/docs/images/gemini_1.png b/docs/images/gemini_1.png new file mode 100644 index 00000000..82c82249 Binary files /dev/null and b/docs/images/gemini_1.png differ diff --git a/docs/images/gemini_2.png b/docs/images/gemini_2.png new file mode 100644 index 00000000..66ba5d75 Binary files /dev/null and b/docs/images/gemini_2.png differ diff --git a/docs/images/gemini_3.png b/docs/images/gemini_3.png new file mode 100644 index 00000000..5acdc4d9 Binary files /dev/null and b/docs/images/gemini_3.png differ diff --git a/docs/images/gemini_4.png b/docs/images/gemini_4.png new file mode 100644 index 00000000..2975f2e6 Binary files /dev/null and b/docs/images/gemini_4.png differ diff --git a/electron/main/index.ts b/electron/main/index.ts index 10dbee40..fa62d7fa 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1316,14 +1316,6 @@ async function createWindow() { }); }); } else { - // REMOVED: Previously this block would directly set initState='done' when installation - // was already complete, bypassing the backend readiness check. - // - // This caused a critical bug where: - // 1. Frontend would show immediately (initState='done') - // 2. Backend would still be starting (10-15 seconds) - // 3. Users could interact before backend was ready, causing connection errors - // // The proper flow is now handled by useInstallationSetup.ts with dual-check mechanism: // 1. Installation complete event → installationCompleted.current = true // 2. Backend ready event → backendReady.current = true @@ -1354,6 +1346,9 @@ async function createWindow() { log.info('Window is ready, processing queued protocol URLs...'); processQueuedProtocolUrls(); + // Wait for React components to mount and register event listeners + await new Promise(resolve => setTimeout(resolve, 500)); + // Now check and install dependencies let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); if (!res.success) { diff --git a/electron/main/init.ts b/electron/main/init.ts index b23c61c1..1ee74eaa 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -1,4 +1,4 @@ -import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, isBinaryExists, runInstallScript } from "./utils/process"; +import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName } from "./utils/process"; import { spawn, exec } from 'child_process' import log from 'electron-log' import fs from 'fs' @@ -21,16 +21,16 @@ export function getMainWindow(): BrowserWindow | null { export async function checkToolInstalled() { return new Promise(async (resolve, reject) => { if (!(await isBinaryExists('uv'))) { - resolve({success: false, message: "uv doesn't exist"}) + resolve({ success: false, message: "uv doesn't exist" }) return } if (!(await isBinaryExists('bun'))) { - resolve({success: false, message: "Bun doesn't exist"}) + resolve({ success: false, message: "Bun doesn't exist" }) return } - resolve({success: true, message: "Tools exist already"}) + resolve({ success: true, message: "Tools exist already" }) }) } @@ -159,12 +159,13 @@ export async function startBackend(setPort?: (port: number) => void): Promise void): Promise void): Promise { try { process.kill(-proc.pid, 'SIGKILL'); - } catch (e) {} + } catch (e) { } }, 1000); } catch (e) { log.error(`Failed to kill process group: ${e}`); diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index aa662a9f..84efae6d 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -3,7 +3,7 @@ import path from 'node:path' import log from 'electron-log' import { getMainWindow } from './init' import fs from 'node:fs' -import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process' +import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, cleanupOldVenvs, isBinaryExists, runInstallScript } from './utils/process' import { spawn } from 'child_process' import { safeMainWindowSend } from './utils/safeWebContentsSend' import os from 'node:os' @@ -242,7 +242,6 @@ class InstallLogs { constructor(extraArgs:string[], version: string) { console.log('start install dependencies', extraArgs, 'version:', version) - const venvPath = getVenvPath(version); this.version = version; this.node_process = spawn(uv_path, [ @@ -253,10 +252,7 @@ class InstallLogs { cwd: backendPath, env: { ...process.env, - UV_TOOL_DIR: getCachePath('uv_tool'), - UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'), - UV_PROJECT_ENVIRONMENT: venvPath, - UV_HTTP_TIMEOUT: '180', // 3 minutes timeout + ...getUvEnv(version), } }) } diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index 66f502d7..7488f332 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -142,3 +142,49 @@ export async function isBinaryExists(name: string): Promise { return await fs.existsSync(cmd) } + +/** + * Get unified UV environment variables for consistent Python environment management. + * This ensures both installation and runtime use the same paths. + * @param version - The app version for venv path + * @returns Environment variables for UV commands + */ +export function getUvEnv(version: string): Record { + return { + UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'), + UV_TOOL_DIR: getCachePath('uv_tool'), + UV_PROJECT_ENVIRONMENT: getVenvPath(version), + UV_HTTP_TIMEOUT: '300', + } +} + +export async function killProcessByName(name: string): Promise { + const platform = process.platform + try { + if (platform === 'win32') { + await new Promise((resolve, reject) => { + // /F = force, /IM = image name + const cmd = spawn('taskkill', ['/F', '/IM', `${name}.exe`]) + cmd.on('close', (code) => { + // code 0 = success, code 128 = process not found (which is fine) + if (code === 0 || code === 128) resolve() + else reject(new Error(`taskkill exited with code ${code}`)) + }) + cmd.on('error', reject) + }) + } else { + await new Promise((resolve, reject) => { + const cmd = spawn('pkill', ['-9', name]) + cmd.on('close', (code) => { + // code 0 = success, code 1 = no process found (which is fine) + if (code === 0 || code === 1) resolve() + else reject(new Error(`pkill exited with code ${code}`)) + }) + cmd.on('error', reject) + }) + } + } catch (err) { + // Ignore errors, just best effort + log.warn(`Failed to kill process ${name}:`, err) + } +} diff --git a/package.json b/package.json index f84e7612..2c72eff8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eigent", - "version": "0.0.72", + "version": "0.0.73", "main": "dist-electron/main/index.js", "description": "Eigent", "author": "Eigent.AI", @@ -133,4 +133,4 @@ "engines": { "node": ">=18.0.0 <23.0.0" } -} +} \ No newline at end of file diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js index fa1ffb00..735d69bf 100644 --- a/resources/scripts/install-bun.js +++ b/resources/scripts/install-bun.js @@ -53,7 +53,9 @@ async function downloadBunBinary(bun_download_url,platform, arch, version = DEFA const downloadUrl = `${bun_download_url}/bun-v${version}/${packageName}` const tempdir = os.tmpdir() // Create a temporary file for the downloaded binary - const tempFilename = path.join(tempdir, packageName) + // Use unique temp filename to avoid race conditions when multiple processes install simultaneously + const uniqueSuffix = `${process.pid}-${Date.now()}` + const tempFilename = path.join(tempdir, `${uniqueSuffix}-${packageName}`) try { console.log(`Downloading bun ${version} for ${platformKey}...`) diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js index 057c2898..b3588908 100644 --- a/resources/scripts/install-uv.js +++ b/resources/scripts/install-uv.js @@ -66,7 +66,9 @@ async function downloadUvBinary( // Download URL for the specific binary const downloadUrl = `${uv_download_url}/${version}/${packageName}`; const tempdir = os.tmpdir(); - const tempFilename = path.join(tempdir, packageName); + // Use unique temp filename to avoid race conditions when multiple processes install simultaneously + const uniqueSuffix = `${process.pid}-${Date.now()}`; + const tempFilename = path.join(tempdir, `${uniqueSuffix}-${packageName}`); try { console.log(`Downloading uv ${version} for ${platformKey}...`); diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts index 4654cd72..11850756 100644 --- a/src/hooks/useInstallationSetup.ts +++ b/src/hooks/useInstallationSetup.ts @@ -155,15 +155,6 @@ export const useInstallationSetup = () => { if (installationStatus.success && installationStatus.isInstalling) { startInstallation(); - } else if (initState !== 'done' && toolResult) { - if (toolResult.success && !toolResult.isInstalled) { - console.log('[useInstallationSetup] Tools missing and not installing. Starting installation...'); - try { - await performInstallation(); - } catch (installError) { - console.error('[useInstallationSetup] Installation failed:', installError); - } - } } } catch (err) { console.error('[useInstallationSetup] Failed to check installation status:', err);