Merge remote-tracking branch 'origin/main' into feat-trigger

This commit is contained in:
a7m-1st 2026-02-17 14:10:18 +03:00
commit 4ec302c0ed
123 changed files with 3146 additions and 554 deletions

View file

@ -12,7 +12,7 @@ body:
id: version
attributes:
label: What version of eigent are you using?
placeholder: E.g., 0.0.82
placeholder: E.g., 0.0.84
validations:
required: true

View file

@ -1,8 +1,25 @@
<!-- Thank you for contributing! -->
### Related Issue
<!-- REQUIRED: Link to the issue this PR resolves. PRs without a linked issue will be closed. -->
<!-- Example: Closes #123 or Fixes #456 -->
Closes #
### Description
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
<!-- REQUIRED: Describe what this PR does and why. PRs without a description will not be reviewed. -->
### Testing Evidence (REQUIRED)
<!-- REQUIRED: Every PR must include human-verified testing proof (e.g., test logs, screenshots, or screen recordings). -->
<!-- REQUIRED for frontend/UI changes: You MUST attach at least one screenshot or screen recording in this PR. -->
<!-- Frontend/UI PRs without visual evidence will not be reviewed. -->
- [ ] I have included human-verified testing evidence in this PR.
- [ ] This PR includes frontend/UI changes, and I attached screenshot(s) or screen recording(s).
- [ ] No frontend/UI changes in this PR.
### What is the purpose of this pull request? <!-- (put an "X" next to an item) -->
@ -10,3 +27,7 @@
- [ ] New Feature
- [ ] Documentation update
- [ ] Other
### Contribution Guidelines Acknowledgement
- [ ] I have read and agree to the [Eigent Contribution Guideline](https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md#eigent-contribution-guideline)

View file

@ -84,16 +84,26 @@ jobs:
sudo apt-get update
sudo apt-get install -y libfuse2
# Install LLVM 20 for macOS Intel - llvmlite 0.46.0 only supports LLVM 20 (not 21)
- name: Install LLVM 20 (macOS Intel)
if: runner.os == 'macOS' && matrix.arch == 'x64'
run: |
brew install llvm@20
echo "LLVM_DIR=$(brew --prefix llvm@20)/lib/cmake/llvm" >> $GITHUB_ENV
echo "CMAKE_PREFIX_PATH=$(brew --prefix llvm@20)/lib/cmake/llvm" >> $GITHUB_ENV
# Step for macOS builds with signing
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
timeout-minutes: 90
run: |
# Increase file descriptor limit to prevent EMFILE errors during signing
# This is needed because electron-builder signs all files recursively,
# and Python venvs contain thousands of files
ulimit -n 65536 || ulimit -n 10240
echo "File descriptor limit set to: $(ulimit -n)"
# Set file descriptor limit to system maximum (hard limit) to prevent EMFILE during signing
HARD=$(ulimit -Hn 2>/dev/null)
if [ -n "$HARD" ] && [ "$HARD" != "unlimited" ]; then
ulimit -n "$HARD" 2>/dev/null || true
fi
ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || true
echo "File descriptor limit: $(ulimit -n) (hard: $(ulimit -Hn 2>/dev/null || echo 'N/A'))"
npm run build -- --arch ${{ matrix.arch }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -29,6 +29,9 @@ jobs:
- os: macos-latest
arch: arm64
artifact_name: macos-arm64
- os: macos-15-intel
arch: x64
artifact_name: macos-intel
- os: windows-latest
arch: x64
artifact_name: windows-latest
@ -93,16 +96,26 @@ jobs:
sudo apt-get update
sudo apt-get install -y libfuse2
# Install LLVM 20 for macOS Intel - llvmlite 0.46.0 only supports LLVM 20 (not 21)
- name: Install LLVM 20 (macOS Intel)
if: runner.os == 'macOS' && matrix.arch == 'x64'
run: |
brew install llvm@20
echo "LLVM_DIR=$(brew --prefix llvm@20)/lib/cmake/llvm" >> $GITHUB_ENV
echo "CMAKE_PREFIX_PATH=$(brew --prefix llvm@20)/lib/cmake/llvm" >> $GITHUB_ENV
# Step for macOS builds with signing
- name: Build Release Files (macOS with signing)
if: runner.os == 'macOS'
timeout-minutes: 90
run: |
# Increase file descriptor limit to prevent EMFILE errors during signing
# This is needed because electron-builder signs all files recursively,
# and Python venvs contain thousands of files
ulimit -n 65536 || ulimit -n 10240
echo "File descriptor limit set to: $(ulimit -n)"
# Set file descriptor limit to system maximum (hard limit) to prevent EMFILE during signing
HARD=$(ulimit -Hn 2>/dev/null)
if [ -n "$HARD" ] && [ "$HARD" != "unlimited" ]; then
ulimit -n "$HARD" 2>/dev/null || true
fi
ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || true
echo "File descriptor limit: $(ulimit -n) (hard: $(ulimit -Hn 2>/dev/null || echo 'N/A'))"
npm run build -- --arch ${{ matrix.arch }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -205,7 +218,7 @@ jobs:
steps:
- name: Create directories
run: |
mkdir -p release/mac-arm64 release/win-x64 release/linux-x64
mkdir -p release/mac-arm64 release/mac-intel release/win-x64 release/linux-x64
- name: Download mac-arm64 artifact
uses: actions/download-artifact@v7
@ -213,6 +226,12 @@ jobs:
name: release-macos-arm64-arm64
path: temp-mac-arm64
- name: Download mac-intel artifact
uses: actions/download-artifact@v7
with:
name: release-macos-intel-x64
path: temp-mac-intel
- name: Download win-x64 artifact
uses: actions/download-artifact@v7
with:
@ -236,6 +255,13 @@ jobs:
find temp-mac-arm64 \( -name "*.dmg" -o -name "*.dmg.blockmap" -o -name "*.zip" -o -name "*.zip.blockmap" -o -name "latest*.yml" \) -exec mv {} release/mac-arm64/ \; || true
fi
# mac-intel - move dmg, zip, blockmap, and yml files
if [ -d "temp-mac-intel/release" ]; then
find temp-mac-intel/release \( -name "*.dmg" -o -name "*.dmg.blockmap" -o -name "*.zip" -o -name "*.zip.blockmap" -o -name "latest*.yml" \) -exec mv {} release/mac-intel/ \; || true
else
find temp-mac-intel \( -name "*.dmg" -o -name "*.dmg.blockmap" -o -name "*.zip" -o -name "*.zip.blockmap" -o -name "latest*.yml" \) -exec mv {} release/mac-intel/ \; || true
fi
# win-x64 - move exe, blockmap, and yml files
if [ -d "temp-win-x64/release" ]; then
find temp-win-x64/release \( -name "*.exe" -o -name "*.exe.blockmap" -o -name "latest*.yml" \) -exec mv {} release/win-x64/ \; || true
@ -251,17 +277,74 @@ jobs:
fi
# Create GitHub Release
- name: Prepare GitHub Release assets
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
# GitHub release assets must have unique filenames.
# Both mac folders contain latest-mac.yml, so stage assets with
# channel-specific manifest names for macOS and keep one compatibility file.
rm -rf gh-release-assets
mkdir -p gh-release-assets
copy_file() {
local src_file="$1"
local dst_name="$2"
[ -f "$src_file" ] || return 0
if [ -e "gh-release-assets/$dst_name" ]; then
echo "Duplicate release asset name detected: $dst_name"
echo " existing: gh-release-assets/$dst_name"
echo " incoming: $src_file"
exit 1
fi
cp -f "$src_file" "gh-release-assets/$dst_name"
}
copy_assets() {
local src_dir="$1"
local skip_name="${2:-}"
[ -d "$src_dir" ] || return 0
while IFS= read -r -d '' file; do
local name
name="$(basename "$file")"
if [ -n "$skip_name" ] && [ "$name" = "$skip_name" ]; then
continue
fi
copy_file "$file" "$name"
done < <(find "$src_dir" -maxdepth 1 -type f -print0)
}
# Stage all normal artifacts (exclude duplicate mac manifest names first).
copy_assets "release/mac-arm64" "latest-mac.yml"
copy_assets "release/mac-intel" "latest-mac.yml"
copy_assets "release/win-x64"
copy_assets "release/linux-x64"
# macOS updater channels configured in electron/main/update.ts:
# arm64 -> latest-arm64-mac.yml, x64 -> latest-x64-mac.yml
copy_file "release/mac-arm64/latest-mac.yml" "latest-arm64-mac.yml"
copy_file "release/mac-intel/latest-mac.yml" "latest-x64-mac.yml"
# Compatibility manifest for clients still using default latest-mac.yml.
copy_file "release/mac-intel/latest-mac.yml" "latest-mac.yml"
echo "Prepared GitHub release assets:"
ls -1 gh-release-assets
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: |
release/mac-arm64/*
release/win-x64/*
release/linux-x64/*
gh-release-assets/*
# Extract version from tag (e.g., v0.0.82 -> 0.0.82)
# Extract version from tag (e.g., v0.0.84 -> 0.0.84)
- name: Extract version
if: startsWith(github.ref, 'refs/tags/')
id: version

View file

@ -30,33 +30,6 @@ jobs:
run: uv sync --group dev
- name: Run pre-commit
run: |
uv run pre-commit run --files \
$(find \
app/agent \
app/controller \
app/exception \
app/middleware \
app/model \
app/service \
tests/app \
-type f ! -path '*__pycache__*') \
app/__init__.py \
app/router.py \
app/component/__init__.py \
app/component/pydantic/__init__.py \
app/utils/listen/__init__.py \
app/utils/server/__init__.py \
app/utils/toolkit/__init__.py \
app/utils/toolkit/google_calendar_toolkit.py \
app/utils/toolkit/google_gmail_mcp_toolkit.py \
app/utils/toolkit/linkedin_toolkit.py \
app/utils/toolkit/reddit_toolkit.py \
app/utils/toolkit/slack_toolkit.py \
app/utils/toolkit/twitter_toolkit.py \
app/utils/toolkit/whatsapp_toolkit.py \
app/utils/workforce.py \
app/utils/single_agent_worker.py \
tests/conftest.py
run: uv run pre-commit run --files $(git ls-files .)
env:
SKIP: no-commit-to-branch

View file

@ -7,3 +7,4 @@ README_PT-BR.md
server/README_CN.md
server/README_EN.md
docs/troubleshooting/bug.md
backend/benchmark/answer/

View file

@ -1,20 +1,54 @@
# 🐫 Welcome to Eigent! 🐫
Thank you for your interest in contributing to the Eigent project! 🎉
We're excited to have your support. As an open-source product build on
We're excited to have your support. As an open-source product built on
CAMEL in a rapidly evolving and open-ended field, we wholeheartedly
welcome contributions of all kinds. Whether you want to introduce new
features, enhance the infrastructure, improve documentation, asking
features, enhance the infrastructure, improve documentation, raise
issues, or fix bugs, we appreciate your enthusiasm and efforts. 🙌
You are welcome to join our [discord](https://discord.com/invite/CNcNpquyDc)
for more efficient communication. 💬
---
## Eigent Contribution Guideline
Eigent is a multi-agent system designed to deliver a high-quality open source Cowork experience for users. We welcome developers who genuinely use Eigent to solve real-world problems to engage with us and build together.
**Our goals are:**
1. Pursue quality over quantity — in both code and features design within the Eigent repository.
2. Welcome any developer or user who truly uses Eigent, or shares our mission and vision, to discuss product and technology with us and bring the multi-agent open source Cowork system to more real users.
### Why This Policy Exists
As AI coding capabilities grow, an increasing number of AI coding bots or vibe code are introducing significant noise and risk to open-source repositories:
1. **Code quality risks.** AI-generated code may contain subtle bugs or hallucinations. An excessive volume of LLM-generated code is presumed to be polluted code and dramatically increases heavy and meaningless maintenance costs.
2. **Community culture.** For Eigent's community, we uphold the core value of human collaboration and oppose low-effort, low-signal spamming.
### Contribution Requirements
We are taking the following precautionary steps to maintain the integrity of this open-source repository:
1. **PRs must reference a prior discussion.** Every PR must link to a previously discussed and accepted issue, Discord thread, or equivalent. Drive-by PRs with no associated accepted issue will be closed.
2. **No unreviewed LLM-generated submissions.** We will close PRs directly that are primarily generated by LLMs or chatbots and submitted without meaningful human review especially "vibe-coded" submissions.
3. **Human-verified testing is required.** Do not submit code that is "theoretically correct but untested." Every PR must include proof of testing (e.g., screenshots, screen recordings, test output logs). Very important!
4. **AI-assisted drafts are acceptable for issues, discussions, and prototypes**, but they must be reviewed and edited by a human to reduce verbosity and noise.
### Enforcement: Grounds for Immediate Ban
The following abusive behaviors will result in an immediate ban (PR submission privileges revoked):
1. **Inauthentic contribution activity.** Using AI tools to artificially inflate open-source contribution metrics for personal or commercial gain.
2. **Bulk, low-quality, irrelevant, or misleading AI-generated content.**
---
## Join Our Community 🌍
### Developer Meeting Time & Link 💻
- English speakers: Mondays at 8 PM PDT. Join via Discord:
[Meeting Link](https://meet.google.com/sez-aomy-ebm?authuser=0&hs=122&ijlm=1753634732982)
- Chinese Speakers: Mondays at 9 PM UTC+8. Join via TecentMeeting:
[Meeting Link](https://meeting.tencent.com/dm/057wap1eeCSY)
@ -63,8 +97,9 @@ contribution you're making:
- Add a demo script in the `examples` directory.
We're a small team focused on building great things. If you have
something in mind that you'd like to add or modify, opening a pull
request is the ideal way to catch our attention. 🚀
something in mind that you'd like to add or modify, please first open
an issue or start a discussion to align with the team before submitting
a pull request. 🚀
### Contributing to Code Reviews 🔍
@ -86,28 +121,25 @@ our coding standards.
- If changes are necessary, the reviewer should leave constructive feedback.
- The contributor addresses feedback and updates the PR.
- The reviewer re-reviews the updated code.
- Once the code is approved by at least two reviewer, it can be merged into the main branch.
- Once the code is approved by at least two reviewers, it can be merged into the main branch.
- Merging should be done by a maintainer or an authorized contributor.
#### Code Review Checklist
- Functionality
- Correctness: Does the code perform the intended task? Are edge cases handled?
- Testing: Is there sufficient test coverage? Do all tests pass?
- Security: Are there any security vulnerabilities introduced by the change?
- Performance: Does the code introduce any performance regressions?
- Code Quality
- Readability: Is the code easy to read and understand? Is it well-commented where necessary?
- Maintainability: Is the code structured in a way that makes future changes easy?
- Style: Does the code follow the projects style guidelines?
Currently we use Ruff for format check and take [Google Python Style Guide](%22https://google.github.io/styleguide/pyguide.html%22) as reference.
Currently we use Ruff for format check and take [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) as reference.
- Documentation: Are public methods, classes, and any complex logic well-documented?
- Design
- Consistency: Does the code follow established design patterns and project architecture?
- Modularity: Are the changes modular and self-contained? Does the code avoid unnecessary duplication?
- Dependencies: Are dependencies minimized and used appropriately?
@ -173,7 +205,7 @@ response generation. Defaults to :obj:`OpenAIModel` with
#### Naming Principle: Avoid Abbreviations in Naming
- Abbreviations can lead to ambiguity, especially since variable names and code in CAMEL are directly used by agents.
- Abbreviations can lead to ambiguity, especially since variable names and code in Eigent are directly used by agents.
- Use clear, descriptive names that convey meaning without requiring additional explanation. This improves both human readability and the agent's ability to interpret the code.
Examples:
@ -181,7 +213,7 @@ Examples:
- Bad: msg_win_sz
- Good: message_window_size
By adhering to this principle, we ensure that CAMEL remains accessible and unambiguous for both developers and AI agents.
By adhering to this principle, we ensure that Eigent remains accessible and unambiguous for both developers and AI agents.
### Board Item Create Workflow 🛠️
@ -234,7 +266,7 @@ npm install
npm run dev
# In a separate terminal, start the backend server
cd eigent/server
cd server
docker compose up -d
# Stream the logs if you needed
docker compose logs -f
@ -245,7 +277,7 @@ To run the application locally in developer mode:
1. Configure `.env.development`:
- Set `VITE_USE_LOCAL_PROXY=true`
- Set `VITE_PROXY_URL=http://localhost:3001`
1. Go to the settings to specify your model key and model type.
2. Go to the settings to specify your model key and model type.
## Common Actions 🔄

View file

@ -38,8 +38,10 @@ repos:
- id: ruff
name: Ruff lint (auto-fix)
args: [--fix]
exclude: 'benchmark/answer/'
- id: ruff-format
name: Ruff format
exclude: 'benchmark/answer/'
# Security scanning
- repo: https://github.com/PyCQA/bandit
@ -56,6 +58,7 @@ repos:
hooks:
- id: mdformat
name: Format Markdown
exclude: 'benchmark/answer/'
additional_dependencies:
- mdformat-gfm
- mdformat_frontmatter

View file

@ -608,16 +608,8 @@ class ListenChatAgent(ChatAgent):
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
# Check if wrapped tool is sync to avoid run_in_executor
if hasattr(tool, "is_async") and not tool.is_async:
# Sync tool: call directly to preserve ContextVar
result = tool(**args)
if asyncio.iscoroutine(result):
result = await result
else:
# Async tool: use async_call
result = await tool.func.async_call(**args)
# MCP FunctionTool: always use async_call (sync wrapper can timeout)
result = await tool.func.async_call(**args)
elif hasattr(tool, "async_call") and callable(tool.async_call):
# Case: tool itself has async_call

View file

@ -599,6 +599,17 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
# Use typing_extensions.TypedDict for Pydantic <3.12 compatibility.
return await super().browser_sheet_input(cells=cells)
def get_tools(self):
tools = super().get_tools()
for tool in tools:
if not getattr(tool.func, "__listen_toolkit__", False):
cls_method = getattr(type(self), tool.func.__name__, None)
if cls_method and getattr(
cls_method, "__listen_toolkit__", False
):
tool.func.__listen_toolkit__ = True
return tools
@classmethod
def toolkit_name(cls) -> str:
return "Browser Toolkit"

View file

@ -41,7 +41,7 @@ logger = logging.getLogger("terminal_toolkit")
# App version - should match electron app version
# TODO: Consider getting this from a shared config
APP_VERSION = "0.0.82"
APP_VERSION = "0.0.84"
def get_terminal_base_venv_path() -> str:

View file

@ -45,7 +45,7 @@ class QuestionAnalysisResult(BaseModel):
McpServers = dict[Literal["mcpServers"], dict[str, dict]]
PLATFORM_MAPPING = {
"Z.ai": "openai-compatible-model",
"z.ai": "openai-compatible-model",
"ModelArk": "openai-compatible-model",
}

View file

@ -168,20 +168,18 @@ def initialize_tracer_provider() -> None:
_GLOBAL_TRACER_PROVIDER = provider
def get_tracer_provider() -> TracerProvider:
def get_tracer_provider() -> TracerProvider | None:
"""Get the global TracerProvider instance.
Returns:
TracerProvider: The global tracer provider
Raises:
RuntimeError: If called before initialization
TracerProvider if initialized, None otherwise
"""
if _GLOBAL_TRACER_PROVIDER is None:
raise RuntimeError(
logger.warning(
"TracerProvider not initialized. "
"Call initialize_tracer_provider() during app startup."
)
return None
return _GLOBAL_TRACER_PROVIDER
@ -258,22 +256,28 @@ class WorkforceMetricsCallback(WorkforceMetrics):
# Get the global shared tracer provider
# This ensures only one BatchSpanProcessor is running
provider = get_tracer_provider()
# Get tracer from the shared provider
# Use CAMEL version for instrumentation versioning
self.tracer = provider.get_tracer(
TRACER_NAME_WORKFORCE, camel.__version__
)
self.root_span = self.tracer.start_span(
f"{SPAN_WORKFORCE_EXECUTION}:{task_id}"
)
# Langfuse-specific attributes
self.root_span.set_attribute(ATTR_LANGFUSE_SESSION_ID, project_id)
tags = json.dumps(DEFAULT_LANGFUSE_TAGS.copy())
self.root_span.set_attribute(ATTR_LANGFUSE_TAGS, tags)
# Custom attributes
self.root_span.set_attribute(ATTR_PROJECT_ID, project_id)
self.root_span.set_attribute(ATTR_TASK_ID, task_id)
if provider is None:
# TracerProvider not initialized (e.g., app startup not
# completed or running in test environment)
self.enabled = False
else:
# Get tracer from the shared provider
# Use CAMEL version for instrumentation versioning
self.tracer = provider.get_tracer(
TRACER_NAME_WORKFORCE, camel.__version__
)
self.root_span = self.tracer.start_span(
f"{SPAN_WORKFORCE_EXECUTION}:{task_id}"
)
# Langfuse-specific attributes
self.root_span.set_attribute(
ATTR_LANGFUSE_SESSION_ID, project_id
)
tags = json.dumps(DEFAULT_LANGFUSE_TAGS.copy())
self.root_span.set_attribute(ATTR_LANGFUSE_TAGS, tags)
# Custom attributes
self.root_span.set_attribute(ATTR_PROJECT_ID, project_id)
self.root_span.set_attribute(ATTR_TASK_ID, task_id)
# Track active spans for task execution
self.task_spans = {}

View file

@ -0,0 +1,4 @@
BENCHMARK_MODEL_PLATFORM="openai"
BENCHMARK_MODEL_TYPE="gpt-5.2"
BENCHMARK_API_KEY=""
BENCHMARK_API_URL="https://api.openai.com/v1"

View file

@ -76,7 +76,29 @@ The `metadata` field (optional) provides information about the benchmark:
- `description`: Brief explanation of what skills or capabilities the benchmark tests
- `tags`: Array of keywords for filtering and organization
`model_platform` and `model_type` default to `"openai"` and `"gpt-4o"`. `api_key` defaults to `$OPENAI_API_KEY`. Set `api_url` for custom endpoints.
The `model_kwargs` field is optional. Defaults come from `BENCHMARK_*` environment variables (see below), falling back to `openai` / `gpt-5.2` / `$OPENAI_API_KEY`. Per-benchmark JSON values override the environment defaults.
### Custom model providers
You can override the model for all benchmarks via environment variables (see `.env.example`):
```bash
export BENCHMARK_MODEL_PLATFORM="openai-compatible-model"
export BENCHMARK_MODEL_TYPE=""
export BENCHMARK_API_KEY=""
export BENCHMARK_API_URL=""
```
| Variable | Default | Description |
| -------------------------- | --------------------------- | --------------------------------------------------------------------------- |
| `BENCHMARK_MODEL_PLATFORM` | `openai` | Provider name. Use `openai-compatible-model` for any OpenAI-compatible API. |
| `BENCHMARK_MODEL_TYPE` | `gpt-5.2` | Model identifier passed to the provider. |
| `BENCHMARK_API_KEY` | `$OPENAI_API_KEY` | API key for the provider. |
| `BENCHMARK_API_URL` | `https://api.openai.com/v1` | Base URL for the provider's API. |
> **Important:** If the model is served through an OpenAI-compatible API (e.g. DeepSeek, MiniMax, Ollama, vLLM, LiteLLM, or any other non-OpenAI provider), set `BENCHMARK_MODEL_PLATFORM` to `openai-compatible-model`**not** `openai`. The `openai` platform value is reserved for the official OpenAI API only.
To override a single benchmark, add `model_kwargs` to its JSON config — these take priority over environment variables.
2. Create `benchmark/checker/<n>.py` with a `check(working_directory: str) -> bool` function.

View file

@ -11,4 +11,3 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

View file

@ -0,0 +1,25 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
@lambda _: _()
class _:
def __format__(_, __):
_.__class__._ = property(lambda _: print(__))
return ""
def __() -> f"{_:Hello, WORLD!}": ...
_._

View file

@ -0,0 +1,7 @@
# warnings
PEP 702: The new `warnings.deprecated()` decorator provides a way to communicate deprecations to a static type checker and to warn on usage of deprecated classes and functions. A `DeprecationWarning` may also be emitted when a decorated function or class is used at runtime. (Contributed by Jelle Zijlstra in `gh-104003`.)
# multiprocessing
The default number of worker threads and processes is now selected using `os.process_cpu_count()` instead of `os.cpu_count()`. (Contributed by Victor Stinner in `gh-109649`.)

View file

@ -0,0 +1,77 @@
company_name,product_description,ai_category
fira,Agentic AI platform for investment firms,ai-fintech
assistant-ui,Open-source React.js library for AI chat,ai-developer-tools
artifact,Collaborative AI-native IDE for hardware engineers,ai-developer-tools
axal,AI observability for modular codebase architecture,ai-developer-tools
trainloop,Reasoning fine-tuning platform for AI models,ai-infrastructure
tally,AI agents for accounting firms automating repetitive tasks,ai-agents
sammy labs,AI that maps every click path in software for user onboarding,ai-customer-support
mercura,AI quoting for distributors and manufacturers,ai-sales
cedar,In-product AI copilot for any app,ai-productivity
browser use,Open-source web agents automating browser workflows,ai-agents
tamlabs,AI-native document editor for Microsoft Word,ai-productivity
copycat,Next-gen RPA powered by browser agents,ai-agents
wildcard,Make APIs work for AI agents,ai-infrastructure
mastra,JavaScript framework for building AI agents,ai-developer-tools
afterquery,High-quality datasets and benchmarks for AI model training,ai-data
fuse ai,AI agents to replace Salesforce,ai-sales
peppr,Self-improving knowledge base synthesizing company data,ai-productivity
sennu ai,AI agents automating the tech consulting market,ai-agents
mesh,AI finance co-worker providing real-time insights,ai-fintech
outlit,AI agents for enterprise deal creation,ai-sales
tire swing,AI for healthcare compliance,ai-healthcare
calltree ai,Enterprise-grade AI support reps for call centers,ai-customer-support
operand,B2B knowledge management platform with AI search,ai-data
gulp information services,Real-time self-improvement infrastructure for AI agents,ai-infrastructure
zeroentropy,High accuracy search API over unstructured data,ai-infrastructure
cardamon,AI compliance co-pilot for regulated financial businesses,ai-fintech
tergle,AI agents for audit workflows,ai-fintech
carecycle,Voice AI teams for Medicare agencies,ai-customer-support
sift dev,AI-powered fraud decisioning for digital businesses,ai-security
maive,AI-native manufacturing execution system for factory operations,ai-other
weave,AI to measure and analyze engineering work,ai-analytics
caseflood,AI inbound sales team for law firms,ai-legal
tejas ai,Risk decisioning platform for banks powered by AI,ai-fintech
vora ai,AI recruiter for hiring managers,ai-hr
a0.dev,AI-powered mobile app builder,ai-coding
general agency company,AI coworkers that can learn and act like humans,ai-agents
a1base,Twilio for AI agents,ai-infrastructure
verbiflow,AI-powered CRM that finds leads and closes deals,ai-sales
contrario,Fully autonomous AI recruiting agency,ai-hr
ovlo,Conversational AI for e-commerce sales,ai-sales
truffle ai,AWS for AI agents,ai-infrastructure
superglue,Self-healing integration agent for enterprise workflows,ai-infrastructure
conntour,AI to monitor thousands of security cameras,ai-security
promptless,AI teammate that auto-updates customer-facing docs,ai-productivity
stamp,AI-native email client for professionals,ai-productivity
guse,Prompt-to-automation platform for business workflows,ai-agents
subimage,AI-powered infrastructure mapping and security platform,ai-security
casixty,Reddit marketing agent for technical audiences,ai-marketing
leaping ai,Self-improving voice AI agents for call center automation,ai-customer-support
vetnio,AI copilot automating admin work for veterinary pros,ai-healthcare
trace,Voice AI customer support for financial services,ai-customer-support
quantstruct,AI documentation engineer for product docs,ai-developer-tools
onlook,AI-powered visual editor for designers,ai-developer-tools
pig,API for automating Windows apps with AI,ai-developer-tools
vantel,AI software for commercial insurance brokers,ai-fintech
agentin ai,AI agents automating enterprise software processes,ai-agents
solidroad,AI agents for sales and support team training,ai-customer-support
trata,AI-powered research desk for hedge funds,ai-analytics
sophris,AI engineer for electronic design automation,ai-developer-tools
mundo ai,High quality multilingual training data for AI models,ai-data
athenahq,AI-powered brand discovery optimization for ChatGPT,ai-marketing
lopus ai,AI agents for revenue intelligence,ai-sales
harbera,AI healthcare provider credentialing software,ai-healthcare
augento,Improving AI agents through reinforcement learning,ai-infrastructure
macadamia,AI mechanical engineer that detects and fixes design errors,ai-other
asteroid,Browser agents for regulated industries,ai-agents
gale,AI-powered immigration law firm,ai-legal
olive,Build internal tools with natural language and AI,ai-developer-tools
cuckoo labs,Real-time AI translator for sales and marketing teams,ai-marketing
mosaic,AI agents for video editing workflows,ai-agents
oki,Track company progress with AI analytics,ai-analytics
amby health,AI copilot for ambulance agencies,ai-healthcare
g lnk,AI collaboration platform for healthcare organizations,ai-healthcare
artificial societies,AI simulation of target audiences for marketing predictions,ai-marketing
overstand labs,AI insights from customer communications across channels,ai-analytics
lucidic ai,Analytics and simulation tools for AI agents,ai-analytics
1 company_name product_description ai_category
2 fira Agentic AI platform for investment firms ai-fintech
3 assistant-ui Open-source React.js library for AI chat ai-developer-tools
4 artifact Collaborative AI-native IDE for hardware engineers ai-developer-tools
5 axal AI observability for modular codebase architecture ai-developer-tools
6 trainloop Reasoning fine-tuning platform for AI models ai-infrastructure
7 tally AI agents for accounting firms automating repetitive tasks ai-agents
8 sammy labs AI that maps every click path in software for user onboarding ai-customer-support
9 mercura AI quoting for distributors and manufacturers ai-sales
10 cedar In-product AI copilot for any app ai-productivity
11 browser use Open-source web agents automating browser workflows ai-agents
12 tamlabs AI-native document editor for Microsoft Word ai-productivity
13 copycat Next-gen RPA powered by browser agents ai-agents
14 wildcard Make APIs work for AI agents ai-infrastructure
15 mastra JavaScript framework for building AI agents ai-developer-tools
16 afterquery High-quality datasets and benchmarks for AI model training ai-data
17 fuse ai AI agents to replace Salesforce ai-sales
18 peppr Self-improving knowledge base synthesizing company data ai-productivity
19 sennu ai AI agents automating the tech consulting market ai-agents
20 mesh AI finance co-worker providing real-time insights ai-fintech
21 outlit AI agents for enterprise deal creation ai-sales
22 tire swing AI for healthcare compliance ai-healthcare
23 calltree ai Enterprise-grade AI support reps for call centers ai-customer-support
24 operand B2B knowledge management platform with AI search ai-data
25 gulp information services Real-time self-improvement infrastructure for AI agents ai-infrastructure
26 zeroentropy High accuracy search API over unstructured data ai-infrastructure
27 cardamon AI compliance co-pilot for regulated financial businesses ai-fintech
28 tergle AI agents for audit workflows ai-fintech
29 carecycle Voice AI teams for Medicare agencies ai-customer-support
30 sift dev AI-powered fraud decisioning for digital businesses ai-security
31 maive AI-native manufacturing execution system for factory operations ai-other
32 weave AI to measure and analyze engineering work ai-analytics
33 caseflood AI inbound sales team for law firms ai-legal
34 tejas ai Risk decisioning platform for banks powered by AI ai-fintech
35 vora ai AI recruiter for hiring managers ai-hr
36 a0.dev AI-powered mobile app builder ai-coding
37 general agency company AI coworkers that can learn and act like humans ai-agents
38 a1base Twilio for AI agents ai-infrastructure
39 verbiflow AI-powered CRM that finds leads and closes deals ai-sales
40 contrario Fully autonomous AI recruiting agency ai-hr
41 ovlo Conversational AI for e-commerce sales ai-sales
42 truffle ai AWS for AI agents ai-infrastructure
43 superglue Self-healing integration agent for enterprise workflows ai-infrastructure
44 conntour AI to monitor thousands of security cameras ai-security
45 promptless AI teammate that auto-updates customer-facing docs ai-productivity
46 stamp AI-native email client for professionals ai-productivity
47 guse Prompt-to-automation platform for business workflows ai-agents
48 subimage AI-powered infrastructure mapping and security platform ai-security
49 casixty Reddit marketing agent for technical audiences ai-marketing
50 leaping ai Self-improving voice AI agents for call center automation ai-customer-support
51 vetnio AI copilot automating admin work for veterinary pros ai-healthcare
52 trace Voice AI customer support for financial services ai-customer-support
53 quantstruct AI documentation engineer for product docs ai-developer-tools
54 onlook AI-powered visual editor for designers ai-developer-tools
55 pig API for automating Windows apps with AI ai-developer-tools
56 vantel AI software for commercial insurance brokers ai-fintech
57 agentin ai AI agents automating enterprise software processes ai-agents
58 solidroad AI agents for sales and support team training ai-customer-support
59 trata AI-powered research desk for hedge funds ai-analytics
60 sophris AI engineer for electronic design automation ai-developer-tools
61 mundo ai High quality multilingual training data for AI models ai-data
62 athenahq AI-powered brand discovery optimization for ChatGPT ai-marketing
63 lopus ai AI agents for revenue intelligence ai-sales
64 harbera AI healthcare provider credentialing software ai-healthcare
65 augento Improving AI agents through reinforcement learning ai-infrastructure
66 macadamia AI mechanical engineer that detects and fixes design errors ai-other
67 asteroid Browser agents for regulated industries ai-agents
68 gale AI-powered immigration law firm ai-legal
69 olive Build internal tools with natural language and AI ai-developer-tools
70 cuckoo labs Real-time AI translator for sales and marketing teams ai-marketing
71 mosaic AI agents for video editing workflows ai-agents
72 oki Track company progress with AI analytics ai-analytics
73 amby health AI copilot for ambulance agencies ai-healthcare
74 g lnk AI collaboration platform for healthcare organizations ai-healthcare
75 artificial societies AI simulation of target audiences for marketing predictions ai-marketing
76 overstand labs AI insights from customer communications across channels ai-analytics
77 lucidic ai Analytics and simulation tools for AI agents ai-analytics

View file

@ -11,7 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
"""Checker for benchmark 0: hello_world.py should print 'Hello, World!'"""
"""Checker for benchmark 0: hello_world.py should print 'Hello, WORLD!'"""
import subprocess
import sys
@ -33,11 +33,11 @@ def check(working_directory: str) -> bool:
)
output = result.stdout.strip()
if output == "Hello, World!":
if output == "Hello, WORLD!":
print("PASS")
return True
else:
print(f"FAIL: expected 'Hello, World!', got '{output}'")
print(f"FAIL: expected 'Hello, WORLD!', got '{output}'")
return False

View file

@ -0,0 +1,61 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
"""Checker for benchmark 1: python313_features.md with warnings and
multiprocessing sections."""
import re
import sys
from pathlib import Path
def check(working_directory: str) -> bool:
md_file = Path(working_directory) / "python313_features.md"
if not md_file.exists():
print(f"FAIL: {md_file} does not exist")
return False
content = md_file.read_text()
if len(content.strip()) < 50:
print("FAIL: file content is too short")
return False
# Check for at least 2 heading sections (# warnings, # multiprocessing)
h1_sections = re.findall(r"^# .+", content, re.MULTILINE)
if len(h1_sections) < 2:
print(
f"FAIL: expected at least 2 # sections, found {len(h1_sections)}"
)
return False
lower = content.lower()
if "warnings" not in lower:
print("FAIL: missing warnings section")
return False
if "multiprocessing" not in lower:
print("FAIL: missing multiprocessing section")
return False
print("PASS")
return True
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <working_directory>")
sys.exit(1)
success = check(sys.argv[1])
sys.exit(0 if success else 1)

View file

@ -0,0 +1,92 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
"""Checker for benchmark 2: yc_w25_b2b_ai.csv with B2B AI companies."""
import csv
import sys
from pathlib import Path
VALID_CATEGORIES = {
"ai-agents",
"ai-infrastructure",
"ai-developer-tools",
"ai-analytics",
"ai-security",
"ai-healthcare",
"ai-sales",
"ai-productivity",
"ai-customer-support",
"ai-coding",
"ai-data",
"ai-fintech",
"ai-legal",
"ai-hr",
"ai-marketing",
"ai-other",
}
REQUIRED_COLUMNS = {"company_name", "product_description", "ai_category"}
def check(working_directory: str) -> bool:
csv_file = Path(working_directory) / "yc_w25_b2b_ai.csv"
if not csv_file.exists():
print(f"FAIL: {csv_file} does not exist")
return False
with open(csv_file, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
headers = set(reader.fieldnames or [])
missing = REQUIRED_COLUMNS - headers
if missing:
print(f"FAIL: missing columns: {missing}")
return False
rows = list(reader)
if len(rows) < 5:
print(f"FAIL: expected at least 5 companies, got {len(rows)}")
return False
for i, row in enumerate(rows):
name = row.get("company_name", "")
if name != name.lower():
print(f"FAIL: row {i}: company_name '{name}' is not lowercase")
return False
desc = row.get("product_description", "")
if len(desc) > 100:
print(
f"FAIL: row {i}: product_description exceeds 100 chars "
f"({len(desc)})"
)
return False
cat = row.get("ai_category", "")
if cat not in VALID_CATEGORIES:
print(f"FAIL: row {i}: invalid ai_category '{cat}'")
return False
print("PASS")
return True
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <working_directory>")
sys.exit(1)
success = check(sys.argv[1])
sys.exit(0 if success else 1)

View file

@ -1,18 +1,20 @@
{
"metadata": {
"difficulty": "easy",
"description": "Google a specific blog post on mathspp.com about obfuscated Python, read and understand the code tricks, then faithfully reproduce the exact program as hello_world.py.",
"tags": ["browser", "coding", "python", "target-searching"]
"description": "1) search tool usage and choosing the appropriate website from results, 2) interpreting advanced obfuscated Python code patterns (requires deep coding comprehension), 3) strict instruction following with implicit output modification instead of directly copying code from the website.",
"tags": [
"instruction-following",
"browser",
"coding",
"python",
"target-searching"
]
},
"data": {
"name": "0",
"question": "Google search 'The most obscure Hello, world! program', choose the link from the website mathspp, read the page, and write a Python script named 'hello_world.py' that faithfully reproduces the exact obfuscated Hello World program shown on that page. Do not simplify or rewrite it — copy the same structure, tricks, and naming conventions used by the author. The script must print 'Hello, World!' when run.",
"question": "Find 'obscure hello world program' from mathspp, read the page, and write a Python script named 'hello_world.py' that faithfully reproduces the obfuscated Hello World program shown on that page. Do not simplify or rewrite it, just use the same structure, tricks, and naming conventions used by the author. Notice that the script MUST print 'Hello, WORLD!' when run.",
"env": {}
},
"model_kwargs": {
"model_platform": "openai",
"model_type": "gpt-5.2"
},
"tests": {
"checker": ["benchmark/checker/0.py"],
"grader": ["benchmark/grader/0.py"]

View file

@ -0,0 +1,22 @@
{
"metadata": {
"difficulty": "easy",
"description": "1) agent autonomously triggers search/browser to retrieve real data instead of hallucinating, 2) browser use with scrolling to locate specific modules, 3) instruction following for file creation with specific name and format.",
"tags": [
"browser",
"research",
"markdown",
"instruction-following",
"code-related"
]
},
"data": {
"name": "1",
"question": "Find what's new in Python 3.13 for the `warnings` and `multiprocessing` modules. Create a markdown file named 'python313_features.md' with each module name as a heading (#) and the exact text description from the official documentation as the content below each heading. Only make sure any code or script references are wrapped in backticks.",
"env": {}
},
"tests": {
"checker": ["benchmark/checker/1.py"],
"grader": ["benchmark/grader/1.py"]
}
}

View file

@ -0,0 +1,16 @@
{
"metadata": {
"difficulty": "medium",
"description": "1) benchmark browser use capability with in-depth browser operations, 2) document generation with strict format constraints on the CSV generation, 3) implicit classification for each company's category.",
"tags": ["browser", "research", "data-extraction", "csv", "multi-step"]
},
"data": {
"name": "2",
"question": "Identify all B2B companies in the Y Combinator Winter 2025 batch whose product is related to AI. After you obtain the full company list, independently investigate each company's product information in detail and consolidate all findings into a clean, well-structured CSV file named 'yc_w25_b2b_ai.csv' with columns: company_name (in lowercase), product_description (100 chars max), ai_category (use a consistent set of values including 'ai-agents', 'ai-infrastructure', 'ai-developer-tools', 'ai-analytics', 'ai-security', 'ai-healthcare', 'ai-sales', 'ai-productivity', 'ai-customer-support', 'ai-coding', 'ai-data', 'ai-fintech', 'ai-legal', 'ai-hr', 'ai-marketing', and 'ai-other').",
"env": {}
},
"tests": {
"checker": ["benchmark/checker/2.py"],
"grader": ["benchmark/grader/2.py"]
}
}

View file

@ -16,11 +16,16 @@ import json
import os
from pathlib import Path
from dotenv import dotenv_values
from dotenv import dotenv_values, load_dotenv
from pydantic import BaseModel
from app.model.chat import Chat, McpServers
# Load benchmark env files (.env takes priority over .env.development)
_BENCHMARK_DIR = Path(__file__).resolve().parent
load_dotenv(_BENCHMARK_DIR / ".env")
load_dotenv(_BENCHMARK_DIR / ".env.development")
class Env(BaseModel):
# TODO: add more environment variables
@ -37,10 +42,12 @@ class Tests(BaseModel):
class ModelKwargs(BaseModel):
model_platform: str = "openai"
model_type: str = "gpt-4o"
api_key: str | None = None
api_url: str | None = None
model_platform: str = os.environ.get("BENCHMARK_MODEL_PLATFORM", "openai")
model_type: str = os.environ.get("BENCHMARK_MODEL_TYPE", "gpt-5.2")
api_key: str | None = os.environ.get("BENCHMARK_API_KEY")
api_url: str = os.environ.get(
"BENCHMARK_API_URL", "https://api.openai.com/v1"
)
class Metadata(BaseModel):
@ -64,7 +71,11 @@ class BenchmarkData(BaseModel):
server_env.update(env_vars)
server_cfg["env"] = server_env
api_key = model_kwargs.api_key or os.environ["OPENAI_API_KEY"]
api_key = (
model_kwargs.api_key
or os.environ.get("BENCHMARK_API_KEY")
or os.environ["OPENAI_API_KEY"]
)
self._chat = Chat(
task_id=f"benchmark_{self.name}",

View file

@ -16,6 +16,7 @@ import ast
import json
import sys
from pathlib import Path
from urllib.parse import urlparse
BROWSER_LOG_DIR = Path(__file__).resolve().parents[2] / "browser_log"
@ -63,63 +64,103 @@ def grade(working_directory: str) -> tuple[int, int]:
# 1. Visited mathspp.com blog page
visited = _visited_urls()
if any(
"mathspp.com/blog/the-most-obscure-hello-world" in u for u in visited
(p := urlparse(u)).hostname is not None
and (
p.hostname == "mathspp.com" or p.hostname.endswith(".mathspp.com")
)
and "/blog/the-most-obscure-hello-world" in p.path
for u in visited
):
completed += 1
else:
print(
"MISS [1]: did not visit "
"mathspp.com/blog/the-most-obscure-hello-world"
)
script = Path(working_directory) / "hello_world.py"
if not script.exists():
print("MISS [2-7]: hello_world.py does not exist")
return completed, total
source = script.read_text()
tree = ast.parse(source)
# 1. Uses a decorator that immediately instantiates a class
# 2. Uses a decorator that immediately instantiates a class
found = False
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.decorator_list:
found = True
completed += 1
break
if not found:
print("MISS [2]: no decorated class definition found")
# 2. Overloads __format__
# 3. Overloads __format__
found = False
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == "__format__":
found = True
completed += 1
break
if not found:
print("MISS [3]: no __format__ method found")
# 3. Uses property injection on the class
# 4. Uses property injection on the class
if "property" in source:
completed += 1
else:
print("MISS [4]: no 'property' usage found in source")
# 4. __format__ returns an empty string
# 5. __format__ returns an empty string
found = False
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == "__format__":
for child in ast.walk(node):
if isinstance(child, ast.Return
) and isinstance(child.value, ast.Constant):
if isinstance(child, ast.Return) and isinstance(
child.value, ast.Constant
):
if child.value.value == "":
found = True
completed += 1
break
break
if not found:
print('MISS [5]: __format__ does not return an empty string ""')
# 5. Uses function annotation to trigger f-string evaluation
# 6. Uses function annotation to trigger f-string evaluation
found = False
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.returns is not None:
if isinstance(node.returns, ast.JoinedStr):
found = True
completed += 1
break
if not found:
print(
"MISS [6]: no function annotation with f-string (JoinedStr) found"
)
# 6. Uses _ as both class name and instance variable
# 7. Uses _ as both class name and instance variable
has_class_underscore = False
has_attr_underscore = False
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == "_":
has_class_underscore = True
if isinstance(node,
ast.Attribute) and isinstance(node.value, ast.Name):
if isinstance(node, ast.Attribute) and isinstance(
node.value, ast.Name
):
if node.value.id == "_" and node.attr == "_":
has_attr_underscore = True
if has_class_underscore and has_attr_underscore:
completed += 1
else:
parts = []
if not has_class_underscore:
parts.append("no class named '_'")
if not has_attr_underscore:
parts.append("no _._ attribute access")
print(f"MISS [7]: {', '.join(parts)}")
return completed, total

View file

@ -0,0 +1,139 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
"""Grader for benchmark 1: evaluate python313_features.md milestones."""
import json
import re
import sys
from pathlib import Path
from urllib.parse import urlparse
BROWSER_LOG_DIR = Path(__file__).resolve().parents[2] / "browser_log"
def _visited_urls() -> set[str]:
"""Extract all URLs seen in browser logs."""
urls: set[str] = set()
if not BROWSER_LOG_DIR.exists():
return urls
for log_file in BROWSER_LOG_DIR.glob("hybrid_browser_toolkit_ws_*.log"):
decoder = json.JSONDecoder()
raw = log_file.read_text()
pos = 0
while pos < len(raw):
stripped = raw[pos:].lstrip()
if not stripped:
break
pos = len(raw) - len(stripped)
try:
obj, end = decoder.raw_decode(raw, pos)
pos = end
if not isinstance(obj, dict):
continue
action = obj.get("action", "")
if action == "visit_page":
args = obj.get("inputs", {}).get("args", [])
if args:
urls.add(args[0])
except (json.JSONDecodeError, ValueError):
pos += 1
return urls
def grade(working_directory: str) -> tuple[int, int]:
total = 7
completed = 0
md_file = Path(working_directory) / "python313_features.md"
# 1. Visited the Python 3.13 What's New page
visited = _visited_urls()
if any(
(p := urlparse(u)).hostname is not None
and (
p.hostname == "docs.python.org"
or p.hostname.endswith(".docs.python.org")
)
and "3.13" in p.path
for u in visited
):
completed += 1
else:
print("MISS [1]: did not visit docs.python.org/3.13 What's New page")
if not md_file.exists():
print("MISS [2-7]: python313_features.md does not exist")
return completed, total
content = md_file.read_text()
lower = content.lower()
# 2. Has a # warnings heading
if re.search(r"^# warnings\b", content, re.MULTILINE | re.IGNORECASE):
completed += 1
else:
print("MISS [2]: no '# warnings' heading found")
# 3. Has a # multiprocessing heading
if re.search(
r"^# multiprocessing\b", content, re.MULTILINE | re.IGNORECASE
):
completed += 1
else:
print("MISS [3]: no '# multiprocessing' heading found")
# 4. Mentions warnings.deprecated() with backticks
if "`warnings.deprecated()`" in content or (
"warnings.deprecated" in lower and "`" in content
):
completed += 1
else:
print(
"MISS [4]: missing `warnings.deprecated()` "
"(expected backtick-wrapped reference)"
)
# 5. Mentions PEP 702
if "pep 702" in lower:
completed += 1
else:
print("MISS [5]: no mention of PEP 702")
# 6. Mentions os.process_cpu_count() with backticks
if "`os.process_cpu_count()`" in content or (
"os.process_cpu_count" in lower and "`" in content
):
completed += 1
else:
print(
"MISS [6]: missing `os.process_cpu_count()` "
"(expected backtick-wrapped reference)"
)
# 7. Mentions os.cpu_count() (the old default being replaced)
if "os.cpu_count" in lower:
completed += 1
else:
print("MISS [7]: no mention of os.cpu_count()")
return completed, total
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <working_directory>")
sys.exit(1)
completed, total = grade(sys.argv[1])
print(f"{completed}/{total}")
sys.exit(0 if completed == total else 1)

View file

@ -0,0 +1,261 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
"""Grader for benchmark 2: evaluate yc_w25_b2b_ai.csv milestones."""
import csv
import json
import sys
from collections import Counter
from pathlib import Path
from urllib.parse import urlparse
BROWSER_LOG_DIR = Path(__file__).resolve().parents[2] / "browser_log"
ANSWER_CSV = (
Path(__file__).resolve().parents[1] / "answer" / "2" / "yc_w25_b2b_ai.csv"
)
VALID_CATEGORIES = {
"ai-agents",
"ai-infrastructure",
"ai-developer-tools",
"ai-analytics",
"ai-security",
"ai-healthcare",
"ai-sales",
"ai-productivity",
"ai-customer-support",
"ai-coding",
"ai-data",
"ai-fintech",
"ai-legal",
"ai-hr",
"ai-marketing",
"ai-other",
}
REQUIRED_COLUMNS = {"company_name", "product_description", "ai_category"}
def _visited_urls() -> set[str]:
"""Extract all URLs seen in browser logs."""
urls: set[str] = set()
if not BROWSER_LOG_DIR.exists():
return urls
for log_file in BROWSER_LOG_DIR.glob("hybrid_browser_toolkit_ws_*.log"):
decoder = json.JSONDecoder()
raw = log_file.read_text()
pos = 0
while pos < len(raw):
stripped = raw[pos:].lstrip()
if not stripped:
break
pos = len(raw) - len(stripped)
try:
obj, end = decoder.raw_decode(raw, pos)
pos = end
if not isinstance(obj, dict):
continue
action = obj.get("action", "")
if action == "visit_page":
args = obj.get("inputs", {}).get("args", [])
if args:
urls.add(args[0])
except (json.JSONDecodeError, ValueError):
pos += 1
return urls
def _load_answer() -> tuple[int, Counter]:
"""Load expected company count and category distribution from answer CSV."""
cat_counts: Counter = Counter()
count = 0
if not ANSWER_CSV.exists():
return 0, cat_counts
with open(ANSWER_CSV, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
count += 1
cat = row.get("ai_category", "")
if cat:
cat_counts[cat] += 1
return count, cat_counts
def _category_overlap(expected: Counter, actual: Counter) -> float:
"""Compute distribution overlap between expected and actual categories.
Normalizes both to proportions, then sums min(expected_pct, actual_pct)
for each category. Returns a value between 0.0 and 1.0.
"""
exp_total = sum(expected.values())
act_total = sum(actual.values())
if exp_total == 0 or act_total == 0:
return 0.0
all_cats = set(expected.keys()) | set(actual.keys())
overlap = 0.0
for cat in all_cats:
exp_pct = expected.get(cat, 0) / exp_total
act_pct = actual.get(cat, 0) / act_total
overlap += min(exp_pct, act_pct)
return overlap
def grade(working_directory: str) -> tuple[int, int]:
total = 10
completed = 0
csv_file = Path(working_directory) / "yc_w25_b2b_ai.csv"
# 1. Visited YC W25 companies page
visited = _visited_urls()
if any(
(p := urlparse(u)).hostname is not None
and (
p.hostname == "ycombinator.com"
or p.hostname.endswith(".ycombinator.com")
)
and "W25" in u
for u in visited
):
completed += 1
else:
print("MISS [1]: did not visit ycombinator.com W25 companies page")
# 2. CSV file exists
if not csv_file.exists():
print(f"MISS [2-10]: {csv_file.name} does not exist")
return completed, total
completed += 1
try:
with open(csv_file, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
headers = set(reader.fieldnames or [])
rows = list(reader)
except Exception as e:
print(f"MISS [3-10]: failed to parse CSV: {e}")
return completed, total
# 3. Has correct columns
if REQUIRED_COLUMNS.issubset(headers):
completed += 1
else:
missing = REQUIRED_COLUMNS - headers
print(f"MISS [3]: missing columns: {missing}")
# 4. All company_name values are lowercase
non_lower = [
row.get("company_name", "")
for row in rows
if row.get("company_name", "") != row.get("company_name", "").lower()
]
if rows and not non_lower:
completed += 1
else:
print(
f"MISS [4]: {len(non_lower)} company_name(s) not lowercase, "
f"e.g. {non_lower[:3]}"
)
# 5. All product_description values are <= 100 chars
too_long = [
(i, len(row.get("product_description", "")))
for i, row in enumerate(rows)
if len(row.get("product_description", "")) > 100
]
if rows and not too_long:
completed += 1
else:
print(
f"MISS [5]: {len(too_long)} description(s) exceed 100 chars, "
f"e.g. row {too_long[0][0]} has {too_long[0][1]} chars"
if too_long
else "MISS [5]: no rows found"
)
# 6. All ai_category values are valid enums
invalid_cats = [
(i, row.get("ai_category", ""))
for i, row in enumerate(rows)
if row.get("ai_category", "") not in VALID_CATEGORIES
]
if rows and not invalid_cats:
completed += 1
else:
print(
f"MISS [6]: {len(invalid_cats)} invalid category value(s), "
f"e.g. row {invalid_cats[0][0]}: '{invalid_cats[0][1]}'"
if invalid_cats
else "MISS [6]: no rows found"
)
# Load answer for approximate matching
expected_count, expected_cats = _load_answer()
actual_count = len(rows)
# 7-8. Company count within 50% → +1, within 25% → +1 more
if expected_count > 0 and actual_count > 0:
ratio = actual_count / expected_count
if 0.5 <= ratio <= 1.5:
completed += 1
if 0.75 <= ratio <= 1.25:
completed += 1
else:
print(
f"MISS [8]: count {actual_count} is within 50% but not "
f"25% of expected {expected_count} (ratio={ratio:.2f})"
)
else:
print(
f"MISS [7-8]: count {actual_count} is not within 50% of "
f"expected {expected_count} (ratio={ratio:.2f})"
)
else:
print(
f"MISS [7-8]: expected_count={expected_count}, "
f"actual_count={actual_count}"
)
# 9-10. Category distribution overlap >= 50% → +1, >= 75% → +1 more
actual_cats: Counter = Counter()
for row in rows:
cat = row.get("ai_category", "")
if cat:
actual_cats[cat] += 1
overlap = _category_overlap(expected_cats, actual_cats)
if overlap >= 0.50:
completed += 1
if overlap >= 0.75:
completed += 1
else:
print(
f"MISS [10]: category overlap {overlap:.2%} >= 50% but < 75%"
)
else:
print(
f"MISS [9-10]: category overlap {overlap:.2%} < 50%. "
f"Expected dist: {dict(expected_cats)}, "
f"actual dist: {dict(actual_cats)}"
)
return completed, total
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <working_directory>")
sys.exit(1)
completed, total = grade(sys.argv[1])
print(f"{completed}/{total}")
sys.exit(0 if completed == total else 1)

View file

@ -15,21 +15,21 @@
import asyncio
import csv
import importlib.util
import shutil
import sys
from datetime import datetime
from pathlib import Path
from benchmark.client import BenchmarkClient
from benchmark.environment import BenchmarkConfig
from benchmark.environment import BenchmarkConfig, ModelKwargs
DATASET_DIR = Path(__file__).parent / "dataset"
RESULTS_DIR = Path(__file__).parent
BROWSER_LOG_DIR = Path(__file__).parent.parent / "browser_log"
async def run_benchmark(
client: BenchmarkClient,
benchmark_path: Path,
verbose: bool = False
client: BenchmarkClient, benchmark_path: Path, verbose: bool = False
) -> dict:
"""Load a benchmark config and run it.
@ -43,15 +43,28 @@ async def run_benchmark(
dict: Results including benchmark name, model, checker and
grader outcomes.
"""
# Clear browser logs so previous benchmark visits don't leak into this run
if BROWSER_LOG_DIR.exists():
for log_file in BROWSER_LOG_DIR.iterdir():
if log_file.is_file():
log_file.unlink()
config = BenchmarkConfig.from_json(benchmark_path)
data = config.data
model_kwargs = config.model_kwargs
model = f"{model_kwargs.model_platform}/{model_kwargs.model_type}"
# Clear previous working directory so results are from a fresh run
working_dir_path = Path(data.get_working_directory(model_kwargs))
if working_dir_path.exists():
shutil.rmtree(working_dir_path)
working_dir_path.mkdir(parents=True, exist_ok=True)
print(f"--- Benchmark: {data.name} ---")
print(f"Question: {data.question}")
print(f"Model: {model}")
print(f"Working directory: {data.get_working_directory(model_kwargs)}")
print(f"Working directory: {working_dir_path}")
print(f"Checkers: {config.tests.checker}")
print(f"Graders: {config.tests.grader}")
@ -133,6 +146,13 @@ async def main() -> None:
print(f"No benchmark configs found in {DATASET_DIR}")
return
defaults = ModelKwargs()
print("=== Benchmark Model Configuration ===")
print(f" Platform: {defaults.model_platform}")
print(f" Model: {defaults.model_type}")
print(f" API URL: {defaults.api_url}")
print()
all_results = []
async with BenchmarkClient() as client:
for path in paths:

View file

@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.11,<3.12"
dependencies = [
"pip>=23.0",
"camel-ai[eigent]==0.2.85a0",
"camel-ai[eigent]==0.2.90a1",
"fastapi>=0.115.12",
"fastapi-babel>=1.0.0",
"uvicorn[standard]>=0.34.2",
@ -38,6 +38,7 @@ dev = [
[tool.ruff]
line-length = 79
target-version = "py311"
exclude = ["benchmark/answer"]
[tool.ruff.lint]
select = [
@ -70,7 +71,7 @@ quote-style = "double"
indent-style = "space"
[tool.bandit]
exclude_dirs = ["tests", ".venv", "venv"]
exclude_dirs = ["tests", ".venv", "venv", "benchmark/answer"]
skips = [
"B101", # assert_used - OK in non-production code
"B105", # hardcoded_password_string - false positive on env var names

View file

@ -35,6 +35,9 @@ def test_browser_agent_creation(sample_chat_data):
_mod = "app.agent.factory.browser"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.HybridBrowserToolkit") as mock_browser_toolkit,

View file

@ -36,6 +36,9 @@ async def test_developer_agent_creation(sample_chat_data):
_mod = "app.agent.factory.developer"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit,
@ -82,6 +85,9 @@ async def test_developer_agent_with_multiple_toolkits(sample_chat_data):
_mod = "app.agent.factory.developer"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit,

View file

@ -36,6 +36,9 @@ async def test_document_agent_creation(sample_chat_data):
_mod = "app.agent.factory.document"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.FileToolkit") as mock_file_toolkit,

View file

@ -35,6 +35,9 @@ def test_multi_modal_agent_creation(sample_chat_data):
_mod = "app.agent.factory.multi_modal"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.VideoDownloaderToolkit") as mock_video_toolkit,

View file

@ -36,6 +36,9 @@ async def test_social_media_agent_creation(sample_chat_data):
mod = "app.agent.factory.social_media"
with (
patch(f"{mod}.agent_model") as mock_agent_model,
patch(
f"{mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{mod}.WhatsAppToolkit") as mock_whatsapp_toolkit,
patch(f"{mod}.TwitterToolkit") as mock_twitter_toolkit,

View file

@ -13,7 +13,7 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import os
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import Response
@ -50,13 +50,14 @@ class TestChatController:
with (
patch(
"app.controller.chat_controller.create_task_lock",
"app.controller.chat_controller.get_or_create_task_lock",
return_value=mock_task_lock,
),
patch(
"app.controller.chat_controller.step_solve"
) as mock_step_solve,
patch("app.controller.chat_controller.load_dotenv"),
patch("app.controller.chat_controller.set_current_task_id"),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.home", return_value=MagicMock()),
):
@ -71,9 +72,6 @@ class TestChatController:
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
mock_step_solve.assert_called_once_with(
chat_data, mock_request, mock_task_lock
)
@pytest.mark.asyncio
async def test_post_chat_sets_environment_variables(
@ -84,13 +82,14 @@ class TestChatController:
with (
patch(
"app.controller.chat_controller.create_task_lock",
"app.controller.chat_controller.get_or_create_task_lock",
return_value=mock_task_lock,
),
patch(
"app.controller.chat_controller.step_solve"
) as mock_step_solve,
patch("app.controller.chat_controller.load_dotenv"),
patch("app.controller.chat_controller.set_current_task_id"),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.home", return_value=MagicMock()),
patch.dict(os.environ, {}, clear=True),
@ -133,18 +132,24 @@ class TestChatController:
# put_queue is invoked when creating the coroutine passed to asyncio.run
mock_task_lock.put_queue.assert_called_once()
def test_improve_chat_task_done_error(self, mock_task_lock):
"""Test improvement fails when task is done."""
def test_improve_chat_task_done_resets_to_confirming(self, mock_task_lock):
"""Test improvement when task is done resets status to confirming."""
task_id = "test_task_123"
supplement_data = SupplementChat(question="Improve this code")
mock_task_lock.status = Status.done
with patch(
"app.controller.chat_controller.get_task_lock",
return_value=mock_task_lock,
with (
patch(
"app.controller.chat_controller.get_task_lock",
return_value=mock_task_lock,
),
patch("asyncio.run") as mock_run,
):
with pytest.raises(UserException):
improve(task_id, supplement_data)
response = improve(task_id, supplement_data)
assert mock_task_lock.status == Status.confirming
assert isinstance(response, Response)
assert response.status_code == 201
def test_supplement_chat_success(self, mock_task_lock):
"""Test successful chat supplementation."""
@ -244,16 +249,18 @@ class TestChatControllerIntegration:
"""Test chat endpoint through FastAPI test client."""
with (
patch(
"app.controller.chat_controller.create_task_lock"
"app.controller.chat_controller.get_or_create_task_lock"
) as mock_create_lock,
patch(
"app.controller.chat_controller.step_solve"
) as mock_step_solve,
patch("app.controller.chat_controller.load_dotenv"),
patch("app.controller.chat_controller.set_current_task_id"),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.home", return_value=MagicMock()),
):
mock_task_lock = MagicMock()
mock_task_lock.put_queue = AsyncMock()
mock_create_lock.return_value = mock_task_lock
async def mock_generator():
@ -455,8 +462,12 @@ class TestChatControllerErrorCases:
with (
patch(
"app.controller.chat_controller.create_task_lock"
"app.controller.chat_controller.get_or_create_task_lock"
) as mock_create_lock,
patch(
"app.controller.chat_controller.sanitize_env_path",
return_value="/tmp/fake.env",
),
patch(
"app.controller.chat_controller.load_dotenv",
side_effect=Exception("Env load failed"),

View file

@ -15,6 +15,7 @@
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.controller.model_controller import (
@ -73,11 +74,9 @@ class TestModelController:
"app.controller.model_controller.create_agent",
side_effect=Exception("Invalid model configuration"),
):
response = await validate_model(request_data)
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
assert "Invalid model name" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_step_failure(self):
@ -93,12 +92,9 @@ class TestModelController:
"app.controller.model_controller.create_agent",
return_value=mock_agent,
):
response = await validate_model(request_data)
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
assert "API call failed" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_tool_calls_false(self):
@ -130,8 +126,10 @@ class TestModelController:
@pytest.mark.asyncio
async def test_validate_model_with_minimal_parameters(self):
"""Test model validation with minimal parameters."""
request_data = ValidateModelRequest() # Uses default values
"""Test model validation with minimal parameters (no API key)."""
request_data = (
ValidateModelRequest()
) # Uses default values, api_key is None
mock_agent = MagicMock()
mock_response = MagicMock()
@ -144,12 +142,12 @@ class TestModelController:
"app.controller.model_controller.create_agent",
return_value=mock_agent,
):
# api_key is None by default, which passes the empty string check
# The agent step succeeds, so validation should pass
response = await validate_model(request_data)
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
assert response.error_code is not None
assert response.error is not None
assert response.is_valid is True
assert response.is_tool_calls is True
@pytest.mark.asyncio
async def test_validate_model_no_response(self):
@ -222,13 +220,7 @@ class TestModelControllerIntegration:
):
response = client.post("/model/validate", json=request_data)
assert (
response.status_code == 200
) # Returns 200 with error in response body
response_data = response.json()
assert response_data["is_valid"] is False
assert response_data["is_tool_calls"] is False
assert "Invalid model name" in response_data["message"]
assert response.status_code == 400
@pytest.mark.model_backend
@ -267,10 +259,9 @@ class TestModelControllerErrorCases:
"app.controller.model_controller.create_agent",
side_effect=ValueError("Invalid configuration"),
):
response = await validate_model(request_data)
assert response.is_valid is False
assert "Invalid configuration" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_with_network_error(self):
@ -288,10 +279,9 @@ class TestModelControllerErrorCases:
"app.controller.model_controller.create_agent",
return_value=mock_agent,
):
response = await validate_model(request_data)
assert response.is_valid is False
assert "Network unreachable" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_with_malformed_tool_calls_response(self):
@ -346,36 +336,21 @@ class TestModelControllerErrorCases:
api_key="", # Empty API key
)
response = await validate_model(request_data)
assert response.is_valid is False
assert response.is_tool_calls is False
assert response.message == "Invalid key. Validation failed."
assert response.error_code == "invalid_api_key"
assert response.error is not None
assert response.error["message"] == "Invalid key. Validation failed."
assert response.error["type"] == "invalid_request_error"
assert response.error["code"] == "invalid_api_key"
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
detail = exc_info.value.detail
assert detail["error_code"] == "invalid_api_key"
@pytest.mark.asyncio
async def test_validate_model_invalid_model_type(self):
"""Test model validation with invalid model type."""
"""Test model validation with invalid model type raises HTTPException."""
request_data = ValidateModelRequest(
model_platform="openai",
model_type="INVALID_MODEL_TYPE",
api_key="test_key",
)
response = await validate_model(request_data)
assert response.is_valid is False
assert response.is_tool_calls is False
assert response.message == "Invalid model name. Validation failed."
assert response.error_code is not None
assert "model_not_found" in response.error_code
assert response.error is not None
assert (
response.error["message"]
== "Invalid model name. Validation failed."
)
assert response.error["type"] == "invalid_request_error"
assert response.error["code"] == "model_not_found"
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400

View file

@ -15,6 +15,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.controller.tool_controller import install_tool
@ -37,14 +38,18 @@ class TestToolController:
return_value=mock_toolkit,
):
result = await install_tool(tool_name)
assert result == ["create_page", "update_page"]
assert result["success"] is True
assert result["tools"] == ["create_page", "update_page"]
assert result["count"] == 2
assert result["toolkit_name"] == "NotionMCPToolkit"
mock_toolkit.connect.assert_called_once()
mock_toolkit.disconnect.assert_called_once()
@pytest.mark.asyncio
async def test_install_unknown_tool(self):
result = await install_tool("unknown_tool")
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool("unknown_tool")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_notion_tool_connection_failure(self):
@ -54,8 +59,11 @@ class TestToolController:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(Exception, match="Connection failed"):
await install_tool("notion")
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
assert "warning" in result
@pytest.mark.asyncio
async def test_install_notion_tool_get_tools_failure(self):
@ -67,8 +75,11 @@ class TestToolController:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(Exception, match="Failed to get tools"):
await install_tool("notion")
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
assert "warning" in result
@pytest.mark.asyncio
async def test_install_notion_tool_disconnect_failure(self):
@ -81,8 +92,11 @@ class TestToolController:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(Exception, match="Disconnect failed"):
await install_tool("notion")
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
assert "warning" in result
@pytest.mark.asyncio
async def test_install_notion_tool_empty_tools(self):
@ -93,7 +107,9 @@ class TestToolController:
return_value=mock_toolkit,
):
result = await install_tool("notion")
assert result == []
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
mock_toolkit.connect.assert_called_once()
mock_toolkit.disconnect.assert_called_once()
@ -117,7 +133,9 @@ class TestToolController:
return_value=mock_toolkit,
):
result = await install_tool("notion")
assert result == names
assert result["success"] is True
assert result["tools"] == names
assert result["count"] == 4
mock_toolkit.connect.assert_called_once()
mock_toolkit.disconnect.assert_called_once()
@ -145,7 +163,10 @@ class TestToolControllerIntegration:
response = client.post(f"/install/tool/{tool_name}")
assert response.status_code == 200
assert response.json() == ["create_page", "update_page"]
data = response.json()
assert data["success"] is True
assert data["tools"] == ["create_page", "update_page"]
assert data["count"] == 2
def test_install_unknown_tool_endpoint_integration(
self, client: TestClient
@ -155,8 +176,7 @@ class TestToolControllerIntegration:
response = client.post(f"/install/tool/{tool_name}")
assert response.status_code == 200
assert response.json() == {"error": "Tool not found"}
assert response.status_code == 404
def test_install_notion_tool_endpoint_with_connection_error(
self, client: TestClient
@ -171,9 +191,12 @@ class TestToolControllerIntegration:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
# The exception should be raised by the endpoint since there's no error handling
with pytest.raises(Exception, match="Connection failed"):
client.post(f"/install/tool/{tool_name}")
response = client.post(f"/install/tool/{tool_name}")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["tools"] == []
assert "warning" in data
@pytest.mark.model_backend
@ -211,8 +234,11 @@ class TestToolControllerErrorCases:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(AttributeError):
await install_tool("notion")
# Inner except catches the AttributeError and returns success with empty tools
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert "warning" in result
@pytest.mark.asyncio
async def test_install_tool_with_none_toolkit(self):
@ -220,23 +246,29 @@ class TestToolControllerErrorCases:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=None,
):
with pytest.raises(AttributeError):
await install_tool("notion")
# Inner except catches AttributeError on None.connect()
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert "warning" in result
@pytest.mark.asyncio
async def test_install_tool_with_special_characters_in_name(self):
result = await install_tool("notion@#$%")
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool("notion@#$%")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_tool_with_empty_string_name(self):
result = await install_tool("")
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool("")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_tool_with_none_name(self):
result = await install_tool(None)
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool(None)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_notion_tool_partial_failure(self):
@ -252,5 +284,8 @@ class TestToolControllerErrorCases:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(AttributeError):
await install_tool("notion")
# Inner except catches the AttributeError from tools[2].func
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert "warning" in result

View file

@ -71,7 +71,6 @@ class TestCollectPreviousTaskContext:
assert "Previous Task Result:" in result
assert "Successfully created script.py" in result
assert "=== END OF PREVIOUS TASK CONTEXT ===" in result
assert "=== NEW TASK ===" in result
def test_collect_previous_task_context_with_generated_files(
self, temp_dir
@ -208,7 +207,6 @@ class TestCollectPreviousTaskContext:
# Should still have the structural elements
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "=== END OF PREVIOUS TASK CONTEXT ===" in result
assert "=== NEW TASK ===" in result
# Should not have content sections for empty inputs
assert "Previous Task:" not in result
@ -289,7 +287,6 @@ class TestBuildContextForWorkforce:
# Create mock TaskLock
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = [
{"role": "user", "content": "Create a Python script"},
{
"role": "assistant",
"content": "I will create a Python script for you",
@ -304,14 +301,10 @@ class TestBuildContextForWorkforce:
result = build_context_for_workforce(task_lock, options)
# Should include conversation history
# Should include conversation history header
assert "=== CONVERSATION HISTORY ===" in result
assert "user: Create a Python script" in result
assert "assistant: I will create a Python script for you" in result
# Should include previous task context
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "Script created successfully" in result
# build_conversation_context only processes assistant and task_result roles
assert "I will create a Python script for you" in result
def test_build_context_for_workforce_empty_history(self, temp_dir):
"""Test build_context_for_workforce with empty conversation history."""
@ -329,15 +322,17 @@ class TestBuildContextForWorkforce:
assert result == ""
def test_build_context_for_workforce_task_result_role(self, temp_dir):
"""Test build_context_for_workforce handles 'task_result' role specially."""
"""Test build_context_for_workforce handles 'task_result' role."""
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = [
{"role": "user", "content": "First question"},
{
"role": "task_result",
"content": "Full task context from previous task",
},
{"role": "user", "content": "Second question"},
{
"role": "assistant",
"content": "Task completed successfully",
},
]
task_lock.last_task_result = "Final result"
task_lock.last_task_summary = "Task summary"
@ -347,22 +342,18 @@ class TestBuildContextForWorkforce:
result = build_context_for_workforce(task_lock, options)
# Should simplify task_result display
assert "[Previous Task Completed]" in result
assert (
"Full task context from previous task" not in result
) # Should not show full content
assert "user: First question" in result
assert "user: Second question" in result
# build_conversation_context appends string task_result content directly
assert "Full task context from previous task" in result
assert "Task completed successfully" in result
def test_build_context_for_workforce_with_last_task_result(self, temp_dir):
"""Test build_context_for_workforce includes last task result context."""
# Create some files in temp directory
(temp_dir / "output.txt").write_text("Task output")
"""Test build_context_for_workforce with assistant entries."""
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = [
{"role": "user", "content": "Test question"}
{
"role": "assistant",
"content": "Task completed with output.txt",
},
]
task_lock.last_task_result = "Task completed with output.txt"
task_lock.last_task_summary = "File creation task"
@ -372,13 +363,9 @@ class TestBuildContextForWorkforce:
result = build_context_for_workforce(task_lock, options)
# Should include conversation history and task context
# Should include conversation history
assert "=== CONVERSATION HISTORY ===" in result
assert "user: Test question" in result
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "Task completed with output.txt" in result
assert "File creation task" in result
assert "output.txt" in result # Generated file should be listed
@pytest.mark.unit
@ -586,17 +573,14 @@ class TestChatServiceAgentOperations:
@pytest.mark.asyncio
async def test_question_confirm_simple_query(self, mock_camel_agent):
"""Test question_confirm with simple query that gets direct response."""
mock_camel_agent.step.return_value.msgs[
0
].content = "Hello! How can I help you today?"
"""Test question_confirm with simple query returns False."""
mock_camel_agent.step.return_value.msgs[0].content = "no"
mock_camel_agent.chat_history = []
result = await question_confirm(mock_camel_agent, "hello")
# Should return SSE formatted response for simple queries
assert "wait_confirm" in result
assert "Hello! How can I help you today?" in result
# Should return False for simple queries (no "yes" in response)
assert result is False
@pytest.mark.asyncio
async def test_question_confirm_complex_task(self, mock_camel_agent):
@ -666,6 +650,10 @@ class TestChatServiceAgentOperations:
with (
patch("app.service.chat_service.agent_model") as mock_agent_model,
patch(
"app.service.chat_service.get_working_directory",
return_value="/tmp/test_workdir",
),
patch(
"app.service.chat_service.Workforce",
return_value=mock_workforce,
@ -682,6 +670,10 @@ class TestChatServiceAgentOperations:
"app.agent.toolkit.human_toolkit.get_task_lock",
return_value=mock_task_lock,
),
patch(
"app.service.chat_service.WorkforceMetricsCallback",
return_value=MagicMock(),
),
):
mock_agent_model.return_value = MagicMock()
@ -738,23 +730,15 @@ class TestChatServiceIntegration:
"def hello(): print('Hello World')"
)
# Mock file_save_path method to return our temp directory
with patch.object(
Chat, "file_save_path", return_value=str(working_dir)
):
# Test the context building directly
context = build_context_for_workforce(task_lock, options)
# Test the context building directly
# build_context_for_workforce now only calls build_conversation_context
# which only processes assistant and task_result roles
context = build_context_for_workforce(task_lock, options)
# Verify context includes conversation history
assert "=== CONVERSATION HISTORY ===" in context
assert "user: Create a Python script" in context
assert "assistant: Script created successfully" in context
# Verify context includes task context with files
assert "=== CONTEXT FROM PREVIOUS TASK ===" in context
assert "def hello(): print('Hello World')" in context
assert "Python Hello World Script" in context
assert "script.py" in context
# Verify context includes conversation history header
assert "=== CONVERSATION HISTORY ===" in context
# assistant entries are included
assert "Script created successfully" in context
@pytest.mark.asyncio
async def test_step_solve_new_task_state_context_collection(
@ -793,7 +777,6 @@ class TestChatServiceIntegration:
assert "main.py" in result
assert "config.json" in result
assert "=== END OF PREVIOUS TASK CONTEXT ===" in result
assert "=== NEW TASK ===" in result
@pytest.mark.asyncio
async def test_step_solve_end_action_context_collection(
@ -1008,26 +991,23 @@ class TestChatServiceErrorCases:
# Should log warning
mock_logger.warning.assert_called_once()
def test_collect_previous_task_context_relpath_exception(self, temp_dir):
"""Test collect_previous_task_context handles os.path.relpath exceptions."""
def test_collect_previous_task_context_abspath_used(self, temp_dir):
"""Test collect_previous_task_context uses absolute paths for files."""
working_directory = str(temp_dir)
# Create a test file
(temp_dir / "test.txt").write_text("test content")
with patch("os.path.relpath", side_effect=ValueError("Invalid path")):
with patch("app.service.chat_service.logger") as mock_logger:
result = collect_previous_task_context(
working_directory=working_directory,
previous_task_content="Test task",
previous_task_result="Test result",
previous_summary="Test summary",
)
result = collect_previous_task_context(
working_directory=working_directory,
previous_task_content="Test task",
previous_task_result="Test result",
previous_summary="Test summary",
)
# Should handle the exception gracefully
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
# Should log warning about file collection failure
mock_logger.warning.assert_called_once()
# Should include absolute path for the file
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "test.txt" in result
def test_build_context_for_workforce_missing_attributes(self, temp_dir):
"""Test build_context_for_workforce handles missing attributes gracefully."""
@ -1045,20 +1025,18 @@ class TestChatServiceErrorCases:
# Should handle missing attributes gracefully
assert result == ""
def test_build_context_for_workforce_file_save_path_exception(self):
"""Test build_context_for_workforce handles file_save_path exceptions."""
def test_build_context_for_workforce_empty_conversation(self):
"""Test build_context_for_workforce returns empty for empty conversation."""
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = []
task_lock.last_task_result = "Test result"
task_lock.last_task_summary = "Test summary"
options = MagicMock()
options.file_save_path.side_effect = Exception("Path error")
with patch("app.service.chat_service.logger") as mock_logger:
# Should handle exception when getting file path
with pytest.raises(Exception, match="Path error"):
build_context_for_workforce(task_lock, options)
# Should return empty string for empty conversation history
result = build_context_for_workforce(task_lock, options)
assert result == ""
def test_collect_previous_task_context_unicode_handling(self, temp_dir):
"""Test collect_previous_task_context handles unicode content correctly."""
@ -1216,6 +1194,22 @@ class TestChatServiceErrorCases:
"app.service.chat_service.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.developer.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.browser.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.document.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.multi_modal.agent_model",
side_effect=Exception("Agent creation failed"),
),
):
with pytest.raises(Exception, match="Agent creation failed"):
await construct_workforce(options)

View file

@ -519,32 +519,29 @@ class TestPeriodicCleanup:
@pytest.mark.asyncio
async def test_periodic_cleanup_handles_exceptions(self):
"""Test that periodic cleanup handles exceptions gracefully."""
import app.service.task as task_module
# Create a stale task lock
task_lock = create_task_lock("test_task")
task_lock.last_accessed = datetime.now() - timedelta(hours=3)
# Mock delete_task_lock to raise exception
# Mock delete_task_lock to raise exception and call through module
with (
patch(
"app.service.task.delete_task_lock",
patch.object(
task_module,
"delete_task_lock",
side_effect=Exception("Test error"),
),
patch(
"app.service.task.logger.error",
) as mock_logger,
patch.object(task_module, "logger") as mock_logger,
):
# Directly call the cleanup logic
# that should trigger the exception
# Simulate what _periodic_cleanup does when encountering an error
try:
await delete_task_lock("test_task")
await task_module.delete_task_lock("test_task")
except Exception as e:
import logging
task_logger = logging.getLogger("task_service")
task_logger.error(f"Error during task cleanup: {e}")
task_module.logger.error(f"Error in periodic cleanup: {e}")
# Should have logged the error
mock_logger.assert_called()
mock_logger.error.assert_called()
@pytest.mark.integration

View file

@ -33,6 +33,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "worker_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker description",
@ -57,6 +58,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "worker_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -123,6 +125,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "worker_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -178,6 +181,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -247,6 +251,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker", worker=mock_worker
@ -280,6 +285,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -333,6 +339,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -382,6 +389,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -431,6 +439,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -476,6 +485,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(description="Test", worker=mock_worker)
assert isinstance(worker, BaseSingleAgentWorker)
@ -491,6 +501,7 @@ class TestSingleAgentWorkerIntegration:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "integration_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "integration_worker"
worker = SingleAgentWorker(
description="Integration test worker",
@ -568,6 +579,7 @@ class TestSingleAgentWorkerErrorCases:
"""Test _process_task when agent returns None response."""
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test",
worker=mock_worker,
@ -600,6 +612,7 @@ class TestSingleAgentWorkerErrorCases:
"""Test _process_task with malformed response structure."""
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test",
worker=mock_worker,
@ -637,6 +650,7 @@ class TestSingleAgentWorkerErrorCases:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.role_name = "test_worker"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test",
worker=mock_worker,

View file

@ -344,6 +344,20 @@ async def async_mock_agent() -> AsyncGenerator[AsyncMock, None]:
yield agent
# Safety net: clean up any MagicMock-named directories that tests may
# accidentally create when mock objects are used as file paths.
@pytest.fixture(autouse=True, scope="session")
def _cleanup_magicmock_dirs():
"""Remove MagicMock-named directories from backend/ after test session."""
yield
import shutil
backend_dir = Path(__file__).parent.parent
for entry in backend_dir.iterdir():
if "MagicMock" in entry.name:
shutil.rmtree(entry, ignore_errors=True)
# Markers for test categorization
pytest_plugins = ["pytest_asyncio"]

9
backend/uv.lock generated
View file

@ -242,7 +242,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.85a0" },
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.90a1" },
{ name = "debugpy", specifier = ">=1.8.17" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastapi-babel", specifier = ">=1.0.0" },
@ -285,7 +285,7 @@ wheels = [
[[package]]
name = "camel-ai"
version = "0.2.85a0"
version = "0.2.90a1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astor" },
@ -299,12 +299,13 @@ dependencies = [
{ name = "pillow" },
{ name = "psutil" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "tiktoken" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/54/ab/7d305f80e868a60c7097ab510063a171e1798d163b5f8fd7fe7c16553e13/camel_ai-0.2.85a0.tar.gz", hash = "sha256:432de9bac1e40bd4ebf434ca80eaf3993121f87924820e26ad2bad69c1fb5cf5", size = 1126159, upload-time = "2026-01-23T02:24:08.868Z" }
sdist = { url = "https://files.pythonhosted.org/packages/85/cc/78345177dfffd532f21889bb4794f197e21ca79451a27243f0240db04840/camel_ai-0.2.90a1.tar.gz", hash = "sha256:0a84a7991a8679a83dcf1c6124d0a5ae953282526cf5a04a07bec8b7338436eb", size = 1156184, upload-time = "2026-02-12T22:32:31.727Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/0c/35d73b5d648413844bdfeaf95172a6b7c19802150829f5f907753a773d19/camel_ai-0.2.85a0-py3-none-any.whl", hash = "sha256:6045e9af72fee918ca3acc92f3b4af8af084af7b0cf6435c01a1252bd04ae6b3", size = 1599866, upload-time = "2026-01-23T02:24:06.78Z" },
{ url = "https://files.pythonhosted.org/packages/05/2c/926157452c27d1f93640a2293a7a0193212cdb4d1d34f62b98c4392491ce/camel_ai-0.2.90a1-py3-none-any.whl", hash = "sha256:2764de542c165d57b35836999500aeb2ba148077d494a168009fb7a4ddc64ca3", size = 1632784, upload-time = "2026-02-12T22:32:29.704Z" },
]
[package.optional-dependencies]

View file

@ -18,7 +18,9 @@ exports.default = async function afterPack(context) {
return;
}
console.log('🧹 Cleaning invalid symlinks and cache directories before signing...');
console.log(
'🧹 Cleaning invalid symlinks and cache directories before signing...'
);
const resourcesPath = path.join(appPath, 'Contents', 'Resources');
const prebuiltPath = path.join(resourcesPath, 'prebuilt');
@ -64,13 +66,23 @@ exports.default = async function afterPack(context) {
const entries = fs.readdirSync(venvLibPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('python')) {
const flacMacPath = path.join(venvLibPath, entry.name, 'site-packages', 'speech_recognition', 'flac-mac');
const flacMacPath = path.join(
venvLibPath,
entry.name,
'site-packages',
'speech_recognition',
'flac-mac'
);
if (fs.existsSync(flacMacPath)) {
console.log(`Removing flac-mac binary (outdated SDK): ${flacMacPath}`);
console.log(
`Removing flac-mac binary (outdated SDK): ${flacMacPath}`
);
try {
fs.unlinkSync(flacMacPath);
} catch (error) {
console.warn(`Warning: Could not remove flac-mac: ${error.message}`);
console.warn(
`Warning: Could not remove flac-mac: ${error.message}`
);
}
}
}
@ -83,7 +95,13 @@ exports.default = async function afterPack(context) {
// Clean Python symlinks in venv/bin
const venvBinDir = path.join(prebuiltPath, 'venv', 'bin');
if (fs.existsSync(venvBinDir)) {
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
const pythonNames = [
'python',
'python3',
'python3.10',
'python3.11',
'python3.12',
];
const bundlePath = path.resolve(appPath);
for (const pythonName of pythonNames) {
@ -94,7 +112,10 @@ exports.default = async function afterPack(context) {
const stats = fs.lstatSync(pythonSymlink);
if (stats.isSymbolicLink()) {
const target = fs.readlinkSync(pythonSymlink);
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
const resolvedPath = path.resolve(
path.dirname(pythonSymlink),
target
);
// If symlink points outside bundle, remove it
if (!resolvedPath.startsWith(bundlePath)) {
@ -103,7 +124,9 @@ exports.default = async function afterPack(context) {
}
}
} catch (error) {
console.warn(`Warning: Could not process ${pythonName} symlink: ${error.message}`);
console.warn(
`Warning: Could not process ${pythonName} symlink: ${error.message}`
);
}
}
}
@ -112,7 +135,13 @@ exports.default = async function afterPack(context) {
// Clean Python symlinks in terminal_venv/bin (same as venv/bin)
const terminalVenvBinDir = path.join(prebuiltPath, 'terminal_venv', 'bin');
if (fs.existsSync(terminalVenvBinDir)) {
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
const pythonNames = [
'python',
'python3',
'python3.10',
'python3.11',
'python3.12',
];
const bundlePath = path.resolve(appPath);
for (const pythonName of pythonNames) {
@ -123,16 +152,23 @@ exports.default = async function afterPack(context) {
const stats = fs.lstatSync(pythonSymlink);
if (stats.isSymbolicLink()) {
const target = fs.readlinkSync(pythonSymlink);
const resolvedPath = path.resolve(path.dirname(pythonSymlink), target);
const resolvedPath = path.resolve(
path.dirname(pythonSymlink),
target
);
// If symlink points outside bundle, remove it
if (!resolvedPath.startsWith(bundlePath)) {
console.log(`Removing invalid terminal_venv ${pythonName} symlink: ${target}`);
console.log(
`Removing invalid terminal_venv ${pythonName} symlink: ${target}`
);
fs.unlinkSync(pythonSymlink);
}
}
} catch (error) {
console.warn(`Warning: Could not process terminal_venv ${pythonName} symlink: ${error.message}`);
console.warn(
`Warning: Could not process terminal_venv ${pythonName} symlink: ${error.message}`
);
}
}
}
@ -156,7 +192,10 @@ exports.default = async function afterPack(context) {
const resolvedPath = path.resolve(path.dirname(fullPath), target);
const bundlePath = path.resolve(bundleRoot);
if (!fs.existsSync(resolvedPath) || !resolvedPath.startsWith(bundlePath)) {
if (
!fs.existsSync(resolvedPath) ||
!resolvedPath.startsWith(bundlePath)
) {
console.log(`Removing invalid symlink: ${fullPath} -> ${target}`);
fs.unlinkSync(fullPath);
}

View file

@ -22,10 +22,6 @@
"to": "backend",
"filter": ["**/*", "!.venv/**/*", "!workspace/.initial_env/**/*"]
},
{
"from": "utils",
"to": "utils"
},
{
"from": "resources/prebuilt",
"to": "prebuilt",
@ -36,7 +32,18 @@
"!uv_python/**/*.pyc",
"!uv_python/**/__pycache__",
"!terminal_venv/**/*.pyc",
"!terminal_venv/**/__pycache__"
"!terminal_venv/**/__pycache__",
"!**/__pycache__/**",
"!**/*.pyc",
"!**/*.pyo",
"!venv/**/__pycache__/**",
"!venv/**/*.pyc",
"!venv/lib/python*/site-packages/yt_dlp/**",
"!venv/lib/python*/site-packages/yt_dlp-*.dist-info/**",
"!uv_python/**/site-packages/pip/**",
"!uv_python/**/site-packages/setuptools/**",
"!uv_python/**/site-packages/wheel/**"
]
}
],

