Merge remote-tracking branch 'origin/main' into feat-trigger
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -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)
|
||||
|
|
|
|||
20
.github/workflows/build-view.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
103
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
29
.github/workflows/pre-commit.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ README_PT-BR.md
|
|||
server/README_CN.md
|
||||
server/README_EN.md
|
||||
docs/troubleshooting/bug.md
|
||||
backend/benchmark/answer/
|
||||
|
|
|
|||
|
|
@ -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 project’s 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 🔄
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
4
backend/benchmark/.env.example
Normal 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"
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. =========
|
||||
|
||||
|
|
|
|||
25
backend/benchmark/answer/0/hello_world.py
Normal 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!}": ...
|
||||
|
||||
|
||||
_._
|
||||
7
backend/benchmark/answer/1/python313_features.md
Normal 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`.)
|
||||
77
backend/benchmark/answer/2/yc_w25_b2b_ai.csv
Normal 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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
61
backend/benchmark/checker/1.py
Normal 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)
|
||||
92
backend/benchmark/checker/2.py
Normal 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)
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
22
backend/benchmark/dataset/1.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
16
backend/benchmark/dataset/2.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
139
backend/benchmark/grader/1.py
Normal 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)
|
||||
261
backend/benchmark/grader/2.py
Normal 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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
43
index.html
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const TERMINAL_BASE_PACKAGES = [
|
|||
'openpyxl',
|
||||
'beautifulsoup4',
|
||||
'pillow',
|
||||
'plotly',
|
||||
];
|
||||
|
||||
console.log('🚀 Starting pre-installation of dependencies...');
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
56
src/components/WorkFlow/workflowWheelHandler.ts
Normal 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 });
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "ساعدني في تنظيم سطح المكتب",
|
||||
|
|
|
|||
|
|
@ -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": "العثور على الملفات المكررة في مجلد التنزيلات",
|
||||
|
|
|
|||
|
|
@ -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}} أنت مسجل الدخول حاليًا باستخدام",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "デスクトップの整理を手伝ってください",
|
||||
|
|
|
|||
|
|
@ -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": "ダウンロードフォルダーで重複ファイルを見つける",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||