Merge branch 'main' into fix/logger

This commit is contained in:
Puzhen Zhang 2025-12-12 16:22:27 +01:00 committed by GitHub
commit e14dc1070c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 487 additions and 168 deletions

View file

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

103
.github/workflows/codeql.yml vendored Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

12
backend/uv.lock generated
View file

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

View file

@ -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? **<u>Watch the full configuration video here</u>**

View file

@ -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
Nowstart 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
<img src="/docs/images/models_configure_tools_key.png" alt="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**

View file

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

BIN
docs/images/gemini_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
docs/images/gemini_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

BIN
docs/images/gemini_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
docs/images/gemini_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

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

View file

@ -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<PromiseReturnType>(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<an
fs.mkdirSync(npmCacheDir, { recursive: true });
}
const uvEnv = getUvEnv(currentVersion);
const env = {
...process.env,
...uvEnv,
SERVER_URL: "https://dev.eigent.ai/api",
PYTHONIOENCODING: 'utf-8',
PYTHONUNBUFFERED: '1',
UV_PROJECT_ENVIRONMENT: venvPath,
npm_config_cache: npmCacheDir,
}
@ -202,10 +203,79 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
{ cwd: backendPath, env: env }
);
log.info(`Python test output: ${pythonTest.trim()}`);
} catch (testErr) {
log.error(`Pre-flight check failed: ${testErr}`);
reject(new Error(`Backend environment check failed: ${testErr}`));
return;
} catch (testErr: any) {
log.warn(`Pre-flight check failed, attempting repair: ${testErr}`);
try {
// Attempt to repair the environment
log.info("Attempting to repair environment...");
// Cleanup stale processes and locks
log.info("Cleaning up stale processes and locks...");
await killProcessByName('uv');
await killProcessByName('python');
// Try to remove the lock file explicitly if it exists
try {
const lockFile = path.join(getCachePath('uv_python'), '.lock');
if (fs.existsSync(lockFile)) {
fs.unlinkSync(lockFile);
}
} catch (e) {
log.warn(`Failed to remove lock file: ${e}`);
}
// Cleanup corrupted python cache
try {
const pythonCacheDir = getCachePath('uv_python');
if (fs.existsSync(pythonCacheDir)) {
log.info(`Removing potentially corrupted Python cache: ${pythonCacheDir}`);
fs.rmSync(pythonCacheDir, { recursive: true, force: true });
}
} catch (e) {
log.warn(`Failed to remove Python cache: ${e}`);
}
// Cleanup corrupted venv (pyvenv.cfg may reference non-existent Python version)
try {
if (fs.existsSync(venvPath)) {
log.info(`Removing potentially corrupted venv: ${venvPath}`);
fs.rmSync(venvPath, { recursive: true, force: true });
}
} catch (e) {
log.warn(`Failed to remove venv: ${e}`);
}
// Use proxy if in China (simple check based on timezone)
// Add official PyPI as fallback for packages not available on mirror
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const proxyArgs = timezone === 'Asia/Shanghai'
? [
'--default-index', 'https://mirrors.aliyun.com/pypi/simple/',
'--index', 'https://pypi.org/simple/'
]
: [];
// Step 1: Ensure Python is installed (fixes corrupted/missing Python)
log.info("Step 1: Ensuring Python is installed...");
await execAsync(`${uv_path} python install 3.10`, { cwd: backendPath, env: env });
// Step 2: Sync dependencies
log.info("Step 2: Syncing dependencies...");
const syncArgs = ['sync', '--no-dev', ...proxyArgs];
await execAsync(`${uv_path} ${syncArgs.join(' ')}`, { cwd: backendPath, env: env });
// Retry the check
const { stdout: pythonTest } = await execAsync(
`${uv_path} run python -c "print('Python OK')"`,
{ cwd: backendPath, env: env }
);
log.info(`Python test output after repair: ${pythonTest.trim()}`);
} catch (repairErr) {
log.error(`Repair failed: ${repairErr}`);
reject(new Error(`Backend environment check failed: ${testErr}\nRepair failed: ${repairErr}`));
return;
}
}
const node_process = spawn(
@ -265,7 +335,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
setTimeout(() => {
try {
process.kill(-proc.pid, 'SIGKILL');
} catch (e) {}
} catch (e) { }
}, 1000);
} catch (e) {
log.error(`Failed to kill process group: ${e}`);

View file

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

View file

@ -142,3 +142,49 @@ export async function isBinaryExists(name: string): Promise<boolean> {
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<string, string> {
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<void> {
const platform = process.platform
try {
if (platform === 'win32') {
await new Promise<void>((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<void>((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)
}
}

View file

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

View file

@ -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}...`)

View file

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

View file

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