View file

@ -58,7 +58,11 @@ import {
} from './utils/envUtil';
import { zipFolder } from './utils/log';
import { addMcp, readMcpConfig, removeMcp, updateMcp } from './utils/mcpConfig';
import { getBackendPath, getVenvPath, isBinaryExists } from './utils/process';
import {
checkVenvExistsForPreCheck,
getBackendPath,
isBinaryExists,
} from './utils/process';
import { WebViewManager } from './webview';
const userData = app.getPath('userData');
@ -1644,11 +1648,8 @@ async function createWindow() {
let hasPrebuiltDeps = false;
if (app.isPackaged) {
const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
const prebuiltVenvDir = path.join(
process.resourcesPath,
'prebuilt',
'venv'
);
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltVenvDir = path.join(prebuiltDir, 'venv');
const uvPath = path.join(
prebuiltBinDir,
process.platform === 'win32' ? 'uv.exe' : 'uv'
@ -1659,10 +1660,9 @@ async function createWindow() {
);
const pyvenvCfg = path.join(prebuiltVenvDir, 'pyvenv.cfg');
const hasVenv = fs.existsSync(pyvenvCfg);
hasPrebuiltDeps =
fs.existsSync(uvPath) &&
fs.existsSync(bunPath) &&
fs.existsSync(pyvenvCfg);
fs.existsSync(uvPath) && fs.existsSync(bunPath) && hasVenv;
if (hasPrebuiltDeps) {
log.info(
'[PRE-CHECK] Prebuilt dependencies found, skipping installation check'
@ -1687,9 +1687,9 @@ async function createWindow() {
const installedLockPath = path.join(backendPath, 'uv_installed.lock');
const installationCompleted = fs.existsSync(installedLockPath);
// Check if venv path exists for current version
const venvPath = getVenvPath(currentVersion);
const venvExists = fs.existsSync(venvPath);
// Check venv existence WITHOUT triggering extraction (defers to startBackend when window is visible)
const { exists: venvExists, path: venvPath } =
checkVenvExistsForPreCheck(currentVersion);
// If prebuilt deps are available, skip installation
const needsInstallation = hasPrebuiltDeps

View file

@ -24,11 +24,13 @@ import { promisify } from 'util';
import { PromiseReturnType } from './install-deps';
import { maskProxyUrl, readGlobalEnvKey } from './utils/envUtil';
import {
ensureTerminalVenvAtUserPath,
findNodejsWheelBinPath,
findNodejsWheelNpmPath,
getBackendPath,
getBinaryPath,
getCachePath,
getPrebuiltPythonDir,
getPrebuiltVenvPath,
getUvEnv,
getVenvPath,
getVenvPythonPath,
@ -305,6 +307,23 @@ export async function startBackend(
`Backend SERVER_URL resolved to: ${serverUrl} (source: ${resolvedSource})`
);
// Ensure prebuilt terminal venv is copied to ~/.eigent/venvs for terminal toolkit
ensureTerminalVenvAtUserPath(currentVersion);
// Add nodejs-wheel paths for browser toolkit (needs npm, npx, and node)
const npmWrapperDir = findNodejsWheelNpmPath(venvPath);
const nodejsWheelBin = findNodejsWheelBinPath(venvPath);
const pathEnv = process.env.PATH || '';
const pathParts: string[] = [];
if (npmWrapperDir) pathParts.push(npmWrapperDir);
if (nodejsWheelBin && nodejsWheelBin !== npmWrapperDir) {
pathParts.push(nodejsWheelBin);
}
const updatedPath =
pathParts.length > 0
? pathParts.join(path.delimiter) + path.delimiter + pathEnv
: pathEnv;
const env = {
...process.env,
...uvEnv,
@ -313,6 +332,7 @@ export async function startBackend(
PYTHONIOENCODING: 'utf-8',
PYTHONUNBUFFERED: '1',
npm_config_cache: npmCacheDir,
PATH: updatedPath,
};
const displayFilteredLogs = (data: String) => {
@ -397,18 +417,9 @@ export async function startBackend(
}
// Cleanup corrupted venv (pyvenv.cfg may reference non-existent Python version)
// This is especially important for prebuilt venvs with hardcoded paths from CI
const prebuiltVenvPath = getPrebuiltVenvPath();
try {
// If the broken venv is the prebuilt venv, we need to remove it
// and let UV recreate it from the bundled Python
if (fs.existsSync(venvPath)) {
log.info(`Removing potentially corrupted venv: ${venvPath}`);
if (venvPath === prebuiltVenvPath) {
log.info(
`This is the prebuilt venv with hardcoded paths - will recreate from bundled Python`
);
}
fs.rmSync(venvPath, { recursive: true, force: true });
}
} catch (e) {

View file

@ -61,11 +61,8 @@ export const checkAndInstallDepsOnUpdate = async ({
return false;
}
const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
const prebuiltVenvDir = path.join(
process.resourcesPath,
'prebuilt',
'venv'
);
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltVenvDir = path.join(prebuiltDir, 'venv');
const uvPath = path.join(
prebuiltBinDir,
process.platform === 'win32' ? 'uv.exe' : 'uv'

View file

@ -12,7 +12,7 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { spawn } from 'child_process';
import { execSync, spawn } from 'child_process';
import { app } from 'electron';
import log from 'electron-log';
import fs from 'fs';
@ -212,32 +212,61 @@ function fixPyvenvCfgPlaceholder(pyvenvCfgPath: string): boolean {
}
/**
* Fix shebang lines in venv scripts by replacing placeholder with actual Python path
* This ensures scripts can be executed directly (not just via `uv run`)
* Note: Windows doesn't use shebangs - it uses .exe wrappers instead
* Get the actual Python interpreter path from venv's pyvenv.cfg (home points to Python's bin dir).
* Used to fix shebangs when venv is in userData but Python is in app bundle.
*/
function getActualPythonPathFromPyvenvCfg(venvPath: string): string | null {
const pyvenvCfgPath = path.join(venvPath, 'pyvenv.cfg');
if (!fs.existsSync(pyvenvCfgPath)) return null;
const content = fs.readFileSync(pyvenvCfgPath, 'utf-8');
const homeMatch = content.match(/^home\s*=\s*(.+)$/m);
if (!homeMatch) return null;
const home = homeMatch[1].trim();
if (!path.isAbsolute(home) || !fs.existsSync(home)) return null;
// home is Python's bin dir; find python3.X or python3
try {
const entries = fs.readdirSync(home);
const py = entries.find(
(e) => e === 'python3' || (e.startsWith('python3.') && !e.endsWith('.py'))
);
if (py) {
const fullPath = path.join(home, py);
if (fs.existsSync(fullPath)) return fullPath;
}
} catch {
// ignore
}
return null;
}
/**
* Fix shebang lines in venv scripts by replacing placeholder or broken relative path with actual Python path.
* The venv/bin/python script was previously skipped but must be fixed when venv is extracted to userData
* (relative paths like ../../uv_python/... break because Python lives in the app bundle).
*/
function fixVenvScriptShebangs(venvPath: string): boolean {
const isWindows = process.platform === 'win32';
// Windows doesn't use shebangs - skip this step
if (isWindows) {
log.info(`[VENV] Skipping shebang fixes on Windows (not needed)`);
return true;
}
const binDir = path.join(venvPath, 'bin');
if (!fs.existsSync(binDir)) {
return false;
}
if (!fs.existsSync(binDir)) return false;
const pythonExe = path.join(binDir, 'python');
if (!fs.existsSync(pythonExe)) {
log.warn(`[VENV] Python executable not found: ${pythonExe}`);
return false;
}
const actualPythonPath =
getActualPythonPathFromPyvenvCfg(venvPath) ?? findPythonForTerminalVenv();
try {
const entries = fs.readdirSync(binDir);
let fixedCount = 0;
@ -247,60 +276,59 @@ function fixVenvScriptShebangs(venvPath: string): boolean {
try {
const stat = fs.lstatSync(filePath);
if (stat.isDirectory() || stat.isSymbolicLink()) {
continue;
}
// Skip .exe files (binary), .dll, .pyd (compiled Python modules)
if (stat.isDirectory() || stat.isSymbolicLink()) continue;
if (
entry.endsWith('.exe') ||
entry.endsWith('.dll') ||
entry.endsWith('.pyd') ||
entry.startsWith('python') ||
entry.startsWith('activate')
entry.endsWith('.pyd')
) {
continue;
}
// Include python/activate scripts - they were previously skipped but need shebang fix
// when venv is in userData with relative paths
} catch {
continue;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const firstLine = content.split('\n')[0];
if (!firstLine?.startsWith('#!')) continue;
// Check if file contains any placeholders
const hasVenvPythonPlaceholder = content.includes(
'{{PREBUILT_VENV_PYTHON}}'
);
const hasPythonDirPlaceholder = content.includes(
'{{PREBUILT_PYTHON_DIR}}'
);
const shebangPath = firstLine.slice(2).trim();
let newContent = content;
if (hasVenvPythonPlaceholder || hasPythonDirPlaceholder) {
let newContent = content;
if (hasVenvPythonPlaceholder) {
// Replace placeholders
if (content.includes('{{PREBUILT_VENV_PYTHON}}')) {
newContent = newContent.replace(
/\{\{PREBUILT_VENV_PYTHON\}\}/g,
actualPythonPath ?? pythonExe
);
}
if (content.includes('{{PREBUILT_PYTHON_DIR}}')) {
const prebuiltPythonDir = getPrebuiltPythonDir();
if (prebuiltPythonDir) {
newContent = newContent.replace(
/\{\{PREBUILT_VENV_PYTHON\}\}/g,
pythonExe
/\{\{PREBUILT_PYTHON_DIR\}\}/g,
prebuiltPythonDir
);
}
if (hasPythonDirPlaceholder) {
const prebuiltPythonDir = getPrebuiltPythonDir();
if (prebuiltPythonDir) {
newContent = newContent.replace(
/\{\{PREBUILT_PYTHON_DIR\}\}/g,
prebuiltPythonDir
);
}
}
}
if (newContent !== content) {
fs.writeFileSync(filePath, newContent, 'utf-8');
if (process.platform !== 'win32') {
fs.chmodSync(filePath, 0o755);
}
fixedCount++;
if (actualPythonPath && shebangPath && !shebangPath.startsWith('{{')) {
const resolved = path.resolve(path.dirname(filePath), shebangPath);
if (!fs.existsSync(resolved)) {
newContent = newContent.replace(/^#!.*$/m, `#!${actualPythonPath}`);
}
}
if (newContent !== content) {
fs.writeFileSync(filePath, newContent, 'utf-8');
if (process.platform !== 'win32') {
fs.chmodSync(filePath, 0o755);
}
fixedCount++;
}
} catch {
// Silently skip files that can't be processed
}
@ -316,30 +344,110 @@ function fixVenvScriptShebangs(venvPath: string): boolean {
}
}
const PREBUILT_FIXED_MARKER = '.prebuilt_fixed';
/**
* Ensure venv/bin/python exists - create symlink if missing or broken.
*/
function ensureVenvPythonSymlink(venvPath: string): boolean {
if (process.platform === 'win32') return true;
const binDir = path.join(venvPath, 'bin');
const pythonPath = path.join(binDir, 'python');
if (!fs.existsSync(binDir)) return false;
try {
fs.accessSync(pythonPath, fs.constants.X_OK);
return true;
} catch {
// python missing or broken symlink - create/fix below
log.info(
`[VENV] python not found or broken at ${pythonPath}, creating symlink...`
);
}
const actualPython = getActualPythonPathFromPyvenvCfg(venvPath);
// Find python3.X in venv/bin as fallback (e.g. python3.10)
const entries = fs.readdirSync(binDir, { withFileTypes: true });
const py3 = entries.find(
(e) =>
!e.isDirectory() &&
(e.name === 'python3' ||
(e.name.startsWith('python3.') && !e.name.endsWith('.py')))
);
const targetInBin = py3 ? path.join(binDir, py3.name) : null;
try {
// Remove existing file/symlink (existsSync is false for broken symlinks, so use lstat)
try {
fs.lstatSync(pythonPath);
fs.unlinkSync(pythonPath);
} catch {
// ENOENT = path doesn't exist, that's fine
}
// Prefer actual Python from pyvenv.cfg (absolute path to app bundle);
// fallback to python3.X in same dir (relative symlink)
let target: string | null = null;
if (actualPython && fs.existsSync(actualPython)) {
target = actualPython;
} else if (targetInBin && fs.existsSync(targetInBin)) {
// Use relative name for symlink within same directory
target = py3!.name;
}
if (!target) {
log.warn(`[VENV] No valid Python target found for symlink`);
return false;
}
fs.symlinkSync(target, pythonPath);
try {
fs.chmodSync(pythonPath, 0o755);
} catch {}
log.info(`[VENV] Created python symlink -> ${target}`);
return true;
} catch (error) {
log.warn(`[VENV] Failed to create python symlink: ${error}`);
return false;
}
}
/**
* Get path to prebuilt venv (if available in packaged app)
* All platforms use prebuilt/venv directory.
*/
export function getPrebuiltVenvPath(): string | null {
if (!app.isPackaged) {
return null;
}
const prebuiltVenvPath = path.join(process.resourcesPath, 'prebuilt', 'venv');
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltVenvPath = path.join(prebuiltDir, 'venv');
const pyvenvCfgPath = path.join(prebuiltVenvPath, 'pyvenv.cfg');
log.info(`[VENV] Checking prebuilt venv at: ${prebuiltVenvPath}`);
const fixedMarkerPath = path.join(prebuiltDir, PREBUILT_FIXED_MARKER);
const currentVersion = app.getVersion();
if (fs.existsSync(prebuiltVenvPath) && fs.existsSync(pyvenvCfgPath)) {
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
fixVenvScriptShebangs(prebuiltVenvPath);
const needsFix =
!fs.existsSync(fixedMarkerPath) ||
fs.readFileSync(fixedMarkerPath, 'utf-8').trim() !== currentVersion;
if (needsFix) {
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
ensureVenvPythonSymlink(prebuiltVenvPath);
fixVenvScriptShebangs(prebuiltVenvPath);
fs.writeFileSync(fixedMarkerPath, currentVersion, 'utf-8');
}
const pythonExePath = getVenvPythonPath(prebuiltVenvPath);
if (fs.existsSync(pythonExePath)) {
log.info(`[VENV] Using prebuilt venv: ${prebuiltVenvPath}`);
return prebuiltVenvPath;
}
log.warn(`[VENV] Prebuilt venv Python missing at: ${pythonExePath}`);
}
return null;
}
@ -395,6 +503,236 @@ function findPythonForTerminalVenv(): string | null {
return null;
}
const TERMINAL_VENV_VERSION_FILE = '.terminal_venv_version';
const BACKEND_VENV_VERSION_FILE = '.backend_venv_version';
/**
* Copy prebuilt backend venv to ~/.eigent/venvs/backend-{version} for unified management.
* The copied venv is the one actually used by the backend (via getVenvPath()).
* The source venv (prebuilt/extracted) is kept as-is for re-copying on version changes.
*
* @param version App version (used for version-specific venv directory)
*/
export function ensureBackendVenvAtUserPath(version: string): void {
if (!app.isPackaged) return;
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltVenvPath = path.join(prebuiltDir, 'venv');
const prebuiltUvPython = path.join(prebuiltDir, 'uv_python');
if (
!fs.existsSync(prebuiltVenvPath) ||
!fs.existsSync(path.join(prebuiltVenvPath, 'pyvenv.cfg'))
) {
return;
}
const sourceVenvPath = prebuiltVenvPath;
const userVenvsDir = path.join(os.homedir(), '.eigent', 'venvs');
const userBackendVenv = path.join(userVenvsDir, `backend-${version}`);
const pyvenvCfgPath = path.join(userBackendVenv, 'pyvenv.cfg');
const versionFile = path.join(userVenvsDir, BACKEND_VENV_VERSION_FILE);
// Ensure uv_python symlink exists (needed even if venv already copied)
const userUvPython = path.join(os.homedir(), '.eigent', 'uv_python');
if (!fs.existsSync(userUvPython) && fs.existsSync(prebuiltUvPython)) {
try {
fs.mkdirSync(path.dirname(userUvPython), { recursive: true });
fs.symlinkSync(prebuiltUvPython, userUvPython);
log.info(`[VENV] Created uv_python symlink: ${userUvPython}`);
} catch (e) {
log.warn(`[VENV] Failed to create uv_python symlink: ${e}`);
}
}
if (fs.existsSync(pyvenvCfgPath)) {
const storedVersion = fs.existsSync(versionFile)
? fs.readFileSync(versionFile, 'utf-8').trim()
: null;
if (storedVersion === version) {
log.info(
`[VENV] Backend venv already at ${userBackendVenv} (v${version})`
);
return;
}
}
log.info(`[VENV] Copying prebuilt backend venv to ${userBackendVenv}...`);
try {
fs.mkdirSync(userVenvsDir, { recursive: true });
if (fs.existsSync(userBackendVenv)) {
fs.rmSync(userBackendVenv, { recursive: true, force: true });
}
fs.cpSync(sourceVenvPath, userBackendVenv, {
recursive: true,
verbatimSymlinks: true,
});
// Fix paths after copying (source venv paths don't match user venv location)
// - pyvenv.cfg: update home path to point to correct Python location
// - shebangs: update #! paths in bin/* scripts to point to correct Python
// - python symlink: ensure bin/python exists and points to correct Python
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
fixVenvScriptShebangs(userBackendVenv);
ensureVenvPythonSymlink(userBackendVenv);
if (process.platform === 'darwin') {
try {
execSync(`xattr -cr "${userBackendVenv}"`, { stdio: 'ignore' });
} catch {
// ignore
}
}
fs.writeFileSync(versionFile, version, 'utf-8');
log.info(`[VENV] Backend venv copied successfully`);
// Sync optional deps from backend/uv.lock into user venv (e.g. yt_dlp if excluded from app bundle).
// Runs in background so app startup is not blocked; uses China mirror when timezone is Asia/Shanghai.
const uvPath = getPrebuiltBinaryPath('uv');
const backendPath = getBackendPath();
const uvLockPath = path.join(backendPath, 'uv.lock');
if (
uvPath &&
fs.existsSync(uvLockPath) &&
fs.existsSync(path.join(backendPath, 'pyproject.toml'))
) {
const prebuiltPython = getPrebuiltPythonDir();
const uvEnv = {
...process.env,
UV_PROJECT_ENVIRONMENT: userBackendVenv,
UV_PYTHON_INSTALL_DIR: prebuiltPython || getCachePath('uv_python'),
UV_TOOL_DIR: getCachePath('uv_tool'),
UV_HTTP_TIMEOUT: '300',
} as NodeJS.ProcessEnv;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const syncArgs =
timezone === 'Asia/Shanghai'
? [
'sync',
'--no-dev',
'--default-index',
'https://mirrors.aliyun.com/pypi/simple/',
'--index',
'https://pypi.org/simple/',
]
: ['sync', '--no-dev'];
log.info(
'[VENV] Starting background uv sync to install optional deps (e.g. yt_dlp); app will not wait.'
);
const child = spawn(uvPath, syncArgs, {
cwd: backendPath,
env: uvEnv,
stdio: 'ignore',
detached: true,
});
child.unref();
child.on('error', (err) => {
log.warn(`[VENV] Background uv sync error: ${err.message}`);
});
child.on('exit', (code) => {
if (code === 0) {
log.info('[VENV] Background uv sync completed');
} else {
log.warn(
`[VENV] Background uv sync exited with code ${code} (optional deps may be missing)`
);
}
});
}
} catch (error) {
log.error(`[VENV] Failed to copy backend venv: ${error}`);
}
}
/**
* Copy prebuilt terminal venv to ~/.eigent/venvs/terminal_base-{version}.
* @param version App version (used for version-specific venv directory)
*/
export function ensureTerminalVenvAtUserPath(version: string): void {
if (!app.isPackaged) return;
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltTerminalVenv = path.join(prebuiltDir, 'terminal_venv');
const prebuiltUvPython = path.join(prebuiltDir, 'uv_python');
if (!fs.existsSync(prebuiltTerminalVenv)) return;
const installedMarker = path.join(
prebuiltTerminalVenv,
'.packages_installed'
);
if (!fs.existsSync(installedMarker)) return;
const userVenvsDir = path.join(os.homedir(), '.eigent', 'venvs');
const userTerminalVenv = path.join(userVenvsDir, `terminal_base-${version}`);
const userVenvMarker = path.join(userTerminalVenv, '.packages_installed');
const versionFile = path.join(userVenvsDir, TERMINAL_VENV_VERSION_FILE);
// Ensure uv_python symlink exists (needed even if venv already copied)
const userUvPython = path.join(os.homedir(), '.eigent', 'uv_python');
if (!fs.existsSync(userUvPython) && fs.existsSync(prebuiltUvPython)) {
try {
fs.mkdirSync(path.dirname(userUvPython), { recursive: true });
fs.symlinkSync(prebuiltUvPython, userUvPython);
log.info(`[VENV] Created uv_python symlink: ${userUvPython}`);
} catch (e) {
log.warn(`[VENV] Failed to create uv_python symlink: ${e}`);
}
}
if (fs.existsSync(userVenvMarker)) {
const storedVersion = fs.existsSync(versionFile)
? fs.readFileSync(versionFile, 'utf-8').trim()
: null;
if (storedVersion === version) {
log.info(
`[VENV] Terminal venv already at ${userTerminalVenv} (v${version})`
);
return;
}
}
log.info(`[VENV] Copying prebuilt terminal venv to ${userTerminalVenv}...`);
try {
fs.mkdirSync(userVenvsDir, { recursive: true });
if (fs.existsSync(userTerminalVenv)) {
fs.rmSync(userTerminalVenv, { recursive: true, force: true });
}
fs.cpSync(prebuiltTerminalVenv, userTerminalVenv, {
recursive: true,
verbatimSymlinks: true,
});
// Fix paths after copying (source venv paths don't match user venv location)
// - pyvenv.cfg: update home path to point to correct Python location
// - shebangs: update #! paths in bin/* scripts to point to correct Python
// - python symlink: ensure bin/python exists and points to correct Python
fixPyvenvCfgPlaceholder(path.join(userTerminalVenv, 'pyvenv.cfg'));
fixVenvScriptShebangs(userTerminalVenv);
ensureVenvPythonSymlink(userTerminalVenv);
if (process.platform === 'darwin') {
try {
execSync(`xattr -cr "${userTerminalVenv}"`, { stdio: 'ignore' });
} catch {
// ignore
}
}
fs.writeFileSync(versionFile, version, 'utf-8');
log.info(`[VENV] Terminal venv copied successfully`);
} catch (error) {
log.error(`[VENV] Failed to copy terminal venv: ${error}`);
}
}
/**
* Get path to prebuilt terminal venv (if available in packaged app)
*/
@ -408,59 +746,74 @@ export function getPrebuiltTerminalVenvPath(): string | null {
'prebuilt',
'terminal_venv'
);
if (fs.existsSync(prebuiltTerminalVenvPath)) {
const pyvenvCfgPath = path.join(prebuiltTerminalVenvPath, 'pyvenv.cfg');
const installedMarker = path.join(
prebuiltTerminalVenvPath,
'.packages_installed'
);
if (fs.existsSync(pyvenvCfgPath) && fs.existsSync(installedMarker)) {
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
fixVenvScriptShebangs(prebuiltTerminalVenvPath);
if (!fs.existsSync(prebuiltTerminalVenvPath)) {
return null;
}
const pythonExePath = getVenvPythonPath(prebuiltTerminalVenvPath);
const pyvenvCfgPath = path.join(prebuiltTerminalVenvPath, 'pyvenv.cfg');
const installedMarker = path.join(
prebuiltTerminalVenvPath,
'.packages_installed'
);
if (!fs.existsSync(pyvenvCfgPath) || !fs.existsSync(installedMarker)) {
return null;
}
// Check if already fixed for this version (avoid repeated fixes)
const fixedMarkerPath = path.join(
process.resourcesPath,
'prebuilt',
'.terminal_venv_fixed'
);
const currentVersion = app.getVersion();
const needsFix =
!fs.existsSync(fixedMarkerPath) ||
fs.readFileSync(fixedMarkerPath, 'utf-8').trim() !== currentVersion;
if (needsFix) {
fixPyvenvCfgPlaceholder(pyvenvCfgPath);
ensureVenvPythonSymlink(prebuiltTerminalVenvPath);
fixVenvScriptShebangs(prebuiltTerminalVenvPath);
fs.writeFileSync(fixedMarkerPath, currentVersion, 'utf-8');
}
const pythonExePath = getVenvPythonPath(prebuiltTerminalVenvPath);
if (fs.existsSync(pythonExePath)) {
return prebuiltTerminalVenvPath;
}
// Try to fix the missing Python executable by creating a symlink to prebuilt Python
const prebuiltPython = findPythonForTerminalVenv();
if (prebuiltPython && fs.existsSync(prebuiltPython)) {
try {
const binDir = path.join(
prebuiltTerminalVenvPath,
process.platform === 'win32' ? 'Scripts' : 'bin'
);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
if (fs.existsSync(pythonExePath)) {
log.info(
`[VENV] Using prebuilt terminal venv: ${prebuiltTerminalVenvPath}`
);
return prebuiltTerminalVenvPath;
fs.unlinkSync(pythonExePath);
}
// Try to fix the missing Python executable by creating a symlink to prebuilt Python
const prebuiltPython = findPythonForTerminalVenv();
if (prebuiltPython && fs.existsSync(prebuiltPython)) {
try {
const binDir = path.join(
prebuiltTerminalVenvPath,
process.platform === 'win32' ? 'Scripts' : 'bin'
);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
if (fs.existsSync(pythonExePath)) {
fs.unlinkSync(pythonExePath);
}
const relativePath = path.relative(binDir, prebuiltPython);
fs.symlinkSync(relativePath, pythonExePath);
log.info(
`[VENV] Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}`
);
return prebuiltTerminalVenvPath;
} catch (error) {
log.warn(
`[VENV] Failed to fix terminal venv Python symlink: ${error}`
);
}
}
log.warn(
`[VENV] Prebuilt terminal venv Python missing, falling back to user venv`
const relativePath = path.relative(binDir, prebuiltPython);
fs.symlinkSync(relativePath, pythonExePath);
log.info(
`[VENV] Fixed terminal venv Python symlink: ${pythonExePath} -> ${prebuiltPython}`
);
return prebuiltTerminalVenvPath;
} catch (error) {
log.warn(`[VENV] Failed to fix terminal venv Python symlink: ${error}`);
}
}
log.warn(
`[VENV] Prebuilt terminal venv Python missing, falling back to user venv`
);
return null;
}
@ -475,9 +828,72 @@ export function getVenvPythonPath(venvPath: string): string {
: path.join(venvPath, 'bin', 'python');
}
/**
* Check venv existence for pre-check WITHOUT triggering extraction.
* Used to avoid blocking app launch - extraction is deferred to startBackend when window is already visible.
*/
export function checkVenvExistsForPreCheck(version: string): {
exists: boolean;
path: string;
} {
if (!app.isPackaged) {
const venvDir = path.join(
os.homedir(),
'.eigent',
'venvs',
`backend-${version}`
);
const pyvenvCfg = path.join(venvDir, 'pyvenv.cfg');
return {
exists: fs.existsSync(pyvenvCfg),
path: venvDir,
};
}
const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
const prebuiltVenvPath = path.join(prebuiltDir, 'venv');
const prebuiltPyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg');
if (fs.existsSync(prebuiltVenvPath) && fs.existsSync(prebuiltPyvenvCfg)) {
return { exists: true, path: prebuiltVenvPath };
}
const venvDir = path.join(
os.homedir(),
'.eigent',
'venvs',
`backend-${version}`
);
const pyvenvCfg = path.join(venvDir, 'pyvenv.cfg');
return {
exists: fs.existsSync(pyvenvCfg),
path: venvDir,
};
}
/**
* Get path to backend venv for the given version.
* @param version App version
* @returns Path to backend venv
*/
export function getVenvPath(version: string): string {
// First check for prebuilt venv in packaged app
// For packaged apps, ensure venv is copied to ~/.eigent/venvs first
if (app.isPackaged) {
ensureBackendVenvAtUserPath(version);
// Check if user venv exists (after ensuring copy)
const userVenvDir = path.join(
os.homedir(),
'.eigent',
'venvs',
`backend-${version}`
);
const pyvenvCfgPath = path.join(userVenvDir, 'pyvenv.cfg');
if (fs.existsSync(pyvenvCfgPath)) {
return userVenvDir;
}
// Fallback to prebuilt venv if copy failed (shouldn't happen normally)
const prebuiltVenv = getPrebuiltVenvPath();
if (prebuiltVenv) {
return prebuiltVenv;
@ -500,6 +916,138 @@ export function getVenvPath(version: string): string {
return venvDir;
}
/**
* Create npm/npx wrapper scripts that use nodejs_wheel Python API.
* The bin/npm from nodejs_wheel can fail with "Cannot find module '../lib/cli.js'"
* when invoked directly. Using the Python API avoids this.
*/
export function ensureNpmWrappersForBrowserToolkit(
venvPath: string
): string | null {
const pythonPath = getVenvPythonPath(venvPath);
if (!fs.existsSync(pythonPath)) return null;
const eigentBinDir = path.join(os.homedir(), '.eigent', 'bin');
fs.mkdirSync(eigentBinDir, { recursive: true });
const wrapperVersion = '1';
const versionFile = path.join(eigentBinDir, '.npm_wrapper_version');
const storedVersion = fs.existsSync(versionFile)
? fs.readFileSync(versionFile, 'utf-8').trim()
: '';
const npmWrapper = path.join(
eigentBinDir,
process.platform === 'win32' ? 'npm.cmd' : 'npm'
);
const npxWrapper = path.join(
eigentBinDir,
process.platform === 'win32' ? 'npx.cmd' : 'npx'
);
const needsUpdate =
storedVersion !== wrapperVersion ||
!fs.existsSync(npmWrapper) ||
!fs.existsSync(npxWrapper);
if (needsUpdate) {
try {
if (process.platform === 'win32') {
const npmContent = `@echo off
"${pythonPath.replace(/\//g, '\\')}" -c "import sys; from nodejs_wheel import npm; sys.exit(npm(sys.argv[1:]))" %*
`;
const npxContent = `@echo off
"${pythonPath.replace(/\//g, '\\')}" -c "import sys; from nodejs_wheel import npx; sys.exit(npx(sys.argv[1:]))" %*
`;
fs.writeFileSync(npmWrapper, npmContent, 'utf-8');
fs.writeFileSync(npxWrapper, npxContent, 'utf-8');
} else {
const shebang = `#!${pythonPath}\n`;
const npmContent =
shebang +
`import sys
from nodejs_wheel import npm
sys.exit(npm(sys.argv[1:]))
`;
const npxContent =
shebang +
`import sys
from nodejs_wheel import npx
sys.exit(npx(sys.argv[1:]))
`;
fs.writeFileSync(npmWrapper, npmContent, 'utf-8');
fs.writeFileSync(npxWrapper, npxContent, 'utf-8');
fs.chmodSync(npmWrapper, 0o755);
fs.chmodSync(npxWrapper, 0o755);
}
fs.writeFileSync(versionFile, wrapperVersion, 'utf-8');
log.info(`[VENV] Created npm/npx wrappers at ${eigentBinDir}`);
} catch (error) {
log.warn(`[VENV] Failed to create npm wrappers: ${error}`);
return null;
}
}
return eigentBinDir;
}
/**
* Find nodejs-wheel npm path in venv for browser toolkit.
* Prefer Python API wrappers over direct bin (which can fail with cli.js error).
*/
export function findNodejsWheelNpmPath(venvPath: string): string | null {
// Prefer wrapper scripts that use Python API (avoids bin/npm "../lib/cli.js" error)
const wrapperDir = ensureNpmWrappersForBrowserToolkit(venvPath);
if (wrapperDir) {
const npmWrapper = path.join(
wrapperDir,
process.platform === 'win32' ? 'npm.cmd' : 'npm'
);
const npxWrapper = path.join(
wrapperDir,
process.platform === 'win32' ? 'npx.cmd' : 'npx'
);
if (fs.existsSync(npmWrapper) && fs.existsSync(npxWrapper)) {
return wrapperDir;
}
}
// Fallback to nodejs_wheel/bin (may fail with cli.js error)
return findNodejsWheelBinPath(venvPath);
}
/**
* Find nodejs_wheel/bin directory for the node executable.
* Browser toolkit needs node in PATH (npm/npx use our wrappers from ~/.eigent/bin).
*/
export function findNodejsWheelBinPath(venvPath: string): string | null {
try {
const libPath = path.join(venvPath, 'lib');
if (!fs.existsSync(libPath)) return null;
const pythonDirs = fs
.readdirSync(libPath)
.filter((n) => n.startsWith('python'));
if (pythonDirs.length === 0) return null;
for (const pythonDir of pythonDirs) {
const sitePackages = path.join(libPath, pythonDir, 'site-packages');
const nodejsWheelBin = path.join(sitePackages, 'nodejs_wheel', 'bin');
const nodePath = path.join(
nodejsWheelBin,
process.platform === 'win32' ? 'node.exe' : 'node'
);
if (fs.existsSync(nodePath)) {
return nodejsWheelBin;
}
}
} catch {
// ignore
}
return null;
}
export function getVenvsBaseDir(): string {
return path.join(os.homedir(), '.eigent', 'venvs');
}
@ -517,6 +1065,7 @@ export const TERMINAL_BASE_PACKAGES = [
'openpyxl',
'beautifulsoup4',
'pillow',
'plotly',
];
/**

View file

@ -4,9 +4,50 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Content Security Policy: CDN allowlist for agent-generated HTML -->
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.amplitude.com; worker-src 'self' blob:; child-src 'self' blob:;frame-src 'self' localfile: blob: data:;"
content="
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.amplitude.com
https://cdnjs.cloudflare.com
https://cdn.jsdelivr.net
https://unpkg.com
https://ajax.googleapis.com
https://code.jquery.com
https://stackpath.bootstrapcdn.com
https://cdn.tailwindcss.com
https://cdn.plot.ly
https://d3js.org
https://cdn.datatables.net
https://cdn.chart.js
https://cdn.canvasjs.com
https://cdn.amcharts.com
https://threejs.org
https://pixijs.download
https://cdn.babylonjs.com
https://aframe.io
https://cesium.com
https://cdn.lottiefiles.com
https://cdn.socket.io
https://cdn.firebase.com
https://maps.googleapis.com
https://api.mapbox.com
https://cdn.tiny.cloud
https://cdn.ckeditor.com
https://cdn.quilljs.com
https://cdn.mathjax.org
https://cdn.ethers.io
https://cdn.auth0.com
https://cdn.plyr.io
https://vjs.zencdn.net
https://cdn.dashjs.org
https://cdn.npmmirror.com
https://registry.npmmirror.com;
worker-src 'self' blob:;
child-src 'self' blob:;
frame-src 'self' localfile: blob: data:;
"
/>
<script src="https://cdn.amplitude.com/libs/analytics-browser-2.11.1-min.js.gz"></script><script src="https://cdn.amplitude.com/libs/plugin-session-replay-browser-1.8.0-min.js.gz"></script><script>window.amplitude.add(window.sessionReplay.plugin({sampleRate: 1}));window.amplitude.init('87ce6adbb14b24ffe1703d18bf405e40', {"autocapture":{"elementInteractions":true}});</script>
<title>Eigent</title>

View file

@ -1,6 +1,6 @@
{
"name": "eigent",
"version": "0.0.82",
"version": "0.0.84",
"main": "dist-electron/main/index.js",
"description": "Eigent",
"author": "Eigent.AI",

View file

@ -48,6 +48,7 @@ const TERMINAL_BASE_PACKAGES = [
'openpyxl',
'beautifulsoup4',
'pillow',
'plotly',
];
console.log('🚀 Starting pre-installation of dependencies...');

View file

@ -26,6 +26,7 @@ class ModelType(StrEnum):
gpt4_1 = "gpt-4.1"
gpt4_mini = "gpt-4.1-mini"
gemini_3_pro = "gemini-3-pro-preview"
minimax_m2_5 = "minimax_m2_5"
class KeyStatus(IntEnum):

View file

@ -7,7 +7,7 @@ requires-python = ">=3.12,<3.13"
dependencies = [
"alembic>=1.15.2",
"openai>=1.99.3,<2",
"camel-ai==0.2.85a0",
"camel-ai==0.2.90a1",
"pydantic[email]>=2.11.1",
"click>=8.1.8",
"fastapi>=0.115.12",

19
server/uv.lock generated
View file

@ -206,6 +206,7 @@ dependencies = [
{ name = "pillow" },
{ name = "psutil" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "tiktoken" },
{ name = "websockets" },
]
@ -1229,6 +1230,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159 },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
]
[[package]]
name = "referencing"
version = "0.37.0"

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 369 B

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Azure</title><path d="M7.242 1.613A1.11 1.11 0 018.295.857h6.977L8.03 22.316a1.11 1.11 0 01-1.052.755h-5.43a1.11 1.11 0 01-1.053-1.466L7.242 1.613z" fill="url(#lobe-icons-azure-fill-0)"></path><path d="M18.397 15.296H7.4a.51.51 0 00-.347.882l7.066 6.595c.206.192.477.298.758.298h6.226l-2.706-7.775z" fill="#0078D4"></path><path d="M15.272.857H7.497L0 23.071h7.775l1.596-4.73 5.068 4.73h6.665l-2.707-7.775h-7.998L15.272.857z" fill="url(#lobe-icons-azure-fill-1)"></path><path d="M17.193 1.613a1.11 1.11 0 00-1.052-.756h-7.81.035c.477 0 .9.304 1.052.756l6.748 19.992a1.11 1.11 0 01-1.052 1.466h-.12 7.895a1.11 1.11 0 001.052-1.466L17.193 1.613z" fill="url(#lobe-icons-azure-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-0" x1="8.247" x2="1.002" y1="1.626" y2="23.03"><stop stop-color="#114A8B"></stop><stop offset="1" stop-color="#0669BC"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-1" x1="14.042" x2="12.324" y1="15.302" y2="15.888"><stop stop-opacity=".3"></stop><stop offset=".071" stop-opacity=".2"></stop><stop offset=".321" stop-opacity=".1"></stop><stop offset=".623" stop-opacity=".05"></stop><stop offset="1" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-2" x1="12.841" x2="20.793" y1="1.626" y2="22.814"><stop stop-color="#3CCBF4"></stop><stop offset="1" stop-color="#2892DF"></stop></linearGradient></defs></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Azure</title><path d="M7.242 1.613A1.11 1.11 0 018.295.857h6.977L8.03 22.316a1.11 1.11 0 01-1.052.755h-5.43a1.11 1.11 0 01-1.053-1.466L7.242 1.613z" fill="url(#lobe-icons-azure-fill-0)"></path><path d="M18.397 15.296H7.4a.51.51 0 00-.347.882l7.066 6.595c.206.192.477.298.758.298h6.226l-2.706-7.775z" fill="#0078D4"></path><path d="M15.272.857H7.497L0 23.071h7.775l1.596-4.73 5.068 4.73h6.665l-2.707-7.775h-7.998L15.272.857z" fill="url(#lobe-icons-azure-fill-1)"></path><path d="M17.193 1.613a1.11 1.11 0 00-1.052-.756h-7.81.035c.477 0 .9.304 1.052.756l6.748 19.992a1.11 1.11 0 01-1.052 1.466h-.12 7.895a1.11 1.11 0 001.052-1.466L17.193 1.613z" fill="url(#lobe-icons-azure-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-0" x1="8.247" x2="1.002" y1="1.626" y2="23.03"><stop stop-color="#114A8B"></stop><stop offset="1" stop-color="#0669BC"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-1" x1="14.042" x2="12.324" y1="15.302" y2="15.888"><stop stop-opacity=".3"></stop><stop offset=".071" stop-opacity=".2"></stop><stop offset=".321" stop-opacity=".1"></stop><stop offset=".623" stop-opacity=".05"></stop><stop offset="1" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-2" x1="12.841" x2="20.793" y1="1.626" y2="22.814"><stop stop-color="#3CCBF4"></stop><stop offset="1" stop-color="#2892DF"></stop></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><defs><linearGradient id="lobe-icons-bedrock-fill" x1="80%" x2="20%" y1="20%" y2="80%"><stop offset="0%" stop-color="#6350FB"></stop><stop offset="50%" stop-color="#3D8FFF"></stop><stop offset="100%" stop-color="#9AD8F8"></stop></linearGradient></defs><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z" fill="url(#lobe-icons-bedrock-fill)" fill-rule="nonzero"></path></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><defs><linearGradient id="lobe-icons-bedrock-fill" x1="80%" x2="20%" y1="20%" y2="80%"><stop offset="0%" stop-color="#6350FB"></stop><stop offset="50%" stop-color="#3D8FFF"></stop><stop offset="100%" stop-color="#9AD8F8"></stop></linearGradient></defs><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z" fill="url(#lobe-icons-bedrock-fill)" fill-rule="nonzero"></path></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-0)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-1)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-0)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-1)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LM Studio</title><path d="M2.84 2a1.273 1.273 0 100 2.547h14.107a1.273 1.273 0 100-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H22.04a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h14.106a1.274 1.274 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H15.38a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h14.106a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h9.698a1.273 1.273 0 100-2.547h-9.698z" fill-opacity=".3"></path><path d="M2.84 2a1.273 1.273 0 100 2.547h10.287a1.274 1.274 0 000-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H18.22a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H11.56a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h5.78a1.273 1.273 0 100-2.547h-5.78z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LM Studio</title><path d="M2.84 2a1.273 1.273 0 100 2.547h14.107a1.273 1.273 0 100-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H22.04a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h14.106a1.274 1.274 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H15.38a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h14.106a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h9.698a1.273 1.273 0 100-2.547h-9.698z" fill-opacity=".3"></path><path d="M2.84 2a1.273 1.273 0 100 2.547h10.287a1.274 1.274 0 000-2.547H2.84zM7.935 5.33a1.273 1.273 0 000 2.548H18.22a1.274 1.274 0 000-2.547H7.935zM3.624 9.935c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 010 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM1.273 12.188a1.273 1.273 0 100 2.547H11.56a1.274 1.274 0 000-2.547H1.273zM3.624 16.792c0-.704.57-1.274 1.274-1.274h10.286a1.273 1.273 0 110 2.547H4.898c-.703 0-1.274-.57-1.274-1.273zM13.029 18.849a1.273 1.273 0 100 2.547h5.78a1.273 1.273 0 100-2.547h-5.78z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><defs><linearGradient id="lobe-icons-minimax-fill" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"></stop><stop offset="100%" stop-color="#FE603C"></stop></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-fill)" fill-rule="nonzero"></path></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><defs><linearGradient id="lobe-icons-minimax-fill" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"></stop><stop offset="100%" stop-color="#FE603C"></stop></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-fill)" fill-rule="nonzero"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Ollama</title><path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Ollama</title><path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path></svg>

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 907 B

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z" fill="url(#lobe-icons-qwen-fill)" fill-rule="nonzero"></path><defs><linearGradient id="lobe-icons-qwen-fill" x1="0%" x2="100%" y1="0%" y2="0%"><stop offset="0%" stop-color="#6336E7" stop-opacity=".84"></stop><stop offset="100%" stop-color="#6F69F7" stop-opacity=".84"></stop></linearGradient></defs></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z" fill="url(#lobe-icons-qwen-fill)" fill-rule="nonzero"></path><defs><linearGradient id="lobe-icons-qwen-fill" x1="0%" x2="100%" y1="0%" y2="0%"><stop offset="0%" stop-color="#6336E7" stop-opacity=".84"></stop><stop offset="100%" stop-color="#6F69F7" stop-opacity=".84"></stop></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>vLLM</title><path d="M0 4.973h9.324V23L0 4.973z" fill="#FDB515"></path><path d="M13.986 4.351L22.378 0l-6.216 23H9.324l4.662-18.649z" fill="#30A2FF"></path></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>vLLM</title><path d="M0 4.973h9.324V23L0 4.973z" fill="#FDB515"></path><path d="M13.986 4.351L22.378 0l-6.216 23H9.324l4.662-18.649z" fill="#30A2FF"></path></svg>

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 287 B

Before After
Before After

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Z.ai</title><path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Z.ai</title><path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path></svg>

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 320 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Before After
Before After

View file

@ -30,13 +30,78 @@ import FolderComponent from './FolderComponent';
import { proxyFetchGet } from '@/api/http';
import { MarkDown } from '@/components/ChatBox/MessageItem/MarkDown';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { injectFontStyles } from '@/lib/htmlFontStyles';
import {
deferInlineScriptsUntilLoad,
injectFontStyles,
} from '@/lib/htmlFontStyles';
import { containsDangerousContent } from '@/lib/htmlSanitization';
import { useAuthStore } from '@/store/authStore';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { ZoomControls } from './ZoomControls';
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'];
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'];
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv'];
type FileTypeTarget = {
name?: string;
path?: string;
type?: string;
};
const loggedFileTypeWarnings = new Set<string>();
function getExt(value?: string) {
if (!value) return '';
const normalized = value.split(/[?#]/)[0];
const lastSegment = normalized.split('/').pop() || normalized;
if (!lastSegment.includes('.')) return '';
return lastSegment.split('.').pop()?.toLowerCase() || '';
}
function getFileType(file: FileTypeTarget) {
const extFromNameOrPath = getExt(file.name) || getExt(file.path);
const normalizedType = (file.type || '').replace(/^\./, '').toLowerCase();
const fileId = file.path || file.name || 'unknown-file';
if (!extFromNameOrPath && normalizedType) {
const key = `missing-ext|${fileId}|${normalizedType}`;
if (!loggedFileTypeWarnings.has(key)) {
loggedFileTypeWarnings.add(key);
console.warn(
`[Folder getFileType] extension missing in name/path, file.type fallback disabled: ${fileId} (type=${normalizedType})`
);
}
}
if (
extFromNameOrPath &&
normalizedType &&
normalizedType !== 'folder' &&
extFromNameOrPath !== normalizedType
) {
const key = `mismatch|${fileId}|${extFromNameOrPath}|${normalizedType}`;
if (!loggedFileTypeWarnings.has(key)) {
loggedFileTypeWarnings.add(key);
console.warn(
`[Folder getFileType] extension/type mismatch for ${fileId}: inferred=${extFromNameOrPath}, type=${normalizedType}`
);
}
}
return extFromNameOrPath;
}
function isImageFile(file: FileTypeTarget) {
return IMAGE_EXTENSIONS.includes(getFileType(file));
}
function isAudioFile(file: FileTypeTarget) {
return AUDIO_EXTENSIONS.includes(getFileType(file));
}
function isVideoFile(file: FileTypeTarget) {
return VIDEO_EXTENSIONS.includes(getFileType(file));
}
// Type definitions
interface FileTreeNode {
name: string;
@ -70,7 +135,7 @@ interface FileTreeProps {
isShowSourceCode: boolean;
}
const FileTree: React.FC<FileTreeProps> = ({
export const FileTree: React.FC<FileTreeProps> = ({
node,
level = 0,
selectedFile,
@ -104,29 +169,33 @@ const FileTree: React.FC<FileTreeProps> = ({
onSelectFile(fileInfo);
}
}}
className={`text-primary flex w-full items-center justify-start rounded-xl bg-fill-fill-transparent p-2 text-left text-sm backdrop-blur-lg transition-colors hover:bg-fill-fill-transparent-active ${
className={`text-primary flex w-full items-center justify-start gap-2 rounded-xl bg-fill-fill-transparent p-2 text-left text-sm backdrop-blur-lg transition-colors hover:bg-fill-fill-transparent-active ${
selectedFile?.path === child.path
? 'bg-fill-fill-transparent-active'
: ''
}`}
>
{child.isFolder && (
<span className="flex h-4 w-4 items-center justify-center">
{child.isFolder ? (
<span className="flex h-4 w-4 flex-shrink-0 items-center justify-center">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</span>
) : (
<span
className="flex h-4 w-4 flex-shrink-0 items-center justify-center"
aria-hidden
/>
)}
{!child.isFolder && <span className="w-4" />}
{child.isFolder ? (
<FolderIcon className="mr-2 h-5 w-5 flex-shrink-0 text-yellow-600" />
<FolderIcon className="h-5 w-5 flex-shrink-0 text-yellow-600" />
) : child.icon ? (
<child.icon className="mr-2 h-5 w-5 flex-shrink-0" />
<child.icon className="h-5 w-5 flex-shrink-0" />
) : (
<FileText className="mr-2 h-5 w-5 flex-shrink-0" />
<FileText className="h-5 w-5 flex-shrink-0" />
)}
<span
@ -236,6 +305,14 @@ export default function Folder({ data: _data }: { data?: Agent }) {
return;
}
// For audio/video files, skip open-file — loaders handle reading themselves
if (isAudioFile(file) || isVideoFile(file)) {
setSelectedFile({ ...file });
chatStore.setSelectedFile(chatStore.activeTaskId as string, file);
setLoading(false);
return;
}
// all other files call open-file interface, the backend handles download and parsing
window.ipcRenderer
.invoke('open-file', file.type, file.path, isShowSourceCode)
@ -641,15 +718,15 @@ export default function Folder({ data: _data }: { data?: Agent }) {
</p>
</div>
</div>
) : [
'png',
'jpg',
'jpeg',
'gif',
'bmp',
'webp',
'svg',
].includes(selectedFile.type.toLowerCase()) ? (
) : isAudioFile(selectedFile) ? (
<div className="flex h-full items-center justify-center">
<AudioLoader selectedFile={selectedFile} />
</div>
) : isVideoFile(selectedFile) ? (
<div className="flex h-full items-center justify-center">
<VideoLoader selectedFile={selectedFile} />
</div>
) : isImageFile(selectedFile) ? (
<div className="flex h-full items-center justify-center">
<ImageLoader selectedFile={selectedFile} />
</div>
@ -708,6 +785,75 @@ function ImageLoader({ selectedFile }: { selectedFile: FileInfo }) {
);
}
function AudioLoader({ selectedFile }: { selectedFile: FileInfo }) {
const [src, setSrc] = useState('');
useEffect(() => {
let cancelled = false;
setSrc('');
if (selectedFile.isRemote) {
setSrc(selectedFile.content || selectedFile.path);
return;
}
window.electronAPI
.readFileAsDataUrl(selectedFile.path)
.then((dataUrl: string) => {
if (!cancelled) setSrc(dataUrl);
})
.catch((err: any) => {
if (cancelled) return;
console.error('Audio load error:', err);
setSrc('');
});
return () => {
cancelled = true;
};
}, [selectedFile]);
return (
<div className="flex w-full flex-col items-center gap-4 px-8">
<p className="text-sm font-medium text-text-primary">
{selectedFile.name}
</p>
<audio controls src={src} className="w-full">
Your browser does not support audio playback.
</audio>
</div>
);
}
function VideoLoader({ selectedFile }: { selectedFile: FileInfo }) {
const [src, setSrc] = useState('');
useEffect(() => {
let cancelled = false;
setSrc('');
if (selectedFile.isRemote) {
setSrc(selectedFile.content || selectedFile.path);
return;
}
window.electronAPI
.readFileAsDataUrl(selectedFile.path)
.then((dataUrl: string) => {
if (!cancelled) setSrc(dataUrl);
})
.catch((err: any) => {
if (cancelled) return;
console.error('Video load error:', err);
setSrc('');
});
return () => {
cancelled = true;
};
}, [selectedFile]);
return (
<video controls src={src} className="max-h-full max-w-full object-contain">
Your browser does not support video playback.
</video>
);
}
// Helper function to get directory path from file path
function getDirPath(filePath: string): string {
const normalizedPath = filePath.replace(/\\/g, '/');
@ -979,8 +1125,12 @@ function HtmlRenderer({
return;
}
// Defer inline scripts until load when document has external scripts (e.g. Chart.js),
const htmlWithDeferredScripts =
deferInlineScriptsUntilLoad(processedHtmlContent);
// Set the processed HTML with font styles - iframe sandbox provides security
setProcessedHtml(injectFontStyles(processedHtmlContent));
setProcessedHtml(injectFontStyles(htmlWithDeferredScripts));
};
processHtml();
@ -1025,6 +1175,7 @@ function HtmlRenderer({
height: `${10000 / zoom}%`,
}}
>
{/*Security is maintained via CSP allowlist in index.html which restricts script sources. */}
<iframe
ref={iframeRef}
srcDoc={processedHtml}

View file

@ -45,7 +45,7 @@ export const InstallDependencies: React.FC = () => {
{isInstalling
? 'System Installing ...'
: installationState === 'waiting-backend'
? 'Starting backend service...'
? 'Starting up... First launch may take a minute.'
: ''}
</div>
<div className="text-body-sm font-medium leading-normal text-text-heading">

View file

@ -22,6 +22,7 @@ import {
} from '@xyflow/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Node as CustomNodeComponent } from './node';
import { createWorkflowWheelHandler } from './workflowWheelHandler';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { share } from '@/lib/share';
@ -387,15 +388,12 @@ export default function Workflow({
document.querySelector('.react-flow__pane');
if (!container) return;
const onWheel = (e: WheelEvent) => {
if (e.deltaY !== 0 && !isEditMode) {
e.preventDefault();
const { x, y, zoom } = getViewport();
const nextX = clampViewportX(x - e.deltaY);
setViewport({ x: nextX, y, zoom }, { duration: 0 });
}
};
const onWheel = createWorkflowWheelHandler({
isEditMode,
getViewport,
setViewport,
clampViewportX,
});
container.addEventListener('wheel', onWheel, { passive: false });

View file

@ -0,0 +1,56 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import type { Viewport } from '@xyflow/react';
export interface WorkflowWheelHandlerOptions {
isEditMode: boolean;
getViewport: () => Viewport;
setViewport: (viewport: Viewport, opts?: { duration: number }) => void;
clampViewportX: (x: number) => number;
}
/**
* Creates a wheel event handler for the Workflow (Agent Canvas) that:
* - Handles horizontal scroll (deltaX) from Mac trackpad two-finger swipe
* - Handles vertical scroll (deltaY) mapped to horizontal pan (carousel style)
* - Prevents pinch-to-zoom (ctrlKey) from triggering browser zoom when zoom is disabled
*/
export function createWorkflowWheelHandler(
options: WorkflowWheelHandlerOptions
): (e: WheelEvent) => void {
const { isEditMode, getViewport, setViewport, clampViewportX } = options;
return (e: WheelEvent) => {
if (isEditMode) return;
// Block zoom gestures (Mac pinch, Windows Ctrl+wheel). Trade-off: disables Ctrl+wheel zoom over canvas.
if (e.ctrlKey) {
e.preventDefault();
return;
}
// Horizontal scroll (deltaX) = trackpad two-finger horizontal swipe
// Vertical scroll (deltaY) = mouse wheel or trackpad vertical swipe (carousel-style pan)
const hasScroll = e.deltaX !== 0 || e.deltaY !== 0;
if (!hasScroll) return;
e.preventDefault();
const { x, y, zoom } = getViewport();
const panDelta = e.deltaX !== 0 ? e.deltaX : e.deltaY;
const nextX = clampViewportX(x - panDelta);
setViewport({ x: nextX, y, zoom }, { duration: 0 });
};
}

View file

@ -380,6 +380,7 @@ export function WorkSpaceMenu({
'developer_agent',
'browser_agent',
'document_agent',
'multi_modal_agent',
].includes(agent.type as AgentNameType) ||
agent.tasks.length === 0
}

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": ".أنت في وضع الاستضافة الذاتية. لا يمكن استخدام النماذج السحابية هنا - قم بإعداد نموذج سحابي محلي خاص بك للحفاظ على سير الأمور",
"you-are-using-self-hosted-mode-mcp": ".أنت تستخدم وضع الاستضافة الذاتية. أدخل مفاتيح بحث Google في \"MCP والأدوات\" لضمان عمل Eigent بشكل صحيح",
"it-ticket-creation": "ساعدني في ملء نموذج عبر الإنترنت",
"it-ticket-creation-message": "قم بالوصول إلى نظام إدارة التذاكر على (https://eiti.eigent.ai/) وأضف جميع هذه التذاكر الجديدة إلى نظامنا باستخدام وكيل المتصفح: '' المستخدم المتأثر: Alice Johnson مجموعة التعيين: فريق خدمات البرمجيات معين إلى: Michael Brown الأولوية: 4 منخفضة | الإلحاح: 3 متوسط | التأثير: 4 منخفض الخدمة المتأثرة: خدمات البرمجيات المشكلة: تدهور أداء التطبيق الوصف: يبلغ المستخدم المتأثر عن أوقات استجابة بطيئة وانقطاعات متقطعة عند الوصول إلى تطبيقات البرمجيات الداخلية خلال ساعات العمل العادية. '' بمجرد الانتهاء، تحقق من التذاكر الواردة وأنشئ تقريرًا إحصائيًا مفصلاً يحلل مجالات تكنولوجيا المعلومات التي لديها أكثر المشكلات وأعلى تأثير مالي. يجب أن يتضمن التقرير مخططات ورسوم بيانية للتصور.",
"it-ticket-creation-message": "قم بالوصول إلى نظام إدارة التذاكر على https://eiti.eigent.ai/ وأضف تذكرة جديدة إلى نظامنا باستخدام وكيل المتصفح:\n\nالمستخدم المتأثر: Alice Johnson\nمجموعة التعيين: فريق خدمات البرمجيات\nمعين إلى: Michael Brown\nالأولوية: 4 منخفضة | الإلحاح: 3 متوسط | التأثير: 4 منخفض\nالخدمة المتأثرة: خدمات البرمجيات\nالمشكلة: تدهور أداء التطبيق\nالوصف:\nيبلغ المستخدم المتأثر عن أوقات استجابة بطيئة وانقطاعات متقطعة عند الوصول إلى تطبيقات البرمجيات الداخلية خلال ساعات العمل العادية.\n\nبمجرد الانتهاء، انتقل إلى عرض قائمة التذاكر \"قيد التنفيذ\" واستخرج بيانات التذاكر المرئية من القائمة (أعمدة: الرقم، المستخدم، الأولوية، الحالة، المشكلة). استخدم JavaScript في وحدة تحكم المتصفح لالتقاط بيانات الجدول إذا لم يكن زر التصدير متاحًا. احفظ البيانات المستخرجة كملف CSV.\n\nأخيرًا، أنشئ تقريرًا إحصائيًا بناءً على البيانات المستخرجة:\n1. حلل توزيع التذاكر حسب مستوى الأولوية (حرج/عالي/متوسط/منخفض)\n2. استخرج أي مبالغ بالدولار مذكورة في نص المشكلة (مثل \"$50M\"، \"$3.2M\") كتأثير مالي تقديري\n3. أنشئ تقرير HTML يتضمن مخططات شريطية تُظهر:\n - عدد التذاكر حسب الأولوية\n - التأثير المالي التقديري حسب الأولوية (مستخرج من نص المشكلة)\nأضف قسم \"ملاحظات البيانات\" يوضح أن قيم التأثير المالي تم استخراجها من نص وصف المشكلة لأنه لا يوجد حقل مالي مخصص في النظام.",
"bank-transfer-csv-analysis-and-visualization": "تحليل سيسفي والتصور للتحويل البنكي",
"bank-transfer-csv-analysis-and-visualization-message": ".تفضل بزيارة ملف سيسفي تجريبي لنماذج بنكية يحتوي على ١٠ أفكار و ١٠ صفوف. اقرأ ملف سيسفي الناتج ولخّص البيانات. تتوفر رسوم بيانية لعرض الاتجاهات أو الرؤى ذات الصلة من البيانات",
"help-organize-my-desktop": "ساعدني في تنظيم سطح المكتب",

View file

@ -123,7 +123,7 @@
"terms-of-use": "شروط الاستخدام",
"and": "و",
"it-ticket-creation": "ساعدني في ملء نموذج عبر الإنترنت",
"it-ticket-creation-message": "قم بالوصول إلى نظام إدارة التذاكر على (https://eiti.eigent.ai/) وأضف جميع هذه التذاكر الجديدة إلى نظامنا باستخدام وكيل المتصفح: '' المستخدم المتأثر: Alice Johnson مجموعة التعيين: فريق خدمات البرمجيات معين إلى: Michael Brown الأولوية: 4 منخفضة | الإلحاح: 3 متوسط | التأثير: 4 منخفض الخدمة المتأثرة: خدمات البرمجيات المشكلة: تدهور أداء التطبيق الوصف: يبلغ المستخدم المتأثر عن أوقات استجابة بطيئة وانقطاعات متقطعة عند الوصول إلى تطبيقات البرمجيات الداخلية خلال ساعات العمل العادية. '' بمجرد الانتهاء، تحقق من التذاكر الواردة وأنشئ تقريرًا إحصائيًا مفصلاً يحلل مجالات تكنولوجيا المعلومات التي لديها أكثر المشكلات وأعلى تأثير مالي. يجب أن يتضمن التقرير مخططات ورسوم بيانية للتصور.",
"it-ticket-creation-message": "قم بالوصول إلى نظام إدارة التذاكر على https://eiti.eigent.ai/ وأضف تذكرة جديدة إلى نظامنا باستخدام وكيل المتصفح:\n\nالمستخدم المتأثر: Alice Johnson\nمجموعة التعيين: فريق خدمات البرمجيات\nمعين إلى: Michael Brown\nالأولوية: 4 منخفضة | الإلحاح: 3 متوسط | التأثير: 4 منخفض\nالخدمة المتأثرة: خدمات البرمجيات\nالمشكلة: تدهور أداء التطبيق\nالوصف:\nيبلغ المستخدم المتأثر عن أوقات استجابة بطيئة وانقطاعات متقطعة عند الوصول إلى تطبيقات البرمجيات الداخلية خلال ساعات العمل العادية.\n\nبمجرد الانتهاء، انتقل إلى عرض قائمة التذاكر \"قيد التنفيذ\" واستخرج بيانات التذاكر المرئية من القائمة (أعمدة: الرقم، المستخدم، الأولوية، الحالة، المشكلة). استخدم JavaScript في وحدة تحكم المتصفح لالتقاط بيانات الجدول إذا لم يكن زر التصدير متاحًا. احفظ البيانات المستخرجة كملف CSV.\n\nأخيرًا، أنشئ تقريرًا إحصائيًا بناءً على البيانات المستخرجة:\n1. حلل توزيع التذاكر حسب مستوى الأولوية (حرج/عالي/متوسط/منخفض)\n2. استخرج أي مبالغ بالدولار مذكورة في نص المشكلة (مثل \"$50M\"، \"$3.2M\") كتأثير مالي تقديري\n3. أنشئ تقرير HTML يتضمن مخططات شريطية تُظهر:\n - عدد التذاكر حسب الأولوية\n - التأثير المالي التقديري حسب الأولوية (مستخرج من نص المشكلة)\nأضف قسم \"ملاحظات البيانات\" يوضح أن قيم التأثير المالي تم استخراجها من نص وصف المشكلة لأنه لا يوجد حقل مالي مخصص في النظام.",
"bank-transfer-csv-analysis": "تحليل وتصور CSV للتحويلات البنكية",
"bank-transfer-csv-analysis-message": "حلل ملف CSV للتحويلات البنكية وأنشئ تصورات تُظهر أنماط الإنفاق",
"find-duplicate-files": "العثور على الملفات المكررة في مجلد التنزيلات",

View file

@ -193,6 +193,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"account": "حساب",
"you-are-currently-signed-in-with": "{{email}} أنت مسجل الدخول حاليًا باستخدام",

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Sie befinden sich im Self-hosted-Modus. Cloud-Modelle können hier nicht verwendet werden richten Sie Ihr eigenes lokales Cloud-Modell ein, um den Betrieb aufrechtzuerhalten.",
"you-are-using-self-hosted-mode-mcp": "Sie verwenden den Self-hosted-Modus. Geben Sie die Google Search Keys in „MCP und Tools“ ein, um sicherzustellen, dass Eigent ordnungsgemäß funktioniert.",
"it-ticket-creation": "Hilf mir, ein Online-Formular auszufüllen",
"it-ticket-creation-message": "Greifen Sie auf das Ticket-Management-System unter (https://eiti.eigent.ai/) zu und fügen Sie alle diese neuen Tickets mit dem Browser-Agenten zu unserem System hinzu: '' Betroffener Benutzer: Alice Johnson Zuweisungsgruppe: Software Services Team Zugewiesen an: Michael Brown Priorität: 4 Niedrig | Dringlichkeit: 3 Mittel | Auswirkung: 4 Niedrig Betroffener Service: Software Services Problem: Leistungsverschlechterung der Anwendung Beschreibung: Der betroffene Benutzer meldet langsame Antwortzeiten und zeitweilige Timeouts beim Zugriff auf interne Softwareanwendungen während der normalen Geschäftszeiten. '' Nach Abschluss überprüfen Sie die eingehenden Tickets und erstellen Sie einen detaillierten statistischen Bericht, der analysiert, welche IT-Bereiche die meisten Probleme und die höchsten finanziellen Auswirkungen haben. Der Bericht sollte Diagramme und Grafiken zur Visualisierung enthalten.",
"it-ticket-creation-message": "Greifen Sie auf das Ticket-Management-System unter https://eiti.eigent.ai/ zu und fügen Sie mit dem Browser-Agenten ein neues Ticket zu unserem System hinzu:\n\nBetroffener Benutzer: Alice Johnson\nZuweisungsgruppe: Software Services Team\nZugewiesen an: Michael Brown\nPriorität: 4 Niedrig | Dringlichkeit: 3 Mittel | Auswirkung: 4 Niedrig\nBetroffener Service: Software Services\nProblem: Leistungsverschlechterung der Anwendung\nBeschreibung:\nDer betroffene Benutzer meldet langsame Antwortzeiten und zeitweilige Timeouts beim Zugriff auf interne Softwareanwendungen während der normalen Geschäftszeiten.\n\nNavigieren Sie nach Abschluss zur Listenansicht \"In Bearbeitung\" und extrahieren Sie die sichtbaren Ticketdaten aus der Liste (Spalten: Nummer, Benutzer, Priorität, Status, Problem). Verwenden Sie Browser-Konsolen-JavaScript, um die Tabellendaten zu erfassen, wenn keine Exportschaltfläche verfügbar ist. Speichern Sie die extrahierten Daten als CSV-Datei.\n\nErstellen Sie abschließend einen statistischen Bericht basierend auf den extrahierten Daten:\n1. Analysieren Sie die Ticketverteilung nach Prioritätsstufe (Kritisch/Hoch/Mittel/Niedrig)\n2. Parsen Sie alle im Problemtext erwähnten Dollarmengen (z.B. \"$50M\", \"$3.2M\") als geschätzte finanzielle Auswirkung\n3. Erstellen Sie einen HTML-Bericht mit Balkendiagrammen, die zeigen:\n - Ticketanzahl nach Priorität\n - Geschätzte finanzielle Auswirkung nach Priorität (aus dem Problemtext geparst)\nFügen Sie einen Abschnitt \"Datenhinweise\" hinzu, der erklärt, dass die Werte der finanziellen Auswirkung aus dem Problembeschreibungstext geparst wurden, da im System kein dediziertes Finanzfeld existiert.",
"bank-transfer-csv-analysis-and-visualization": "Banküberweisung CSV-Analyse und Visualisierung",
"bank-transfer-csv-analysis-and-visualization-message": "Erstellen Sie eine Mock-CSV-Datei für Banküberweisungen mit 10 Spalten und 10 Zeilen. Lesen Sie die generierte CSV-Datei und fassen Sie die Daten zusammen. Erstellen Sie ein Diagramm, um relevante Trends oder Erkenntnisse aus den Daten zu visualisieren.",
"help-organize-my-desktop": "Bitte helfen Sie, meinen Desktop zu organisieren",

View file

@ -123,7 +123,7 @@
"terms-of-use": "Nutzungsbedingungen",
"and": "und",
"it-ticket-creation": "Hilf mir, ein Online-Formular auszufüllen",
"it-ticket-creation-message": "Greifen Sie auf das Ticket-Management-System unter (https://eiti.eigent.ai/) zu und fügen Sie alle diese neuen Tickets mit dem Browser-Agenten zu unserem System hinzu: '' Betroffener Benutzer: Alice Johnson Zuweisungsgruppe: Software Services Team Zugewiesen an: Michael Brown Priorität: 4 Niedrig | Dringlichkeit: 3 Mittel | Auswirkung: 4 Niedrig Betroffener Service: Software Services Problem: Leistungsverschlechterung der Anwendung Beschreibung: Der betroffene Benutzer meldet langsame Antwortzeiten und zeitweilige Timeouts beim Zugriff auf interne Softwareanwendungen während der normalen Geschäftszeiten. '' Nach Abschluss überprüfen Sie die eingehenden Tickets und erstellen Sie einen detaillierten statistischen Bericht, der analysiert, welche IT-Bereiche die meisten Probleme und die höchsten finanziellen Auswirkungen haben. Der Bericht sollte Diagramme und Grafiken zur Visualisierung enthalten.",
"it-ticket-creation-message": "Greifen Sie auf das Ticket-Management-System unter https://eiti.eigent.ai/ zu und fügen Sie mit dem Browser-Agenten ein neues Ticket zu unserem System hinzu:\n\nBetroffener Benutzer: Alice Johnson\nZuweisungsgruppe: Software Services Team\nZugewiesen an: Michael Brown\nPriorität: 4 Niedrig | Dringlichkeit: 3 Mittel | Auswirkung: 4 Niedrig\nBetroffener Service: Software Services\nProblem: Leistungsverschlechterung der Anwendung\nBeschreibung:\nDer betroffene Benutzer meldet langsame Antwortzeiten und zeitweilige Timeouts beim Zugriff auf interne Softwareanwendungen während der normalen Geschäftszeiten.\n\nNavigieren Sie nach Abschluss zur Listenansicht \"In Bearbeitung\" und extrahieren Sie die sichtbaren Ticketdaten aus der Liste (Spalten: Nummer, Benutzer, Priorität, Status, Problem). Verwenden Sie Browser-Konsolen-JavaScript, um die Tabellendaten zu erfassen, wenn keine Exportschaltfläche verfügbar ist. Speichern Sie die extrahierten Daten als CSV-Datei.\n\nErstellen Sie abschließend einen statistischen Bericht basierend auf den extrahierten Daten:\n1. Analysieren Sie die Ticketverteilung nach Prioritätsstufe (Kritisch/Hoch/Mittel/Niedrig)\n2. Parsen Sie alle im Problemtext erwähnten Dollarmengen (z.B. \"$50M\", \"$3.2M\") als geschätzte finanzielle Auswirkung\n3. Erstellen Sie einen HTML-Bericht mit Balkendiagrammen, die zeigen:\n - Ticketanzahl nach Priorität\n - Geschätzte finanzielle Auswirkung nach Priorität (aus dem Problemtext geparst)\nFügen Sie einen Abschnitt \"Datenhinweise\" hinzu, der erklärt, dass die Werte der finanziellen Auswirkung aus dem Problembeschreibungstext geparst wurden, da im System kein dediziertes Finanzfeld existiert.",
"bank-transfer-csv-analysis": "Banküberweisung CSV-Analyse und Visualisierung",
"bank-transfer-csv-analysis-message": "Analysieren Sie meine Banküberweisung CSV-Datei und erstellen Sie Visualisierungen, die Ausgabemuster zeigen",
"find-duplicate-files": "Doppelte Dateien im Download-Ordner finden",

View file

@ -261,6 +261,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"network-proxy": "Netzwerk-Proxy",
"network-proxy-description": "Konfigurieren Sie einen Proxy-Server für Netzwerkanfragen. Dies ist nützlich, wenn Sie über einen Proxy auf externe APIs zugreifen müssen.",
"proxy-placeholder": "http://127.0.0.1:7890",

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "You're in Self-hosted mode. Cloud models can't be used here — set up your own local cloud model to keep things running.",
"you-are-using-self-hosted-mode-mcp": "You're using Self-hosted mode. Enter the Google Search Keys in “MCP and Tools” to ensure Eigent works properly.",
"it-ticket-creation": "Help me complete an online form",
"it-ticket-creation-message": "Access the ticket management system at https://eiti.eigent.ai/ and add all these new tickets into our system with Browser Agent:\n''\nAffected User: Alice Johnson\nAssignment Group: Software Services Team\nAssigned To: Michael Brown\nPriority: 4 Low | Urgency: 3 Medium | Impact: 4 Low\nAffected Service: Software Services\nIssue: Application Performance Degradation\nDescription:\nThe affected user reports slow response times and intermittent timeouts when accessing internal software applications during normal business hours.\n''\nOnce done, check the in progress and generate a detailed statistical report analyzing which IT areas have the most issues and the highest financial impact. The report should include charts and diagrams for visualization.",
"it-ticket-creation-message": "Access the ticket management system at https://eiti.eigent.ai/ and add a new ticket into our system with Browser Agent:\n\nAffected User: Alice Johnson\nAssignment Group: Software Services Team\nAssigned To: Michael Brown\nPriority: 4 Low | Urgency: 3 Medium | Impact: 4 Low\nAffected Service: Software Services\nIssue: Application Performance Degradation\nDescription:\nThe affected user reports slow response times and intermittent timeouts when accessing internal software applications during normal business hours.\n\nOnce done, navigate to the \"In Progress\" tickets list view and extract the visible ticket data from the list (Number, User, Priority, State, Issue columns). Use browser console JavaScript to capture the table data if no export button is available. Save the extracted data as a CSV file.\n\nFinally, generate a statistical report based on the extracted data:\n1. Analyze ticket distribution by Priority level (Critical/High/Moderate/Low)\n2. Parse any dollar amounts mentioned in the Issue text (e.g., \"$50M\", \"$3.2M\") as estimated financial impact\n3. Create an HTML report with bar charts showing:\n - Ticket count by Priority\n - Estimated financial impact by Priority (parsed from Issue text)\nInclude a \"Data Notes\" section explaining that financial impact values were parsed from Issue description text since no dedicated financial field exists in the system.",
"bank-transfer-csv-analysis-and-visualization": "Bank Transfer CSV Analysis and Visualization",
"bank-transfer-csv-analysis-and-visualization-message": "Create a mock bank transfer CSV file include 10 columns and 10 rows. Read the generated CSV file and summarize the data, generate a chart to visualize relevant trends or insights from the data.",
"help-organize-my-desktop": "Please Help Organize My Desktop",

View file

@ -126,7 +126,7 @@
"terms-of-use": "Terms of Use",
"and": "and",
"it-ticket-creation": "Help me complete an online form",
"it-ticket-creation-message": "Access the ticket management system at https://eiti.eigent.ai/ and add all these new tickets into our system with Browser Agent:\n''\nAffected User: Alice Johnson\nAssignment Group: Software Services Team\nAssigned To: Michael Brown\nPriority: 4 Low | Urgency: 3 Medium | Impact: 4 Low\nAffected Service: Software Services\nIssue: Application Performance Degradation\nDescription:\nThe affected user reports slow response times and intermittent timeouts when accessing internal software applications during normal business hours.\n''\nOnce done, check the in progress and generate a detailed statistical report analyzing which IT areas have the most issues and the highest financial impact. The report should include charts and diagrams for visualization.",
"it-ticket-creation-message": "Access the ticket management system at https://eiti.eigent.ai/ and add a new ticket into our system with Browser Agent:\n\nAffected User: Alice Johnson\nAssignment Group: Software Services Team\nAssigned To: Michael Brown\nPriority: 4 Low | Urgency: 3 Medium | Impact: 4 Low\nAffected Service: Software Services\nIssue: Application Performance Degradation\nDescription:\nThe affected user reports slow response times and intermittent timeouts when accessing internal software applications during normal business hours.\n\nOnce done, navigate to the \"In Progress\" tickets list view and extract the visible ticket data from the list (Number, User, Priority, State, Issue columns). Use browser console JavaScript to capture the table data if no export button is available. Save the extracted data as a CSV file.\n\nFinally, generate a statistical report based on the extracted data:\n1. Analyze ticket distribution by Priority level (Critical/High/Moderate/Low)\n2. Parse any dollar amounts mentioned in the Issue text (e.g., \"$50M\", \"$3.2M\") as estimated financial impact\n3. Create an HTML report with bar charts showing:\n - Ticket count by Priority\n - Estimated financial impact by Priority (parsed from Issue text)\nInclude a \"Data Notes\" section explaining that financial impact values were parsed from Issue description text since no dedicated financial field exists in the system.",
"bank-transfer-csv-analysis": "Bank Transfer CSV Analysis and Visualization",
"bank-transfer-csv-analysis-message": "Create a mock bank transfer CSV file include 10 columns and 10 rows. Read the generated CSV file and summarize the data, generate a chart to visualize relevant trends or insights from the data.",
"find-duplicate-files": "Please Help Organize My Desktop",

View file

@ -221,6 +221,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"account": "Account",
"you-are-currently-signed-in-with": "You are currently signed in with {{email}}",

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Estás en modo autohospedado. No se pueden usar modelos en la nube aquí; configura tu propio modelo en la nube local para que todo siga funcionando.",
"you-are-using-self-hosted-mode-mcp": "Estás usando el modo autohospedado. Ingresa las claves de búsqueda de Google en “MCP y herramientas” para asegurar que Eigent funcione correctamente.",
"it-ticket-creation": "Ayúdame a completar un formulario en línea",
"it-ticket-creation-message": "Acceda al sistema de gestión de tickets en (https://eiti.eigent.ai/) y agregue todos estos nuevos tickets a nuestro sistema con el Agente del Navegador: '' Usuario Afectado: Alice Johnson Grupo de Asignación: Equipo de Servicios de Software Asignado a: Michael Brown Prioridad: 4 Baja | Urgencia: 3 Media | Impacto: 4 Bajo Servicio Afectado: Servicios de Software Problema: Degradación del Rendimiento de la Aplicación Descripción: El usuario afectado informa tiempos de respuesta lentos e interrupciones intermitentes al acceder a aplicaciones de software internas durante el horario comercial normal. '' Una vez completado, revise los tickets entrantes y genere un informe estadístico detallado que analice qué áreas de TI tienen más problemas y el mayor impacto financiero. El informe debe incluir gráficos y diagramas para visualización.",
"it-ticket-creation-message": "Acceda al sistema de gestión de tickets en https://eiti.eigent.ai/ y agregue un nuevo ticket a nuestro sistema con el Agente del Navegador:\n\nUsuario Afectado: Alice Johnson\nGrupo de Asignación: Equipo de Servicios de Software\nAsignado a: Michael Brown\nPrioridad: 4 Baja | Urgencia: 3 Media | Impacto: 4 Bajo\nServicio Afectado: Servicios de Software\nProblema: Degradación del Rendimiento de la Aplicación\nDescripción:\nEl usuario afectado informa tiempos de respuesta lentos e interrupciones intermitentes al acceder a aplicaciones de software internas durante el horario comercial normal.\n\nUna vez completado, navegue a la vista de lista de tickets \"En Progreso\" y extraiga los datos de tickets visibles de la lista (columnas: Número, Usuario, Prioridad, Estado, Problema). Use JavaScript de la consola del navegador para capturar los datos de la tabla si no hay un botón de exportación disponible. Guarde los datos extraídos como un archivo CSV.\n\nFinalmente, genere un informe estadístico basado en los datos extraídos:\n1. Analice la distribución de tickets por nivel de prioridad (Crítico/Alto/Moderado/Bajo)\n2. Analice cualquier cantidad en dólares mencionada en el texto del problema (por ejemplo, \"$50M\", \"$3.2M\") como impacto financiero estimado\n3. Cree un informe HTML con gráficos de barras que muestren:\n - Cantidad de tickets por prioridad\n - Impacto financiero estimado por prioridad (analizado del texto del problema)\nIncluya una sección de \"Notas de Datos\" explicando que los valores de impacto financiero fueron analizados del texto de descripción del problema ya que no existe un campo financiero dedicado en el sistema.",
"bank-transfer-csv-analysis-and-visualization": "Análisis y visualización de transferencias bancarias en CSV",
"bank-transfer-csv-analysis-and-visualization-message": "Crea un archivo CSV simulado de transferencias bancarias que incluya 10 columnas y 10 filas. Lee el CSV generado y resume los datos; genera un gráfico para visualizar tendencias o ideas relevantes a partir de los datos.",
"help-organize-my-desktop": "Por favor ayúdame a organizar mi escritorio",

View file

@ -123,7 +123,7 @@
"terms-of-use": "Términos de Uso",
"and": "y",
"it-ticket-creation": "Ayúdame a completar un formulario en línea",
"it-ticket-creation-message": "Acceda al sistema de gestión de tickets en (https://eiti.eigent.ai/) y agregue todos estos nuevos tickets a nuestro sistema con el Agente del Navegador: '' Usuario Afectado: Alice Johnson Grupo de Asignación: Equipo de Servicios de Software Asignado a: Michael Brown Prioridad: 4 Baja | Urgencia: 3 Media | Impacto: 4 Bajo Servicio Afectado: Servicios de Software Problema: Degradación del Rendimiento de la Aplicación Descripción: El usuario afectado informa tiempos de respuesta lentos e interrupciones intermitentes al acceder a aplicaciones de software internas durante el horario comercial normal. '' Una vez completado, revise los tickets entrantes y genere un informe estadístico detallado que analice qué áreas de TI tienen más problemas y el mayor impacto financiero. El informe debe incluir gráficos y diagramas para visualización.",
"it-ticket-creation-message": "Acceda al sistema de gestión de tickets en https://eiti.eigent.ai/ y agregue un nuevo ticket a nuestro sistema con el Agente del Navegador:\n\nUsuario Afectado: Alice Johnson\nGrupo de Asignación: Equipo de Servicios de Software\nAsignado a: Michael Brown\nPrioridad: 4 Baja | Urgencia: 3 Media | Impacto: 4 Bajo\nServicio Afectado: Servicios de Software\nProblema: Degradación del Rendimiento de la Aplicación\nDescripción:\nEl usuario afectado informa tiempos de respuesta lentos e interrupciones intermitentes al acceder a aplicaciones de software internas durante el horario comercial normal.\n\nUna vez completado, navegue a la vista de lista de tickets \"En Progreso\" y extraiga los datos de tickets visibles de la lista (columnas: Número, Usuario, Prioridad, Estado, Problema). Use JavaScript de la consola del navegador para capturar los datos de la tabla si no hay un botón de exportación disponible. Guarde los datos extraídos como un archivo CSV.\n\nFinalmente, genere un informe estadístico basado en los datos extraídos:\n1. Analice la distribución de tickets por nivel de prioridad (Crítico/Alto/Moderado/Bajo)\n2. Analice cualquier cantidad en dólares mencionada en el texto del problema (por ejemplo, \"$50M\", \"$3.2M\") como impacto financiero estimado\n3. Cree un informe HTML con gráficos de barras que muestren:\n - Cantidad de tickets por prioridad\n - Impacto financiero estimado por prioridad (analizado del texto del problema)\nIncluya una sección de \"Notas de Datos\" explicando que los valores de impacto financiero fueron analizados del texto de descripción del problema ya que no existe un campo financiero dedicado en el sistema.",
"bank-transfer-csv-analysis": "Análisis y Visualización CSV de Transferencias Bancarias",
"bank-transfer-csv-analysis-message": "Analiza mi archivo CSV de transferencias bancarias y crea visualizaciones mostrando patrones de gastos",
"find-duplicate-files": "Encontrar Archivos Duplicados en la Carpeta de Descargas",

View file

@ -261,6 +261,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"network-proxy": "Proxy de red",
"network-proxy-description": "Configure un servidor proxy para las solicitudes de red. Esto es útil si necesita acceder a APIs externas a través de un proxy.",
"proxy-placeholder": "http://127.0.0.1:7890",

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Vous êtes en mode auto-hébergé. Les modèles cloud ne peuvent pas être utilisés ici — configurez votre propre modèle cloud local pour que tout continue de fonctionner.",
"you-are-using-self-hosted-mode-mcp": "Vous utilisez le mode auto-hébergé. Entrez les clés de recherche Google dans « MCP et Outils » pour garantir le bon fonctionnement d'Eigent.",
"it-ticket-creation": "Aidez-moi à remplir un formulaire en ligne",
"it-ticket-creation-message": "Accédez au système de gestion des tickets sur (https://eiti.eigent.ai/) et ajoutez tous ces nouveaux tickets à notre système avec l'Agent Navigateur : '' Utilisateur Affecté : Alice Johnson Groupe d'Attribution : Équipe Services Logiciels Attribué à : Michael Brown Priorité : 4 Basse | Urgence : 3 Moyenne | Impact : 4 Bas Service Affecté : Services Logiciels Problème : Dégradation des Performances de l'Application Description : L'utilisateur affecté signale des temps de réponse lents et des délais d'attente intermittents lors de l'accès aux applications logicielles internes pendant les heures de bureau normales. '' Une fois terminé, vérifiez les tickets entrants et générez un rapport statistique détaillé analysant quels domaines IT ont le plus de problèmes et l'impact financier le plus élevé. Le rapport doit inclure des graphiques et des diagrammes pour la visualisation.",
"it-ticket-creation-message": "Accédez au système de gestion des tickets sur https://eiti.eigent.ai/ et ajoutez un nouveau ticket à notre système avec l'Agent Navigateur :\n\nUtilisateur Affecté : Alice Johnson\nGroupe d'Attribution : Équipe Services Logiciels\nAttribué à : Michael Brown\nPriorité : 4 Basse | Urgence : 3 Moyenne | Impact : 4 Bas\nService Affecté : Services Logiciels\nProblème : Dégradation des Performances de l'Application\nDescription :\nL'utilisateur affecté signale des temps de réponse lents et des délais d'attente intermittents lors de l'accès aux applications logicielles internes pendant les heures de bureau normales.\n\nUne fois terminé, naviguez vers la vue de liste des tickets \"En cours\" et extrayez les données de tickets visibles de la liste (colonnes : Numéro, Utilisateur, Priorité, État, Problème). Utilisez JavaScript dans la console du navigateur pour capturer les données du tableau si aucun bouton d'exportation n'est disponible. Enregistrez les données extraites dans un fichier CSV.\n\nEnfin, générez un rapport statistique basé sur les données extraites :\n1. Analysez la distribution des tickets par niveau de priorité (Critique/Élevé/Modéré/Bas)\n2. Parsez tous les montants en dollars mentionnés dans le texte du problème (par ex. \"$50M\", \"$3.2M\") comme impact financier estimé\n3. Créez un rapport HTML avec des graphiques à barres montrant :\n - Nombre de tickets par priorité\n - Impact financier estimé par priorité (parsé à partir du texte du problème)\nIncluez une section \"Notes sur les données\" expliquant que les valeurs d'impact financier ont été parsées à partir du texte de description du problème car aucun champ financier dédié n'existe dans le système.",
"bank-transfer-csv-analysis-and-visualization": "Analyse et visualisation CSV des virements bancaires",
"bank-transfer-csv-analysis-and-visualization-message": "Créez un fichier CSV de virements bancaires fictifs comprenant 10 colonnes et 10 lignes. Lisez le fichier CSV généré et résumez les données, générez un graphique pour visualiser les tendances ou les informations pertinentes à partir des données.",
"help-organize-my-desktop": "Aidez-moi à organiser mon bureau",

View file

@ -123,7 +123,7 @@
"terms-of-use": "Conditions d'Utilisation",
"and": "et",
"it-ticket-creation": "Aidez-moi à remplir un formulaire en ligne",
"it-ticket-creation-message": "Accédez au système de gestion des tickets sur (https://eiti.eigent.ai/) et ajoutez tous ces nouveaux tickets à notre système avec l'Agent Navigateur : '' Utilisateur Affecté : Alice Johnson Groupe d'Attribution : Équipe Services Logiciels Attribué à : Michael Brown Priorité : 4 Basse | Urgence : 3 Moyenne | Impact : 4 Bas Service Affecté : Services Logiciels Problème : Dégradation des Performances de l'Application Description : L'utilisateur affecté signale des temps de réponse lents et des délais d'attente intermittents lors de l'accès aux applications logicielles internes pendant les heures de bureau normales. '' Une fois terminé, vérifiez les tickets entrants et générez un rapport statistique détaillé analysant quels domaines IT ont le plus de problèmes et l'impact financier le plus élevé. Le rapport doit inclure des graphiques et des diagrammes pour la visualisation.",
"it-ticket-creation-message": "Accédez au système de gestion des tickets sur https://eiti.eigent.ai/ et ajoutez un nouveau ticket à notre système avec l'Agent Navigateur :\n\nUtilisateur Affecté : Alice Johnson\nGroupe d'Attribution : Équipe Services Logiciels\nAttribué à : Michael Brown\nPriorité : 4 Basse | Urgence : 3 Moyenne | Impact : 4 Bas\nService Affecté : Services Logiciels\nProblème : Dégradation des Performances de l'Application\nDescription :\nL'utilisateur affecté signale des temps de réponse lents et des délais d'attente intermittents lors de l'accès aux applications logicielles internes pendant les heures de bureau normales.\n\nUne fois terminé, naviguez vers la vue de liste des tickets \"En cours\" et extrayez les données de tickets visibles de la liste (colonnes : Numéro, Utilisateur, Priorité, État, Problème). Utilisez JavaScript dans la console du navigateur pour capturer les données du tableau si aucun bouton d'exportation n'est disponible. Enregistrez les données extraites dans un fichier CSV.\n\nEnfin, générez un rapport statistique basé sur les données extraites :\n1. Analysez la distribution des tickets par niveau de priorité (Critique/Élevé/Modéré/Bas)\n2. Parsez tous les montants en dollars mentionnés dans le texte du problème (par ex. \"$50M\", \"$3.2M\") comme impact financier estimé\n3. Créez un rapport HTML avec des graphiques à barres montrant :\n - Nombre de tickets par priorité\n - Impact financier estimé par priorité (parsé à partir du texte du problème)\nIncluez une section \"Notes sur les données\" expliquant que les valeurs d'impact financier ont été parsées à partir du texte de description du problème car aucun champ financier dédié n'existe dans le système.",
"bank-transfer-csv-analysis": "Analyse et Visualisation CSV des Virements Bancaires",
"bank-transfer-csv-analysis-message": "Analysez mon fichier CSV de virements bancaires et créez des visualisations montrant les modèles de dépenses",
"find-duplicate-files": "Trouver des Fichiers en Double dans le Dossier Téléchargements",

View file

@ -237,6 +237,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"network-proxy": "Proxy réseau",
"network-proxy-description": "Configurez un serveur proxy pour les requêtes réseau. Utile si vous devez accéder à des API externes via un proxy.",
"proxy-placeholder": "http://127.0.0.1:7890",

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "Sei in modalità Self-hosted. I modelli cloud non possono essere utilizzati qui — configura il tuo modello cloud locale per mantenere tutto in funzione.",
"you-are-using-self-hosted-mode-mcp": "Stai utilizzando la modalità Self-hosted. Inserisci le Chiavi di Ricerca Google in \"MCP e Strumenti\" per garantire che Eigent funzioni correttamente.",
"it-ticket-creation": "Aiutami a compilare un modulo online",
"it-ticket-creation-message": "Accedi al sistema di gestione ticket su (https://eiti.eigent.ai/) e aggiungi tutti questi nuovi ticket al nostro sistema con l'Agente Browser: '' Utente Interessato: Alice Johnson Gruppo di Assegnazione: Team Servizi Software Assegnato a: Michael Brown Priorità: 4 Bassa | Urgenza: 3 Media | Impatto: 4 Basso Servizio Interessato: Servizi Software Problema: Degradazione delle Prestazioni dell'Applicazione Descrizione: L'utente interessato segnala tempi di risposta lenti e timeout intermittenti durante l'accesso alle applicazioni software interne durante il normale orario lavorativo. '' Una volta completato, controlla i ticket in arrivo e genera un report statistico dettagliato che analizzi quali aree IT hanno più problemi e il maggiore impatto finanziario. Il report deve includere grafici e diagrammi per la visualizzazione.",
"it-ticket-creation-message": "Accedi al sistema di gestione ticket su https://eiti.eigent.ai/ e aggiungi un nuovo ticket al nostro sistema con l'Agente Browser:\n\nUtente Interessato: Alice Johnson\nGruppo di Assegnazione: Team Servizi Software\nAssegnato a: Michael Brown\nPriorità: 4 Bassa | Urgenza: 3 Media | Impatto: 4 Basso\nServizio Interessato: Servizi Software\nProblema: Degradazione delle Prestazioni dell'Applicazione\nDescrizione:\nL'utente interessato segnala tempi di risposta lenti e timeout intermittenti durante l'accesso alle applicazioni software interne durante il normale orario lavorativo.\n\nUna volta completato, naviga alla vista elenco ticket \"In Corso\" ed estrai i dati dei ticket visibili dall'elenco (colonne: Numero, Utente, Priorità, Stato, Problema). Usa JavaScript della console del browser per catturare i dati della tabella se non è disponibile un pulsante di esportazione. Salva i dati estratti come file CSV.\n\nInfine, genera un report statistico basato sui dati estratti:\n1. Analizza la distribuzione dei ticket per livello di priorità (Critico/Alto/Moderato/Basso)\n2. Analizza qualsiasi importo in dollari menzionato nel testo del problema (ad es. \"$50M\", \"$3.2M\") come impatto finanziario stimato\n3. Crea un report HTML con grafici a barre che mostrano:\n - Conteggio ticket per priorità\n - Impatto finanziario stimato per priorità (analizzato dal testo del problema)\nIncludi una sezione \"Note sui Dati\" che spiega che i valori dell'impatto finanziario sono stati analizzati dal testo della descrizione del problema poiché nel sistema non esiste un campo finanziario dedicato.",
"bank-transfer-csv-analysis-and-visualization": "Analisi e Visualizzazione CSV di Bonifici Bancari",
"bank-transfer-csv-analysis-and-visualization-message": "Crea un file CSV di bonifici bancari fittizio con 10 colonne e 10 righe. Leggi il file CSV generato e riassumi i dati, genera un grafico per visualizzare tendenze o intuizioni pertinenti dai dati.",
"help-organize-my-desktop": "Aiutami a organizzare il mio desktop",

View file

@ -123,7 +123,7 @@
"terms-of-use": "Termini di Utilizzo",
"and": "e",
"it-ticket-creation": "Aiutami a compilare un modulo online",
"it-ticket-creation-message": "Accedi al sistema di gestione ticket su (https://eiti.eigent.ai/) e aggiungi tutti questi nuovi ticket al nostro sistema con l'Agente Browser: '' Utente Interessato: Alice Johnson Gruppo di Assegnazione: Team Servizi Software Assegnato a: Michael Brown Priorità: 4 Bassa | Urgenza: 3 Media | Impatto: 4 Basso Servizio Interessato: Servizi Software Problema: Degradazione delle Prestazioni dell'Applicazione Descrizione: L'utente interessato segnala tempi di risposta lenti e timeout intermittenti durante l'accesso alle applicazioni software interne durante il normale orario lavorativo. '' Una volta completato, controlla i ticket in arrivo e genera un report statistico dettagliato che analizzi quali aree IT hanno più problemi e il maggiore impatto finanziario. Il report deve includere grafici e diagrammi per la visualizzazione.",
"it-ticket-creation-message": "Accedi al sistema di gestione ticket su https://eiti.eigent.ai/ e aggiungi un nuovo ticket al nostro sistema con l'Agente Browser:\n\nUtente Interessato: Alice Johnson\nGruppo di Assegnazione: Team Servizi Software\nAssegnato a: Michael Brown\nPriorità: 4 Bassa | Urgenza: 3 Media | Impatto: 4 Basso\nServizio Interessato: Servizi Software\nProblema: Degradazione delle Prestazioni dell'Applicazione\nDescrizione:\nL'utente interessato segnala tempi di risposta lenti e timeout intermittenti durante l'accesso alle applicazioni software interne durante il normale orario lavorativo.\n\nUna volta completato, naviga alla vista elenco ticket \"In Corso\" ed estrai i dati dei ticket visibili dall'elenco (colonne: Numero, Utente, Priorità, Stato, Problema). Usa JavaScript della console del browser per catturare i dati della tabella se non è disponibile un pulsante di esportazione. Salva i dati estratti come file CSV.\n\nInfine, genera un report statistico basato sui dati estratti:\n1. Analizza la distribuzione dei ticket per livello di priorità (Critico/Alto/Moderato/Basso)\n2. Analizza qualsiasi importo in dollari menzionato nel testo del problema (ad es. \"$50M\", \"$3.2M\") come impatto finanziario stimato\n3. Crea un report HTML con grafici a barre che mostrano:\n - Conteggio ticket per priorità\n - Impatto finanziario stimato per priorità (analizzato dal testo del problema)\nIncludi una sezione \"Note sui Dati\" che spiega che i valori dell'impatto finanziario sono stati analizzati dal testo della descrizione del problema poiché nel sistema non esiste un campo finanziario dedicato.",
"bank-transfer-csv-analysis": "Analisi e Visualizzazione CSV Trasferimenti Bancari",
"bank-transfer-csv-analysis-message": "Analizza il mio file CSV trasferimenti bancari e crea visualizzazioni che mostrano modelli di spesa",
"find-duplicate-files": "Trova File Duplicati nella Cartella Download",

View file

@ -261,6 +261,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"network-proxy": "Proxy di rete",
"network-proxy-description": "Configura un server proxy per le richieste di rete. Utile se devi accedere ad API esterne tramite un proxy.",
"proxy-placeholder": "http://127.0.0.1:7890",

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "セルフホストモードを使用しています。クラウドモデルはここでは使用できません。続行するには、独自のローカルクラウドモデルを設定してください。",
"you-are-using-self-hosted-mode-mcp": "セルフホストモードを使用しています。Eigentが正しく機能するように、「MCPとツール」にGoogle検索キーを入力してください。",
"it-ticket-creation": "オンラインフォームの入力を手伝ってください",
"it-ticket-creation-message": "(https://eiti.eigent.ai/)のチケット管理システムにアクセスし、ブラウザエージェントを使用してこれらの新しいチケットをすべてシステムに追加してください:'' 影響を受けるユーザーAlice Johnson 割り当てグループ:ソフトウェアサービスチーム 担当者Michael Brown 優先度4 低 | 緊急度3 中 | 影響4 低 影響を受けるサービス:ソフトウェアサービス 問題:アプリケーションパフォーマンスの低下 説明:影響を受けるユーザーは、通常の営業時間中に内部ソフトウェアアプリケーションにアクセスする際、応答時間の遅延と断続的なタイムアウトを報告しています。'' 完了後、受信チケットを確認し、どのIT分野で最も問題が多く、財務的影響が最も大きいかを分析した詳細な統計レポートを生成してください。レポートには可視化のためのチャートと図を含める必要があります。",
"it-ticket-creation-message": "https://eiti.eigent.ai/ のチケット管理システムにアクセスし、ブラウザエージェントを使用して新しいチケットをシステムに追加してください:\n\n影響を受けるユーザーAlice Johnson\n割り当てグループソフトウェアサービスチーム\n担当者Michael Brown\n優先度4 低 | 緊急度3 中 | 影響4 低\n影響を受けるサービスソフトウェアサービス\n問題アプリケーションパフォーマンスの低下\n説明\n影響を受けるユーザーは、通常の営業時間中に内部ソフトウェアアプリケーションにアクセスする際、応答時間の遅延と断続的なタイムアウトを報告しています。\n\n完了後、「進行中」チケットリストビューに移動し、リストから表示されているチケットデータを抽出してください番号、ユーザー、優先度、状態、問題の列。エクスポートボタンがない場合は、ブラウザコンソールのJavaScriptを使用してテーブルデータをキャプチャしてください。抽出したデータをCSVファイルとして保存してください。\n\n最後に、抽出したデータに基づいて統計レポートを生成してください\n1. 優先度レベル(緊急/高/中/低)別のチケット分布を分析\n2. 問題テキストに記載されている金額(例:「$50M」、「$3.2M」)を推定財務影響として解析\n3. 以下を示す棒グラフ付きのHTMLレポートを作成\n - 優先度別のチケット数\n - 優先度別の推定財務影響(問題テキストから解析)\nシステムに専用の財務フィールドが存在しないため、財務影響値は問題説明テキストから解析されたことを説明する「データート」セクションを含めてください。",
"bank-transfer-csv-analysis-and-visualization": "銀行振込CSV分析と可視化",
"bank-transfer-csv-analysis-and-visualization-message": "10列10行のモック銀行振込CSVファイルを作成してください。生成されたCSVファイルを読み込み、データを要約し、データから関連する傾向や洞察を可視化するためのグラフを生成してください。",
"help-organize-my-desktop": "デスクトップの整理を手伝ってください",

View file

@ -123,7 +123,7 @@
"terms-of-use": "利用規約",
"and": "および",
"it-ticket-creation": "オンラインフォームの入力を手伝ってください",
"it-ticket-creation-message": "(https://eiti.eigent.ai/)のチケット管理システムにアクセスし、ブラウザエージェントを使用してこれらの新しいチケットをすべてシステムに追加してください:'' 影響を受けるユーザーAlice Johnson 割り当てグループ:ソフトウェアサービスチーム 担当者Michael Brown 優先度4 低 | 緊急度3 中 | 影響4 低 影響を受けるサービス:ソフトウェアサービス 問題:アプリケーションパフォーマンスの低下 説明:影響を受けるユーザーは、通常の営業時間中に内部ソフトウェアアプリケーションにアクセスする際、応答時間の遅延と断続的なタイムアウトを報告しています。'' 完了後、受信チケットを確認し、どのIT分野で最も問題が多く、財務的影響が最も大きいかを分析した詳細な統計レポートを生成してください。レポートには可視化のためのチャートと図を含める必要があります。",
"it-ticket-creation-message": "https://eiti.eigent.ai/ のチケット管理システムにアクセスし、ブラウザエージェントを使用して新しいチケットをシステムに追加してください:\n\n影響を受けるユーザーAlice Johnson\n割り当てグループソフトウェアサービスチーム\n担当者Michael Brown\n優先度4 低 | 緊急度3 中 | 影響4 低\n影響を受けるサービスソフトウェアサービス\n問題アプリケーションパフォーマンスの低下\n説明\n影響を受けるユーザーは、通常の営業時間中に内部ソフトウェアアプリケーションにアクセスする際、応答時間の遅延と断続的なタイムアウトを報告しています。\n\n完了後、「進行中」チケットリストビューに移動し、リストから表示されているチケットデータを抽出してください番号、ユーザー、優先度、状態、問題の列。エクスポートボタンがない場合は、ブラウザコンソールのJavaScriptを使用してテーブルデータをキャプチャしてください。抽出したデータをCSVファイルとして保存してください。\n\n最後に、抽出したデータに基づいて統計レポートを生成してください\n1. 優先度レベル(緊急/高/中/低)別のチケット分布を分析\n2. 問題テキストに記載されている金額(例:「$50M」、「$3.2M」)を推定財務影響として解析\n3. 以下を示す棒グラフ付きのHTMLレポートを作成\n - 優先度別のチケット数\n - 優先度別の推定財務影響(問題テキストから解析)\nシステムに専用の財務フィールドが存在しないため、財務影響値は問題説明テキストから解析されたことを説明する「データート」セクションを含めてください。",
"bank-transfer-csv-analysis": "銀行振込CSV分析と可視化",
"bank-transfer-csv-analysis-message": "銀行振込CSVファイルを分析し、支出パターンを示す可視化を作成",
"find-duplicate-files": "ダウンロードフォルダーで重複ファイルを見つける",

View file

@ -261,6 +261,7 @@
"gpt-5.2-name": "GPT-5.2",
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"minimax-m2-5-name": "Minimax M2.5",
"network-proxy": "ネットワークプロキシ",
"network-proxy-description": "ネットワークリクエスト用のプロキシサーバーを設定します。プロキシ経由で外部APIにアクセスする必要がある場合に便利です。",
"proxy-placeholder": "http://127.0.0.1:7890",

Some files were not shown because too many files have changed in this diff Show more