mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-31 21:39:28 +00:00
Merge branch 'main' into feature/password-reset-884
This commit is contained in:
commit
b73fcf147d
84 changed files with 4016 additions and 635 deletions
120
.github/workflows/build-view.yml
vendored
120
.github/workflows/build-view.yml
vendored
|
|
@ -20,8 +20,72 @@ jobs:
|
|||
arch: x64
|
||||
- os: windows-latest
|
||||
arch: x64
|
||||
- os: ubuntu-latest
|
||||
arch: x64
|
||||
|
||||
steps:
|
||||
- name: Free Disk Space (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
# Remove Xcode completely (not needed for Electron builds) - saves ~15GB
|
||||
sudo rm -rf /Applications/Xcode.app || true
|
||||
sudo rm -rf /Applications/Xcode_*.app || true
|
||||
# Note: Keep /Library/Developer/CommandLineTools as codesign needs it
|
||||
# Remove iOS simulators
|
||||
sudo rm -rf ~/Library/Developer/CoreSimulator || true
|
||||
# Remove all Xcode Developer files
|
||||
sudo rm -rf ~/Library/Developer/Xcode || true
|
||||
sudo rm -rf /Library/Developer/Xcode || true
|
||||
# Remove provisioning profiles
|
||||
sudo rm -rf ~/Library/MobileDevice/Provisioning\ Profiles || true
|
||||
# Remove Android SDK if present
|
||||
sudo rm -rf ~/Library/Android/sdk || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
# Remove .NET
|
||||
sudo rm -rf /usr/local/share/dotnet || true
|
||||
# Remove Go
|
||||
sudo rm -rf /usr/local/go || true
|
||||
sudo rm -rf ~/go || true
|
||||
# Remove Ruby
|
||||
sudo rm -rf /usr/local/lib/ruby || true
|
||||
sudo rm -rf ~/.gem || true
|
||||
# Remove Swift toolchains
|
||||
sudo rm -rf /Library/Developer/Toolchains || true
|
||||
# Remove Homebrew cache
|
||||
rm -rf ~/Library/Caches/Homebrew/* || true
|
||||
brew cleanup --prune=all 2>/dev/null || true
|
||||
# Remove npm cache
|
||||
npm cache clean --force || true
|
||||
# Remove pip cache
|
||||
pip cache purge 2>/dev/null || true
|
||||
# Note: Don't delete ~/Library/Caches/* as subsequent steps may need it
|
||||
|
||||
# Additional cleanup for more disk space
|
||||
# Remove hosted tool cache (can be several GB)
|
||||
sudo rm -rf /Users/runner/hostedtoolcache || true
|
||||
sudo rm -rf /opt/hostedtoolcache || true
|
||||
# Remove browsers (not needed for Electron builds)
|
||||
sudo rm -rf "/Applications/Google Chrome.app" || true
|
||||
sudo rm -rf "/Applications/Firefox.app" || true
|
||||
sudo rm -rf "/Applications/Safari Technology Preview.app" || true
|
||||
# Remove PowerShell
|
||||
sudo rm -rf /usr/local/microsoft/powershell || true
|
||||
sudo rm -rf /usr/local/share/powershell || true
|
||||
# Remove more from /usr/local
|
||||
sudo rm -rf /usr/local/aws-cli || true
|
||||
sudo rm -rf /usr/local/julia* || true
|
||||
sudo rm -rf /usr/local/miniconda || true
|
||||
# Remove unused large directories
|
||||
sudo rm -rf /usr/share/swift || true
|
||||
sudo rm -rf /usr/share/miniconda || true
|
||||
# Remove Docker images if present
|
||||
docker system prune -af 2>/dev/null || true
|
||||
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
|
@ -46,6 +110,23 @@ jobs:
|
|||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
# Install libfuse2 for Linux AppImage builds
|
||||
- name: Install libfuse2 (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
|
||||
# Verify disk space before build
|
||||
- name: Check Disk Space Before Build (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
echo "Disk space available before build:"
|
||||
df -h
|
||||
echo ""
|
||||
echo "Largest directories in home:"
|
||||
du -sh ~/* 2>/dev/null | sort -rh | head -10 || true
|
||||
|
||||
# Step for macOS builds with signing
|
||||
- name: Build Release Files (macOS with signing)
|
||||
if: runner.os == 'macOS'
|
||||
|
|
@ -78,6 +159,19 @@ jobs:
|
|||
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
|
||||
USE_NPM_INSTALL_BUN: 'true'
|
||||
|
||||
# Step for Linux builds
|
||||
- name: Build Release Files (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
timeout-minutes: 90
|
||||
run: npm run build:linux
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
|
||||
VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }}
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
|
||||
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
|
||||
USE_NPM_INSTALL_BUN: 'true'
|
||||
|
||||
- name: Upload Artifact (macOS - dmg only)
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/upload-artifact@v6
|
||||
|
|
@ -95,13 +189,22 @@ jobs:
|
|||
path: |
|
||||
release/*.exe
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload Artifact (Linux - AppImage only)
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: |
|
||||
release/*.AppImage
|
||||
retention-days: 5
|
||||
merge-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create directories
|
||||
run: |
|
||||
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64
|
||||
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64
|
||||
|
||||
# Download all artifacts with correct names
|
||||
- name: Download mac-x64 artifact
|
||||
|
|
@ -122,7 +225,13 @@ jobs:
|
|||
name: release-windows-latest-x64
|
||||
path: temp-win-x64
|
||||
|
||||
# Move only dmg files for macOS and exe files for Windows
|
||||
- name: Download linux-x64 artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-ubuntu-latest-x64
|
||||
path: temp-linux-x64
|
||||
|
||||
# Move only dmg files for macOS, exe files for Windows, and AppImage for Linux
|
||||
- name: Move files to clean folders
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
@ -146,3 +255,10 @@ jobs:
|
|||
else
|
||||
find temp-win-x64 -name "*.exe" -exec mv {} release/win-x64/ \; || true
|
||||
fi
|
||||
|
||||
# linux-x64 - only move AppImage files
|
||||
if [ -d "temp-linux-x64/release" ]; then
|
||||
find temp-linux-x64/release -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
|
||||
else
|
||||
find temp-linux-x64 -name "*.AppImage" -exec mv {} release/linux-x64/ \; || true
|
||||
fi
|
||||
|
|
|
|||
110
.github/workflows/build.yml
vendored
110
.github/workflows/build.yml
vendored
|
|
@ -31,8 +31,72 @@ jobs:
|
|||
arch: x64
|
||||
- os: windows-latest
|
||||
arch: x64
|
||||
- os: ubuntu-latest
|
||||
arch: x64
|
||||
|
||||
steps:
|
||||
- name: Free Disk Space (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
# Remove Xcode completely (not needed for Electron builds) - saves ~15GB
|
||||
sudo rm -rf /Applications/Xcode.app || true
|
||||
sudo rm -rf /Applications/Xcode_*.app || true
|
||||
# Note: Keep /Library/Developer/CommandLineTools as codesign needs it
|
||||
# Remove iOS simulators
|
||||
sudo rm -rf ~/Library/Developer/CoreSimulator || true
|
||||
# Remove all Xcode Developer files
|
||||
sudo rm -rf ~/Library/Developer/Xcode || true
|
||||
sudo rm -rf /Library/Developer/Xcode || true
|
||||
# Remove provisioning profiles
|
||||
sudo rm -rf ~/Library/MobileDevice/Provisioning\ Profiles || true
|
||||
# Remove Android SDK if present
|
||||
sudo rm -rf ~/Library/Android/sdk || true
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
# Remove .NET
|
||||
sudo rm -rf /usr/local/share/dotnet || true
|
||||
# Remove Go
|
||||
sudo rm -rf /usr/local/go || true
|
||||
sudo rm -rf ~/go || true
|
||||
# Remove Ruby
|
||||
sudo rm -rf /usr/local/lib/ruby || true
|
||||
sudo rm -rf ~/.gem || true
|
||||
# Remove Swift toolchains
|
||||
sudo rm -rf /Library/Developer/Toolchains || true
|
||||
# Remove Homebrew cache
|
||||
rm -rf ~/Library/Caches/Homebrew/* || true
|
||||
brew cleanup --prune=all 2>/dev/null || true
|
||||
# Remove npm cache
|
||||
npm cache clean --force || true
|
||||
# Remove pip cache
|
||||
pip cache purge 2>/dev/null || true
|
||||
# Note: Don't delete ~/Library/Caches/* as subsequent steps may need it
|
||||
|
||||
# Additional cleanup for more disk space
|
||||
# Remove hosted tool cache (can be several GB)
|
||||
sudo rm -rf /Users/runner/hostedtoolcache || true
|
||||
sudo rm -rf /opt/hostedtoolcache || true
|
||||
# Remove browsers (not needed for Electron builds)
|
||||
sudo rm -rf "/Applications/Google Chrome.app" || true
|
||||
sudo rm -rf "/Applications/Firefox.app" || true
|
||||
sudo rm -rf "/Applications/Safari Technology Preview.app" || true
|
||||
# Remove PowerShell
|
||||
sudo rm -rf /usr/local/microsoft/powershell || true
|
||||
sudo rm -rf /usr/local/share/powershell || true
|
||||
# Remove more from /usr/local
|
||||
sudo rm -rf /usr/local/aws-cli || true
|
||||
sudo rm -rf /usr/local/julia* || true
|
||||
sudo rm -rf /usr/local/miniconda || true
|
||||
# Remove unused large directories
|
||||
sudo rm -rf /usr/share/swift || true
|
||||
sudo rm -rf /usr/share/miniconda || true
|
||||
# Remove Docker images if present
|
||||
docker system prune -af 2>/dev/null || true
|
||||
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
|
@ -54,6 +118,23 @@ jobs:
|
|||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
# Install libfuse2 for Linux AppImage builds
|
||||
- name: Install libfuse2 (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
|
||||
# Verify disk space before build
|
||||
- name: Check Disk Space Before Build (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
echo "Disk space available before build:"
|
||||
df -h
|
||||
echo ""
|
||||
echo "Largest directories in home:"
|
||||
du -sh ~/* 2>/dev/null | sort -rh | head -10 || true
|
||||
|
||||
# Step for macOS builds with signing
|
||||
- name: Build Release Files (macOS with signing)
|
||||
if: runner.os == 'macOS'
|
||||
|
|
@ -82,6 +163,17 @@ jobs:
|
|||
VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
|
||||
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
|
||||
|
||||
# Step for Linux builds
|
||||
- name: Build Release Files (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: npm run build:linux
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VITE_BASE_URL: ${{ secrets.VITE_BASE_URL }}
|
||||
VITE_STACK_PROJECT_ID: ${{ secrets.VITE_STACK_PROJECT_ID }}
|
||||
VITE_STACK_PUBLISHABLE_CLIENT_KEY: ${{ secrets.VITE_STACK_PUBLISHABLE_CLIENT_KEY }}
|
||||
VITE_STACK_SECRET_SERVER_KEY: ${{ secrets.VITE_STACK_SECRET_SERVER_KEY }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
|
|
@ -97,7 +189,7 @@ jobs:
|
|||
steps:
|
||||
- name: Create directories
|
||||
run: |
|
||||
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64
|
||||
mkdir -p release/mac-x64 release/mac-arm64 release/win-x64 release/linux-x64
|
||||
|
||||
# Download all artifacts with correct names
|
||||
- name: Download mac-x64 artifact
|
||||
|
|
@ -118,6 +210,12 @@ jobs:
|
|||
name: release-windows-latest-x64
|
||||
path: temp-win-x64
|
||||
|
||||
- name: Download linux-x64 artifact
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-ubuntu-latest-x64
|
||||
path: temp-linux-x64
|
||||
|
||||
# Move files to final release directory, removing any nested release/ directory
|
||||
- name: Move files to clean folders
|
||||
shell: bash
|
||||
|
|
@ -143,6 +241,13 @@ jobs:
|
|||
mv temp-win-x64/* release/win-x64/ || true
|
||||
fi
|
||||
|
||||
# linux-x64
|
||||
if [ -d "temp-linux-x64/release" ]; then
|
||||
mv temp-linux-x64/release/* release/linux-x64/ || true
|
||||
else
|
||||
mv temp-linux-x64/* release/linux-x64/ || true
|
||||
fi
|
||||
|
||||
- name: Rename duplicate files
|
||||
run: |
|
||||
mv release/mac-x64/latest-mac.yml release/mac-x64/latest-x64-mac.yml || true
|
||||
|
|
@ -157,5 +262,6 @@ jobs:
|
|||
release/mac-x64/*
|
||||
release/mac-arm64/*
|
||||
release/win-x64/*
|
||||
release/linux-x64/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -59,3 +59,6 @@ __pycache__/
|
|||
resources/prebuilt/bin/
|
||||
resources/prebuilt/venv/
|
||||
resources/prebuilt/cache/
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
|
|
|||
13
.storybook/main.ts
Normal file
13
.storybook/main.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { StorybookConfig } from '@storybook/react-vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: ['@storybook/addon-docs', '@storybook/addon-a11y'],
|
||||
framework: '@storybook/react-vite',
|
||||
viteFinal: async (config) => {
|
||||
// Reuse project's vite config for path aliases
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
47
.storybook/preview.tsx
Normal file
47
.storybook/preview.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { Preview } from '@storybook/react-vite'
|
||||
import React from 'react'
|
||||
import '@fontsource/inter/400.css'
|
||||
import '@fontsource/inter/500.css'
|
||||
import '@fontsource/inter/600.css'
|
||||
import '@fontsource/inter/700.css'
|
||||
import '@fontsource/inter/800.css'
|
||||
import '../src/style/index.css'
|
||||
import './storybook.css' // Storybook-specific overrides
|
||||
import { Toaster } from 'sonner'
|
||||
|
||||
// Apply theme immediately via script
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
document.documentElement.classList.add('root')
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
controls: {
|
||||
expanded: true,
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{ name: 'light', value: '#f5f5f5' },
|
||||
{ name: 'dark', value: '#1d1c1b' },
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="root" data-theme="light" style={{ padding: '1rem' }}>
|
||||
<Story />
|
||||
<Toaster style={{ zIndex: '999999 !important', position: 'fixed' }} />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export default preview
|
||||
1
.storybook/storybook.css
Normal file
1
.storybook/storybook.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* Storybook-specific CSS overrides - currently empty as component handles styling */
|
||||
27
README.md
27
README.md
|
|
@ -116,6 +116,19 @@ npm run dev
|
|||
|
||||
> Note: This mode connects to Eigent cloud services and requires account registration. For a fully standalone experience, use [Local Deployment](#-local-deployment-recommended) instead.
|
||||
|
||||
#### Updating Dependencies
|
||||
|
||||
After pulling new code (`git pull`), update both frontend and backend dependencies:
|
||||
|
||||
```bash
|
||||
# 1. Update frontend dependencies (in project root)
|
||||
npm install
|
||||
|
||||
# 2. Update backend/Python dependencies (in backend directory)
|
||||
cd backend
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 🏢 Enterprise
|
||||
|
||||
For organizations requiring maximum security, customization, and control:
|
||||
|
|
@ -294,13 +307,13 @@ Please add this signature image to the Signature Areas in the PDF. You could ins
|
|||
|
||||
| Topics | Issues | Discord Channel |
|
||||
| ------------------------ | -- |-- |
|
||||
| **Context Engineering** | - Prompt caching<br> - System prompt optimize<br> - Toolkit docstring optimize<br> - Context compression | [**Join Discord →**](https://discord.gg/D2e3rBWD) |
|
||||
| **Multi-modal Enhancement** | - More accurate image understanding when using browser<br> - Advanced video generation | [**Join Discord →**](https://discord.gg/kyapNCeJ) |
|
||||
| **Multi-agent system** | - Workforce support fixed workflow<br> - Workforce support multi-round conversion | [**Join Discord →**](https://discord.gg/bFRmPuDB) |
|
||||
| **Browser Toolkit** | - BrowseComp integration<br> - Benchmark improvement<br> - Forbid repeated page visiting<br> - Automatic cache button clicking | [**Join Discord →**](https://discord.gg/NF73ze5v) |
|
||||
| **Document Toolkit** | - Support dynamic file editing | [**Join Discord →**](https://discord.gg/4yAWJxYr) |
|
||||
| **Terminal Toolkit** | - Benchmark improvement<br> - Terminal-Bench integration | [**Join Discord →**](https://discord.gg/FjQfnsrV) |
|
||||
| **Environment & RL** | - Environment design<br> - Data-generation<br> - RL framework integration (VERL, TRL, OpenRLHF) | [**Join Discord →**](https://discord.gg/MaVZXEn8) |
|
||||
| **Context Engineering** | - Prompt caching<br> - System prompt optimize<br> - Toolkit docstring optimize<br> - Context compression | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Multi-modal Enhancement** | - More accurate image understanding when using browser<br> - Advanced video generation | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Multi-agent system** | - Workforce support fixed workflow<br> - Workforce support multi-round conversion | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Browser Toolkit** | - BrowseComp integration<br> - Benchmark improvement<br> - Forbid repeated page visiting<br> - Automatic cache button clicking | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Document Toolkit** | - Support dynamic file editing | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Terminal Toolkit** | - Benchmark improvement<br> - Terminal-Bench integration | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Environment & RL** | - Environment design<br> - Data-generation<br> - RL framework integration (VERL, TRL, OpenRLHF) | [**Join Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
|
||||
|
||||
## [🤝 Contributing][contribution-link]
|
||||
|
|
|
|||
27
README_CN.md
27
README_CN.md
|
|
@ -124,6 +124,19 @@ npm run dev
|
|||
#### 3. 本地开发(使用完全和云端服务分离的版本)
|
||||
[server/README_CN.md](./server/README_CN.md)
|
||||
|
||||
#### 4. 更新依赖
|
||||
|
||||
拉取新代码(`git pull`)后,需要分别更新前端和后端依赖:
|
||||
|
||||
```bash
|
||||
# 1. 更新前端依赖(在项目根目录)
|
||||
npm install
|
||||
|
||||
# 2. 更新后端/Python 依赖(在 backend 目录)
|
||||
cd backend
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 🏢 企业版
|
||||
|
||||
适合需要最高安全性、定制化和控制的组织:
|
||||
|
|
@ -281,13 +294,13 @@ Eigent 完全开源。您可以下载、检查和修改代码,确保透明度
|
|||
|
||||
| 主题 | 问题 | Discord 频道 |
|
||||
| ------------------------ | -- |-- |
|
||||
| **上下文工程** | - 提示缓存<br> - 系统提示优化<br> - 工具包文档优化<br> - 上下文压缩 | [**加入 Discord →**](https://discord.gg/D2e3rBWD) |
|
||||
| **多模态增强** | - 使用浏览器时更准确的图像理解<br> - 高级视频生成 | [**加入 Discord →**](https://discord.gg/kyapNCeJ) |
|
||||
| **多智能体系统** | - 工作流支持固定流程<br> - 工作流支持多轮对话 | [**加入 Discord →**](https://discord.gg/bFRmPuDB) |
|
||||
| **浏览器工具包** | - BrowseComp 集成<br> - 基准测试改进<br> - 禁止重复访问页面<br> - 自动缓存按钮点击 | [**加入 Discord →**](https://discord.gg/NF73ze5v) |
|
||||
| **文档工具包** | - 支持动态文件编辑 | [**加入 Discord →**](https://discord.gg/4yAWJxYr) |
|
||||
| **终端工具包** | - 基准测试改进<br> - Terminal-Bench 集成 | [**加入 Discord →**](https://discord.gg/FjQfnsrV) |
|
||||
| **环境与强化学习** | - 环境设计<br> - 数据生成<br> - 强化学习框架集成(VERL, TRL, OpenRLHF) | [**加入 Discord →**](https://discord.gg/MaVZXEn8) |
|
||||
| **上下文工程** | - 提示缓存<br> - 系统提示优化<br> - 工具包文档优化<br> - 上下文压缩 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **多模态增强** | - 使用浏览器时更准确的图像理解<br> - 高级视频生成 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **多智能体系统** | - 工作流支持固定流程<br> - 工作流支持多轮对话 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **浏览器工具包** | - BrowseComp 集成<br> - 基准测试改进<br> - 禁止重复访问页面<br> - 自动缓存按钮点击 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **文档工具包** | - 支持动态文件编辑 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **终端工具包** | - 基准测试改进<br> - Terminal-Bench 集成 | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **环境与强化学习** | - 环境设计<br> - 数据生成<br> - 强化学习框架集成(VERL, TRL, OpenRLHF) | [**加入 Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
|
||||
## [🤝 贡献][contribution-link]
|
||||
|
||||
|
|
|
|||
27
README_JA.md
27
README_JA.md
|
|
@ -114,6 +114,19 @@ npm run dev
|
|||
|
||||
> 注:このモードはEigentクラウドサービスに接続し、アカウント登録が必要です。完全にスタンドアロンで使用する場合は、代わりに[ローカルデプロイメント](#-ローカルデプロイメント推奨)を使用してください。
|
||||
|
||||
#### 依存関係の更新
|
||||
|
||||
新しいコードを取得(`git pull`)した後、フロントエンドとバックエンドの両方の依存関係を更新します:
|
||||
|
||||
```bash
|
||||
# 1. フロントエンド依存関係を更新(プロジェクトルートで)
|
||||
npm install
|
||||
|
||||
# 2. バックエンド/Python依存関係を更新(backendディレクトリで)
|
||||
cd backend
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 🏢 エンタープライズ
|
||||
|
||||
最大限のセキュリティ、カスタマイズ、制御を必要とする組織向け:
|
||||
|
|
@ -292,13 +305,13 @@ Documentsディレクトリにmydocsというフォルダがあります。ス
|
|||
|
||||
| トピック | 課題 | Discordチャンネル |
|
||||
| ------------------------ | -- |-- |
|
||||
| **コンテキストエンジニアリング** | - プロンプトキャッシング<br> - システムプロンプト最適化<br> - ツールキットdocstring最適化<br> - コンテキスト圧縮 | [**Discordに参加 →**](https://discord.gg/D2e3rBWD) |
|
||||
| **マルチモーダル強化** | - ブラウザ使用時のより正確な画像理解<br> - 高度な動画生成 | [**Discordに参加 →**](https://discord.gg/kyapNCeJ) |
|
||||
| **マルチエージェントシステム** | - 固定ワークフローをサポートするワークフォース<br> - マルチラウンド変換をサポートするワークフォース | [**Discordに参加 →**](https://discord.gg/bFRmPuDB) |
|
||||
| **ブラウザツールキット** | - BrowseComp統合<br> - ベンチマーク改善<br> - 繰り返しページ訪問の禁止<br> - 自動キャッシュボタンクリック | [**Discordに参加 →**](https://discord.gg/NF73ze5v) |
|
||||
| **ドキュメントツールキット** | - 動的ファイル編集のサポート | [**Discordに参加 →**](https://discord.gg/4yAWJxYr) |
|
||||
| **ターミナルツールキット** | - ベンチマーク改善<br> - Terminal-Bench統合 | [**Discordに参加 →**](https://discord.gg/FjQfnsrV) |
|
||||
| **環境 & RL** | - 環境設計<br> - データ生成<br> - RLフレームワーク統合(VERL、TRL、OpenRLHF) | [**Discordに参加 →**](https://discord.gg/MaVZXEn8) |
|
||||
| **コンテキストエンジニアリング** | - プロンプトキャッシング<br> - システムプロンプト最適化<br> - ツールキットdocstring最適化<br> - コンテキスト圧縮 | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **マルチモーダル強化** | - ブラウザ使用時のより正確な画像理解<br> - 高度な動画生成 | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **マルチエージェントシステム** | - 固定ワークフローをサポートするワークフォース<br> - マルチラウンド変換をサポートするワークフォース | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **ブラウザツールキット** | - BrowseComp統合<br> - ベンチマーク改善<br> - 繰り返しページ訪問の禁止<br> - 自動キャッシュボタンクリック | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **ドキュメントツールキット** | - 動的ファイル編集のサポート | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **ターミナルツールキット** | - ベンチマーク改善<br> - Terminal-Bench統合 | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **環境 & RL** | - 環境設計<br> - データ生成<br> - RLフレームワーク統合(VERL、TRL、OpenRLHF) | [**Discordに参加 →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
|
||||
|
||||
## [🤝 コントリビューション][contribution-link]
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], noss
|
|||
|
||||
A forma recomendada de executar o Eigent — totalmente independente, com controle completo sobre seus dados, sem necessidade de conta em nuvem.
|
||||
|
||||
👉 **[Guia Completo de Implantação Local](./server/README_EN.md)**
|
||||
👉 **[Guia Completo de Implantação Local](./server/README_PT-BR.md)**
|
||||
|
||||
Esta configuração inclui:
|
||||
- Servidor backend local com API completa
|
||||
|
|
@ -115,6 +115,19 @@ npm run dev
|
|||
|
||||
> Nota: Este modo se conecta aos serviços em nuvem do Eigent e requer registro de conta. Para uma experiência totalmente independente, utilize a [Implantação Local](#-implantação-local-recomendado) em vez disso.
|
||||
|
||||
#### Atualizando Dependências
|
||||
|
||||
Após baixar novo código (`git pull`), atualize as dependências do frontend e do backend:
|
||||
|
||||
```bash
|
||||
# 1. Atualizar dependências do frontend (no diretório raiz do projeto)
|
||||
npm install
|
||||
|
||||
# 2. Atualizar dependências do backend/Python (no diretório backend)
|
||||
cd backend
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 🏢 Empresarial
|
||||
|
||||
Para organizações que requerem máxima segurança, personalização e controle:
|
||||
|
|
@ -289,13 +302,13 @@ Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. V
|
|||
|
||||
| Tópicos | Issues | Canal do Discord |
|
||||
| ------------------------- | -- |-- |
|
||||
| **Engenharia de Contexto** | - Cache de prompts<br> - Otimização de prompt do sistema<br> - Otimização de docstrings do toolkit<br> - Compressão de contexto | [**Entrar no Discord →**](https://discord.gg/D2e3rBWD) |
|
||||
| **Aprimoramento Multimodal** | - Compreensão de imagens mais precisa ao usar o navegador<br> - Geração avançada de vídeo | [**Entrar no Discord →**](https://discord.gg/kyapNCeJ) |
|
||||
| **Sistema Multiagente** | - Suporte do Workforce a fluxos fixos<br> - Suporte do Workforce a conversas em múltiplas rodadas | [**Entrar no Discord →**](https://discord.gg/bFRmPuDB) |
|
||||
| **Toolkit de Navegador** | - Integração com BrowseComp<br> - Melhoria de benchmark<br> - Proibir visitas repetidas a páginas<br> - Clique automático em botões de cache | [**Entrar no Discord →**](https://discord.gg/NF73ze5v) |
|
||||
| **Toolkit de Documentos** | - Suporte à edição dinâmica de arquivos | [**Entrar no Discord →**](https://discord.gg/4yAWJxYr) |
|
||||
| **Toolkit de Terminal** | - Melhoria de benchmark<br> - Integração com Terminal-Bench | [**Entrar no Discord →**](https://discord.gg/FjQfnsrV) |
|
||||
| **Ambiente & RL** | - Design de ambiente<br> - Geração de dados<br> - Integração de frameworks de RL (VERL, TRL, OpenRLHF) | [**Entrar no Discord →**](https://discord.gg/MaVZXEn8) |
|
||||
| **Engenharia de Contexto** | - Cache de prompts<br> - Otimização de prompt do sistema<br> - Otimização de docstrings do toolkit<br> - Compressão de contexto | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Aprimoramento Multimodal** | - Compreensão de imagens mais precisa ao usar o navegador<br> - Geração avançada de vídeo | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Sistema Multiagente** | - Suporte do Workforce a fluxos fixos<br> - Suporte do Workforce a conversas em múltiplas rodadas | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Toolkit de Navegador** | - Integração com BrowseComp<br> - Melhoria de benchmark<br> - Proibir visitas repetidas a páginas<br> - Clique automático em botões de cache | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Toolkit de Documentos** | - Suporte à edição dinâmica de arquivos | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Toolkit de Terminal** | - Melhoria de benchmark<br> - Integração com Terminal-Bench | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
| **Ambiente & RL** | - Design de ambiente<br> - Geração de dados<br> - Integração de frameworks de RL (VERL, TRL, OpenRLHF) | [**Entrar no Discord →**](https://discord.com/invite/CNcNpquyDc) |
|
||||
|
||||
|
||||
## [🤝 Contribuição][contribution-link]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ def create_agent(
|
|||
model_type=mtype,
|
||||
api_key=api_key,
|
||||
url=url,
|
||||
timeout=10,
|
||||
timeout=60, # 1 minute for validation
|
||||
model_config_dict=model_config_dict,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -37,6 +37,6 @@ def create_agent(
|
|||
system_message="You are a helpful assistant that must use the tool get_website_content to get the content of a website.",
|
||||
model=model,
|
||||
tools=[get_website_content],
|
||||
step_timeout=900,
|
||||
step_timeout=1800, # 30 minutes
|
||||
)
|
||||
return agent
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ from app.service.task import (
|
|||
get_or_create_task_lock,
|
||||
get_task_lock,
|
||||
set_current_task_id,
|
||||
delete_task_lock,
|
||||
task_locks,
|
||||
)
|
||||
from app.component.environment import set_user_env_path
|
||||
from app.utils.workforce import Workforce
|
||||
|
|
@ -34,18 +36,50 @@ router = APIRouter()
|
|||
# Create traceroot logger for chat controller
|
||||
chat_logger = traceroot.get_logger("chat_controller")
|
||||
|
||||
# SSE timeout configuration (30 minutes in seconds)
|
||||
SSE_TIMEOUT_SECONDS = 30 * 60
|
||||
# SSE timeout configuration (60 minutes in seconds)
|
||||
SSE_TIMEOUT_SECONDS = 60 * 60
|
||||
|
||||
|
||||
async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS):
|
||||
async def _cleanup_task_lock_safe(task_lock, reason: str) -> bool:
|
||||
"""Safely cleanup task lock with existence check.
|
||||
|
||||
Args:
|
||||
task_lock: The task lock to cleanup
|
||||
reason: Reason for cleanup (for logging)
|
||||
|
||||
Returns:
|
||||
True if cleanup was performed, False otherwise
|
||||
"""
|
||||
Wraps a stream generator with timeout handling.
|
||||
if not task_lock:
|
||||
return False
|
||||
|
||||
# Check if task_lock still exists before attempting cleanup
|
||||
if task_lock.id not in task_locks:
|
||||
chat_logger.debug(f"[{reason}] Task lock already removed, skipping cleanup",
|
||||
extra={"task_id": task_lock.id})
|
||||
return False
|
||||
|
||||
try:
|
||||
task_lock.status = Status.done
|
||||
await delete_task_lock(task_lock.id)
|
||||
chat_logger.info(f"[{reason}] Task lock cleanup completed",
|
||||
extra={"task_id": task_lock.id})
|
||||
return True
|
||||
except Exception as e:
|
||||
chat_logger.error(f"[{reason}] Failed to cleanup task lock",
|
||||
extra={"task_id": task_lock.id, "error": str(e)}, exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS, task_lock=None):
|
||||
"""Wraps a stream generator with timeout handling.
|
||||
|
||||
Closes the SSE connection if no data is received within the timeout period.
|
||||
Triggers cleanup if timeout occurs to prevent resource leaks.
|
||||
"""
|
||||
last_data_time = time.time()
|
||||
generator = stream_generator.__aiter__()
|
||||
cleanup_triggered = False
|
||||
|
||||
try:
|
||||
while True:
|
||||
|
|
@ -57,17 +91,24 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
|
|||
last_data_time = time.time()
|
||||
yield data
|
||||
except asyncio.TimeoutError:
|
||||
chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection")
|
||||
chat_logger.warning("SSE timeout: No data received, closing connection",
|
||||
extra={"timeout_seconds": timeout_seconds})
|
||||
yield sse_json("error", {"message": f"Connection timeout: No data received for {timeout_seconds // 60} minutes"})
|
||||
cleanup_triggered = await _cleanup_task_lock_safe(task_lock, "TIMEOUT")
|
||||
break
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
except asyncio.CancelledError:
|
||||
chat_logger.info("Stream cancelled")
|
||||
chat_logger.info("[STREAM-CANCELLED] Stream cancelled, triggering cleanup")
|
||||
if not cleanup_triggered:
|
||||
await _cleanup_task_lock_safe(task_lock, "CANCELLED")
|
||||
raise
|
||||
except Exception as e:
|
||||
chat_logger.error(f"Error in stream wrapper: {e}", exc_info=True)
|
||||
chat_logger.error("[STREAM-ERROR] Unexpected error in stream wrapper",
|
||||
extra={"error": str(e)}, exc_info=True)
|
||||
if not cleanup_triggered:
|
||||
await _cleanup_task_lock_safe(task_lock, "ERROR")
|
||||
raise
|
||||
|
||||
|
||||
|
|
@ -75,8 +116,10 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI
|
|||
@traceroot.trace()
|
||||
async def post(data: Chat, request: Request):
|
||||
chat_logger.info(
|
||||
"Starting new chat session", extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
|
||||
"Starting new chat session",
|
||||
extra={"project_id": data.project_id, "task_id": data.task_id, "user": data.email}
|
||||
)
|
||||
|
||||
task_lock = get_or_create_task_lock(data.project_id)
|
||||
|
||||
# Set user-specific environment path for this thread
|
||||
|
|
@ -92,9 +135,9 @@ async def post(data: Chat, request: Request):
|
|||
# Set user-specific search engine configuration if provided
|
||||
if data.search_config:
|
||||
for key, value in data.search_config.items():
|
||||
if value: # Only set non-empty values
|
||||
if value:
|
||||
os.environ[key] = value
|
||||
chat_logger.info(f"Set search config: {key}", extra={"project_id": data.project_id})
|
||||
chat_logger.debug(f"Set search config: {key}", extra={"project_id": data.project_id})
|
||||
|
||||
email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", data.email.split("@")[0]).strip(".")
|
||||
camel_log = (
|
||||
|
|
@ -119,11 +162,11 @@ async def post(data: Chat, request: Request):
|
|||
await task_lock.put_queue(ActionImproveData(data=data.question, new_task_id=data.task_id))
|
||||
|
||||
chat_logger.info(
|
||||
"Chat session initialized, starting streaming response",
|
||||
"Chat session initialized",
|
||||
extra={"project_id": data.project_id, "task_id": data.task_id, "log_dir": str(camel_log)},
|
||||
)
|
||||
return StreamingResponse(
|
||||
timeout_stream_wrapper(step_solve(data, request, task_lock)), media_type="text/event-stream"
|
||||
timeout_stream_wrapper(step_solve(data, request, task_lock), task_lock=task_lock), media_type="text/event-stream"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -314,5 +357,5 @@ def skip_task(project_id: str):
|
|||
return Response(status_code=201)
|
||||
|
||||
except Exception as e:
|
||||
chat_logger.error(f"[STOP-BUTTON] ❌ Error skipping task for project_id: {project_id}: {e}")
|
||||
chat_logger.error(f"[STOP-BUTTON] Error skipping task for project_id: {project_id}: {e}")
|
||||
raise UserException(code.error, f"Failed to skip task: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from app.utils.agent import (
|
|||
social_medium_agent,
|
||||
task_summary_agent,
|
||||
question_confirm_agent,
|
||||
set_main_event_loop,
|
||||
)
|
||||
from app.service.task import Action, Agents
|
||||
from app.utils.server.sync_step import sync_step
|
||||
|
|
@ -153,7 +154,7 @@ def collect_previous_task_context(working_directory: str, previous_task_content:
|
|||
return "\n".join(context_parts)
|
||||
|
||||
|
||||
def check_conversation_history_length(task_lock: TaskLock, max_length: int = 100000) -> tuple[bool, int]:
|
||||
def check_conversation_history_length(task_lock: TaskLock, max_length: int = 200000) -> tuple[bool, int]:
|
||||
"""
|
||||
Check if conversation history exceeds maximum length
|
||||
|
||||
|
|
@ -237,15 +238,9 @@ def build_context_for_workforce(task_lock: TaskLock, options: Chat) -> str:
|
|||
@sync_step
|
||||
@traceroot.trace()
|
||||
async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
||||
# if True:
|
||||
# import faulthandler
|
||||
|
||||
# faulthandler.enable()
|
||||
# for second in [5, 10, 20, 30, 60, 120, 240]:
|
||||
# faulthandler.dump_traceback_later(second)
|
||||
|
||||
start_event_loop = True
|
||||
|
||||
# Initialize task_lock attributes
|
||||
if not hasattr(task_lock, 'conversation_history'):
|
||||
task_lock.conversation_history = []
|
||||
if not hasattr(task_lock, 'last_task_result'):
|
||||
|
|
@ -258,15 +253,15 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
# Create or reuse persistent question_agent
|
||||
if task_lock.question_agent is None:
|
||||
task_lock.question_agent = question_confirm_agent(options)
|
||||
logger.info(f"Created new persistent question_agent for project {options.project_id}")
|
||||
else:
|
||||
logger.info(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
|
||||
logger.debug(f"Reusing existing question_agent with {len(task_lock.conversation_history)} history entries")
|
||||
|
||||
question_agent = task_lock.question_agent
|
||||
|
||||
# Other variables
|
||||
camel_task = None
|
||||
workforce = None
|
||||
mcp = None
|
||||
last_completed_task_result = "" # Track the last completed task result
|
||||
summary_task_content = "" # Track task summary
|
||||
loop_iteration = 0
|
||||
|
|
@ -339,14 +334,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
})
|
||||
continue
|
||||
|
||||
# Simplified logic: attachments mean workforce, otherwise let agent decide
|
||||
# Determine task complexity: attachments mean workforce, otherwise let agent decide
|
||||
is_complex_task: bool
|
||||
if len(options.attaches) > 0:
|
||||
# Questions with attachments always need workforce
|
||||
is_complex_task = True
|
||||
logger.info(f"[NEW-QUESTION] Has attachments, treating as complex task")
|
||||
else:
|
||||
logger.info(f"[NEW-QUESTION] Calling question_confirm to determine complexity")
|
||||
is_complex_task = await question_confirm(question_agent, question, task_lock)
|
||||
logger.info(f"[NEW-QUESTION] question_confirm result: is_complex={is_complex_task}")
|
||||
|
||||
|
|
@ -386,56 +379,35 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
except Exception as e:
|
||||
logger.error(f"Error cleaning up folder: {e}")
|
||||
else:
|
||||
logger.info(f"[NEW-QUESTION] 🔧 Complex task, creating workforce and decomposing")
|
||||
logger.info(f"[NEW-QUESTION] Complex task, creating workforce and decomposing")
|
||||
# Update the sync_step with new task_id
|
||||
if hasattr(item, 'new_task_id') and item.new_task_id:
|
||||
set_current_task_id(options.project_id, item.new_task_id)
|
||||
# Reset summary generation flag for new tasks to ensure proper summaries
|
||||
task_lock.summary_generated = False
|
||||
logger.info("[NEW-QUESTION] Reset summary_generated flag for new task", extra={"project_id": options.project_id, "new_task_id": item.new_task_id})
|
||||
|
||||
logger.info(f"[NEW-QUESTION] Sending 'confirmed' SSE to frontend")
|
||||
yield sse_json("confirmed", {"question": question})
|
||||
|
||||
logger.info(f"[NEW-QUESTION] Building context for coordinator")
|
||||
context_for_coordinator = build_context_for_workforce(task_lock, options)
|
||||
|
||||
# Check if workforce exists - if so, reuse it (agents are preserved)
|
||||
# Otherwise create new workforce
|
||||
# Check if workforce exists - if so, reuse it; otherwise create new workforce
|
||||
if workforce is not None:
|
||||
logger.info(f"[NEW-QUESTION] 🔄 Workforce exists (id={id(workforce)}), state={workforce._state.name}, _running={workforce._running}")
|
||||
logger.info(f"[NEW-QUESTION] ✅ Reusing existing workforce with preserved agents")
|
||||
# Workforce is already stopped from skip_task, ready for new decomposition
|
||||
logger.debug(f"[NEW-QUESTION] Reusing existing workforce (id={id(workforce)})")
|
||||
else:
|
||||
logger.info(f"[NEW-QUESTION] 🏭 Creating NEW workforce instance (workforce=None)")
|
||||
logger.info(f"[NEW-QUESTION] Creating NEW workforce instance")
|
||||
(workforce, mcp) = await construct_workforce(options)
|
||||
logger.info(f"[NEW-QUESTION] ✅ NEW Workforce instance created, id={id(workforce)}")
|
||||
for new_agent in options.new_agents:
|
||||
workforce.add_single_agent_worker(
|
||||
format_agent_description(new_agent), await new_agent_model(new_agent, options)
|
||||
)
|
||||
task_lock.status = Status.confirmed
|
||||
|
||||
# If camel_task already exists (from previous paused task), add new question as subtask
|
||||
# Otherwise, create a new camel_task
|
||||
if camel_task is not None:
|
||||
logger.info(f"[NEW-QUESTION] 🔄 camel_task exists (id={camel_task.id}), adding new question as context")
|
||||
# Update the task content with new question
|
||||
clean_task_content = question + options.summary_prompt
|
||||
logger.info(f"[NEW-QUESTION] Updating existing camel_task content with new question")
|
||||
# We keep the existing task structure but update content for new decomposition
|
||||
camel_task = Task(content=clean_task_content, id=options.task_id)
|
||||
if len(options.attaches) > 0:
|
||||
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
|
||||
else:
|
||||
clean_task_content = question + options.summary_prompt
|
||||
logger.info(f"[NEW-QUESTION] Creating NEW camel_task with id={options.task_id}")
|
||||
camel_task = Task(content=clean_task_content, id=options.task_id)
|
||||
if len(options.attaches) > 0:
|
||||
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
|
||||
# Create camel_task for the question
|
||||
clean_task_content = question + options.summary_prompt
|
||||
camel_task = Task(content=clean_task_content, id=options.task_id)
|
||||
if len(options.attaches) > 0:
|
||||
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
|
||||
|
||||
# Stream decomposition in background so queue items (decompose_text) are processed immediately
|
||||
logger.info(f"[NEW-QUESTION] 🧩 Starting task decomposition via workforce.eigent_make_sub_tasks")
|
||||
# Stream decomposition in background
|
||||
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
|
||||
state_holder: dict[str, Any] = {"sub_tasks": [], "summary_task": ""}
|
||||
|
||||
|
|
@ -447,8 +419,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
|
||||
def on_stream_text(chunk):
|
||||
try:
|
||||
# With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
|
||||
# We need to calculate the delta to send only new content to frontend
|
||||
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
|
||||
last_content = stream_state["last_content"]
|
||||
|
||||
|
|
@ -486,52 +456,45 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
on_stream_batch,
|
||||
on_stream_text,
|
||||
)
|
||||
|
||||
if stream_state["subtasks"]:
|
||||
sub_tasks = stream_state["subtasks"]
|
||||
state_holder["sub_tasks"] = sub_tasks
|
||||
logger.info(f"[NEW-QUESTION] ✅ Task decomposed into {len(sub_tasks)} subtasks")
|
||||
logger.info(f"Task decomposed into {len(sub_tasks)} subtasks")
|
||||
try:
|
||||
setattr(task_lock, "decompose_sub_tasks", sub_tasks)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"[NEW-QUESTION] Generating task summary")
|
||||
# Generate task summary
|
||||
summary_task_agent = task_summary_agent(options)
|
||||
try:
|
||||
summary_task_content = await asyncio.wait_for(
|
||||
summary_task(summary_task_agent, camel_task), timeout=10
|
||||
)
|
||||
task_lock.summary_generated = True
|
||||
logger.info("[NEW-QUESTION] ✅ Summary generated successfully", extra={"project_id": options.project_id})
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id})
|
||||
task_lock.summary_generated = True
|
||||
fallback_name = "Task"
|
||||
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
|
||||
if content_preview is None:
|
||||
content_preview = ""
|
||||
summary_task_content = (
|
||||
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
)
|
||||
summary_task_content = f"{fallback_name}|{summary_task_content}"
|
||||
summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
summary_task_content = f"Task|{summary_task_content}"
|
||||
except Exception:
|
||||
task_lock.summary_generated = True
|
||||
fallback_name = "Task"
|
||||
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
|
||||
if content_preview is None:
|
||||
content_preview = ""
|
||||
summary_task_content = (
|
||||
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
)
|
||||
summary_task_content = f"{fallback_name}|{summary_task_content}"
|
||||
summary_task_content = (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
summary_task_content = f"Task|{summary_task_content}"
|
||||
|
||||
state_holder["summary_task"] = summary_task_content
|
||||
try:
|
||||
setattr(task_lock, "summary_task_content", summary_task_content)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"[NEW-QUESTION] 📤 Sending to_sub_tasks SSE to frontend (task card)")
|
||||
logger.info(f"[NEW-QUESTION] to_sub_tasks data: task_id={camel_task.id}, summary={summary_task_content[:50]}..., subtasks_count={len(camel_task.subtasks)}")
|
||||
|
||||
payload = {
|
||||
"project_id": options.project_id,
|
||||
"task_id": options.task_id,
|
||||
|
|
@ -541,7 +504,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
"summary_task": summary_task_content,
|
||||
}
|
||||
await task_lock.put_queue(ActionDecomposeProgressData(data=payload))
|
||||
logger.info(f"[NEW-QUESTION] ✅ to_sub_tasks SSE sent")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in background decomposition: {e}", exc_info=True)
|
||||
|
||||
|
|
@ -781,7 +743,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
logger.info(f"[LIFECYCLE] Multi-turn: building context for workforce")
|
||||
context_for_multi_turn = build_context_for_workforce(task_lock, options)
|
||||
|
||||
logger.info(f"[LIFECYCLE] Multi-turn: calling workforce.handle_decompose_append_task for new task decomposition")
|
||||
stream_state = {"subtasks": [], "seen_ids": set(), "last_content": ""}
|
||||
|
||||
def on_stream_batch(new_tasks: list[Task], is_final: bool = False):
|
||||
|
|
@ -792,8 +753,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
|
||||
def on_stream_text(chunk):
|
||||
try:
|
||||
# With task_agent using stream_accumulate=True, chunk.msg.content is accumulated content
|
||||
# We need to calculate the delta to send only new content to frontend
|
||||
accumulated_content = chunk.msg.content if hasattr(chunk, 'msg') and chunk.msg else str(chunk)
|
||||
last_content = stream_state["last_content"]
|
||||
|
||||
|
|
@ -907,6 +866,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
elif item.action == Action.search_mcp:
|
||||
yield sse_json("search_mcp", item.data)
|
||||
elif item.action == Action.install_mcp:
|
||||
if mcp is None:
|
||||
logger.error(f"Cannot install MCP: mcp agent not initialized for project {options.project_id}")
|
||||
yield sse_json("error", {"message": "MCP agent not initialized. Please start a complex task first."})
|
||||
continue
|
||||
task = asyncio.create_task(install_mcp(mcp, item))
|
||||
task_lock.add_background_task(task)
|
||||
elif item.action == Action.terminal:
|
||||
|
|
@ -1300,89 +1263,132 @@ async def get_task_result_with_optional_summary(task: Task, options: Chat) -> st
|
|||
|
||||
@traceroot.trace()
|
||||
async def construct_workforce(options: Chat) -> tuple[Workforce, ListenChatAgent]:
|
||||
logger.info("Constructing workforce", extra={"project_id": options.project_id, "task_id": options.task_id})
|
||||
"""Construct a workforce with all required agents.
|
||||
|
||||
This function creates all agents in PARALLEL to minimize startup time.
|
||||
Sync functions are run in thread pool, async functions are awaited concurrently.
|
||||
"""
|
||||
logger.debug("construct_workforce started", extra={"project_id": options.project_id, "task_id": options.task_id})
|
||||
|
||||
# Store main event loop reference for thread-safe async task scheduling
|
||||
# This allows agent_model() to schedule tasks when called from worker threads
|
||||
set_main_event_loop(asyncio.get_running_loop())
|
||||
|
||||
working_directory = get_working_directory(options)
|
||||
logger.debug("Working directory set", extra={"working_directory": working_directory})
|
||||
[coordinator_agent, task_agent] = [
|
||||
agent_model(
|
||||
key,
|
||||
prompt,
|
||||
options,
|
||||
[
|
||||
*(
|
||||
ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(options.project_id, key).send_message_to_user
|
||||
).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
|
||||
).get_tools()
|
||||
],
|
||||
)
|
||||
for key, prompt in {
|
||||
Agents.coordinator_agent: f"""
|
||||
|
||||
# ========================================================================
|
||||
# Define agent creation functions
|
||||
# ========================================================================
|
||||
|
||||
def _create_coordinator_and_task_agents() -> list[ListenChatAgent]:
|
||||
"""Create coordinator and task agents (sync, runs in thread pool)."""
|
||||
return [
|
||||
agent_model(
|
||||
key,
|
||||
prompt,
|
||||
options,
|
||||
[
|
||||
*(
|
||||
ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(options.project_id, key).send_message_to_user
|
||||
).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
|
||||
).get_tools()
|
||||
],
|
||||
)
|
||||
for key, prompt in {
|
||||
Agents.coordinator_agent: f"""
|
||||
You are a helpful coordinator.
|
||||
- You are now working in system {platform.system()} with architecture
|
||||
{platform.machine()} at working directory `{working_directory}`. All local file operations must occur here, but you can access files from any place in the file system. For all file system operations, you MUST use absolute paths to ensure precision and avoid ambiguity.
|
||||
The current date is {datetime.date.today()}. For any date-related tasks, you MUST use this as the current date.
|
||||
|
||||
- If a task assigned to another agent fails, you should re-assign it to the
|
||||
`Developer_Agent`. The `Developer_Agent` is a powerful agent with terminal
|
||||
access and can resolve a wide range of issues.
|
||||
""",
|
||||
Agents.task_agent: f"""
|
||||
Agents.task_agent: f"""
|
||||
You are a helpful task planner.
|
||||
- You are now working in system {platform.system()} with architecture
|
||||
{platform.machine()} at working directory `{working_directory}`. All local file operations must occur here, but you can access files from any place in the file system. For all file system operations, you MUST use absolute paths to ensure precision and avoid ambiguity.
|
||||
The current date is {datetime.date.today()}. For any date-related tasks, you MUST use this as the current date.
|
||||
""",
|
||||
}.items()
|
||||
]
|
||||
new_worker_agent = agent_model(
|
||||
Agents.new_worker_agent,
|
||||
f"""
|
||||
}.items()
|
||||
]
|
||||
|
||||
def _create_new_worker_agent() -> ListenChatAgent:
|
||||
"""Create new worker agent (sync, runs in thread pool)."""
|
||||
return agent_model(
|
||||
Agents.new_worker_agent,
|
||||
f"""
|
||||
You are a helpful assistant.
|
||||
- You are now working in system {platform.system()} with architecture
|
||||
{platform.machine()} at working directory `{working_directory}`. All local file operations must occur here, but you can access files from any place in the file system. For all file system operations, you MUST use absolute paths to ensure precision and avoid ambiguity.
|
||||
The current date is {datetime.date.today()}. For any date-related tasks, you MUST use this as the current date.
|
||||
""",
|
||||
options,
|
||||
[
|
||||
*HumanToolkit.get_can_use_tools(options.project_id, Agents.new_worker_agent),
|
||||
*(
|
||||
ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(options.project_id, Agents.new_worker_agent).send_message_to_user
|
||||
).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
|
||||
).get_tools(),
|
||||
],
|
||||
)
|
||||
# msg_toolkit = AgentCommunicationToolkit(max_message_history=100)
|
||||
options,
|
||||
[
|
||||
*HumanToolkit.get_can_use_tools(options.project_id, Agents.new_worker_agent),
|
||||
*(
|
||||
ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(options.project_id, Agents.new_worker_agent).send_message_to_user
|
||||
).register_toolkits(NoteTakingToolkit(options.project_id, working_directory=working_directory))
|
||||
).get_tools(),
|
||||
],
|
||||
)
|
||||
|
||||
searcher = browser_agent(options)
|
||||
developer = await developer_agent(options)
|
||||
documenter = await document_agent(options)
|
||||
multi_modaler = multi_modal_agent(options)
|
||||
# ========================================================================
|
||||
# Execute all agent creations in PARALLEL
|
||||
# ========================================================================
|
||||
|
||||
# msg_toolkit.register_agent("Worker", new_worker_agent)
|
||||
# msg_toolkit.register_agent("Browser_Agent", searcher)
|
||||
# msg_toolkit.register_agent("Developer_Agent", developer)
|
||||
# msg_toolkit.register_agent("Document_Agent", documenter)
|
||||
# msg_toolkit.register_agent("Multi_Modal_Agent", multi_modaler)
|
||||
try:
|
||||
# asyncio.gather runs all coroutines concurrently
|
||||
# asyncio.to_thread runs sync functions in thread pool without blocking event loop
|
||||
results = await asyncio.gather(
|
||||
asyncio.to_thread(_create_coordinator_and_task_agents),
|
||||
asyncio.to_thread(_create_new_worker_agent),
|
||||
asyncio.to_thread(browser_agent, options),
|
||||
developer_agent(options),
|
||||
document_agent(options),
|
||||
asyncio.to_thread(multi_modal_agent, options),
|
||||
mcp_agent(options),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create agents in parallel: {e}", exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
# Always clear event loop reference after parallel agent creation completes
|
||||
# This prevents stale references and potential cross-request interference
|
||||
set_main_event_loop(None)
|
||||
|
||||
# Unpack results
|
||||
(
|
||||
coord_task_agents,
|
||||
new_worker_agent,
|
||||
searcher,
|
||||
developer,
|
||||
documenter,
|
||||
multi_modaler,
|
||||
mcp,
|
||||
) = results
|
||||
|
||||
coordinator_agent, task_agent = coord_task_agents
|
||||
|
||||
# ========================================================================
|
||||
# Create Workforce instance and add workers (must be sequential)
|
||||
# ========================================================================
|
||||
|
||||
# Convert string model_platform to enum for comparison
|
||||
try:
|
||||
model_platform_enum = ModelPlatformType(options.model_platform.lower())
|
||||
except (ValueError, AttributeError):
|
||||
# If conversion fails, default to non-OpenAI behavior
|
||||
model_platform_enum = None
|
||||
|
||||
workforce = Workforce(
|
||||
options.project_id,
|
||||
"A workforce",
|
||||
graceful_shutdown_timeout=3, # 30 seconds for debugging
|
||||
graceful_shutdown_timeout=3,
|
||||
share_memory=False,
|
||||
coordinator_agent=coordinator_agent,
|
||||
task_agent=task_agent,
|
||||
new_worker_agent=new_worker_agent,
|
||||
use_structured_output_handler=False if model_platform_enum == ModelPlatformType.OPENAI else True,
|
||||
)
|
||||
|
||||
workforce.add_single_agent_worker(
|
||||
"Developer Agent: A master-level coding assistant with a powerful "
|
||||
"terminal. It can write and execute code, manage files, automate "
|
||||
|
|
@ -1410,18 +1416,7 @@ The current date is {datetime.date.today()}. For any date-related tasks, you MUS
|
|||
"generate new images from text prompts.",
|
||||
multi_modaler,
|
||||
)
|
||||
# workforce.add_single_agent_worker(
|
||||
# "Social Media Agent: A social media management assistant for "
|
||||
# "handling tasks related to WhatsApp, Twitter, LinkedIn, Reddit, "
|
||||
# "Notion, Slack, and other social platforms.",
|
||||
# await social_medium_agent(options),
|
||||
# )
|
||||
mcp = await mcp_agent(options)
|
||||
# workforce.add_single_agent_worker(
|
||||
# "MCP Agent: A Model Context Protocol agent that provides access "
|
||||
# "to external tools and services through MCP integrations.",
|
||||
# mcp,
|
||||
# )
|
||||
|
||||
return workforce, mcp
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,8 @@ class TaskLock:
|
|||
last_accessed: datetime
|
||||
background_tasks: set[asyncio.Task]
|
||||
"""Track all background tasks for cleanup"""
|
||||
registered_toolkits: list[Any]
|
||||
"""Track toolkits for cleanup (e.g., TerminalToolkit venvs)"""
|
||||
|
||||
# Context management fields
|
||||
conversation_history: List[Dict[str, Any]]
|
||||
|
|
@ -297,6 +299,7 @@ class TaskLock:
|
|||
self.created_at = datetime.now()
|
||||
self.last_accessed = datetime.now()
|
||||
self.background_tasks = set()
|
||||
self.registered_toolkits = []
|
||||
|
||||
# Initialize context management fields
|
||||
self.conversation_history = []
|
||||
|
|
@ -346,8 +349,42 @@ class TaskLock:
|
|||
except asyncio.CancelledError:
|
||||
pass
|
||||
self.background_tasks.clear()
|
||||
|
||||
# Clean up registered toolkits (e.g., remove TerminalToolkit venvs)
|
||||
for toolkit in self.registered_toolkits:
|
||||
try:
|
||||
if hasattr(toolkit, 'cleanup'):
|
||||
toolkit.cleanup()
|
||||
logger.info("Toolkit cleanup completed", extra={"task_id": self.id, "toolkit": type(toolkit).__name__})
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup toolkit: {e}", extra={"task_id": self.id, "toolkit": type(toolkit).__name__})
|
||||
self.registered_toolkits.clear()
|
||||
|
||||
logger.info("Task lock cleanup completed", extra={"task_id": self.id})
|
||||
|
||||
def register_toolkit(self, toolkit: Any) -> None:
|
||||
"""Register a toolkit for cleanup when task ends.
|
||||
|
||||
This is used to track toolkits that create resources (like venvs) that
|
||||
should be cleaned up when the task is complete.
|
||||
|
||||
Note: Duplicate registrations of the same toolkit instance are ignored.
|
||||
"""
|
||||
# Prevent duplicate registration of the same toolkit instance
|
||||
if any(t is toolkit for t in self.registered_toolkits):
|
||||
logger.debug("Toolkit already registered, skipping", extra={
|
||||
"task_id": self.id,
|
||||
"toolkit": type(toolkit).__name__
|
||||
})
|
||||
return
|
||||
|
||||
self.registered_toolkits.append(toolkit)
|
||||
logger.debug("Toolkit registered for cleanup", extra={
|
||||
"task_id": self.id,
|
||||
"toolkit": type(toolkit).__name__,
|
||||
"total_registered": len(self.registered_toolkits)
|
||||
})
|
||||
|
||||
def add_conversation(self, role: str, content: str | dict):
|
||||
"""Add a conversation entry to history"""
|
||||
logger.debug("Adding conversation entry", extra={"task_id": self.id, "role": role, "content_length": len(str(content))})
|
||||
|
|
@ -466,7 +503,7 @@ async def _periodic_cleanup():
|
|||
await asyncio.sleep(300) # Run every 5 minutes
|
||||
|
||||
current_time = datetime.now()
|
||||
stale_timeout = timedelta(hours=2) # Consider tasks stale after 2 hours
|
||||
stale_timeout = timedelta(hours=4) # Consider tasks stale after 4 hours
|
||||
|
||||
stale_ids = []
|
||||
for task_id, task_lock in task_locks.items():
|
||||
|
|
|
|||
|
|
@ -1,12 +1,64 @@
|
|||
import asyncio
|
||||
import contextvars
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
from threading import Event
|
||||
from threading import Event, Lock
|
||||
import traceback
|
||||
from typing import Any, Callable, Dict, List, Tuple
|
||||
import uuid
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
# Thread-safe reference to main event loop using contextvars
|
||||
# This ensures each request has its own event loop reference, avoiding race conditions
|
||||
_main_event_loop_var: contextvars.ContextVar[asyncio.AbstractEventLoop | None] = contextvars.ContextVar(
|
||||
'_main_event_loop', default=None
|
||||
)
|
||||
|
||||
# Global fallback for main event loop reference
|
||||
# Used when contextvars don't propagate to worker threads (e.g., asyncio.to_thread)
|
||||
_GLOBAL_MAIN_LOOP: asyncio.AbstractEventLoop | None = None
|
||||
_GLOBAL_MAIN_LOOP_LOCK = Lock()
|
||||
|
||||
|
||||
def set_main_event_loop(loop: asyncio.AbstractEventLoop | None):
|
||||
"""Set the main event loop reference for thread-safe task scheduling.
|
||||
|
||||
This should be called from the main async context before spawning threads
|
||||
that need to schedule async tasks. Uses both contextvars (for request isolation)
|
||||
and a global fallback (for thread pool workers where contextvars may not propagate).
|
||||
"""
|
||||
global _GLOBAL_MAIN_LOOP
|
||||
_main_event_loop_var.set(loop)
|
||||
with _GLOBAL_MAIN_LOOP_LOCK:
|
||||
_GLOBAL_MAIN_LOOP = loop
|
||||
|
||||
|
||||
def _schedule_async_task(coro):
|
||||
"""Schedule an async coroutine as a task, thread-safe.
|
||||
|
||||
This function handles scheduling from both the main event loop thread
|
||||
and from worker threads (e.g., when using asyncio.to_thread).
|
||||
"""
|
||||
try:
|
||||
# Try to get the running loop (works in main event loop thread)
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_task(coro)
|
||||
except RuntimeError:
|
||||
# No running loop in this thread (we're in a worker thread)
|
||||
# First try contextvars, then fallback to global reference
|
||||
main_loop = _main_event_loop_var.get()
|
||||
if main_loop is None:
|
||||
with _GLOBAL_MAIN_LOOP_LOCK:
|
||||
main_loop = _GLOBAL_MAIN_LOOP
|
||||
if main_loop is not None and main_loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(coro, main_loop)
|
||||
else:
|
||||
# This should not happen in normal operation - log error and skip
|
||||
traceroot.get_logger("agent").error(
|
||||
"No event loop available for async task scheduling, task skipped. "
|
||||
"Ensure set_main_event_loop() is called before parallel agent creation."
|
||||
)
|
||||
from camel.agents import ChatAgent
|
||||
from camel.agents.chat_agent import (
|
||||
StreamingChatAgentResponse,
|
||||
|
|
@ -121,7 +173,7 @@ class ListenChatAgent(ChatAgent):
|
|||
pause_event: asyncio.Event | None = None,
|
||||
prune_tool_calls_from_memory: bool = False,
|
||||
enable_snapshot_clean: bool = False,
|
||||
step_timeout: float | None = 900,
|
||||
step_timeout: float | None = 1800, # 30 minutes
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
|
|
@ -373,7 +425,9 @@ class ListenChatAgent(ChatAgent):
|
|||
|
||||
# Check if tool is wrapped by @listen_toolkit decorator
|
||||
# If so, the decorator will handle activate/deactivate events
|
||||
has_listen_decorator = hasattr(tool.func, "__wrapped__")
|
||||
# TODO: Refactor - current marker detection is a workaround. The proper fix is to
|
||||
# unify event sending: remove activate/deactivate from @listen_toolkit, only send here
|
||||
has_listen_decorator = getattr(tool.func, "__listen_toolkit__", False)
|
||||
|
||||
try:
|
||||
task_lock = get_task_lock(self.api_task_id)
|
||||
|
|
@ -511,19 +565,23 @@ class ListenChatAgent(ChatAgent):
|
|||
f"Agent {self.agent_name} executing async tool: {func_name} from toolkit: {toolkit_name} with args: {json.dumps(args, ensure_ascii=False)}"
|
||||
)
|
||||
|
||||
# Always send activate event from agent to ensure consistent logging
|
||||
# This ensures all tool calls are logged, regardless of decorator detection issues
|
||||
await task_lock.put_queue(
|
||||
ActionActivateToolkitData(
|
||||
data={
|
||||
"agent_name": self.agent_name,
|
||||
"process_task_id": self.process_task_id,
|
||||
"toolkit_name": toolkit_name,
|
||||
"method_name": func_name,
|
||||
"message": json.dumps(args, ensure_ascii=False),
|
||||
},
|
||||
# Check if tool is wrapped by @listen_toolkit decorator
|
||||
# If so, the decorator will handle activate/deactivate events
|
||||
has_listen_decorator = getattr(tool.func, "__listen_toolkit__", False)
|
||||
|
||||
# Only send activate event if tool is NOT wrapped by @listen_toolkit
|
||||
if not has_listen_decorator:
|
||||
await task_lock.put_queue(
|
||||
ActionActivateToolkitData(
|
||||
data={
|
||||
"agent_name": self.agent_name,
|
||||
"process_task_id": self.process_task_id,
|
||||
"toolkit_name": toolkit_name,
|
||||
"method_name": func_name,
|
||||
"message": json.dumps(args, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
try:
|
||||
# Set process_task context for all tool executions
|
||||
with set_process_task(self.process_task_id):
|
||||
|
|
@ -591,18 +649,19 @@ class ListenChatAgent(ChatAgent):
|
|||
else:
|
||||
result_msg = result_str
|
||||
|
||||
# Always send deactivate event from agent to ensure consistent logging
|
||||
await task_lock.put_queue(
|
||||
ActionDeactivateToolkitData(
|
||||
data={
|
||||
"agent_name": self.agent_name,
|
||||
"process_task_id": self.process_task_id,
|
||||
"toolkit_name": toolkit_name,
|
||||
"method_name": func_name,
|
||||
"message": result_msg,
|
||||
},
|
||||
# Only send deactivate event if tool is NOT wrapped by @listen_toolkit
|
||||
if not has_listen_decorator:
|
||||
await task_lock.put_queue(
|
||||
ActionDeactivateToolkitData(
|
||||
data={
|
||||
"agent_name": self.agent_name,
|
||||
"process_task_id": self.process_task_id,
|
||||
"toolkit_name": toolkit_name,
|
||||
"method_name": func_name,
|
||||
"message": result_msg,
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
return self._record_tool_calling(
|
||||
func_name,
|
||||
args,
|
||||
|
|
@ -639,7 +698,9 @@ class ListenChatAgent(ChatAgent):
|
|||
mask_tool_output=self.mask_tool_output,
|
||||
pause_event=self.pause_event,
|
||||
prune_tool_calls_from_memory=self.prune_tool_calls_from_memory,
|
||||
enable_snapshot_clean=self._enable_snapshot_clean,
|
||||
step_timeout=self.step_timeout,
|
||||
stream_accumulate=self.stream_accumulate,
|
||||
)
|
||||
|
||||
new_agent.process_task_id = self.process_task_id
|
||||
|
|
@ -668,10 +729,11 @@ def agent_model(
|
|||
):
|
||||
task_lock = get_task_lock(options.project_id)
|
||||
agent_id = str(uuid.uuid4())
|
||||
traceroot_logger.info(
|
||||
traceroot_logger.debug(
|
||||
f"Creating agent: {agent_name} with id: {agent_id} for project: {options.project_id}"
|
||||
)
|
||||
asyncio.create_task(
|
||||
# Use thread-safe scheduling to support parallel agent creation
|
||||
_schedule_async_task(
|
||||
task_lock.put_queue(
|
||||
ActionCreateAgentData(
|
||||
data={
|
||||
|
|
@ -737,19 +799,21 @@ def agent_model(
|
|||
)
|
||||
model_platform_enum = None
|
||||
|
||||
model = ModelFactory.create(
|
||||
model_platform=options.model_platform,
|
||||
model_type=options.model_type,
|
||||
api_key=options.api_key,
|
||||
url=options.api_url,
|
||||
model_config_dict=model_config or None,
|
||||
timeout=600, # 10 minutes
|
||||
**init_params,
|
||||
)
|
||||
|
||||
return ListenChatAgent(
|
||||
options.project_id,
|
||||
agent_name,
|
||||
system_message,
|
||||
model=ModelFactory.create(
|
||||
model_platform=options.model_platform,
|
||||
model_type=options.model_type,
|
||||
api_key=options.api_key,
|
||||
url=options.api_url,
|
||||
model_config_dict=model_config or None,
|
||||
**init_params,
|
||||
),
|
||||
# output_language=options.language,
|
||||
model=model,
|
||||
tools=tools,
|
||||
agent_id=agent_id,
|
||||
prune_tool_calls_from_memory=prune_tool_calls_from_memory,
|
||||
|
|
@ -803,9 +867,10 @@ async def developer_agent(options: Chat):
|
|||
|
||||
terminal_toolkit = TerminalToolkit(
|
||||
options.project_id,
|
||||
Agents.document_agent,
|
||||
Agents.developer_agent,
|
||||
working_directory=working_directory,
|
||||
safe_mode=True,
|
||||
clone_current_env=False,
|
||||
clone_current_env=True,
|
||||
)
|
||||
terminal_toolkit = message_integration.register_toolkits(terminal_toolkit)
|
||||
|
||||
|
|
@ -970,9 +1035,10 @@ these tips to maximize your effectiveness:
|
|||
@traceroot.trace()
|
||||
def browser_agent(options: Chat):
|
||||
working_directory = get_working_directory(options)
|
||||
traceroot_logger.info(
|
||||
traceroot_logger.debug(
|
||||
f"Creating browser agent for project: {options.project_id} in directory: {working_directory}"
|
||||
)
|
||||
|
||||
message_integration = ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(
|
||||
options.project_id, Agents.browser_agent
|
||||
|
|
@ -998,19 +1064,23 @@ def browser_agent(options: Chat):
|
|||
"browser_switch_tab",
|
||||
"browser_enter",
|
||||
"browser_visit_page",
|
||||
"browser_scroll",
|
||||
"browser_sheet_read",
|
||||
"browser_sheet_input",
|
||||
"browser_get_page_snapshot",
|
||||
# "browser_get_som_screenshot",
|
||||
],
|
||||
)
|
||||
|
||||
# Save reference before registering for toolkits_to_register_agent
|
||||
web_toolkit_for_agent_registration = web_toolkit_custom
|
||||
web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom)
|
||||
|
||||
terminal_toolkit = TerminalToolkit(
|
||||
options.project_id,
|
||||
Agents.browser_agent,
|
||||
working_directory=working_directory,
|
||||
safe_mode=True,
|
||||
clone_current_env=False,
|
||||
clone_current_env=True,
|
||||
)
|
||||
terminal_toolkit = message_integration.register_functions(
|
||||
[terminal_toolkit.shell_exec]
|
||||
|
|
@ -1020,8 +1090,8 @@ def browser_agent(options: Chat):
|
|||
options.project_id, Agents.browser_agent, working_directory=working_directory
|
||||
)
|
||||
note_toolkit = message_integration.register_toolkits(note_toolkit)
|
||||
|
||||
search_tools = SearchToolkit.get_can_use_tools(options.project_id)
|
||||
# Only register search tools if any are available
|
||||
if search_tools:
|
||||
search_tools = message_integration.register_functions(search_tools)
|
||||
else:
|
||||
|
|
@ -1171,9 +1241,10 @@ Your approach depends on available search tools:
|
|||
@traceroot.trace()
|
||||
async def document_agent(options: Chat):
|
||||
working_directory = get_working_directory(options)
|
||||
traceroot_logger.info(
|
||||
traceroot_logger.debug(
|
||||
f"Creating document agent for project: {options.project_id} in directory: {working_directory}"
|
||||
)
|
||||
|
||||
message_integration = ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(
|
||||
options.project_id, Agents.task_agent
|
||||
|
|
@ -1194,13 +1265,20 @@ async def document_agent(options: Chat):
|
|||
options.project_id, Agents.document_agent, working_directory=working_directory
|
||||
)
|
||||
note_toolkit = message_integration.register_toolkits(note_toolkit)
|
||||
|
||||
terminal_toolkit = TerminalToolkit(
|
||||
options.project_id,
|
||||
Agents.document_agent,
|
||||
working_directory=working_directory,
|
||||
safe_mode=True,
|
||||
clone_current_env=False,
|
||||
clone_current_env=True,
|
||||
)
|
||||
terminal_toolkit = message_integration.register_toolkits(terminal_toolkit)
|
||||
|
||||
google_drive_tools = await GoogleDriveMCPToolkit.get_can_use_tools(
|
||||
options.project_id, options.get_bun_env()
|
||||
)
|
||||
|
||||
tools = [
|
||||
*file_write_toolkit.get_tools(),
|
||||
*pptx_toolkit.get_tools(),
|
||||
|
|
@ -1209,9 +1287,7 @@ async def document_agent(options: Chat):
|
|||
*excel_toolkit.get_tools(),
|
||||
*note_toolkit.get_tools(),
|
||||
*terminal_toolkit.get_tools(),
|
||||
*await GoogleDriveMCPToolkit.get_can_use_tools(
|
||||
options.project_id, options.get_bun_env()
|
||||
),
|
||||
*google_drive_tools,
|
||||
]
|
||||
# if env("EXA_API_KEY") or options.is_cloud():
|
||||
# search_toolkit = SearchToolkit(options.project_id, Agents.document_agent).search_exa
|
||||
|
|
@ -1394,9 +1470,10 @@ supported formats including advanced spreadsheet functionality.
|
|||
@traceroot.trace()
|
||||
def multi_modal_agent(options: Chat):
|
||||
working_directory = get_working_directory(options)
|
||||
traceroot_logger.info(
|
||||
traceroot_logger.debug(
|
||||
f"Creating multi-modal agent for project: {options.project_id} in directory: {working_directory}"
|
||||
)
|
||||
|
||||
message_integration = ToolkitMessageIntegration(
|
||||
message_handler=HumanToolkit(
|
||||
options.project_id, Agents.multi_modal_agent
|
||||
|
|
@ -1416,10 +1493,12 @@ def multi_modal_agent(options: Chat):
|
|||
terminal_toolkit = TerminalToolkit(
|
||||
options.project_id,
|
||||
agent_name=Agents.multi_modal_agent,
|
||||
working_directory=working_directory,
|
||||
safe_mode=True,
|
||||
clone_current_env=False,
|
||||
clone_current_env=True,
|
||||
)
|
||||
terminal_toolkit = message_integration.register_toolkits(terminal_toolkit)
|
||||
|
||||
note_toolkit = NoteTakingToolkit(
|
||||
options.project_id,
|
||||
Agents.multi_modal_agent,
|
||||
|
|
@ -1609,7 +1688,8 @@ async def social_medium_agent(options: Chat):
|
|||
*TerminalToolkit(
|
||||
options.project_id,
|
||||
agent_name=Agents.social_medium_agent,
|
||||
clone_current_env=False,
|
||||
working_directory=working_directory,
|
||||
clone_current_env=True,
|
||||
).get_tools(),
|
||||
*NoteTakingToolkit(
|
||||
options.project_id,
|
||||
|
|
@ -1776,6 +1856,7 @@ async def mcp_agent(options: Chat):
|
|||
if options.is_cloud()
|
||||
else None
|
||||
),
|
||||
timeout=600, # 10 minutes
|
||||
**{
|
||||
k: v
|
||||
for k, v in (options.extra_params or {}).items()
|
||||
|
|
|
|||
|
|
@ -185,6 +185,8 @@ def listen_toolkit(
|
|||
raise error
|
||||
return res
|
||||
|
||||
# Mark this wrapper as decorated by @listen_toolkit for detection in agent.py
|
||||
async_wrapper.__listen_toolkit__ = True
|
||||
return async_wrapper
|
||||
|
||||
else:
|
||||
|
|
@ -291,6 +293,8 @@ def listen_toolkit(
|
|||
raise error
|
||||
return res
|
||||
|
||||
# Mark this wrapper as decorated by @listen_toolkit for detection in agent.py
|
||||
sync_wrapper.__listen_toolkit__ = True
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
|
@ -350,14 +354,9 @@ def auto_listen_toolkit(base_toolkit_class: Type[T]) -> Callable[[Type[T]], Type
|
|||
# Method is overridden, check if it already has @listen_toolkit decorator
|
||||
overridden_method = cls.__dict__[method_name]
|
||||
|
||||
# Check if already decorated by looking for the wrapper attributes
|
||||
# that listen_toolkit adds (like __wrapped__ or specific markers)
|
||||
is_already_decorated = (
|
||||
hasattr(overridden_method, '__wrapped__') or
|
||||
(hasattr(overridden_method, '__name__') and
|
||||
hasattr(getattr(overridden_method, '__code__', None), 'co_freevars') and
|
||||
'toolkit' in getattr(overridden_method.__code__, 'co_freevars', []))
|
||||
)
|
||||
# Check if already decorated by looking for the __listen_toolkit__ marker
|
||||
# that listen_toolkit adds to its wrappers
|
||||
is_already_decorated = getattr(overridden_method, '__listen_toolkit__', False)
|
||||
|
||||
if is_already_decorated:
|
||||
# Already has @listen_toolkit, skip
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import os
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing_extensions import TypedDict
|
||||
import websockets
|
||||
import websockets.exceptions
|
||||
|
||||
|
|
@ -18,12 +20,29 @@ from utils import traceroot_wrapper as traceroot
|
|||
|
||||
logger = traceroot.get_logger("hybrid_browser_toolkit")
|
||||
|
||||
# Global navigation lock to prevent concurrent visit_page conflicts (ERR_ABORTED)
|
||||
# This is needed because multiple sessions may share the same browser via CDP
|
||||
_global_navigation_lock = asyncio.Lock()
|
||||
|
||||
# Global registry: tab_id -> session_id (ensures each tab belongs to only one session)
|
||||
_global_tab_registry: Dict[str, str] = {}
|
||||
_global_tab_registry_lock = asyncio.Lock()
|
||||
|
||||
|
||||
class SheetCell(TypedDict):
|
||||
row: int
|
||||
col: int
|
||||
text: str
|
||||
|
||||
|
||||
class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||
"""Initialize wrapper."""
|
||||
super().__init__(config)
|
||||
logger.info(f"WebSocketBrowserWrapper using ts_dir: {self.ts_dir}")
|
||||
# Track tabs opened by this session for isolation
|
||||
self._session_tab_ids: set = set()
|
||||
self._wrapper_session_id: str = str(uuid.uuid4())
|
||||
|
||||
def _ensure_local_no_proxy(self) -> None:
|
||||
local_hosts = ["localhost", "127.0.0.1", "::1"]
|
||||
|
|
@ -136,6 +155,100 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
|
|||
logger.error(f"Unexpected error sending command '{command}': {type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
async def visit_page(self, url: str) -> Dict[str, Any]:
|
||||
"""Override visit_page to add global navigation lock preventing ERR_ABORTED.
|
||||
|
||||
Multiple sessions sharing the same browser via CDP can cause conflicts
|
||||
when they try to navigate simultaneously (e.g., both trying to use a
|
||||
blank page). This lock serializes navigation operations at the WebSocket
|
||||
wrapper level.
|
||||
"""
|
||||
global _global_navigation_lock
|
||||
|
||||
async with _global_navigation_lock:
|
||||
logger.debug(f"[visit_page] Acquired navigation lock, navigating to {url}")
|
||||
try:
|
||||
result = await super().visit_page(url)
|
||||
logger.debug(f"[visit_page] Navigation completed, releasing lock")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"[visit_page] Navigation failed: {e}")
|
||||
raise
|
||||
|
||||
async def get_tab_info(self) -> List[Dict[str, Any]]:
|
||||
"""Override get_tab_info to track and filter tabs for session isolation.
|
||||
|
||||
Automatically tracks the current tab (is_current=true) as belonging to
|
||||
this session, then filters to only return tabs owned by this session.
|
||||
Uses global registry to ensure each tab belongs to only one session.
|
||||
"""
|
||||
global _global_tab_registry, _global_tab_registry_lock
|
||||
|
||||
all_tabs = await super().get_tab_info()
|
||||
session_id = self._wrapper_session_id # Stable UUID for this wrapper
|
||||
|
||||
# Auto-track: add current tab to this session's tracked tabs (with global lock)
|
||||
current_tab = next((t for t in all_tabs if t.get('is_current')),
|
||||
None)
|
||||
if current_tab and current_tab.get('tab_id'):
|
||||
tab_id = current_tab['tab_id']
|
||||
async with _global_tab_registry_lock:
|
||||
# Only track if not already owned by another session
|
||||
if tab_id not in _global_tab_registry:
|
||||
_global_tab_registry[tab_id] = session_id
|
||||
self._session_tab_ids.add(tab_id)
|
||||
logger.info(
|
||||
f"[Session Tab Tracking] Auto-tracked current tab: {tab_id}, session {session_id} now has tabs: {self._session_tab_ids}")
|
||||
elif _global_tab_registry[tab_id] == session_id:
|
||||
# Already owned by this session, ensure local tracking
|
||||
self._session_tab_ids.add(tab_id)
|
||||
|
||||
# Filter: only return tabs belonging to this session
|
||||
filtered_tabs = [tab for tab in all_tabs if
|
||||
tab.get('tab_id') in self._session_tab_ids]
|
||||
logger.info(
|
||||
f"[Session Tab Filtering] Session {session_id}: Returning {len(filtered_tabs)}/{len(all_tabs)} tabs, tracked: {self._session_tab_ids}")
|
||||
|
||||
return filtered_tabs
|
||||
|
||||
async def close_tab(self, tab_id: str) -> Dict[str, Any]:
|
||||
"""Override close_tab to update tracking."""
|
||||
global _global_tab_registry, _global_tab_registry_lock
|
||||
|
||||
result = await super().close_tab(tab_id)
|
||||
|
||||
# Remove from tracking if it was ours
|
||||
if tab_id in self._session_tab_ids:
|
||||
self._session_tab_ids.discard(tab_id)
|
||||
async with _global_tab_registry_lock:
|
||||
if tab_id in _global_tab_registry:
|
||||
del _global_tab_registry[tab_id]
|
||||
logger.info(
|
||||
f"[Session Tab Tracking] Removed closed tab: {tab_id}, session now has tabs: {self._session_tab_ids}")
|
||||
|
||||
return result
|
||||
|
||||
async def cleanup_tab_tracking(self):
|
||||
"""Clean up all tab tracking for this session from the global registry.
|
||||
|
||||
Should be called when the wrapper is being stopped/destroyed to prevent
|
||||
memory leaks and stale entries in the global registry.
|
||||
"""
|
||||
global _global_tab_registry, _global_tab_registry_lock
|
||||
|
||||
if not self._session_tab_ids:
|
||||
return
|
||||
|
||||
async with _global_tab_registry_lock:
|
||||
cleaned_count = len(self._session_tab_ids)
|
||||
for tab_id in list(self._session_tab_ids):
|
||||
if tab_id in _global_tab_registry:
|
||||
del _global_tab_registry[tab_id]
|
||||
# Clear inside lock to prevent race with concurrent get_tab_info
|
||||
self._session_tab_ids.clear()
|
||||
logger.info(
|
||||
f"[Session Tab Tracking] Cleaned up {cleaned_count} tabs for session {self._wrapper_session_id}")
|
||||
|
||||
|
||||
# WebSocket connection pool
|
||||
class WebSocketConnectionPool:
|
||||
|
|
@ -183,6 +296,7 @@ class WebSocketConnectionPool:
|
|||
# Connection is unhealthy, clean it up
|
||||
logger.info(f"Removing unhealthy WebSocket connection for session {session_id}")
|
||||
try:
|
||||
await wrapper.cleanup_tab_tracking()
|
||||
await wrapper.stop()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error stopping unhealthy wrapper: {e}")
|
||||
|
|
@ -202,6 +316,7 @@ class WebSocketConnectionPool:
|
|||
if session_id in self._connections:
|
||||
wrapper = self._connections[session_id]
|
||||
try:
|
||||
await wrapper.cleanup_tab_tracking()
|
||||
await wrapper.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing WebSocket connection for session {session_id}: {e}")
|
||||
|
|
@ -213,6 +328,7 @@ class WebSocketConnectionPool:
|
|||
if session_id in self._connections:
|
||||
wrapper = self._connections[session_id]
|
||||
try:
|
||||
await wrapper.cleanup_tab_tracking()
|
||||
await wrapper.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing WebSocket connection for session {session_id}: {e}")
|
||||
|
|
@ -230,6 +346,7 @@ class WebSocketConnectionPool:
|
|||
# Global connection pool instance
|
||||
websocket_connection_pool = WebSocketConnectionPool()
|
||||
|
||||
|
||||
@auto_listen_toolkit(BaseHybridBrowserToolkit)
|
||||
class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
|
||||
agent_name: str = Agents.browser_agent
|
||||
|
|
@ -360,6 +477,10 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
|
|||
full_visual_mode=self._full_visual_mode,
|
||||
)
|
||||
|
||||
async def browser_sheet_input(self, *, cells: List[SheetCell]) -> Dict[str, Any]:
|
||||
# Use typing_extensions.TypedDict for Pydantic <3.12 compatibility.
|
||||
return await super().browser_sheet_input(cells=cells)
|
||||
|
||||
@classmethod
|
||||
def toolkit_name(cls) -> str:
|
||||
return "Browser Toolkit"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Optional
|
||||
from camel.toolkits.terminal_toolkit import TerminalToolkit as BaseTerminalToolkit
|
||||
|
|
@ -15,6 +19,20 @@ from utils import traceroot_wrapper as traceroot
|
|||
|
||||
logger = traceroot.get_logger("terminal_toolkit")
|
||||
|
||||
# App version - should match electron app version
|
||||
# TODO: Consider getting this from a shared config
|
||||
APP_VERSION = "0.0.80"
|
||||
|
||||
|
||||
def get_terminal_base_venv_path() -> str:
|
||||
"""Get the path to the terminal base venv created during app installation."""
|
||||
return os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
".eigent",
|
||||
"venvs",
|
||||
f"terminal_base-{APP_VERSION}"
|
||||
)
|
||||
|
||||
|
||||
@auto_listen_toolkit(BaseTerminalToolkit)
|
||||
class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
||||
|
|
@ -33,20 +51,23 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
session_logs_dir: str | None = None,
|
||||
safe_mode: bool = True,
|
||||
allowed_commands: list[str] | None = None,
|
||||
clone_current_env: bool = False,
|
||||
clone_current_env: bool = True,
|
||||
):
|
||||
self.api_task_id = api_task_id
|
||||
if agent_name is not None:
|
||||
self.agent_name = agent_name
|
||||
if working_directory is None:
|
||||
working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
|
||||
|
||||
logger.info("Initializing TerminalToolkit", extra={
|
||||
# Get base directory from environment
|
||||
base_dir = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
|
||||
|
||||
if working_directory is None:
|
||||
working_directory = base_dir
|
||||
self._agent_venv_dir = os.path.join(base_dir, self.agent_name)
|
||||
|
||||
logger.debug(f"Initializing TerminalToolkit for agent={self.agent_name}", extra={
|
||||
"api_task_id": api_task_id,
|
||||
"agent_name": self.agent_name,
|
||||
"working_directory": working_directory,
|
||||
"safe_mode": safe_mode,
|
||||
"use_docker_backend": use_docker_backend
|
||||
"agent_venv_dir": self._agent_venv_dir,
|
||||
})
|
||||
|
||||
if TerminalToolkit._thread_pool is None:
|
||||
|
|
@ -54,7 +75,6 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
max_workers=1,
|
||||
thread_name_prefix="terminal_toolkit"
|
||||
)
|
||||
logger.debug("Created terminal toolkit thread pool")
|
||||
|
||||
super().__init__(
|
||||
timeout=timeout,
|
||||
|
|
@ -64,16 +84,156 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
session_logs_dir=session_logs_dir,
|
||||
safe_mode=safe_mode,
|
||||
allowed_commands=allowed_commands,
|
||||
clone_current_env=clone_current_env,
|
||||
install_dependencies=[
|
||||
"pandas",
|
||||
"numpy",
|
||||
"matplotlib",
|
||||
"requests",
|
||||
"openpyxl",
|
||||
],
|
||||
clone_current_env=True,
|
||||
install_dependencies=[],
|
||||
)
|
||||
|
||||
# Auto-register with TaskLock for cleanup when task ends
|
||||
from app.service.task import get_task_lock_if_exists
|
||||
task_lock = get_task_lock_if_exists(api_task_id)
|
||||
if task_lock:
|
||||
task_lock.register_toolkit(self)
|
||||
logger.info("TerminalToolkit registered for cleanup", extra={
|
||||
"api_task_id": api_task_id,
|
||||
"working_directory": working_directory
|
||||
})
|
||||
|
||||
def _setup_cloned_environment(self):
|
||||
"""Override to clone from terminal_base venv instead of current process venv.
|
||||
|
||||
Creates a lightweight clone using symlinks to the terminal_base venv,
|
||||
which contains pre-installed packages (pandas, numpy, matplotlib, etc.).
|
||||
"""
|
||||
self.cloned_env_path = os.path.join(self._agent_venv_dir, ".venv")
|
||||
terminal_base_path = get_terminal_base_venv_path()
|
||||
|
||||
# Check if terminal_base exists
|
||||
if platform.system() == 'Windows':
|
||||
base_python = os.path.join(terminal_base_path, "Scripts", "python.exe")
|
||||
else:
|
||||
base_python = os.path.join(terminal_base_path, "bin", "python")
|
||||
|
||||
if not os.path.exists(base_python):
|
||||
logger.warning(
|
||||
f"Terminal base venv not found at {terminal_base_path}, "
|
||||
"falling back to system Python"
|
||||
)
|
||||
return
|
||||
|
||||
# Check if cloned env already exists
|
||||
if platform.system() == 'Windows':
|
||||
cloned_python = os.path.join(self.cloned_env_path, "Scripts", "python.exe")
|
||||
else:
|
||||
cloned_python = os.path.join(self.cloned_env_path, "bin", "python")
|
||||
|
||||
if os.path.exists(cloned_python):
|
||||
logger.info(f"Using existing cloned environment: {self.cloned_env_path}")
|
||||
self.python_executable = cloned_python
|
||||
return
|
||||
|
||||
logger.info(f"Cloning terminal_base venv to: {self.cloned_env_path}")
|
||||
|
||||
try:
|
||||
# Create the cloned venv directory
|
||||
os.makedirs(self.cloned_env_path, exist_ok=True)
|
||||
|
||||
# Clone using symlinks for efficiency
|
||||
# We need to create proper venv structure with symlinks to terminal_base
|
||||
self._clone_venv_with_symlinks(terminal_base_path, self.cloned_env_path)
|
||||
|
||||
self.python_executable = cloned_python
|
||||
logger.info(f"Successfully cloned environment to: {self.cloned_env_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clone terminal_base venv: {e}", exc_info=True)
|
||||
# Cleanup partial clone
|
||||
if os.path.exists(self.cloned_env_path):
|
||||
shutil.rmtree(self.cloned_env_path, ignore_errors=True)
|
||||
logger.warning("Falling back to system Python")
|
||||
|
||||
def _get_venv_path(self):
|
||||
"""Return the cloned venv path for shell activation."""
|
||||
cloned_env_path = getattr(self, 'cloned_env_path', None)
|
||||
if cloned_env_path and os.path.exists(cloned_env_path):
|
||||
return cloned_env_path
|
||||
return None
|
||||
|
||||
def _clone_venv_with_symlinks(self, source_venv: str, target_venv: str):
|
||||
"""Clone a venv using symlinks for efficiency.
|
||||
|
||||
Creates the structure needed: pyvenv.cfg, bin/python, lib symlink, and activate scripts.
|
||||
"""
|
||||
is_windows = platform.system() == 'Windows'
|
||||
|
||||
# Read source pyvenv.cfg to get Python home
|
||||
source_cfg = os.path.join(source_venv, "pyvenv.cfg")
|
||||
python_home = None
|
||||
|
||||
with open(source_cfg, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('home = '):
|
||||
python_home = line.split('=', 1)[1].strip()
|
||||
break
|
||||
|
||||
if not python_home:
|
||||
raise RuntimeError(f"Could not determine Python home from {source_cfg}")
|
||||
|
||||
# Copy pyvenv.cfg (simpler than recreating)
|
||||
shutil.copy2(source_cfg, os.path.join(target_venv, "pyvenv.cfg"))
|
||||
|
||||
if is_windows:
|
||||
# Windows: copy executables from source
|
||||
target_bin = os.path.join(target_venv, "Scripts")
|
||||
os.makedirs(target_bin, exist_ok=True)
|
||||
source_scripts = os.path.join(source_venv, "Scripts")
|
||||
for exe in ["python.exe", "pythonw.exe"]:
|
||||
src = os.path.join(source_scripts, exe)
|
||||
if os.path.exists(src):
|
||||
shutil.copy2(src, os.path.join(target_bin, exe))
|
||||
# Copy activate scripts (need to modify VIRTUAL_ENV path)
|
||||
for script in ["activate.bat", "activate.ps1", "deactivate.bat"]:
|
||||
src = os.path.join(source_scripts, script)
|
||||
if os.path.exists(src):
|
||||
with open(src, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
content = content.replace(source_venv, target_venv)
|
||||
dst = os.path.join(target_bin, script)
|
||||
with open(dst, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
# Use directory junction for Lib (no admin rights needed, unlike symlink)
|
||||
source_lib = os.path.join(source_venv, "Lib")
|
||||
target_lib = os.path.join(target_venv, "Lib")
|
||||
subprocess.run(["cmd", "/c", "mklink", "/J", target_lib, source_lib],
|
||||
check=True, capture_output=True)
|
||||
else:
|
||||
# Unix: symlink python executable and lib directory
|
||||
target_bin = os.path.join(target_venv, "bin")
|
||||
os.makedirs(target_bin, exist_ok=True)
|
||||
|
||||
# Symlink python to the base Python
|
||||
python_exe = os.path.join(python_home, "python3")
|
||||
if not os.path.exists(python_exe):
|
||||
python_exe = os.path.join(python_home, "python")
|
||||
os.symlink(python_exe, os.path.join(target_bin, "python"))
|
||||
os.symlink("python", os.path.join(target_bin, "python3"))
|
||||
|
||||
# Copy activate scripts (need to modify VIRTUAL_ENV path)
|
||||
source_bin = os.path.join(source_venv, "bin")
|
||||
for script in ["activate", "activate.csh", "activate.fish"]:
|
||||
src = os.path.join(source_bin, script)
|
||||
if os.path.exists(src):
|
||||
with open(src, 'r') as f:
|
||||
content = f.read()
|
||||
# Replace source venv path with target venv path
|
||||
content = content.replace(source_venv, target_venv)
|
||||
dst = os.path.join(target_bin, script)
|
||||
with open(dst, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# Symlink lib directory
|
||||
source_lib = os.path.join(source_venv, "lib")
|
||||
os.symlink(source_lib, os.path.join(target_venv, "lib"))
|
||||
|
||||
def _write_to_log(self, log_file: str, content: str) -> None:
|
||||
r"""Write content to log file with optional ANSI stripping.
|
||||
|
||||
|
|
@ -175,6 +335,51 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
|
||||
return result
|
||||
|
||||
def cleanup(self, remove_venv: bool = True):
|
||||
"""Clean up all active sessions and optionally remove the virtual environment.
|
||||
|
||||
Args:
|
||||
remove_venv: If True, removes the .venv or .initial_env folder created
|
||||
by this toolkit. Defaults to True to prevent disk bloat.
|
||||
"""
|
||||
# First call parent cleanup to kill all shell sessions
|
||||
super().cleanup()
|
||||
|
||||
if not remove_venv:
|
||||
return
|
||||
|
||||
# Remove cloned env (.venv) if it exists
|
||||
cloned_env_path = getattr(self, 'cloned_env_path', None)
|
||||
if cloned_env_path and os.path.exists(cloned_env_path):
|
||||
try:
|
||||
shutil.rmtree(cloned_env_path)
|
||||
logger.info("Removed cloned venv", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"path": cloned_env_path
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning("Failed to remove cloned venv", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"path": cloned_env_path,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
# Remove initial env (.initial_env) if it exists
|
||||
initial_env_path = getattr(self, 'initial_env_path', None)
|
||||
if initial_env_path and os.path.exists(initial_env_path):
|
||||
try:
|
||||
shutil.rmtree(initial_env_path)
|
||||
logger.info("Removed initial env", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"path": initial_env_path
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning("Failed to remove initial env", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"path": initial_env_path,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def shutdown(cls):
|
||||
if cls._thread_pool:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class Workforce(BaseWorkforce):
|
|||
graceful_shutdown_timeout=graceful_shutdown_timeout,
|
||||
share_memory=share_memory,
|
||||
use_structured_output_handler=use_structured_output_handler,
|
||||
task_timeout_seconds=1800, # 30 minutes
|
||||
task_timeout_seconds=3600, # 60 minutes
|
||||
failure_handling_config=FailureHandlingConfig(
|
||||
enabled_strategies=["retry", "replan"],
|
||||
),
|
||||
|
|
@ -87,85 +87,66 @@ class Workforce(BaseWorkforce):
|
|||
on_stream_batch: Optional callback for streaming batches signature (List[Task], bool)
|
||||
on_stream_text: Optional callback for raw streaming text chunks
|
||||
"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("🧩 [DECOMPOSE] eigent_make_sub_tasks CALLED", extra={
|
||||
logger.debug("[DECOMPOSE] eigent_make_sub_tasks called", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"workforce_id": id(self),
|
||||
"task_id": task.id
|
||||
})
|
||||
logger.info(f"[DECOMPOSE] Task content preview: '{task.content[:200]}...'")
|
||||
logger.info(f"[DECOMPOSE] Has coordinator context: {bool(coordinator_context)}")
|
||||
logger.info(f"[DECOMPOSE] Current workforce state: {self._state.name}, _running: {self._running}")
|
||||
logger.info("=" * 80)
|
||||
|
||||
if not validate_task_content(task.content, task.id):
|
||||
task.state = TaskState.FAILED
|
||||
task.result = "Task failed: Invalid or empty content provided"
|
||||
logger.warning("❌ [DECOMPOSE] Task rejected: Invalid or empty content", extra={
|
||||
logger.warning("[DECOMPOSE] Task rejected: Invalid or empty content", extra={
|
||||
"task_id": task.id,
|
||||
"content_preview": task.content[:50] + "..." if len(task.content) > 50 else task.content
|
||||
})
|
||||
raise UserException(code.error, task.result)
|
||||
|
||||
logger.info(f"[DECOMPOSE] Resetting workforce state")
|
||||
self.reset()
|
||||
self._task = task
|
||||
self.set_channel(TaskChannel())
|
||||
self._state = WorkforceState.RUNNING
|
||||
task.state = TaskState.OPEN
|
||||
logger.info(f"[DECOMPOSE] Workforce reset complete, state: {self._state.name}")
|
||||
|
||||
logger.info(f"[DECOMPOSE] Calling handle_decompose_append_task")
|
||||
subtasks = asyncio.run(
|
||||
self.handle_decompose_append_task(
|
||||
task,
|
||||
reset=False,
|
||||
task,
|
||||
reset=False,
|
||||
coordinator_context=coordinator_context,
|
||||
on_stream_batch=on_stream_batch,
|
||||
on_stream_batch=on_stream_batch,
|
||||
on_stream_text=on_stream_text
|
||||
)
|
||||
)
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"✅ [DECOMPOSE] Task decomposition COMPLETED", extra={
|
||||
|
||||
logger.info(f"[DECOMPOSE] Task decomposition completed", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"task_id": task.id,
|
||||
"subtasks_count": len(subtasks)
|
||||
})
|
||||
logger.info("=" * 80)
|
||||
return subtasks
|
||||
|
||||
async def eigent_start(self, subtasks: list[Task]):
|
||||
"""start the workforce"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("▶️ [WF-LIFECYCLE] eigent_start CALLED", extra={"api_task_id": self.api_task_id, "workforce_id": id(self)})
|
||||
logger.info(f"[WF-LIFECYCLE] Starting workforce execution with {len(subtasks)} subtasks")
|
||||
logger.info(f"[WF-LIFECYCLE] Current workforce state: {self._state.name}, _running: {self._running}")
|
||||
logger.info("=" * 80)
|
||||
logger.debug(f"[WF-LIFECYCLE] eigent_start called with {len(subtasks)} subtasks", extra={
|
||||
"api_task_id": self.api_task_id
|
||||
})
|
||||
self._pending_tasks.extendleft(reversed(subtasks))
|
||||
# Save initial snapshot
|
||||
self.save_snapshot("Initial task decomposition")
|
||||
|
||||
try:
|
||||
logger.info(f"[WF-LIFECYCLE] Calling base class start() method")
|
||||
await self.start()
|
||||
logger.info(f"[WF-LIFECYCLE] ✅ Base class start() method completed")
|
||||
except Exception as e:
|
||||
logger.error(f"[WF-LIFECYCLE] ❌ Error in workforce execution: {e}", extra={
|
||||
logger.error(f"[WF-LIFECYCLE] Error in workforce execution: {e}", extra={
|
||||
"api_task_id": self.api_task_id,
|
||||
"error": str(e)
|
||||
}, exc_info=True)
|
||||
self._state = WorkforceState.STOPPED
|
||||
logger.info(f"[WF-LIFECYCLE] Workforce state set to STOPPED after error")
|
||||
raise
|
||||
finally:
|
||||
logger.info(f"[WF-LIFECYCLE] eigent_start finally block, current state: {self._state.name}")
|
||||
if self._state != WorkforceState.STOPPED:
|
||||
self._state = WorkforceState.IDLE
|
||||
logger.info(f"[WF-LIFECYCLE] Workforce state set to IDLE")
|
||||
|
||||
def _decompose_task(self, task: Task, stream_callback=None):
|
||||
"""Decompose task with optional streaming text callback."""
|
||||
|
||||
decompose_prompt = str(
|
||||
TASK_DECOMPOSE_PROMPT.format(
|
||||
content=task.content,
|
||||
|
|
@ -173,6 +154,7 @@ class Workforce(BaseWorkforce):
|
|||
additional_info=task.additional_info,
|
||||
)
|
||||
)
|
||||
|
||||
self.task_agent.reset()
|
||||
result = task.decompose(
|
||||
self.task_agent, decompose_prompt, stream_callback=stream_callback
|
||||
|
|
@ -217,7 +199,7 @@ class Workforce(BaseWorkforce):
|
|||
Returns:
|
||||
List[Task]: The decomposed subtasks or the original task
|
||||
"""
|
||||
logger.info(f"[DECOMPOSE] handle_decompose_append_task CALLED, task_id={task.id}, reset={reset}")
|
||||
logger.debug(f"[DECOMPOSE] handle_decompose_append_task called, task_id={task.id}, reset={reset}")
|
||||
|
||||
if not validate_task_content(task.content, task.id):
|
||||
task.state = TaskState.FAILED
|
||||
|
|
@ -229,31 +211,20 @@ class Workforce(BaseWorkforce):
|
|||
return [task]
|
||||
|
||||
if reset and self._state != WorkforceState.RUNNING:
|
||||
logger.info(f"[DECOMPOSE] Resetting workforce (reset={reset}, state={self._state.name})")
|
||||
self.reset()
|
||||
logger.info("[DECOMPOSE] Workforce reset complete")
|
||||
|
||||
self._task = task
|
||||
task.state = TaskState.FAILED
|
||||
|
||||
if coordinator_context:
|
||||
logger.info(f"[DECOMPOSE] Adding coordinator context to task")
|
||||
original_content = task.content
|
||||
task_with_context = coordinator_context
|
||||
if coordinator_context:
|
||||
task_with_context += "\n=== CURRENT TASK ===\n"
|
||||
task_with_context += original_content
|
||||
task_with_context = coordinator_context + "\n=== CURRENT TASK ===\n" + original_content
|
||||
task.content = task_with_context
|
||||
|
||||
logger.info(f"[DECOMPOSE] Calling _decompose_task with context")
|
||||
subtasks_result = self._decompose_task(task, stream_callback=on_stream_text)
|
||||
|
||||
task.content = original_content
|
||||
else:
|
||||
logger.info(f"[DECOMPOSE] Calling _decompose_task without context")
|
||||
subtasks_result = self._decompose_task(task, stream_callback=on_stream_text)
|
||||
|
||||
logger.info(f"[DECOMPOSE] _decompose_task returned, processing results")
|
||||
if isinstance(subtasks_result, Generator):
|
||||
subtasks = []
|
||||
for new_tasks in subtasks_result:
|
||||
|
|
@ -263,18 +234,15 @@ class Workforce(BaseWorkforce):
|
|||
on_stream_batch(new_tasks, False)
|
||||
except Exception as e:
|
||||
logger.warning(f"Streaming callback failed: {e}")
|
||||
logger.info(f"[DECOMPOSE] Collected {len(subtasks)} subtasks from generator")
|
||||
|
||||
# After consuming the generator, check task.subtasks for final result as fallback
|
||||
if not subtasks and task.subtasks:
|
||||
subtasks = task.subtasks
|
||||
else:
|
||||
subtasks = subtasks_result
|
||||
logger.info(f"[DECOMPOSE] Got {len(subtasks) if subtasks else 0} subtasks directly")
|
||||
|
||||
if subtasks:
|
||||
self._pending_tasks.extendleft(reversed(subtasks))
|
||||
logger.info(f"[DECOMPOSE] ✅ Appended {len(subtasks)} subtasks to pending tasks")
|
||||
|
||||
if not subtasks:
|
||||
logger.warning(f"[DECOMPOSE] No subtasks returned, creating fallback task")
|
||||
|
|
@ -285,7 +253,6 @@ class Workforce(BaseWorkforce):
|
|||
)
|
||||
task.subtasks = [fallback_task]
|
||||
subtasks = [fallback_task]
|
||||
logger.info(f"[DECOMPOSE] Created fallback task: {fallback_task.id}")
|
||||
|
||||
if on_stream_batch:
|
||||
try:
|
||||
|
|
@ -293,6 +260,7 @@ class Workforce(BaseWorkforce):
|
|||
except Exception as e:
|
||||
logger.warning(f"Final streaming callback failed: {e}")
|
||||
|
||||
logger.debug(f"[DECOMPOSE] handle_decompose_append_task completed, returned {len(subtasks)} subtasks")
|
||||
return subtasks
|
||||
|
||||
def _get_agent_id_from_node_id(self, node_id: str) -> str | None:
|
||||
|
|
@ -387,6 +355,7 @@ class Workforce(BaseWorkforce):
|
|||
f"Task {task.id} will not be properly tracked on frontend. "
|
||||
f"Available workers: {[c.node_id for c in self._children if hasattr(c, 'node_id')]}"
|
||||
)
|
||||
else:
|
||||
await task_lock.put_queue(
|
||||
ActionAssignTaskData(
|
||||
action=Action.assign_task,
|
||||
|
|
@ -423,7 +392,7 @@ class Workforce(BaseWorkforce):
|
|||
worker=worker,
|
||||
pool_max_size=pool_max_size,
|
||||
use_structured_output_handler=self.use_structured_output_handler,
|
||||
context_utility=None, # Will be set during save/load operations
|
||||
context_utility=None,
|
||||
enable_workflow_memory=enable_workflow_memory,
|
||||
)
|
||||
self._children.append(worker_node)
|
||||
|
|
@ -444,6 +413,7 @@ class Workforce(BaseWorkforce):
|
|||
role=worker_node.description,
|
||||
)
|
||||
metrics_callbacks[0].log_worker_created(event)
|
||||
|
||||
return self
|
||||
|
||||
async def _handle_completed_task(self, task: Task) -> None:
|
||||
|
|
@ -480,6 +450,11 @@ class Workforce(BaseWorkforce):
|
|||
|
||||
result = await super()._handle_failed_task(task)
|
||||
|
||||
# Only send completion report to frontend when all retries are exhausted
|
||||
max_retries = self.failure_handling_config.max_retries
|
||||
if task.failure_count < max_retries:
|
||||
return result
|
||||
|
||||
error_message = ""
|
||||
# Use proper CAMEL pattern for metrics logging
|
||||
metrics_callbacks = [cb for cb in self._callbacks if isinstance(cb, WorkforceMetrics)]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ readme = "README.md"
|
|||
requires-python = ">=3.10,<3.11"
|
||||
dependencies = [
|
||||
"pip>=23.0",
|
||||
"camel-ai[eigent]==0.2.83a9",
|
||||
"camel-ai[eigent]==0.2.84",
|
||||
"fastapi>=0.115.12",
|
||||
"fastapi-babel>=1.0.0",
|
||||
"uvicorn[standard]>=0.34.2",
|
||||
|
|
|
|||
154
backend/uv.lock
generated
154
backend/uv.lock
generated
|
|
@ -192,15 +192,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "azure-core"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/83/41c9371c8298999c67b007e308a0a3c4d6a59c6908fa9c62101f031f886f/azure_core-1.37.0.tar.gz", hash = "sha256:7064f2c11e4b97f340e8e8c6d923b822978be3016e46b7bc4aa4b337cfb48aee", size = 357620, upload-time = "2025-12-11T20:05:13.518Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/34/a9914e676971a13d6cc671b1ed172f9804b50a3a80a143ff196e52f4c7ee/azure_core-1.37.0-py3-none-any.whl", hash = "sha256:b3abe2c59e7d6bb18b38c275a5029ff80f98990e7c90a5e646249a56630fcc19", size = 214006, upload-time = "2025-12-11T20:05:14.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -261,7 +261,7 @@ dev = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.83a9" },
|
||||
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.84" },
|
||||
{ name = "debugpy", specifier = ">=1.8.17" },
|
||||
{ name = "fastapi", specifier = ">=0.115.12" },
|
||||
{ name = "fastapi-babel", specifier = ">=1.0.0" },
|
||||
|
|
@ -309,35 +309,35 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.42.24"
|
||||
version = "1.42.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/79/2dac8b7cb075cfa43908ee9af3f8ee06880d84b86013854c5cca8945afac/boto3-1.42.30.tar.gz", hash = "sha256:ba9cd2f7819637d15bfbeb63af4c567fcc8a7dcd7b93dd12734ec58601169538", size = 112809, upload-time = "2026-01-16T20:37:23.636Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b3/2c0d828c9f668292e277ca5232e6160dd5b4b660a3f076f20dd5378baa1e/boto3-1.42.30-py3-none-any.whl", hash = "sha256:d7e548bea65e0ae2c465c77de937bc686b591aee6a352d5a19a16bc751e591c1", size = 140573, upload-time = "2026-01-16T20:37:22.089Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.42.24"
|
||||
version = "1.42.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/38/23862628a0eb044c8b8b3d7a9ad1920b3bfd6bce6d746d5a871e8382c7e4/botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116", size = 14891439, upload-time = "2026-01-16T20:37:13.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/8d/6d7b016383b1f74dd93611b1c5078bbaddaca901553ab886dcda87cae365/botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02", size = 14566340, upload-time = "2026-01-16T20:37:10.94Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camel-ai"
|
||||
version = "0.2.83a9"
|
||||
version = "0.2.84"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "astor" },
|
||||
|
|
@ -354,9 +354,9 @@ dependencies = [
|
|||
{ name = "tiktoken" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/8c/7d8071776ba973bb6e734edb6caaf4fdbdf60ecebdc1c4017948cc67ad48/camel_ai-0.2.83a9.tar.gz", hash = "sha256:2ee560551797b089f9849d3b9d63cd3a2b4eb45d339d17e6bf95eba2b85c4b50", size = 1124774, upload-time = "2026-01-15T21:28:24.51Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/62/96d922750b304ab2dd0ac1ef35dd3fbbf0e9a44f147e08a1ddfeb94c51c6/camel_ai-0.2.84.tar.gz", hash = "sha256:173c79755fc986e3fa8e27523606222c5f8816fc085abb24831912dcd4a0dec3", size = 1125724, upload-time = "2026-01-20T17:23:13.524Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/77/f7594707571af9c86351a69ff9f7f580602b42ffe8113803153c069b6bff/camel_ai-0.2.83a9-py3-none-any.whl", hash = "sha256:7cfe97b590096c1cc5afddf6dca023c5b9a47d104196c16b4b2b1934931af260", size = 1595808, upload-time = "2026-01-15T21:28:21.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/8b/246abd2c47154de6220fd0a286c3fe50f343d49943ae17e26b9f824a1ca0/camel_ai-0.2.84-py3-none-any.whl", hash = "sha256:63bfbd09e605f9087bb73eb9b929e162fbc6778084ce50e43a367fc0e98cbc65", size = 1599378, upload-time = "2026-01-20T17:23:11.216Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -990,14 +990,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "httplib2"
|
||||
version = "0.31.0"
|
||||
version = "0.31.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyparsing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/77/6653db69c1f7ecfe5e3f9726fdadc981794656fcd7d98c4209fecfea9993/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c", size = 250759, upload-time = "2025-09-11T12:16:03.403Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/df/6eb1d485a513776bbdbb1c919b72e59b5acc51c5e7ef28ad1cd444e252a3/httplib2-0.31.1.tar.gz", hash = "sha256:21591655ac54953624c6ab8d587c71675e379e31e2cfe3147c83c11e9ef41f92", size = 250746, upload-time = "2026-01-13T12:14:14.365Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/d8/1b05076441c2f01e4b64f59e5255edc2f0384a711b6d618845c023dc269b/httplib2-0.31.1-py3-none-any.whl", hash = "sha256:d520d22fa7e50c746a7ed856bac298c4300105d01bc2d8c2580a9b57fb9ed617", size = 91101, upload-time = "2026-01-13T12:14:12.676Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1046,7 +1046,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "1.3.1"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
|
|
@ -1060,9 +1060,9 @@ dependencies = [
|
|||
{ name = "typer-slim" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/dd/1cc985c5dda36298b152f75e82a1c81f52243b78fb7e9cad637a29561ad1/huggingface_hub-1.3.1.tar.gz", hash = "sha256:e80e0cfb4a75557c51ab20d575bdea6bb6106c2f97b7c75d8490642f1efb6df5", size = 622356, upload-time = "2026-01-09T14:08:16.888Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/d6/02d1c505e1d3364230e5fa16d2b58c8f36a39c5efe8e99bc4d03d06fd0ca/huggingface_hub-1.3.2.tar.gz", hash = "sha256:15d7902e154f04174a0816d1e9594adcf15cdad57596920a5dc70fadb5d896c7", size = 624018, upload-time = "2026-01-14T13:57:39.635Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/fb/cb8fe5f71d5622427f20bcab9e06a696a5aaf21bfe7bd0a8a0c63c88abf5/huggingface_hub-1.3.1-py3-none-any.whl", hash = "sha256:efbc7f3153cb84e2bb69b62ed90985e21ecc9343d15647a419fc0ee4b85f0ac3", size = 533351, upload-time = "2026-01-09T14:08:14.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/1d/acd3ef8aabb7813c6ef2f91785d855583ac5cd7c3599e5c1a1a2ed1ec2e5/huggingface_hub-1.3.2-py3-none-any.whl", hash = "sha256:b552b9562a5532102a041fa31a6966bb9de95138fc7aa578bb3703198c25d1b6", size = 534504, upload-time = "2026-01-14T13:57:37.555Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1456,30 +1456,30 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "nodejs-wheel"
|
||||
version = "24.12.0"
|
||||
version = "24.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodejs-wheel-binaries" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/30/73/0e8cd7c336f64d3b72c608bc7a7a5074cf6a9721c5d3630b8803f3176a3d/nodejs_wheel-24.12.0.tar.gz", hash = "sha256:edfaa3482bd21a2da03a9e7ebda7d4d738cdc864a2d9ddfe87760994a9644232", size = 2968, upload-time = "2025-12-11T21:12:26.103Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/f0/3345c6ec958c96eaa9d59355e59c0e93359aec54634f38bc4cd06baf23aa/nodejs_wheel-24.13.0.tar.gz", hash = "sha256:8c423cbf434b4c853ebac076d563b0899d3c6594ef0f99f6cd368ca4e3a28ca2", size = 2965, upload-time = "2026-01-14T11:05:32.811Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/5f/72f857250e54c9dacdfbf35f9d77eefdf954de0679f905e2fd03d8faf980/nodejs_wheel-24.12.0-py3-none-any.whl", hash = "sha256:0234fa0c46902d7efb858d41f5d055948cafa6a824812e9e8eeb64662d8963b6", size = 3988, upload-time = "2025-12-11T21:11:56.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/29/f259f6c5d31a0dae8257afd8dd7ab4b60a1b6e03fab793789e5bad480d83/nodejs_wheel-24.13.0-py3-none-any.whl", hash = "sha256:c0fc56a4677f55f7639f306a6381fb253d11ce24189c87a5489ea848f6e2bf24", size = 3986, upload-time = "2026-01-14T11:05:02.807Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodejs-wheel-binaries"
|
||||
version = "24.12.0"
|
||||
version = "24.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/f1/73182280e2c05f49a7c2c8dbd46144efe3f74f03f798fb90da67b4a93bbf/nodejs_wheel_binaries-24.13.0.tar.gz", hash = "sha256:766aed076e900061b83d3e76ad48bfec32a035ef0d41bd09c55e832eb93ef7a4", size = 8056, upload-time = "2026-01-14T11:05:33.653Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/dc/4d7548aa74a5b446d093f03aff4fb236b570959d793f21c9c42ab6ad870a/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:356654baa37bfd894e447e7e00268db403ea1d223863963459a0fbcaaa1d9d48", size = 55133268, upload-time = "2026-01-14T11:05:05.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/8a/8a4454d28339487240dd2232f42f1090e4a58544c581792d427f6239798c/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:92fdef7376120e575f8b397789bafcb13bbd22a1b4d21b060d200b14910f22a5", size = 55314800, upload-time = "2026-01-14T11:05:09.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/fb/46c600fcc748bd13bc536a735f11532a003b14f5c4dfd6865f5911672175/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3f619ac140e039ecd25f2f71d6e83ad1414017a24608531851b7c31dc140cdfd", size = 59666320, upload-time = "2026-01-14T11:05:12.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/47/d48f11fc5d1541ace5d806c62a45738a1db9ce33e85a06fe4cd3d9ce83f6/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:dfb31ebc2c129538192ddb5bedd3d63d6de5d271437cd39ea26bf3fe229ba430", size = 60162447, upload-time = "2026-01-14T11:05:16.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/74/d285c579ae8157c925b577dde429543963b845e69cd006549e062d1cf5b6/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdd720d7b378d5bb9b2710457bbc880d4c4d1270a94f13fbe257198ac707f358", size = 61659994, upload-time = "2026-01-14T11:05:19.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/97/88b4254a2ff93ed2eaed725f77b7d3d2d8d7973bf134359ce786db894faf/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ad6383613f3485a75b054647a09f1cd56d12380d7459184eebcf4a5d403f35c", size = 62244373, upload-time = "2026-01-14T11:05:23.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/c3/0e13a3da78f08cb58650971a6957ac7bfef84164b405176e53ab1e3584e2/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_amd64.whl", hash = "sha256:605be4763e3ef427a3385a55da5a1bcf0a659aa2716eebbf23f332926d7e5f23", size = 41345528, upload-time = "2026-01-14T11:05:27.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/f1/0578d65b4e3dc572967fd702221ea1f42e1e60accfb6b0dd8d8f15410139/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_arm64.whl", hash = "sha256:2e3431d869d6b2dbeef1d469ad0090babbdcc8baaa72c01dd3cc2c6121c96af5", size = 39054688, upload-time = "2026-01-14T11:05:30.739Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1933,26 +1933,26 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pyarrow"
|
||||
version = "22.0.0"
|
||||
version = "23.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/2f/23e042a5aa99bcb15e794e14030e8d065e00827e846e53a66faec73c7cd6/pyarrow-23.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cbdc2bf5947aa4d462adcf8453cf04aee2f7932653cb67a27acd96e5e8528a67", size = 34281861, upload-time = "2026-01-18T16:13:34.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/65/1651933f504b335ec9cd8f99463718421eb08d883ed84f0abd2835a16cad/pyarrow-23.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4d38c836930ce15cd31dce20114b21ba082da231c884bdc0a7b53e1477fe7f07", size = 35825067, upload-time = "2026-01-18T16:13:42.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ec/d6fceaec050c893f4e35c0556b77d4cc9973fcc24b0a358a5781b1234582/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4222ff8f76919ecf6c716175a0e5fddb5599faeed4c56d9ea41a2c42be4998b2", size = 44458539, upload-time = "2026-01-18T16:13:52.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/d9/369f134d652b21db62fe3ec1c5c2357e695f79eb67394b8a93f3a2b2cffa/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:87f06159cbe38125852657716889296c83c37b4d09a5e58f3d10245fd1f69795", size = 47535889, upload-time = "2026-01-18T16:14:03.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/95/f37b6a252fdbf247a67a78fb3f61a529fe0600e304c4d07741763d3522b1/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1675c374570d8b91ea6d4edd4608fa55951acd44e0c31bd146e091b4005de24f", size = 48157777, upload-time = "2026-01-18T16:14:12.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ab/fb94923108c9c6415dab677cf1f066d3307798eafc03f9a65ab4abc61056/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:247374428fde4f668f138b04031a7e7077ba5fa0b5b1722fdf89a017bf0b7ee0", size = 50580441, upload-time = "2026-01-18T16:14:20.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/78/897ba6337b517fc8e914891e1bd918da1c4eb8e936a553e95862e67b80f6/pyarrow-23.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:de53b1bd3b88a2ee93c9af412c903e57e738c083be4f6392288294513cd8b2c1", size = 27530028, upload-time = "2026-01-18T16:14:27.353Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2051,14 +2051,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pydash"
|
||||
version = "8.0.5"
|
||||
version = "8.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/24/91c037f47e434172c2112d65c00c84d475a6715425e3315ba2cbb7a87e66/pydash-8.0.5.tar.gz", hash = "sha256:7cc44ebfe5d362f4f5f06c74c8684143c5ac481376b059ff02570705523f9e2e", size = 164861, upload-time = "2025-01-17T16:08:50.562Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/c1/1c55272f49d761cec38ddb80be9817935b9c91ebd6a8988e10f532868d56/pydash-8.0.6.tar.gz", hash = "sha256:b2821547e9723f69cf3a986be4db64de41730be149b2641947ecd12e1e11025a", size = 164338, upload-time = "2026-01-17T16:42:56.576Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/86/e74c978800131c657fc5145f2c1c63e0cea01a49b6216f729cf77a2e1edf/pydash-8.0.5-py3-none-any.whl", hash = "sha256:b2625f8981862e19911daa07f80ed47b315ce20d9b5eb57aaf97aaf570c3892f", size = 102077, upload-time = "2025-01-17T16:08:47.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2277,38 +2277,40 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2025.11.3"
|
||||
version = "2026.1.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087, upload-time = "2025-11-03T21:30:47.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544, upload-time = "2025-11-03T21:30:49.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408, upload-time = "2025-11-03T21:30:51.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584, upload-time = "2025-11-03T21:30:52.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733, upload-time = "2025-11-03T21:30:53.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691, upload-time = "2025-11-03T21:30:55.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662, upload-time = "2025-11-03T21:30:57.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587, upload-time = "2025-11-03T21:30:58.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709, upload-time = "2025-11-03T21:31:00.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773, upload-time = "2025-11-03T21:31:01.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164, upload-time = "2025-11-03T21:31:03.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832, upload-time = "2025-11-03T21:31:04.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802, upload-time = "2025-11-03T21:31:06.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722, upload-time = "2025-11-03T21:31:08.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289, upload-time = "2025-11-03T21:31:10.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "4.4.7"
|
||||
version = "4.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/a7/4600cb1cfc975a06552e8927844ddcb8fd90217e9a6068f5c7aa76c3f221/reportlab-4.4.7.tar.gz", hash = "sha256:41e8287af965e5996764933f3e75e7f363c3b6f252ba172f9429e81658d7b170", size = 3714000, upload-time = "2025-12-21T11:50:11.336Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/39/42cf24aee570a80e1903221ae3a92a2e34c324794a392eb036cbb6dc3839/reportlab-4.4.9.tar.gz", hash = "sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378", size = 3911246, upload-time = "2026-01-15T10:07:56.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263, upload-time = "2025-12-21T11:50:08.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/77/546e50edfaba6a0e58e8ec5fdc4446510227cec9e8f40172b60941d5a633/reportlab-4.4.9-py3-none-any.whl", hash = "sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd", size = 1954401, upload-time = "2026-01-15T09:27:59.133Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2453,11 +2455,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8.1"
|
||||
version = "2.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2474,15 +2476,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.1.2"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2540,11 +2542,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ description: "Configure your own API keys to use various LLM providers with Eige
|
|||
- **Direct access** to the latest models from each provider
|
||||
- **Privacy** - your requests go directly to the provider
|
||||
|
||||
---
|
||||
|
||||
## OpenAI Configuration (Example)
|
||||
|
||||
### Step 1: Get Your API Key
|
||||
|
|
@ -23,70 +21,70 @@ description: "Configure your own API keys to use various LLM providers with Eige
|
|||
|
||||
### Step 2: Configure in Eigent
|
||||
|
||||
1. Launch Eigent and go to **Settings** > **Models**
|
||||
1. Launch Eigent and go to **Settings** \> **Models**
|
||||
2. Find the **OpenAI** card in the Custom Model section
|
||||
|
||||
<img
|
||||
src="/images/Screenshot2026-01-20at18.13.45.png"
|
||||
alt="Screenshot 2026 01 20 At 18 13 45"
|
||||
title="Screenshot 2026 01 20 At 18 13 45"
|
||||
className="mr-auto"
|
||||
style={{ width:"74%" }}
|
||||
/>
|
||||
3. Fill in the following fields:
|
||||
|
||||
| Field | Value | Example |
|
||||
|-------|-------|---------|
|
||||
| **API Key** | Your OpenAI secret key | `sk-proj-xxxx...` |
|
||||
| **API Host** | OpenAI API endpoint | `https://api.openai.com/v1` |
|
||||
| **Model Type** | The model you want to use | `gpt-4o`, `gpt-4o-mini` |
|
||||
| Field | Value | Example |
|
||||
| -------------- | ------------------------- | --------------------------- |
|
||||
| **API Key** | Your OpenAI secret key | `sk-proj-xxxx...` |
|
||||
| **API Host** | OpenAI API endpoint | `https://api.openai.com/v1` |
|
||||
| **Model Type** | The model you want to use | `gpt-4o`, `gpt-4o-mini` |
|
||||
|
||||
4. Click **Save** to validate and store your configuration
|
||||
5. Click **Set as Default** to use this provider for your agents
|
||||
|
||||
---
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Field | Description | Required |
|
||||
|-------|-------------|----------|
|
||||
| **API Key** | Your authentication key from the provider | Yes |
|
||||
| **API Host** | The API endpoint URL | Yes (pre-filled for most providers) |
|
||||
| **Model Type** | The specific model variant to use | Yes |
|
||||
| **External Config** | Provider-specific settings (e.g., Azure deployment name) | Only for certain providers |
|
||||
| Field | Description | Required |
|
||||
| ------------------- | -------------------------------------------------------- | ----------------------------------- |
|
||||
| **API Key** | Your authentication key from the provider | Yes |
|
||||
| **API Host** | The API endpoint URL | Yes (pre-filled for most providers) |
|
||||
| **Model Type** | The specific model variant to use | Yes |
|
||||
| **External Config** | Provider-specific settings (e.g., Azure deployment name) | Only for certain providers |
|
||||
|
||||
### Azure-Specific Fields
|
||||
|
||||
| Field | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| **API Version** | Azure OpenAI API version | `2024-02-15-preview` |
|
||||
| Field | Description | Example |
|
||||
| ------------------- | -------------------------- | -------------------- |
|
||||
| **API Version** | Azure OpenAI API version | `2024-02-15-preview` |
|
||||
| **Deployment Name** | Your Azure deployment name | `my-gpt4-deployment` |
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
When saving your configuration, Eigent validates your API key and model. Here are the errors you may encounter:
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| **Invalid key. Validation failed.** | API key is incorrect, expired, or malformed | Double-check your API key. Regenerate a new key if needed. |
|
||||
| **Invalid model name. Validation failed.** | The specified model does not exist or is not available for your account | Verify the model name is correct. Check if you have access to that model. |
|
||||
| **You exceeded your current quota** | API quota exhausted or billing issue | Check your provider's billing dashboard. Add credits or upgrade your plan. |
|
||||
|
||||
---
|
||||
| Error | Cause | Solution |
|
||||
| ------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| **Invalid key. Validation failed.** | API key is incorrect, expired, or malformed | Double-check your API key. Regenerate a new key if needed. |
|
||||
| **Invalid model name. Validation failed.** | The specified model does not exist or is not available for your account | Verify the model name is correct. Check if you have access to that model. |
|
||||
| **You exceeded your current quota** | API quota exhausted or billing issue | Check your provider's billing dashboard. Add credits or upgrade your plan. |
|
||||
|
||||
## Supported Providers
|
||||
|
||||
Eigent supports the following BYOK providers:
|
||||
|
||||
| Provider | Default API Host | Official Documentation |
|
||||
|----------|------------------|------------------------|
|
||||
| **OpenAI** | `https://api.openai.com/v1` | [OpenAI API Docs](https://platform.openai.com/docs/api-reference) |
|
||||
| **Anthropic** | `https://api.anthropic.com/v1/` | [Anthropic API Docs](https://docs.anthropic.com/en/api/getting-started) |
|
||||
| **Google Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai/` | [Gemini API Docs](https://ai.google.dev/gemini-api/docs) |
|
||||
| **OpenRouter** | `https://openrouter.ai/api/v1` | [OpenRouter Docs](https://openrouter.ai/docs) |
|
||||
| **Qwen (Alibaba)** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | [Qwen API Docs](https://help.aliyun.com/zh/dashscope/developer-reference/api-details) |
|
||||
| **DeepSeek** | `https://api.deepseek.com` | [DeepSeek API Docs](https://platform.deepseek.com/api-docs) |
|
||||
| **Minimax** | `https://api.minimax.io/v1` | [Minimax API Docs](https://platform.minimaxi.com/document/Announcement) |
|
||||
| **Z.ai** | `https://api.z.ai/api/coding/paas/v4/` | [Z.ai Platform](https://z.ai) |
|
||||
| **Azure OpenAI** | *(user-provided)* | [Azure OpenAI Docs](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) |
|
||||
| **AWS Bedrock** | *(user-provided)* | [AWS Bedrock Docs](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) |
|
||||
| **OpenAI Compatible** | *(user-provided)* | For custom endpoints (e.g., xAI, local servers) |
|
||||
|
||||
---
|
||||
| Provider | Default API Host | Official Documentation |
|
||||
| --------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| **OpenAI** | `https://api.openai.com/v1` | [OpenAI API Docs](https://platform.openai.com/docs/api-reference) |
|
||||
| **Anthropic** | `https://api.anthropic.com/v1/` | [Anthropic API Docs](https://docs.anthropic.com/en/api/getting-started) |
|
||||
| **Google Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai/` | [Gemini API Docs](https://ai.google.dev/gemini-api/docs) |
|
||||
| **OpenRouter** | `https://openrouter.ai/api/v1` | [OpenRouter Docs](https://openrouter.ai/docs) |
|
||||
| **Qwen (Alibaba)** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | [Qwen API Docs](https://help.aliyun.com/zh/dashscope/developer-reference/api-details) |
|
||||
| **DeepSeek** | `https://api.deepseek.com` | [DeepSeek API Docs](https://platform.deepseek.com/api-docs) |
|
||||
| **Minimax** | `https://api.minimax.io/v1` | [Minimax API Docs](https://platform.minimaxi.com/document/Announcement) |
|
||||
| **Z.ai** | `https://api.z.ai/api/coding/paas/v4/` | [Z.ai Platform](https://z.ai) |
|
||||
| **Azure OpenAI** | _(user-provided)_ | [Azure OpenAI Docs](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) |
|
||||
| **AWS Bedrock** | _(user-provided)_ | [AWS Bedrock Docs](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) |
|
||||
| **OpenAI Compatible** | _(user-provided)_ | For custom endpoints (e.g., xAI, local servers) |
|
||||
|
||||
## Tips
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
title: "Models (Local Model)"
|
||||
description: "Configure and deploy your preferred LLM models with Eigent."
|
||||
icon: "server"
|
||||
---
|
||||
|
||||
## **Self-Host Model**
|
||||
|
|
|
|||
|
|
@ -65,10 +65,10 @@
|
|||
"icon": "brain",
|
||||
"expanded": true,
|
||||
"pages": [
|
||||
"/core/models/byok",
|
||||
"/core/models/local-model",
|
||||
"/core/models/gemini",
|
||||
"/core/models/minimax",
|
||||
"/core/models/local-model"
|
||||
"/core/models/byok"
|
||||
"/core/models/minimax"
|
||||
]
|
||||
},
|
||||
"/core/tools",
|
||||
|
|
|
|||
BIN
docs/images/Screenshot2026-01-20at18.12.10.png
Normal file
BIN
docs/images/Screenshot2026-01-20at18.12.10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/images/Screenshot2026-01-20at18.13.45.png
Normal file
BIN
docs/images/Screenshot2026-01-20at18.13.45.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
docs/images/Screenshot2026-01-20at18.14.03.png
Normal file
BIN
docs/images/Screenshot2026-01-20at18.14.03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
|
|
@ -29,7 +29,13 @@
|
|||
{
|
||||
"from": "resources/prebuilt",
|
||||
"to": "prebuilt",
|
||||
"filter": ["**/*", "!cache/**/*", "!**/.npm-cache/**/*"]
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!cache/**/*",
|
||||
"!**/.npm-cache/**/*",
|
||||
"!uv_python/**/*.pyc",
|
||||
"!uv_python/**/__pycache__"
|
||||
]
|
||||
}
|
||||
],
|
||||
"protocols": [
|
||||
|
|
@ -67,7 +73,6 @@
|
|||
}
|
||||
},
|
||||
"win": {
|
||||
"certificateFile": null,
|
||||
"icon": "build/icon.ico",
|
||||
"artifactName": "${productName}.Setup.${version}.exe",
|
||||
"target": [
|
||||
|
|
@ -77,6 +82,11 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "build/icon.png",
|
||||
"target": ["AppImage"],
|
||||
"category": "Development"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
|
|
|
|||
|
|
@ -514,6 +514,15 @@ export class FileReader {
|
|||
})
|
||||
}
|
||||
|
||||
// Folders to hide in the Agent Folder view
|
||||
private readonly hiddenFolders = [
|
||||
'browser_agent',
|
||||
'developer_agent',
|
||||
'document_agent',
|
||||
'multi_modal_agent',
|
||||
'terminal_logs'
|
||||
];
|
||||
|
||||
private getFilesRecursive(dirPath: string, basePath: string): FileInfo[] {
|
||||
try {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
|
@ -521,6 +530,8 @@ export class FileReader {
|
|||
|
||||
for (const file of files) {
|
||||
if (file.startsWith(".")) continue;
|
||||
// Skip hidden folders
|
||||
if (this.hiddenFolders.includes(file)) continue;
|
||||
|
||||
const filePath = path.join(dirPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
|
|
|||
|
|
@ -34,12 +34,9 @@ import { zipFolder } from './utils/log';
|
|||
import mime from 'mime';
|
||||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import {
|
||||
checkAndInstallDepsOnUpdate,
|
||||
PromiseReturnType,
|
||||
getInstallationStatus,
|
||||
} from './install-deps';
|
||||
import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process';
|
||||
import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
|
||||
import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process'
|
||||
import { setVibrancy, setRoundedCorners, setTransparentTitlebar } from './native/macos-window'
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
|
||||
|
|
@ -112,6 +109,32 @@ app.commandLine.appendSwitch('max_old_space_size', '4096');
|
|||
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
|
||||
app.commandLine.appendSwitch('renderer-process-limit', '8');
|
||||
|
||||
// ==================== Anti-fingerprint settings ====================
|
||||
// Disable automation controlled indicator to avoid detection
|
||||
app.commandLine.appendSwitch(
|
||||
'disable-blink-features',
|
||||
'AutomationControlled'
|
||||
);
|
||||
|
||||
// Override User Agent to remove Electron/eigent identifiers
|
||||
// Dynamically generate User Agent based on actual platform and Chrome version
|
||||
const getPlatformUA = () => {
|
||||
// Use actual Chrome version from Electron instead of hardcoded value
|
||||
const chromeVersion = process.versions.chrome || '131.0.0.0';
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
case 'win32':
|
||||
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
case 'linux':
|
||||
return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
default:
|
||||
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
||||
}
|
||||
};
|
||||
const normalUserAgent = getPlatformUA();
|
||||
app.userAgentFallback = normalUserAgent;
|
||||
|
||||
// ==================== protocol privileges ====================
|
||||
// Register custom protocol privileges before app ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
|
|
@ -1253,9 +1276,7 @@ async function createWindow() {
|
|||
minHeight: 650,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
backgroundColor: '#f5f5f580',
|
||||
backgroundColor: '#00000000',
|
||||
titleBarStyle: isMac ? 'hidden' : undefined,
|
||||
trafficLightPosition: isMac ? { x: 10, y: 10 } : undefined,
|
||||
icon: path.join(VITE_PUBLIC, 'favicon.ico'),
|
||||
|
|
@ -1273,6 +1294,28 @@ async function createWindow() {
|
|||
},
|
||||
});
|
||||
|
||||
// Apply native macOS effects
|
||||
if (process.platform === 'darwin') {
|
||||
win.once('ready-to-show', () => {
|
||||
if (win && !win.isDestroyed()) {
|
||||
try {
|
||||
// Apply vibrancy with HUDWindow material (or others like 'Sidebar', 'UnderWindowBackground')
|
||||
setVibrancy(win, 'HUDWindow');
|
||||
|
||||
// Apply rounded corners
|
||||
setRoundedCorners(win, 20);
|
||||
|
||||
// Make titlebar transparent
|
||||
setTransparentTitlebar(win);
|
||||
|
||||
log.info('[MacOS] Applied native visual effects');
|
||||
} catch (error) {
|
||||
log.error('[MacOS] Failed to apply native visual effects:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Main window now uses default userData directly with partition 'persist:main_window'
|
||||
// No migration needed - data is already persistent
|
||||
|
||||
|
|
@ -1827,6 +1870,15 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== Anti-fingerprint: Set User Agent for all sessions ====================
|
||||
// Use the same dynamic User Agent as app.userAgentFallback
|
||||
session.defaultSession.setUserAgent(normalUserAgent);
|
||||
// Also set for the user_login partition used by webviews
|
||||
session.fromPartition('persist:user_login').setUserAgent(normalUserAgent);
|
||||
// And for main_window partition
|
||||
session.fromPartition('persist:main_window').setUserAgent(normalUserAgent);
|
||||
log.info('[ANTI-FINGERPRINT] User Agent set for all sessions');
|
||||
|
||||
// ==================== download handle ====================
|
||||
session.defaultSession.on('will-download', (event, item, webContents) => {
|
||||
item.once('done', (event, state) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName } from "./utils/process";
|
||||
import { getBackendPath, getBinaryPath, getCachePath, getVenvPath, getUvEnv, isBinaryExists, runInstallScript, killProcessByName, getPrebuiltPythonDir, getPrebuiltVenvPath } from "./utils/process";
|
||||
import { spawn, exec } from 'child_process'
|
||||
import log from 'electron-log'
|
||||
import fs from 'fs'
|
||||
|
|
@ -225,21 +225,34 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
log.warn(`Failed to remove lock file: ${e}`);
|
||||
}
|
||||
|
||||
// Cleanup corrupted python cache
|
||||
// Cleanup corrupted python cache ONLY if we don't have bundled Python
|
||||
// If we have bundled Python, we want to keep it and reuse it
|
||||
const prebuiltPythonDir = getPrebuiltPythonDir();
|
||||
try {
|
||||
const pythonCacheDir = getCachePath('uv_python');
|
||||
if (fs.existsSync(pythonCacheDir)) {
|
||||
// Only remove cache if we DON'T have prebuilt Python available
|
||||
// When prebuilt Python exists, UV will use it via UV_PYTHON_INSTALL_DIR
|
||||
if (fs.existsSync(pythonCacheDir) && !prebuiltPythonDir) {
|
||||
log.info(`Removing potentially corrupted Python cache: ${pythonCacheDir}`);
|
||||
fs.rmSync(pythonCacheDir, { recursive: true, force: true });
|
||||
} else if (prebuiltPythonDir) {
|
||||
log.info(`Preserving bundled Python at: ${prebuiltPythonDir}`);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(`Failed to remove Python cache: ${e}`);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import {
|
|||
getBinaryPath,
|
||||
getCachePath,
|
||||
getVenvPath,
|
||||
getTerminalVenvPath,
|
||||
getUvEnv,
|
||||
cleanupOldVenvs,
|
||||
isBinaryExists,
|
||||
runInstallScript,
|
||||
TERMINAL_BASE_PACKAGES,
|
||||
} from './utils/process';
|
||||
import { spawn } from 'child_process';
|
||||
import { safeMainWindowSend } from './utils/safeWebContentsSend';
|
||||
|
|
@ -482,6 +484,139 @@ const runInstall = (extraArgs: string[], version: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Install terminal base venv with common packages for terminal tasks.
|
||||
* This is a lightweight venv separate from the backend venv.
|
||||
*/
|
||||
async function installTerminalBaseVenv(version: string): Promise<PromiseReturnType> {
|
||||
const terminalVenvPath = getTerminalVenvPath(version);
|
||||
const pythonPath = process.platform === 'win32'
|
||||
? path.join(terminalVenvPath, 'Scripts', 'python.exe')
|
||||
: path.join(terminalVenvPath, 'bin', 'python');
|
||||
// Marker file to indicate packages were installed successfully
|
||||
const installedMarker = path.join(terminalVenvPath, '.packages_installed');
|
||||
|
||||
// Check if terminal base venv already exists and packages are installed
|
||||
if (fs.existsSync(pythonPath) && fs.existsSync(installedMarker)) {
|
||||
log.info('[DEPS INSTALL] Terminal base venv already exists with packages, skipping creation');
|
||||
return { message: 'Terminal base venv already exists', success: true };
|
||||
}
|
||||
|
||||
// If python exists but marker doesn't, packages may not be installed - need to reinstall
|
||||
const needsPackageInstall = fs.existsSync(pythonPath) && !fs.existsSync(installedMarker);
|
||||
|
||||
if (needsPackageInstall) {
|
||||
log.info('[DEPS INSTALL] Terminal venv exists but packages not installed, installing packages...');
|
||||
} else {
|
||||
log.info('[DEPS INSTALL] Creating terminal base venv...');
|
||||
}
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: needsPackageInstall
|
||||
? 'Installing missing packages in terminal environment...\n'
|
||||
: 'Creating terminal base environment...\n',
|
||||
});
|
||||
|
||||
try {
|
||||
// Create the venv using uv (skip if only need package install)
|
||||
if (!needsPackageInstall) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const createVenv = spawn(
|
||||
uv_path,
|
||||
['venv', '--python', '3.10', terminalVenvPath],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
createVenv.stdout.on('data', (data) => {
|
||||
log.info(`[DEPS INSTALL] terminal venv: ${data}`);
|
||||
});
|
||||
|
||||
createVenv.stderr.on('data', (data) => {
|
||||
log.info(`[DEPS INSTALL] terminal venv: ${data}`);
|
||||
});
|
||||
|
||||
createVenv.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to create terminal venv, exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
createVenv.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Install base packages
|
||||
log.info('[DEPS INSTALL] Installing terminal base packages...');
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: `Installing packages: ${TERMINAL_BASE_PACKAGES.join(', ')}...\n`,
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const installPkgs = spawn(
|
||||
uv_path,
|
||||
[
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
pythonPath,
|
||||
...TERMINAL_BASE_PACKAGES,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
installPkgs.stdout.on('data', (data) => {
|
||||
log.info(`[DEPS INSTALL] terminal packages: ${data}`);
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
installPkgs.stderr.on('data', (data) => {
|
||||
log.info(`[DEPS INSTALL] terminal packages: ${data}`);
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
installPkgs.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to install terminal packages, exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
installPkgs.on('error', reject);
|
||||
});
|
||||
|
||||
// Create marker file to indicate successful installation
|
||||
fs.writeFileSync(installedMarker, new Date().toISOString());
|
||||
log.info('[DEPS INSTALL] Terminal base venv created successfully');
|
||||
return { message: 'Terminal base venv created successfully', success: true };
|
||||
} catch (error) {
|
||||
log.error('[DEPS INSTALL] Failed to create terminal base venv:', error);
|
||||
return {
|
||||
message: `Failed to create terminal base venv: ${error}`,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function installDependencies(
|
||||
version: string
|
||||
): Promise<PromiseReturnType> {
|
||||
|
|
@ -890,6 +1025,13 @@ export async function installDependencies(
|
|||
// try default install
|
||||
const installSuccess = await runInstall([], version);
|
||||
if (installSuccess.success) {
|
||||
// Install terminal base venv (lightweight venv for terminal tasks)
|
||||
log.info('[DEPS INSTALL] Installing terminal base venv...');
|
||||
const terminalResult = await installTerminalBaseVenv(version);
|
||||
if (!terminalResult.success) {
|
||||
log.warn('[DEPS INSTALL] Terminal base venv installation failed, but continuing...', terminalResult.message);
|
||||
}
|
||||
|
||||
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
|
||||
log.info(
|
||||
'[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...'
|
||||
|
|
@ -922,6 +1064,13 @@ export async function installDependencies(
|
|||
: await runInstall([], version);
|
||||
|
||||
if (mirrorInstallSuccess.success) {
|
||||
// Install terminal base venv (lightweight venv for terminal tasks)
|
||||
log.info('[DEPS INSTALL] Installing terminal base venv...');
|
||||
const terminalResult = await installTerminalBaseVenv(version);
|
||||
if (!terminalResult.success) {
|
||||
log.warn('[DEPS INSTALL] Terminal base venv installation failed, but continuing...', terminalResult.message);
|
||||
}
|
||||
|
||||
// Install hybrid_browser_toolkit npm dependencies after Python packages are installed
|
||||
log.info(
|
||||
'[DEPS INSTALL] Installing hybrid_browser_toolkit dependencies...'
|
||||
|
|
|
|||
171
electron/main/native/macos-window.ts
Normal file
171
electron/main/native/macos-window.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { BrowserWindow } from 'electron';
|
||||
import koffi from 'koffi';
|
||||
import os from 'os';
|
||||
|
||||
// NSVisualEffectView material constants (enum values)
|
||||
export const NSVisualEffectMaterial = {
|
||||
Titlebar: 3,
|
||||
Selection: 4,
|
||||
Menu: 5,
|
||||
Popover: 6,
|
||||
Sidebar: 7,
|
||||
HeaderView: 10,
|
||||
Sheet: 11,
|
||||
WindowBackground: 12,
|
||||
HUDWindow: 13,
|
||||
FullScreenUI: 15,
|
||||
ToolTip: 17,
|
||||
ContentBackground: 18,
|
||||
UnderWindowBackground: 21,
|
||||
UnderPageBackground: 22
|
||||
} as const;
|
||||
|
||||
export type MaterialType = keyof typeof NSVisualEffectMaterial;
|
||||
|
||||
// Interface for our module functions
|
||||
interface MacWindowUtils {
|
||||
setVibrancy: (window: BrowserWindow, material?: MaterialType) => void;
|
||||
setRoundedCorners: (window: BrowserWindow, radius?: number) => void;
|
||||
setTransparentTitlebar: (window: BrowserWindow) => void;
|
||||
}
|
||||
|
||||
let utils: MacWindowUtils;
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
try {
|
||||
const objc = koffi.load('libobjc.A.dylib');
|
||||
|
||||
// Types
|
||||
const Ptr = 'size_t';
|
||||
|
||||
const objc_getClass = objc.func('objc_getClass', Ptr, ['string']);
|
||||
const sel_registerName = objc.func('sel_registerName', Ptr, ['string']);
|
||||
const objc_msgSend = objc.func('objc_msgSend', Ptr, [Ptr, Ptr]);
|
||||
const objc_msgSend_long = objc.func('objc_msgSend', Ptr, [Ptr, Ptr, 'long']);
|
||||
const objc_msgSend_double = objc.func('objc_msgSend', Ptr, [Ptr, Ptr, 'double']);
|
||||
const objc_msgSend_bool = objc.func('objc_msgSend', Ptr, [Ptr, Ptr, 'bool']);
|
||||
|
||||
const NSRect = koffi.struct('NSRect', {
|
||||
x: 'double',
|
||||
y: 'double',
|
||||
width: 'double',
|
||||
height: 'double'
|
||||
});
|
||||
|
||||
const NSVisualEffectBlendingMode = {
|
||||
BehindWindow: 0,
|
||||
WithinWindow: 1
|
||||
};
|
||||
|
||||
utils = {
|
||||
setVibrancy: (window: BrowserWindow, material: MaterialType = 'HUDWindow') => {
|
||||
try {
|
||||
const windowHandle = window.getNativeWindowHandle();
|
||||
if (windowHandle.length === 0) return;
|
||||
|
||||
// Electron calls valid native handle returns the NSView (BridgedContentView) on macOS
|
||||
const nsViewPtr = windowHandle.readBigUInt64LE();
|
||||
if (!nsViewPtr) return;
|
||||
|
||||
// Selectors
|
||||
const selAlloc = sel_registerName('alloc');
|
||||
const selInit = sel_registerName('init');
|
||||
const selSetMaterial = sel_registerName('setMaterial:');
|
||||
const selSetBlendingMode = sel_registerName('setBlendingMode:');
|
||||
const selSetState = sel_registerName('setState:');
|
||||
const selSetAutoresizingMask = sel_registerName('setAutoresizingMask:');
|
||||
const selSetFrame = sel_registerName('setFrame:');
|
||||
const selAddSubview = sel_registerName('addSubview:positioned:relativeTo:');
|
||||
|
||||
const NSVisualEffectViewClass = objc_getClass('NSVisualEffectView');
|
||||
if (!NSVisualEffectViewClass) return;
|
||||
|
||||
// Allocation
|
||||
const visualEffectView = objc_msgSend(NSVisualEffectViewClass, selAlloc);
|
||||
objc_msgSend(visualEffectView, selInit);
|
||||
|
||||
const materialValue = NSVisualEffectMaterial[material] || NSVisualEffectMaterial.HUDWindow;
|
||||
|
||||
// Configuration
|
||||
objc_msgSend_long(visualEffectView, selSetMaterial, materialValue);
|
||||
objc_msgSend_long(visualEffectView, selSetBlendingMode, NSVisualEffectBlendingMode.BehindWindow);
|
||||
objc_msgSend_long(visualEffectView, selSetState, 1);
|
||||
objc_msgSend_long(visualEffectView, selSetAutoresizingMask, 18);
|
||||
|
||||
// Frame
|
||||
const bounds = window.getBounds();
|
||||
const viewFrame = { x: 0, y: 0, width: bounds.width, height: bounds.height };
|
||||
|
||||
const objc_msgSend_frame = objc.func('objc_msgSend', 'void', [Ptr, Ptr, NSRect]);
|
||||
objc_msgSend_frame(visualEffectView, selSetFrame, viewFrame);
|
||||
|
||||
// Add Subview to the CONTENT VIEW (which we already have as nsViewPtr)
|
||||
const objc_msgSend_positioned = objc.func('objc_msgSend', 'void', [Ptr, Ptr, Ptr, 'long', Ptr]);
|
||||
objc_msgSend_positioned(nsViewPtr, selAddSubview, visualEffectView, -1, 0); // -1 = NSWindowBelow
|
||||
|
||||
console.log(`[MacOS] Vibrancy applied successfully`);
|
||||
} catch (error) {
|
||||
console.error('[MacOS] Error applying vibrancy:', error);
|
||||
}
|
||||
},
|
||||
|
||||
setRoundedCorners: (window: BrowserWindow, radius = 20) => {
|
||||
try {
|
||||
const windowHandle = window.getNativeWindowHandle();
|
||||
const nsViewPtr = windowHandle.readBigUInt64LE();
|
||||
|
||||
const selLayer = sel_registerName('layer');
|
||||
const selSetWantsLayer = sel_registerName('setWantsLayer:');
|
||||
const selSetCornerRadius = sel_registerName('setCornerRadius:');
|
||||
const selSetMasksToBounds = sel_registerName('setMasksToBounds:');
|
||||
|
||||
// Ensure layer-backing
|
||||
objc_msgSend_bool(nsViewPtr, selSetWantsLayer, true);
|
||||
|
||||
// Get layer
|
||||
const nsLayer = objc_msgSend(nsViewPtr, selLayer);
|
||||
if (!nsLayer) return console.error('[MacOS] Failed to get layer');
|
||||
|
||||
// Apply Corner Radius
|
||||
objc_msgSend_double(nsLayer, selSetCornerRadius, radius);
|
||||
objc_msgSend_bool(nsLayer, selSetMasksToBounds, true);
|
||||
|
||||
console.log(`[MacOS] Rounded corners applied: ${radius}`);
|
||||
} catch (error) {
|
||||
console.error('[MacOS] Error applying rounded corners:', error);
|
||||
}
|
||||
},
|
||||
|
||||
setTransparentTitlebar: (window: BrowserWindow) => {
|
||||
try {
|
||||
const windowHandle = window.getNativeWindowHandle();
|
||||
const nsViewPtr = windowHandle.readBigUInt64LE();
|
||||
|
||||
// We have the View, we need the Window
|
||||
const selWindow = sel_registerName('window');
|
||||
const nsWindowPtr = objc_msgSend(nsViewPtr, selWindow);
|
||||
|
||||
if (!nsWindowPtr) return console.error('[MacOS] Failed to get NSWindow from NSView');
|
||||
|
||||
const selSetTitlebarAppearsTransparent = sel_registerName('setTitlebarAppearsTransparent:');
|
||||
objc_msgSend_bool(nsWindowPtr, selSetTitlebarAppearsTransparent, true);
|
||||
|
||||
console.log('[MacOS] Transparent titlebar applied');
|
||||
} catch (error) {
|
||||
console.error('[MacOS] Error setting transparent titlebar:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('[MacOS] Failed to load native libraries:', e);
|
||||
utils = { setVibrancy: () => { }, setRoundedCorners: () => { }, setTransparentTitlebar: () => { } };
|
||||
}
|
||||
} else {
|
||||
utils = {
|
||||
setVibrancy: () => { },
|
||||
setRoundedCorners: () => { },
|
||||
setTransparentTitlebar: () => { }
|
||||
};
|
||||
}
|
||||
|
||||
export const { setVibrancy, setRoundedCorners, setTransparentTitlebar } = utils;
|
||||
|
|
@ -149,8 +149,21 @@ export function getPrebuiltVenvPath(): string | null {
|
|||
if (fs.existsSync(prebuiltVenvPath)) {
|
||||
const pyvenvCfg = path.join(prebuiltVenvPath, 'pyvenv.cfg');
|
||||
if (fs.existsSync(pyvenvCfg)) {
|
||||
log.info(`Using prebuilt venv: ${prebuiltVenvPath}`);
|
||||
return prebuiltVenvPath;
|
||||
// Verify Python executable exists (Windows: Scripts/python.exe, Unix: bin/python)
|
||||
const isWindows = process.platform === 'win32';
|
||||
const pythonExePath = isWindows
|
||||
? path.join(prebuiltVenvPath, 'Scripts', 'python.exe')
|
||||
: path.join(prebuiltVenvPath, 'bin', 'python');
|
||||
|
||||
if (fs.existsSync(pythonExePath)) {
|
||||
log.info(`Using prebuilt venv: ${prebuiltVenvPath}`);
|
||||
return prebuiltVenvPath;
|
||||
} else {
|
||||
log.warn(
|
||||
`Prebuilt venv found but Python executable missing at: ${pythonExePath}. ` +
|
||||
`Falling back to user venv.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
@ -185,6 +198,43 @@ export function getVenvsBaseDir(): string {
|
|||
return path.join(os.homedir(), '.eigent', 'venvs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Packages to install in the terminal base venv.
|
||||
* These are commonly used packages for terminal tasks (data processing, visualization, etc.)
|
||||
* Keep this list minimal - users can install additional packages as needed.
|
||||
*/
|
||||
export const TERMINAL_BASE_PACKAGES = [
|
||||
'pandas',
|
||||
'numpy',
|
||||
'matplotlib',
|
||||
'requests',
|
||||
'openpyxl',
|
||||
'beautifulsoup4',
|
||||
'pillow',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get path to the terminal base venv.
|
||||
* This is a lightweight venv with common packages for terminal tasks,
|
||||
* separate from the backend venv.
|
||||
*/
|
||||
export function getTerminalVenvPath(version: string): string {
|
||||
const venvDir = path.join(
|
||||
os.homedir(),
|
||||
'.eigent',
|
||||
'venvs',
|
||||
`terminal_base-${version}`
|
||||
);
|
||||
|
||||
// Ensure venvs directory exists
|
||||
const venvsBaseDir = path.dirname(venvDir);
|
||||
if (!fs.existsSync(venvsBaseDir)) {
|
||||
fs.mkdirSync(venvsBaseDir, { recursive: true });
|
||||
}
|
||||
|
||||
return venvDir;
|
||||
}
|
||||
|
||||
export async function cleanupOldVenvs(currentVersion: string): Promise<void> {
|
||||
const venvsBaseDir = getVenvsBaseDir();
|
||||
|
||||
|
|
@ -193,23 +243,34 @@ export async function cleanupOldVenvs(currentVersion: string): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Patterns to match: backend-{version} and terminal_base-{version}
|
||||
const venvPatterns = [
|
||||
{ prefix: 'backend-', regex: /^backend-(.+)$/ },
|
||||
{ prefix: 'terminal_base-', regex: /^terminal_base-(.+)$/ },
|
||||
];
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(venvsBaseDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('backend-')) {
|
||||
const versionMatch = entry.name.match(/^backend-(.+)$/);
|
||||
if (versionMatch && versionMatch[1] !== currentVersion) {
|
||||
const oldVenvPath = path.join(venvsBaseDir, entry.name);
|
||||
console.log(`Cleaning up old venv: ${oldVenvPath}`);
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
try {
|
||||
// Remove old venv directory recursively
|
||||
fs.rmSync(oldVenvPath, { recursive: true, force: true });
|
||||
console.log(`Successfully removed old venv: ${entry.name}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to remove old venv ${entry.name}:`, err);
|
||||
for (const pattern of venvPatterns) {
|
||||
if (entry.name.startsWith(pattern.prefix)) {
|
||||
const versionMatch = entry.name.match(pattern.regex);
|
||||
if (versionMatch && versionMatch[1] !== currentVersion) {
|
||||
const oldVenvPath = path.join(venvsBaseDir, entry.name);
|
||||
console.log(`Cleaning up old venv: ${oldVenvPath}`);
|
||||
|
||||
try {
|
||||
// Remove old venv directory recursively
|
||||
fs.rmSync(oldVenvPath, { recursive: true, force: true });
|
||||
console.log(`Successfully removed old venv: ${entry.name}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to remove old venv ${entry.name}:`, err);
|
||||
}
|
||||
}
|
||||
break; // Found matching pattern, no need to check others
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +285,23 @@ export async function isBinaryExists(name: string): Promise<boolean> {
|
|||
return fs.existsSync(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to prebuilt Python installation (if available in packaged app)
|
||||
*/
|
||||
export function getPrebuiltPythonDir(): string | null {
|
||||
if (!app.isPackaged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prebuiltPythonDir = path.join(process.resourcesPath, 'prebuilt', 'uv_python');
|
||||
if (fs.existsSync(prebuiltPythonDir)) {
|
||||
log.info(`Using prebuilt Python: ${prebuiltPythonDir}`);
|
||||
return prebuiltPythonDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unified UV environment variables for consistent Python environment management.
|
||||
* This ensures both installation and runtime use the same paths.
|
||||
|
|
@ -231,8 +309,12 @@ export async function isBinaryExists(name: string): Promise<boolean> {
|
|||
* @returns Environment variables for UV commands
|
||||
*/
|
||||
export function getUvEnv(version: string): Record<string, string> {
|
||||
// Use prebuilt Python if available (packaged app)
|
||||
const prebuiltPython = getPrebuiltPythonDir();
|
||||
const pythonInstallDir = prebuiltPython || getCachePath('uv_python');
|
||||
|
||||
return {
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
UV_PYTHON_INSTALL_DIR: pythonInstallDir,
|
||||
UV_TOOL_DIR: getCachePath('uv_tool'),
|
||||
UV_PROJECT_ENVIRONMENT: getVenvPath(version),
|
||||
UV_HTTP_TIMEOUT: '300',
|
||||
|
|
|
|||
|
|
@ -72,13 +72,108 @@ export class WebViewManager {
|
|||
backgroundThrottling: true,
|
||||
offscreen: false,
|
||||
sandbox: true,
|
||||
disableBlinkFeatures: 'Accelerated2dCanvas',
|
||||
disableBlinkFeatures: 'Accelerated2dCanvas,AutomationControlled',
|
||||
enableBlinkFeatures: 'IdleDetection',
|
||||
autoplayPolicy: 'document-user-activation-required',
|
||||
},
|
||||
})
|
||||
view.webContents.on('did-finish-load', () => {
|
||||
// Inject stealth script to avoid bot detection
|
||||
view.webContents.executeJavaScript(`
|
||||
// Save original values before overriding to maintain consistency
|
||||
const originalLanguages = navigator.languages ? [...navigator.languages] : ['en-US', 'en'];
|
||||
const originalHardwareConcurrency = navigator.hardwareConcurrency || 8;
|
||||
const originalDeviceMemory = navigator.deviceMemory || 8;
|
||||
|
||||
// Hide webdriver property
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => undefined,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Override plugins with proper PluginArray-like behavior
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => {
|
||||
const plugins = {
|
||||
length: 3,
|
||||
0: { name: 'Chrome PDF Plugin', description: 'Portable Document Format', filename: 'internal-pdf-viewer' },
|
||||
1: { name: 'Chrome PDF Viewer', description: '', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
|
||||
2: { name: 'Native Client', description: '', filename: 'internal-nacl-plugin' },
|
||||
item: function(index) { return this[index] || null; },
|
||||
namedItem: function(name) {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (this[i].name === name) return this[i];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
refresh: function() {},
|
||||
[Symbol.iterator]: function* () {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
yield this[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
return plugins;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Use original system languages for consistency with other browser data
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => originalLanguages,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Use original hardwareConcurrency, clamped to common range (4-16) to avoid extreme fingerprints
|
||||
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
||||
get: () => Math.min(Math.max(originalHardwareConcurrency, 4), 16),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Use original deviceMemory, clamped to common range (4-16) to avoid extreme fingerprints
|
||||
Object.defineProperty(navigator, 'deviceMemory', {
|
||||
get: () => Math.min(Math.max(originalDeviceMemory, 4), 16),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Fix WebGL vendor/renderer for both WebGL and WebGL2
|
||||
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
||||
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
||||
if (parameter === 37445) return 'Intel Inc.';
|
||||
if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
|
||||
return getParameter.call(this, parameter);
|
||||
};
|
||||
|
||||
// Also patch WebGL2RenderingContext
|
||||
if (typeof WebGL2RenderingContext !== 'undefined') {
|
||||
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
||||
WebGL2RenderingContext.prototype.getParameter = function(parameter) {
|
||||
if (parameter === 37445) return 'Intel Inc.';
|
||||
if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
|
||||
return getParameter2.call(this, parameter);
|
||||
};
|
||||
}
|
||||
|
||||
// Override chrome runtime - real Chrome has window.chrome but runtime is undefined
|
||||
if (!window.chrome) {
|
||||
window.chrome = {};
|
||||
}
|
||||
// In real Chrome, runtime exists but is undefined outside extensions
|
||||
// Don't set it to an object, that's detectable
|
||||
|
||||
// Hide automation variables
|
||||
const automationVars = ['__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn',
|
||||
'__driver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', 'domAutomation', 'domAutomationController'];
|
||||
automationVars.forEach(v => {
|
||||
Object.defineProperty(window, v, {
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
configurable: true,
|
||||
enumerable: false
|
||||
});
|
||||
});
|
||||
|
||||
// Mouse event handler
|
||||
window.addEventListener('mousedown', (e) => {
|
||||
if (!(e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement)) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
32
package.json
32
package.json
|
|
@ -23,14 +23,17 @@
|
|||
"build:mac": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --mac",
|
||||
"build:mac:test": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --mac && npm run test-signing",
|
||||
"build:win": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --win",
|
||||
"build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win",
|
||||
"build:linux": "npm run preinstall-deps && npm run clean-symlinks && npm run compile-babel && tsc && vite build && electron-builder --linux",
|
||||
"build:all": "npm run preinstall-deps && npm run compile-babel && tsc && vite build && electron-builder --mac --win --linux",
|
||||
"preview": "vite preview",
|
||||
"pretest": "vite build --mode=test",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "vitest run --config vitest.config.ts",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build -o storybook-static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/notarize": "^2.5.0",
|
||||
|
|
@ -67,11 +70,12 @@
|
|||
"csv-parser": "^3.2.0",
|
||||
"dompurify": "^3.2.7",
|
||||
"electron-log": "^5.4.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-updater": "^6.7.3",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.17.0",
|
||||
"gsap": "^3.13.0",
|
||||
"koffi": "^2.14.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lottie-web": "^5.13.0",
|
||||
"lucide-react": "^0.509.0",
|
||||
|
|
@ -114,11 +118,11 @@
|
|||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"electron": "^33.2.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron": "^33.4.11",
|
||||
"electron-builder": "^26.4.0",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"i18next": "^25.4.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-import": "^16.1.0",
|
||||
"react": "^18.3.1",
|
||||
|
|
@ -130,12 +134,22 @@
|
|||
"vite": "^5.4.11",
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.6",
|
||||
"vitest": "^2.1.5"
|
||||
"vitest": "^2.1.5",
|
||||
"storybook": "^10.1.11",
|
||||
"@storybook/react-vite": "^10.1.11",
|
||||
"@storybook/addon-a11y": "^10.1.11",
|
||||
"@storybook/addon-docs": "^10.1.11"
|
||||
},
|
||||
"overrides": {
|
||||
"glob": "^10.4.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"neverBuiltDependencies": []
|
||||
"neverBuiltDependencies": [],
|
||||
"overrides": {
|
||||
"glob": "^10.4.5"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <23.0.0"
|
||||
"node": ">=20.0.0 <23.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function isValidSymlink(symlinkPath, bundleRoot) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fix Python symlinks in venv/bin
|
||||
* Fix Python symlinks in venv/bin (Unix) or venv/Scripts (Windows)
|
||||
* Remove symlinks that point outside the bundle (to cache directory)
|
||||
*/
|
||||
function fixPythonSymlinks(venvBinDir, bundleRoot) {
|
||||
|
|
@ -55,7 +55,10 @@ function fixPythonSymlinks(venvBinDir, bundleRoot) {
|
|||
}
|
||||
|
||||
const bundlePath = path.resolve(bundleRoot);
|
||||
const pythonNames = ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
|
||||
const isWindows = process.platform === 'win32';
|
||||
const pythonNames = isWindows
|
||||
? ['python.exe', 'python3.exe', 'python3.10.exe', 'python3.11.exe', 'python3.12.exe']
|
||||
: ['python', 'python3', 'python3.10', 'python3.11', 'python3.12'];
|
||||
|
||||
for (const pythonName of pythonNames) {
|
||||
const pythonSymlink = path.join(venvBinDir, pythonName);
|
||||
|
|
@ -127,7 +130,8 @@ function main() {
|
|||
console.log('🧹 Cleaning invalid symbolic links...');
|
||||
|
||||
const bundleRoot = path.join(projectRoot, 'resources', 'prebuilt');
|
||||
const venvBinDir = path.join(bundleRoot, 'venv', 'bin');
|
||||
const isWindows = process.platform === 'win32';
|
||||
const venvBinDir = path.join(bundleRoot, 'venv', isWindows ? 'Scripts' : 'bin');
|
||||
|
||||
// First, try to fix Python symlinks specifically
|
||||
if (fs.existsSync(venvBinDir)) {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ const __filename = fileURLToPath(import.meta.url);
|
|||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const BIN_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'bin');
|
||||
const VENV_DIR = path.join(projectRoot, 'resources', 'prebuilt', 'venv');
|
||||
const PREBUILT_DIR = path.join(projectRoot, 'resources', 'prebuilt');
|
||||
const BIN_DIR = path.join(PREBUILT_DIR, 'bin');
|
||||
const VENV_DIR = path.join(PREBUILT_DIR, 'venv');
|
||||
const BACKEND_DIR = path.join(projectRoot, 'backend');
|
||||
|
||||
console.log('🚀 Starting pre-installation of dependencies...');
|
||||
|
|
@ -197,6 +198,45 @@ async function downloadFileWithValidation(urlsToTry, dest, validateFn, fileType
|
|||
throw new Error(`Failed to download ${fileType} from all sources`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy directory, handling symlinks properly
|
||||
*/
|
||||
function copyDirRecursiveSync(src, dest) {
|
||||
if (!fs.existsSync(src)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create destination directory
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
// Get all files and directories
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copyDirRecursiveSync(srcPath, destPath);
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
try {
|
||||
const realPath = fs.realpathSync(srcPath);
|
||||
const realStat = fs.statSync(realPath);
|
||||
if (realStat.isDirectory()) {
|
||||
copyDirRecursiveSync(realPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(realPath, destPath);
|
||||
}
|
||||
} catch (err) {
|
||||
// If symlink target doesn't exist, skip it
|
||||
console.log(` Skipping broken symlink: ${srcPath}`);
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Bun download URL list
|
||||
*/
|
||||
|
|
@ -389,6 +429,28 @@ async function installUv() {
|
|||
await tar.extract({ file: tempFilename, cwd: BIN_DIR });
|
||||
}
|
||||
|
||||
// Handle nested directory from tarball if needed
|
||||
if (!isWindows) {
|
||||
const nestedDir = fs.readdirSync(BIN_DIR).find(f =>
|
||||
fs.statSync(path.join(BIN_DIR, f)).isDirectory() && f.startsWith('uv-')
|
||||
);
|
||||
if (nestedDir) {
|
||||
const nestedUvPath = path.join(BIN_DIR, nestedDir, 'uv');
|
||||
const targetPath = path.join(BIN_DIR, 'uv');
|
||||
if (fs.existsSync(nestedUvPath)) {
|
||||
console.log(` Found uv in ${nestedDir}, moving...`);
|
||||
try {
|
||||
if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath);
|
||||
fs.renameSync(nestedUvPath, targetPath);
|
||||
// Clean up directory
|
||||
fs.rmSync(path.join(BIN_DIR, nestedDir), { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
console.log(` Warning: Failed to move uv from nested dir: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const extractedUvPath = path.join(BIN_DIR, isWindows ? 'uv.exe' : 'uv');
|
||||
if (fs.existsSync(extractedUvPath)) {
|
||||
if (!isWindows && extractedUvPath !== uvPath) {
|
||||
|
|
@ -591,11 +653,66 @@ async function installPythonDeps(uvPath) {
|
|||
console.log('📦 Creating Python venv...');
|
||||
}
|
||||
|
||||
// Ensure Python is installed before syncing
|
||||
// This is critical for Windows where Python might not be in the venv
|
||||
console.log('🐍 Ensuring Python is installed...');
|
||||
try {
|
||||
execSync(
|
||||
`"${uvPath}" python install 3.10`,
|
||||
{ cwd: BACKEND_DIR, env: env, stdio: 'inherit' }
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Python install command failed, continuing with sync (Python may already be installed)...');
|
||||
}
|
||||
|
||||
// Use --python-preference only-managed to ensure uv uses its own managed Python
|
||||
// This makes the venv more portable
|
||||
execSync(
|
||||
`"${uvPath}" sync --no-dev --cache-dir "${cacheDir}"`,
|
||||
`"${uvPath}" sync --no-dev --cache-dir "${cacheDir}" --python-preference only-managed`,
|
||||
{ cwd: BACKEND_DIR, env: env, stdio: 'inherit' }
|
||||
);
|
||||
|
||||
// Verify Python executable exists in the virtual environment
|
||||
const isWindows = process.platform === 'win32';
|
||||
const pythonExePath = isWindows
|
||||
? path.join(venvPath, 'Scripts', 'python.exe')
|
||||
: path.join(venvPath, 'bin', 'python');
|
||||
|
||||
if (!fs.existsSync(pythonExePath)) {
|
||||
throw new Error(
|
||||
`Python executable not found in virtual environment at: ${pythonExePath}\n` +
|
||||
`Virtual environment may be corrupted. Please ensure uv sync completed successfully.`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✅ Python executable verified: ${pythonExePath}`);
|
||||
|
||||
// Bundle the actual Python installation from UV cache into prebuilt
|
||||
console.log('📦 Bundling Python installation...');
|
||||
try {
|
||||
const uvPythonDir = pythonCacheDir;
|
||||
const prebuiltPythonDir = path.join(PREBUILT_DIR, 'uv_python');
|
||||
|
||||
if (fs.existsSync(uvPythonDir)) {
|
||||
console.log(` Copying from: ${uvPythonDir}`);
|
||||
console.log(` Copying to: ${prebuiltPythonDir}`);
|
||||
|
||||
// Remove existing python dir if it exists
|
||||
if (fs.existsSync(prebuiltPythonDir)) {
|
||||
fs.rmSync(prebuiltPythonDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Copy the Python installation
|
||||
copyDirRecursiveSync(uvPythonDir, prebuiltPythonDir);
|
||||
console.log('✅ Python installation bundled');
|
||||
} else {
|
||||
console.log('⚠️ UV Python cache not found, venv may not be portable');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Failed to bundle Python: ${error.message}`);
|
||||
console.log(' The app may fail to start without internet connection');
|
||||
}
|
||||
|
||||
console.log('✅ Python dependencies installed');
|
||||
|
||||
console.log('📝 Compiling babel...');
|
||||
|
|
|
|||
296
scripts/test-notarization.js
Normal file
296
scripts/test-notarization.js
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test script for macOS notarization issues
|
||||
* This script checks for common issues that cause notarization to fail:
|
||||
* 1. .npm-cache directories
|
||||
* 2. flac-mac binary (outdated SDK)
|
||||
* 3. Unsigned native binaries (.node files)
|
||||
* 4. Other problematic files
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const RELEASE_DIR = path.join(projectRoot, 'release');
|
||||
const APP_BUNDLE_PATTERN = /Eigent\.app$/;
|
||||
|
||||
/**
|
||||
* Find the app bundle in release directory
|
||||
*/
|
||||
function findAppBundle() {
|
||||
if (!fs.existsSync(RELEASE_DIR)) {
|
||||
console.log('❌ Release directory does not exist. Please build the app first.');
|
||||
console.log(' Run: npm run build:mac');
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(RELEASE_DIR, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.match(APP_BUNDLE_PATTERN)) {
|
||||
return path.join(RELEASE_DIR, entry.name);
|
||||
}
|
||||
|
||||
// Check subdirectories (e.g., mac-arm64/Eigent.app)
|
||||
if (entry.isDirectory()) {
|
||||
const subDir = path.join(RELEASE_DIR, entry.name);
|
||||
const subEntries = fs.readdirSync(subDir, { withFileTypes: true });
|
||||
for (const subEntry of subEntries) {
|
||||
if (subEntry.isDirectory() && subEntry.name.match(APP_BUNDLE_PATTERN)) {
|
||||
return path.join(subDir, subEntry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for .npm-cache directories
|
||||
*/
|
||||
function checkNpmCache(bundlePath) {
|
||||
console.log('\n🔍 Checking for .npm-cache directories...');
|
||||
const issues = [];
|
||||
|
||||
function scanDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.name === '.npm-cache' && entry.isDirectory()) {
|
||||
issues.push(fullPath);
|
||||
} else if (entry.isDirectory()) {
|
||||
// Skip node_modules to avoid deep scanning
|
||||
if (entry.name !== 'node_modules' && entry.name !== '__pycache__') {
|
||||
scanDir(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
const resourcesPath = path.join(bundlePath, 'Contents', 'Resources');
|
||||
const prebuiltPath = path.join(resourcesPath, 'prebuilt');
|
||||
|
||||
if (fs.existsSync(prebuiltPath)) {
|
||||
scanDir(prebuiltPath);
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.log(`❌ Found ${issues.length} .npm-cache directory(ies):`);
|
||||
issues.forEach(issue => console.log(` - ${issue}`));
|
||||
return false;
|
||||
} else {
|
||||
console.log('✅ No .npm-cache directories found');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for flac-mac binary
|
||||
*/
|
||||
function checkFlacMac(bundlePath) {
|
||||
console.log('\n🔍 Checking for flac-mac binary...');
|
||||
const issues = [];
|
||||
|
||||
const resourcesPath = path.join(bundlePath, 'Contents', 'Resources');
|
||||
const prebuiltPath = path.join(resourcesPath, 'prebuilt');
|
||||
const venvLibPath = path.join(prebuiltPath, 'venv', 'lib');
|
||||
|
||||
if (fs.existsSync(venvLibPath)) {
|
||||
try {
|
||||
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');
|
||||
if (fs.existsSync(flacMacPath)) {
|
||||
issues.push(flacMacPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.log(`❌ Found ${issues.length} flac-mac binary(ies) (outdated SDK):`);
|
||||
issues.forEach(issue => console.log(` - ${issue}`));
|
||||
return false;
|
||||
} else {
|
||||
console.log('✅ No flac-mac binaries found');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for unsigned native binaries
|
||||
*/
|
||||
function checkUnsignedBinaries(bundlePath) {
|
||||
console.log('\n🔍 Checking for unsigned native binaries (.node files)...');
|
||||
const issues = [];
|
||||
|
||||
function scanForNodeFiles(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.node')) {
|
||||
// Check if file is signed
|
||||
try {
|
||||
const output = execSync(`codesign -dv "${fullPath}" 2>&1 || true`, { encoding: 'utf-8' });
|
||||
if (output.includes('code object is not signed')) {
|
||||
issues.push({
|
||||
path: fullPath,
|
||||
reason: 'Not signed'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// If codesign fails, assume it's not signed
|
||||
issues.push({
|
||||
path: fullPath,
|
||||
reason: 'Could not verify signature'
|
||||
});
|
||||
}
|
||||
} else if (entry.isDirectory()) {
|
||||
// Skip certain directories
|
||||
if (entry.name !== 'node_modules' && entry.name !== '__pycache__' && !entry.name.startsWith('.')) {
|
||||
scanForNodeFiles(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
const resourcesPath = path.join(bundlePath, 'Contents', 'Resources');
|
||||
const prebuiltPath = path.join(resourcesPath, 'prebuilt');
|
||||
|
||||
if (fs.existsSync(prebuiltPath)) {
|
||||
scanForNodeFiles(prebuiltPath);
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.log(`❌ Found ${issues.length} unsigned .node file(s):`);
|
||||
issues.forEach(issue => {
|
||||
console.log(` - ${issue.path}`);
|
||||
console.log(` Reason: ${issue.reason}`);
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
console.log('✅ No unsigned .node files found');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check app bundle size
|
||||
*/
|
||||
function checkBundleSize(bundlePath) {
|
||||
console.log('\n🔍 Checking app bundle size...');
|
||||
|
||||
try {
|
||||
function getDirSize(dir) {
|
||||
let size = 0;
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isFile()) {
|
||||
size += fs.statSync(fullPath).size;
|
||||
} else if (entry.isDirectory()) {
|
||||
size += getDirSize(fullPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const size = getDirSize(bundlePath);
|
||||
const sizeInMB = (size / (1024 * 1024)).toFixed(2);
|
||||
console.log(` App bundle size: ${sizeInMB} MB`);
|
||||
|
||||
if (size > 500 * 1024 * 1024) {
|
||||
console.log(` ⚠️ Large bundle size (>500MB) may cause slow notarization (30-60 minutes)`);
|
||||
} else if (size > 200 * 1024 * 1024) {
|
||||
console.log(` ⚠️ Medium bundle size (200-500MB) may take 15-30 minutes to notarize`);
|
||||
} else {
|
||||
console.log(` ✅ Bundle size is reasonable for notarization`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not calculate bundle size: ${error.message}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main() {
|
||||
console.log('🧪 macOS Notarization Test Script\n');
|
||||
|
||||
const appBundle = findAppBundle();
|
||||
|
||||
if (!appBundle) {
|
||||
console.log('\n💡 To build the app for testing:');
|
||||
console.log(' npm run build:mac');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📦 Found app bundle: ${appBundle}\n`);
|
||||
|
||||
const results = {
|
||||
npmCache: checkNpmCache(appBundle),
|
||||
flacMac: checkFlacMac(appBundle),
|
||||
unsignedBinaries: checkUnsignedBinaries(appBundle),
|
||||
bundleSize: checkBundleSize(appBundle),
|
||||
};
|
||||
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` .npm-cache directories: ${results.npmCache ? '✅' : '❌'}`);
|
||||
console.log(` flac-mac binaries: ${results.flacMac ? '✅' : '❌'}`);
|
||||
console.log(` Unsigned .node files: ${results.unsignedBinaries ? '✅' : '❌'}`);
|
||||
console.log(` Bundle size: ${results.bundleSize ? '✅' : '⚠️'}`);
|
||||
|
||||
const allPassed = Object.values(results).every(r => r);
|
||||
|
||||
if (allPassed) {
|
||||
console.log('\n✅ All checks passed! The app should be ready for notarization.');
|
||||
console.log('\n💡 Note: This script only checks for common issues.');
|
||||
console.log(' Actual notarization may still fail for other reasons.');
|
||||
console.log(' To test actual notarization, you need:');
|
||||
console.log(' - Valid Apple Developer ID certificate');
|
||||
console.log(' - APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID environment variables');
|
||||
} else {
|
||||
console.log('\n❌ Some checks failed. Please fix the issues above before notarization.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -4,8 +4,8 @@ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
|||
# Install the project into `/app`
|
||||
WORKDIR /app
|
||||
|
||||
# Enable bytecode compilation
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
# Disable bytecode transfer during compilation to avoid EMFILE during build on low nofile limits
|
||||
ENV UV_COMPILE_BYTECODE=0
|
||||
|
||||
# Copy from the cache instead of linking since it's a mounted volume
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
|
@ -15,7 +15,7 @@ ENV UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-buil
|
|||
ARG database_url
|
||||
ENV database_url=$database_url
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update -o Acquire::Retries=3 && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
python3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
|
@ -43,7 +43,7 @@ RUN uv run pybabel extract -F babel.cfg -o messages.pot . && \
|
|||
|
||||
|
||||
# Install netcat for database connectivity check
|
||||
RUN apt-get update && apt-get install -y curl netcat-openbsd && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update -o Acquire::Retries=3 && apt-get install -y --no-install-recommends curl netcat-openbsd && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
|
@ -58,4 +58,4 @@ ENTRYPOINT []
|
|||
EXPOSE 5678
|
||||
|
||||
# Use the start script
|
||||
CMD ["/app/start.sh"]
|
||||
CMD ["/app/start.sh"]
|
||||
|
|
|
|||
111
server/README_PT-BR.md
Normal file
111
server/README_PT-BR.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
### Propósito
|
||||
`server/` fornece um backend local (FastAPI + PostgreSQL) para alcançar separação completa entre ambientes locais e na nuvem. Após implantar este serviço, dados sensíveis como registro de usuários, configurações de provedores de modelos, configurações de ferramentas e histórico de bate-papo são armazenados em sua máquina e não são enviados para nossa nuvem, a menos que você configure explicitamente serviços externos (por exemplo, provedores de modelos na nuvem ou servidores MCP remotos).
|
||||
|
||||
### Serviços Fornecidos (Módulos Principais)
|
||||
- Usuários e Contas
|
||||
- `POST /register`: Registro por email + senha (apenas banco de dados local)
|
||||
- `POST /login`: Login com email + senha; retorna um token emitido localmente
|
||||
- `GET/PUT /user`, `/user/profile`, `/user/privacy`, `/user/current_credits`, `/user/stat`, etc.
|
||||
- Provedores de Modelos (armazenar configurações de acesso a modelos locais/na nuvem)
|
||||
- `GET /providers`, `POST /provider`, `PUT /provider/{id}`, `DELETE /provider/{id}`
|
||||
- `POST /provider/prefer`: Definir um provedor preferido (frontend/backend priorizará)
|
||||
- Centro de Configuração (armazenar segredos/parâmetros necessários para ferramentas/capacidades)
|
||||
- `GET /configs`, `POST /configs`, `PUT /configs/{id}`, `DELETE /configs/{id}`, `GET /config/info`
|
||||
- Chat e Dados
|
||||
- Histórico, snapshots, compartilhamento, etc. em `app/controller/chat/`, todos persistidos no banco de dados local
|
||||
- Gerenciamento de MCP (importar servidores MCP locais/remotos)
|
||||
- `GET /mcps`, `POST /mcp/install`, `POST /mcp/import/{Local|Remote}`, etc.
|
||||
|
||||
Nota: Todos os dados acima são armazenados no volume PostgreSQL local no Docker (veja "Persistência de Dados" abaixo). Se você configurar modelos externos ou MCP remoto, as solicitações vão para os serviços de terceiros que você especificar.
|
||||
|
||||
---
|
||||
|
||||
### Início Rápido (Docker)
|
||||
|
||||
#### Pré-requisitos
|
||||
- **Docker Desktop**: Instalado e em execução
|
||||
- **Python**: 3.10.* (3.10.15 recomendado)
|
||||
- **Node.js**: >=18.0.0 <23.0.0
|
||||
|
||||
#### Etapas de Configuração
|
||||
|
||||
1) Inicie os serviços
|
||||
```bash
|
||||
cd server
|
||||
# Copie .env.example para .env (ou crie .env de acordo com .env.example)
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
2) Inicie o Frontend (Modo Local)
|
||||
- No diretório raiz do projeto, crie ou modifique `.env.development` para ativar o modo local e apontar para o backend local:
|
||||
```bash
|
||||
VITE_BASE_URL=/api
|
||||
VITE_USE_LOCAL_PROXY=true
|
||||
VITE_PROXY_URL=http://localhost:3001
|
||||
```
|
||||
- Inicie a aplicação frontend:
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Abra a documentação da API
|
||||
- `http://localhost:3001/docs` (Interface do Swagger)
|
||||
|
||||
### Portas
|
||||
- API: Host `3001` → Contêiner `5678`
|
||||
- PostgreSQL: Host `5432` → Contêiner `5432`
|
||||
|
||||
### Persistência de Dados
|
||||
- Os dados do banco de dados são armazenados no volume Docker `server_postgres_data` em `/var/lib/postgresql/data` dentro do contêiner
|
||||
- As migrações de banco de dados são executadas automaticamente na inicialização do contêiner (veja `start.sh` → `alembic upgrade head`)
|
||||
|
||||
### Comandos Comuns
|
||||
```bash
|
||||
# Listar contêineres em execução
|
||||
docker ps
|
||||
|
||||
# Parar/Iniciar contêiner da API (manter DB)
|
||||
docker stop eigent_api
|
||||
docker start eigent_api
|
||||
|
||||
# Parar/Iniciar tudo (API + DB)
|
||||
docker compose stop
|
||||
docker compose start
|
||||
|
||||
# Exibir logs
|
||||
docker logs -f eigent_api | cat
|
||||
docker logs -f eigent_postgres | cat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Modo Desenvolvedor (Opcional)
|
||||
Você pode executar a API localmente com hot-reload enquanto mantém o banco de dados no Docker:
|
||||
```bash
|
||||
# Parar API no contêiner, manter DB
|
||||
docker stop eigent_api
|
||||
|
||||
# Inicializar banco de dados (primeira execução ou quando o esquema do BD muda)
|
||||
cd server
|
||||
uv run alembic upgrade head
|
||||
|
||||
# Executar localmente (fornecer string de conexão do BD)
|
||||
export database_url=postgresql://postgres:123456@localhost:5432/eigent
|
||||
uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Outros
|
||||
- Documentação da API: `http://localhost:3001/docs`
|
||||
- Logs de tempo de execução: `/app/runtime/log/app.log` no contêiner
|
||||
- i18n (para desenvolvedores)
|
||||
```bash
|
||||
uv run pybabel extract -F babel.cfg -o messages.pot .
|
||||
uv run pybabel init -i messages.pot -d lang -l zh_CN
|
||||
uv run pybabel compile -d lang -l zh_CN
|
||||
```
|
||||
|
||||
Para um ambiente completamente offline, use apenas modelos locais e servidores MCP locais, e evite configurar quaisquer Provedores externos ou endereços MCP remotos.
|
||||
|
|
@ -10,7 +10,7 @@ def permissions():
|
|||
return [
|
||||
{
|
||||
"name": _("User"),
|
||||
"description": _("User manger"),
|
||||
"description": _("User manager"),
|
||||
"children": [
|
||||
{
|
||||
"identity": "user:view",
|
||||
|
|
@ -26,7 +26,7 @@ def permissions():
|
|||
},
|
||||
{
|
||||
"name": _("Admin"),
|
||||
"description": _("Admin manger"),
|
||||
"description": _("Admin manager"),
|
||||
"children": [
|
||||
{
|
||||
"identity": "admin:view",
|
||||
|
|
@ -42,7 +42,7 @@ def permissions():
|
|||
},
|
||||
{
|
||||
"name": _("Role"),
|
||||
"description": _("Role manger"),
|
||||
"description": _("Role manager"),
|
||||
"children": [
|
||||
{
|
||||
"identity": "role:view",
|
||||
|
|
@ -58,7 +58,7 @@ def permissions():
|
|||
},
|
||||
{
|
||||
"name": _("Mcp"),
|
||||
"description": _("Mcp manger"),
|
||||
"description": _("Mcp manager"),
|
||||
"children": [
|
||||
{
|
||||
"identity": "mcp:edit",
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ msgid "User"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:13
|
||||
msgid "User manger"
|
||||
msgid "User manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:17
|
||||
|
|
@ -55,7 +55,7 @@ msgid "Admin"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:29
|
||||
msgid "Admin manger"
|
||||
msgid "Admin manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:33
|
||||
|
|
@ -79,7 +79,7 @@ msgid "Role"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:45
|
||||
msgid "Role manger"
|
||||
msgid "Role manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:49
|
||||
|
|
@ -103,7 +103,7 @@ msgid "Mcp"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:61
|
||||
msgid "Mcp manger"
|
||||
msgid "Mcp manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:65
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ msgid "User"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:13
|
||||
msgid "User manger"
|
||||
msgid "User manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:17
|
||||
|
|
@ -54,7 +54,7 @@ msgid "Admin"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:29
|
||||
msgid "Admin manger"
|
||||
msgid "Admin manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:33
|
||||
|
|
@ -78,7 +78,7 @@ msgid "Role"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:45
|
||||
msgid "Role manger"
|
||||
msgid "Role manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:49
|
||||
|
|
@ -102,7 +102,7 @@ msgid "Mcp"
|
|||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:61
|
||||
msgid "Mcp manger"
|
||||
msgid "Mcp manager"
|
||||
msgstr ""
|
||||
|
||||
#: app/component/permission.py:65
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ requires-python = ">=3.12,<3.13"
|
|||
dependencies = [
|
||||
"alembic>=1.15.2",
|
||||
"openai>=1.99.3,<2",
|
||||
"camel-ai==0.2.83a9",
|
||||
"camel-ai==0.2.84",
|
||||
"pydantic[email]>=2.11.1",
|
||||
"click>=8.1.8",
|
||||
"fastapi>=0.115.12",
|
||||
|
|
|
|||
105
server/uv.lock
generated
105
server/uv.lock
generated
|
|
@ -4,16 +4,16 @@ requires-python = "==3.12.*"
|
|||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.17.2"
|
||||
version = "1.18.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/cc/aca263693b2ece99fa99a09b6d092acb89973eb2bb575faef1777e04f8b4/alembic-1.18.1.tar.gz", hash = "sha256:83ac6b81359596816fb3b893099841a0862f2117b2963258e965d70dc62fb866", size = 2044319, upload-time = "2026-01-14T18:53:14.907Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/36/cd9cb6101e81e39076b2fbe303bfa3c85ca34e55142b0324fcbf22c5c6e2/alembic-1.18.1-py3-none-any.whl", hash = "sha256:f1c3b0920b87134e851c25f1f7f236d8a332c34b75416802d06971df5d1b7810", size = 260973, upload-time = "2026-01-14T18:53:17.533Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -117,35 +117,35 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.42.24"
|
||||
version = "1.42.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/21/8be0e3685c3a4868be48d8d2f6e5b4641727e1d8a5d396b8b401d2b5f06e/boto3-1.42.24.tar.gz", hash = "sha256:c47a2f40df933e3861fc66fd8d6b87ee36d4361663a7e7ba39a87f5a78b2eae1", size = 112788, upload-time = "2026-01-07T20:30:51.019Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/79/2dac8b7cb075cfa43908ee9af3f8ee06880d84b86013854c5cca8945afac/boto3-1.42.30.tar.gz", hash = "sha256:ba9cd2f7819637d15bfbeb63af4c567fcc8a7dcd7b93dd12734ec58601169538", size = 112809, upload-time = "2026-01-16T20:37:23.636Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/75/bbfccb268f9faa4f59030888e859dca9797a980b77d6a074113af73bd4bf/boto3-1.42.24-py3-none-any.whl", hash = "sha256:8ed6ad670a5a2d7f66c1b0d3362791b48392c7a08f78479f5d8ab319a4d9118f", size = 140572, upload-time = "2026-01-07T20:30:49.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b3/2c0d828c9f668292e277ca5232e6160dd5b4b660a3f076f20dd5378baa1e/boto3-1.42.30-py3-none-any.whl", hash = "sha256:d7e548bea65e0ae2c465c77de937bc686b591aee6a352d5a19a16bc751e591c1", size = 140573, upload-time = "2026-01-16T20:37:22.089Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.42.24"
|
||||
version = "1.42.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/d7/bb4a4e839b238ffb67b002d7326b328ebe5eb23ed5180f2ca10399a802de/botocore-1.42.24.tar.gz", hash = "sha256:be8d1bea64fb91eea08254a1e5fea057e4428d08e61f4e11083a02cafc1f8cc6", size = 14878455, upload-time = "2026-01-07T20:30:40.379Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/38/23862628a0eb044c8b8b3d7a9ad1920b3bfd6bce6d746d5a871e8382c7e4/botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116", size = 14891439, upload-time = "2026-01-16T20:37:13.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/d4/f2655d777eed8b069ecab3761454cb83f830f8be8b5b0d292e4b3a980d00/botocore-1.42.24-py3-none-any.whl", hash = "sha256:8fca9781d7c84f7ad070fceffaff7179c4aa7a5ffb27b43df9d1d957801e0a8d", size = 14551806, upload-time = "2026-01-07T20:30:38.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/8d/6d7b016383b1f74dd93611b1c5078bbaddaca901553ab886dcda87cae365/botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02", size = 14566340, upload-time = "2026-01-16T20:37:10.94Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camel-ai"
|
||||
version = "0.2.83a9"
|
||||
version = "0.2.84"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "astor" },
|
||||
|
|
@ -162,9 +162,9 @@ dependencies = [
|
|||
{ name = "tiktoken" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/8c/7d8071776ba973bb6e734edb6caaf4fdbdf60ecebdc1c4017948cc67ad48/camel_ai-0.2.83a9.tar.gz", hash = "sha256:2ee560551797b089f9849d3b9d63cd3a2b4eb45d339d17e6bf95eba2b85c4b50", size = 1124774, upload-time = "2026-01-15T21:28:24.51Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/62/96d922750b304ab2dd0ac1ef35dd3fbbf0e9a44f147e08a1ddfeb94c51c6/camel_ai-0.2.84.tar.gz", hash = "sha256:173c79755fc986e3fa8e27523606222c5f8816fc085abb24831912dcd4a0dec3", size = 1125724, upload-time = "2026-01-20T17:23:13.524Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/77/f7594707571af9c86351a69ff9f7f580602b42ffe8113803153c069b6bff/camel_ai-0.2.83a9-py3-none-any.whl", hash = "sha256:7cfe97b590096c1cc5afddf6dca023c5b9a47d104196c16b4b2b1934931af260", size = 1595808, upload-time = "2026-01-15T21:28:21.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/8b/246abd2c47154de6220fd0a286c3fe50f343d49943ae17e26b9f824a1ca0/camel_ai-0.2.84-py3-none-any.whl", hash = "sha256:63bfbd09e605f9087bb73eb9b929e162fbc6778084ce50e43a367fc0e98cbc65", size = 1599378, upload-time = "2026-01-20T17:23:11.216Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -364,7 +364,7 @@ requires-dist = [
|
|||
{ name = "alembic", specifier = ">=1.15.2" },
|
||||
{ name = "arrow", specifier = ">=1.3.0" },
|
||||
{ name = "bcrypt", specifier = "==4.0.1" },
|
||||
{ name = "camel-ai", specifier = "==0.2.83a9" },
|
||||
{ name = "camel-ai", specifier = "==0.2.84" },
|
||||
{ name = "click", specifier = ">=8.1.8" },
|
||||
{ name = "convert-case", specifier = ">=1.2.3" },
|
||||
{ name = "cryptography", specifier = ">=45.0.4" },
|
||||
|
|
@ -473,16 +473,16 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "fastapi-pagination"
|
||||
version = "0.15.5"
|
||||
version = "0.15.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/5e/656fa2a88b25f3362d96b838d4858cb10f75ae62a917584375be3dae30fc/fastapi_pagination-0.15.5.tar.gz", hash = "sha256:65871797e53392f5a62eb206b4e1f5494d1f64a8ed4d085a32c4f7c1a1987ee1", size = 572714, upload-time = "2026-01-08T16:19:07.372Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/da/ad34e0fc98ca9731b0f76d07faeb39d525cb80440ac5814e270cb379d92a/fastapi_pagination-0.15.6.tar.gz", hash = "sha256:c59ca1aa056dccee3526953357c2d1128f988f83d3034d95ddb8de6f5a68e9f8", size = 573720, upload-time = "2026-01-11T22:15:36.385Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f8/6d3dbbd818a106309d073c81081706541e8ffbf9ed581f799cf525e01e15/fastapi_pagination-0.15.5-py3-none-any.whl", hash = "sha256:a9276ad322d0c85b46f1d5e43b2ef33dce21d1a4dbf5598269752b7542a2b47b", size = 56576, upload-time = "2026-01-08T16:19:05.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/28/0cf3b51115e98c0b84553b9e11ec07f59ae580bf8585eb7876fa9afe4c7a/fastapi_pagination-0.15.6-py3-none-any.whl", hash = "sha256:5c44bfaa78c1c968ca6f027b01a27c1805194c7cc8776eb84ec78235abcdaece", size = 59624, upload-time = "2026-01-11T22:15:37.746Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -515,7 +515,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||
|
|
@ -738,21 +737,21 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1175,14 +1174,14 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pydash"
|
||||
version = "8.0.5"
|
||||
version = "8.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/24/91c037f47e434172c2112d65c00c84d475a6715425e3315ba2cbb7a87e66/pydash-8.0.5.tar.gz", hash = "sha256:7cc44ebfe5d362f4f5f06c74c8684143c5ac481376b059ff02570705523f9e2e", size = 164861, upload-time = "2025-01-17T16:08:50.562Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/c1/1c55272f49d761cec38ddb80be9817935b9c91ebd6a8988e10f532868d56/pydash-8.0.6.tar.gz", hash = "sha256:b2821547e9723f69cf3a986be4db64de41730be149b2641947ecd12e1e11025a", size = 164338, upload-time = "2026-01-17T16:42:56.576Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/86/e74c978800131c657fc5145f2c1c63e0cea01a49b6216f729cf77a2e1edf/pydash-8.0.5-py3-none-any.whl", hash = "sha256:b2625f8981862e19911daa07f80ed47b315ce20d9b5eb57aaf97aaf570c3892f", size = 102077, upload-time = "2025-01-17T16:08:47.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1281,24 +1280,26 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2025.11.3"
|
||||
version = "2026.1.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1424,15 +1425,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.1.2"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 163 KiB |
|
|
@ -630,7 +630,7 @@ const ToolSelect = forwardRef<
|
|||
key={item.id + item.key + (item.isLocal + "")}
|
||||
className="h-5 bg-button-tertiery-fill-default flex items-center gap-1 w-auto flex-shrink-0 px-xs"
|
||||
>
|
||||
{item.name || item.mcp_name}
|
||||
{item.name || item.mcp_name || item.key || `tool_${item.id}`}
|
||||
<div className="flex items-center justify-center bg-button-secondary-fill-disabled rounded-sm">
|
||||
<X
|
||||
className="w-4 h-4 cursor-pointer text-button-secondary-icon-disabled"
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ export function AddWorker({
|
|||
name: workerName,
|
||||
type: workerName as AgentNameType,
|
||||
log: [],
|
||||
tools: [...selectedTools.map((tool) => tool.name)],
|
||||
tools: [...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`)],
|
||||
activeWebviewIds: [],
|
||||
workerInfo: {
|
||||
name: workerName,
|
||||
|
|
@ -310,7 +310,7 @@ export function AddWorker({
|
|||
type: workerName as AgentNameType,
|
||||
log: [],
|
||||
tools: [
|
||||
...selectedTools.map((tool) => tool?.key || tool?.mcp_name || ""),
|
||||
...selectedTools.map((tool) => tool.name || tool.mcp_name || tool.key || `tool_${tool.id}`),
|
||||
],
|
||||
activeWebviewIds: [],
|
||||
workerInfo: {
|
||||
|
|
|
|||
404
src/components/BrowserAgentWrokSpace/index.tsx
Normal file
404
src/components/BrowserAgentWrokSpace/index.tsx
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
import { useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Bird,
|
||||
Bot,
|
||||
ChevronLeft,
|
||||
CodeXml,
|
||||
FileText,
|
||||
GalleryThumbnails,
|
||||
Globe,
|
||||
Hand,
|
||||
Image,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { fetchPut } from "@/api/http";
|
||||
import { TaskState } from "../TaskState";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
|
||||
export default function Home() {
|
||||
//Get Chatstore for the active project's task
|
||||
const { chatStore, projectStore } = useChatStoreAdapter();
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const [isSingleMode, setIsSingleMode] = useState(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const agentMap = {
|
||||
developer_agent: {
|
||||
name: "Developer Agent",
|
||||
icon: <CodeXml size={16} className="text-text-primary" />,
|
||||
textColor: "text-emerald-700",
|
||||
bgColor: "bg-bg-fill-coding-active",
|
||||
shapeColor: "bg-bg-fill-coding-default",
|
||||
borderColor: "border-bg-fill-coding-active",
|
||||
bgColorLight: "bg-emerald-200",
|
||||
},
|
||||
browser_agent: {
|
||||
name: "Browser agent",
|
||||
icon: <Globe size={16} className="text-text-primary" />,
|
||||
textColor: "text-blue-700",
|
||||
bgColor: "bg-bg-fill-browser-active",
|
||||
shapeColor: "bg-bg-fill-browser-default",
|
||||
borderColor: "border-bg-fill-browser-active",
|
||||
bgColorLight: "bg-blue-200",
|
||||
},
|
||||
document_agent: {
|
||||
name: "Document Agent",
|
||||
icon: <FileText size={16} className="text-text-primary" />,
|
||||
textColor: "text-yellow-700",
|
||||
bgColor: "bg-bg-fill-writing-active",
|
||||
shapeColor: "bg-bg-fill-writing-default",
|
||||
borderColor: "border-bg-fill-writing-active",
|
||||
bgColorLight: "bg-yellow-200",
|
||||
},
|
||||
multi_modal_agent: {
|
||||
name: "Multi Modal Agent",
|
||||
icon: <Image size={16} className="text-text-primary" />,
|
||||
textColor: "text-fuchsia-700",
|
||||
bgColor: "bg-bg-fill-multimodal-active",
|
||||
shapeColor: "bg-bg-fill-multimodal-default",
|
||||
borderColor: "border-bg-fill-multimodal-active",
|
||||
bgColorLight: "bg-fuchsia-200",
|
||||
},
|
||||
social_medium_agent: {
|
||||
name: "Social Media Agent",
|
||||
icon: <Bird size={16} className="text-text-primary" />,
|
||||
textColor: "text-purple-700",
|
||||
bgColor: "bg-violet-700",
|
||||
shapeColor: "bg-violet-300",
|
||||
borderColor: "border-violet-700",
|
||||
bgColorLight: "bg-purple-50",
|
||||
},
|
||||
};
|
||||
const [activeAgent, setActiveAgent] = useState<Agent | null>(null);
|
||||
useEffect(() => {
|
||||
const taskAssigning =
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning;
|
||||
if (taskAssigning) {
|
||||
const activeAgent = taskAssigning.find(
|
||||
(item) =>
|
||||
item.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.activeWorkSpace
|
||||
);
|
||||
setActiveAgent(() => {
|
||||
if (activeAgent) {
|
||||
return activeAgent;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
chatStore.tasks[chatStore.activeTaskId as string].taskAssigning,
|
||||
chatStore.tasks[chatStore.activeTaskId as string].activeWorkSpace,
|
||||
]);
|
||||
|
||||
const [isTakeControl, setIsTakeControl] = useState(false);
|
||||
const handleTakeControl = (id: string) => {
|
||||
console.log("handleTakeControl", id);
|
||||
fetchPut(`/task/${projectStore.activeProjectId}/take-control`, {
|
||||
action: "pause",
|
||||
});
|
||||
|
||||
setIsTakeControl(true);
|
||||
setTimeout(() => {
|
||||
getSize();
|
||||
// show corresponding webview
|
||||
window.electronAPI.showWebview(id);
|
||||
}, 400);
|
||||
};
|
||||
|
||||
// listen to webview container size
|
||||
useEffect(() => {
|
||||
if (!projectStore.activeProjectId) {
|
||||
projectStore.createProject("new project");
|
||||
console.warn("No active projectId found in WorkSpace, creating a new project");
|
||||
}
|
||||
|
||||
const webviewContainer = document.getElementById("webview-container");
|
||||
if (webviewContainer) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
getSize();
|
||||
});
|
||||
resizeObserver.observe(webviewContainer);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
const getSize = () => {
|
||||
const webviewContainer = document.getElementById("webview-container");
|
||||
if (webviewContainer) {
|
||||
const rect = webviewContainer.getBoundingClientRect();
|
||||
window.electronAPI.setSize({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
window.ipcRenderer?.on("url-updated", (_event, newUrl) => {
|
||||
setUrl(newUrl);
|
||||
});
|
||||
|
||||
// optional: clear listener when uninstall
|
||||
return () => {
|
||||
window.ipcRenderer.removeAllListeners("url-updated");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isTakeControl ? (
|
||||
<div className="rounded-xl bg-menutabs-bg-default w-full h-full flex flex-col items-center justify-start border border-solid border-border-success">
|
||||
<div className="flex justify-start items-start w-full p-sm gap-sm">
|
||||
<div className="p-1 rounded-full bg-transparent border border-solid border-border-primary">
|
||||
<Button
|
||||
onClick={() => {
|
||||
fetchPut(`/task/${projectStore.activeProjectId}/take-control`, {
|
||||
action: "resume",
|
||||
});
|
||||
setIsTakeControl(false);
|
||||
window.electronAPI.hideAllWebview();
|
||||
}}
|
||||
size="sm"
|
||||
variant="success"
|
||||
className="rounded-full"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
<span>Give back to Agent</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/* <div className="mx-2 bg-border-primary">{url}</div> */}
|
||||
</div>
|
||||
<div id="webview-container" className="w-full h-full"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`w-full flex-1 h-[calc(100vh-104px)] flex items-center justify-center transition-all duration-300 ease-in-out`}
|
||||
>
|
||||
<div className="w-full h-full flex flex-col rounded-2xl relative bg-menutabs-bg-default overflow-hidden">
|
||||
<div className="pt-3 pb-2 px-2 rounded-t-2xl flex items-center justify-between flex-shrink-0">
|
||||
<div className="flex items-center justify-start gap-sm">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
chatStore.setActiveWorkSpace(
|
||||
chatStore.activeTaskId as string,
|
||||
"workflow"
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</Button>
|
||||
<div
|
||||
className={`text-base leading-snug font-bold ${
|
||||
agentMap[activeAgent?.type as keyof typeof agentMap]?.textColor
|
||||
}`}
|
||||
>
|
||||
{agentMap[activeAgent?.type as keyof typeof agentMap]?.name}
|
||||
</div>
|
||||
<TaskState
|
||||
all={activeAgent?.tasks?.length || 0}
|
||||
reAssignTo={
|
||||
activeAgent?.tasks?.filter((task) => task.reAssignTo).length ||
|
||||
0
|
||||
}
|
||||
done={
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) => task.status === "completed"
|
||||
).length || 0
|
||||
}
|
||||
progress={
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) =>
|
||||
task.status !== "failed" &&
|
||||
task.status !== "completed" &&
|
||||
task.status !== "skipped" &&
|
||||
task.status !== "waiting"
|
||||
).length || 0
|
||||
}
|
||||
failed={
|
||||
activeAgent?.tasks?.filter((task) => task.status === "failed")
|
||||
.length || 0
|
||||
}
|
||||
skipped={
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) =>
|
||||
task.status === "skipped" || task.status === "waiting"
|
||||
).length || 0
|
||||
}
|
||||
/>
|
||||
{/* <div className="text-[10px] leading-17 font-medium text-text-tertiary">
|
||||
{
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) => task.status && task.status !== "running"
|
||||
).length
|
||||
}
|
||||
/{activeAgent?.tasks?.length}
|
||||
</div> */}
|
||||
</div>
|
||||
{/* <div className="w-6 h-6 flex items-center justify-center">
|
||||
<Settings2 size={16} />
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{activeAgent?.activeWebviewIds?.length === 1 ? (
|
||||
<div className="flex-1 min-h-0">
|
||||
{activeAgent?.activeWebviewIds[0]?.img && (
|
||||
<div
|
||||
onClick={() =>
|
||||
handleTakeControl(
|
||||
activeAgent?.activeWebviewIds?.[0]?.id || ""
|
||||
)
|
||||
}
|
||||
className="cursor-pointer relative h-full w-full group pt-sm rounded-b-2xl"
|
||||
>
|
||||
<img
|
||||
src={activeAgent?.activeWebviewIds[0]?.img}
|
||||
alt=""
|
||||
className="w-full h-full object-contain rounded-b-2xl"
|
||||
/>
|
||||
<div className=" flex justify-center items-center opacity-0 transition-all group-hover:opacity-100 rounded-b-lg absolute inset-0 w-full h-full bg-black/20 pointer-events-none">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
className="cursor-pointer rounded-full"
|
||||
>
|
||||
<Hand size={24} />
|
||||
<span className="text-base leading-9 font-medium">
|
||||
Take Control
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={`${
|
||||
isSingleMode ? "px-0" : "px-2 pb-2"
|
||||
} flex-1 min-h-0 overflow-y-auto scrollbar flex gap-4 justify-start flex-wrap relative`}
|
||||
>
|
||||
{activeAgent?.activeWebviewIds
|
||||
?.filter((item) => item?.img)
|
||||
.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleTakeControl(item.id)}
|
||||
className={`cursor-pointer relative card-box rounded-lg group ${
|
||||
isSingleMode
|
||||
? "h-[calc(100%)] w-[calc(100%)]"
|
||||
: "h-[calc(50%-8px)] w-[calc(50%-8px)]"
|
||||
}`}
|
||||
>
|
||||
{item.img && (
|
||||
<img
|
||||
src={item.img}
|
||||
alt=""
|
||||
className="w-full h-full object-contain rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
onClick={() =>
|
||||
handleTakeControl(
|
||||
activeAgent?.activeWebviewIds?.[0]?.id || ""
|
||||
)
|
||||
}
|
||||
className="flex justify-center items-center opacity-0 transition-all group-hover:opacity-100 rounded-lg absolute inset-0 w-full h-full bg-black/20 pointer-events-none"
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
className="cursor-pointer rounded-full"
|
||||
>
|
||||
<Hand size={24} />
|
||||
<span className="text-base leading-9 font-medium">
|
||||
Take Control
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{activeAgent?.activeWebviewIds?.length !== 1 && (
|
||||
<div className="flex items-center gap-1 rounded-lg p-1 absolute bottom-2 right-2 w-auto bg-menutabs-bg-default border border-solid border-border-primary z-100">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const card = container.querySelector("div.card-box");
|
||||
if (!card) return;
|
||||
const cardHeight = card.getBoundingClientRect().height;
|
||||
const gap = 16;
|
||||
const rowCount = isSingleMode ? 1 : 2;
|
||||
const scrollAmount = (cardHeight + gap) * rowCount;
|
||||
container.scrollTo({
|
||||
top: Math.min(
|
||||
container.scrollHeight - container.clientHeight,
|
||||
container.scrollTop + scrollAmount
|
||||
),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowDown size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const card = container.querySelector("div.card-box");
|
||||
if (!card) return;
|
||||
const cardHeight = card.getBoundingClientRect().height;
|
||||
const gap = 16;
|
||||
const rowCount = isSingleMode ? 1 : 2;
|
||||
const scrollAmount = (cardHeight + gap) * rowCount;
|
||||
container.scrollTo({
|
||||
top: Math.max(0, container.scrollTop - scrollAmount),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowUp size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setIsSingleMode(!isSingleMode);
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GalleryThumbnails size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ export const InstallDependencies: React.FC = () => {
|
|||
} = useInstallationUI();
|
||||
|
||||
return (
|
||||
<div className="fixed !z-[100] inset-0 !bg-bg-page bg-opacity-80 h-full w-full flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="fixed !z-[100] inset-0 bg-opacity-80 h-full w-full flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="w-[1200px] p-[40px] h-full flex flex-col justify-center gap-xl">
|
||||
<div className="relative">
|
||||
{/* {isInstalling.toString()} */}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ const Layout = () => {
|
|||
onOpenChange={setNoticeOpen}
|
||||
open={noticeOpen}
|
||||
/>
|
||||
<Halo />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function VerticalNavigation({
|
|||
>
|
||||
<TabsList
|
||||
className={cn(
|
||||
"flex w-48 flex-col gap-1 bg-transparent p-0 border-0",
|
||||
"flex w-48 flex-col gap-1 bg-transparent rounded-0 p-0 border-0",
|
||||
listClassName
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -332,4 +332,4 @@ export default function TerminalAgentWrokSpace() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,17 +14,10 @@ import {
|
|||
ChevronLeft,
|
||||
House,
|
||||
Share,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import "./index.css";
|
||||
import folderIcon from "@/assets/Folder.svg";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useSidebarStore } from "@/store/sidebarStore";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
|
|
@ -264,16 +257,16 @@ function HeaderWin() {
|
|||
<House className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="no-drag"
|
||||
onClick={createNewProject}
|
||||
>
|
||||
<TooltipSimple content={t("layout.new-project")} side="bottom" align="center">
|
||||
<Plus className="w-4 h-4" />
|
||||
</TooltipSimple>
|
||||
</Button>
|
||||
<TooltipSimple content={t("layout.new-project")} side="bottom" align="center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="no-drag"
|
||||
onClick={createNewProject}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
)}
|
||||
{location.pathname !== "/history" && (
|
||||
|
|
@ -344,37 +337,42 @@ function HeaderWin() {
|
|||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
|
||||
<TooltipSimple content={t("layout.report-bug")} side="bottom" align="end">
|
||||
<Button
|
||||
onClick={exportLog}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="no-drag"
|
||||
className="no-drag rounded-full"
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
<FileDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-36">
|
||||
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
|
||||
<DropdownMenuItem onClick={exportLog} className="cursor-pointer">
|
||||
<FileDown className="w-4 h-4" />
|
||||
{t("layout.report-bug")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={getReferFriendsLink} className="cursor-pointer">
|
||||
<img
|
||||
src={giftIcon}
|
||||
alt="gift-icon"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{t("layout.refer-friends")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate("/history?tab=settings")} className="cursor-pointer">
|
||||
<Settings className="w-4 h-4" />
|
||||
{t("layout.settings")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<TooltipSimple content={t("layout.refer-friends")} side="bottom" align="end">
|
||||
<Button
|
||||
onClick={getReferFriendsLink}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="no-drag"
|
||||
>
|
||||
<img
|
||||
src={giftIcon}
|
||||
alt="gift-icon"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple content={t("layout.settings")} side="bottom" align="end">
|
||||
<Button
|
||||
onClick={() => navigate("/history?tab=settings")}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="no-drag"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
)}
|
||||
{location.pathname === "/history" && (
|
||||
|
|
|
|||
|
|
@ -233,13 +233,22 @@ export default function Workflow({
|
|||
// console.log("workerList ", workerList);
|
||||
setNodes((prev: CustomNode[]) => {
|
||||
if (!taskAssigning) return prev;
|
||||
// Agents not yet in taskAssigning (from baseWorker or workerList)
|
||||
const base = [...baseWorker, ...workerList].filter(
|
||||
(worker) => !taskAssigning.find((agent) => agent.type === worker.type)
|
||||
);
|
||||
let targetData = [...prev];
|
||||
taskAssigning = [...base, ...taskAssigning];
|
||||
// taskAssigning = taskAssigning.filter((agent) => agent.tasks.length > 0);
|
||||
targetData = taskAssigning.map((agent, index) => {
|
||||
// Merge all agents
|
||||
const allAgents = [...taskAssigning, ...base];
|
||||
// Sort: agents with tasks come first, then agents without tasks
|
||||
const sortedAgents = allAgents.sort((a, b) => {
|
||||
const aHasTasks = a.tasks && a.tasks.length > 0;
|
||||
const bHasTasks = b.tasks && b.tasks.length > 0;
|
||||
if (aHasTasks && !bHasTasks) return -1;
|
||||
if (!aHasTasks && bHasTasks) return 1;
|
||||
return 0;
|
||||
});
|
||||
targetData = sortedAgents.map((agent, index) => {
|
||||
const node = targetData.find((node) => node.id === agent.agent_id);
|
||||
if (node) {
|
||||
return {
|
||||
|
|
@ -450,4 +459,4 @@ export default function Workflow({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -420,7 +420,7 @@ export function Node({ id, data }: NodeProps) {
|
|||
</div>
|
||||
<div
|
||||
ref={toolsRef}
|
||||
className="flex-shrink-0 text-text-label text-xs leading-tight min-h-4 font-normal mb-sm pr-3 text-"
|
||||
className="flex-shrink-0 text-text-label text-xs leading-tight min-h-4 font-normal mb-sm pr-3"
|
||||
>
|
||||
{/* {JSON.stringify(data.agent)} */}
|
||||
{agentToolkits[
|
||||
|
|
@ -894,4 +894,4 @@ export function Node({ id, data }: NodeProps) {
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
223
src/components/ui/button.stories.tsx
Normal file
223
src/components/ui/button.stories.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { Button } from './button'
|
||||
import { Plus, Download, Trash2 } from 'lucide-react'
|
||||
import { expect, fn, userEvent, within } from 'storybook/test'
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'UI/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'outline',
|
||||
'ghost',
|
||||
'success',
|
||||
'cuation',
|
||||
'information',
|
||||
'warning',
|
||||
],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['xxs', 'xs', 'sm', 'md', 'lg', 'icon'],
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
asChild: {
|
||||
control: 'boolean',
|
||||
},
|
||||
children: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
children: 'Button',
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Button>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: 'Primary Button',
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
children: 'Secondary Button',
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
variant: 'outline',
|
||||
children: 'Outline Button',
|
||||
},
|
||||
}
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: {
|
||||
variant: 'ghost',
|
||||
children: 'Ghost Button',
|
||||
},
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
variant: 'success',
|
||||
children: 'Success Button',
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
variant: 'warning',
|
||||
children: 'Warning Button',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: 'Disabled Button',
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIcon: Story = {
|
||||
render: (args) => (
|
||||
<Button {...args}>
|
||||
<Plus /> Add Item
|
||||
</Button>
|
||||
),
|
||||
args: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
|
||||
export const IconOnly: Story = {
|
||||
render: (args) => (
|
||||
<Button {...args}>
|
||||
<Download />
|
||||
</Button>
|
||||
),
|
||||
args: {
|
||||
variant: 'ghost',
|
||||
size: 'icon',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="success">Success</Button>
|
||||
<Button variant="warning">Warning</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<Button variant="primary" size="xxs">
|
||||
XXS
|
||||
</Button>
|
||||
<Button variant="primary" size="xs">
|
||||
XS
|
||||
</Button>
|
||||
<Button variant="primary" size="sm">
|
||||
SM
|
||||
</Button>
|
||||
<Button variant="primary" size="md">
|
||||
MD
|
||||
</Button>
|
||||
<Button variant="primary" size="lg">
|
||||
LG
|
||||
</Button>
|
||||
<Button variant="primary" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
// Interaction test stories
|
||||
export const ClickInteraction: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: 'Click Me',
|
||||
onClick: fn(),
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const button = canvas.getByRole('button', { name: /click me/i })
|
||||
|
||||
// Test that button is visible and enabled
|
||||
await expect(button).toBeVisible()
|
||||
await expect(button).toBeEnabled()
|
||||
|
||||
// Click the button
|
||||
await userEvent.click(button)
|
||||
|
||||
// Verify the onClick handler was called
|
||||
await expect(args.onClick).toHaveBeenCalledTimes(1)
|
||||
},
|
||||
}
|
||||
|
||||
export const DisabledInteraction: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: 'Disabled Button',
|
||||
disabled: true,
|
||||
onClick: fn(),
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const button = canvas.getByRole('button', { name: /disabled button/i })
|
||||
|
||||
// Test that button is visible but disabled
|
||||
await expect(button).toBeVisible()
|
||||
await expect(button).toBeDisabled()
|
||||
|
||||
// Verify the onClick handler was NOT called (disabled buttons block pointer events)
|
||||
await expect(args.onClick).not.toHaveBeenCalled()
|
||||
},
|
||||
}
|
||||
|
||||
export const HoverInteraction: Story = {
|
||||
args: {
|
||||
variant: 'outline',
|
||||
children: 'Hover Over Me',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const button = canvas.getByRole('button', { name: /hover over me/i })
|
||||
|
||||
// Test initial state
|
||||
await expect(button).toBeVisible()
|
||||
|
||||
// Hover over the button
|
||||
await userEvent.hover(button)
|
||||
|
||||
// The button should still be visible after hover
|
||||
await expect(button).toBeVisible()
|
||||
|
||||
// Unhover
|
||||
await userEvent.unhover(button)
|
||||
},
|
||||
}
|
||||
512
src/components/ui/dialog.stories.tsx
Normal file
512
src/components/ui/dialog.stories.tsx
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogContentSection,
|
||||
DialogFooter,
|
||||
} from './dialog'
|
||||
import { Button } from './button'
|
||||
import { Input } from './input'
|
||||
import { expect, userEvent, within } from 'storybook/test'
|
||||
|
||||
const meta: Meta<typeof Dialog> = {
|
||||
title: 'UI/Dialog',
|
||||
component: Dialog,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Dialog>
|
||||
|
||||
export const Default: Story = {
|
||||
render: function DefaultDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader title="Dialog Title" subtitle="Optional subtitle text" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
This is the main content area of the dialog. You can add any content
|
||||
here including forms, text, images, or other components.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => {
|
||||
console.log('Confirmed!')
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallSize: Story = {
|
||||
render: function SmallDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Small Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader title="Small Dialog" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">A compact dialog for simple actions.</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText="OK"
|
||||
onConfirm={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const LargeSize: Story = {
|
||||
render: function LargeDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Large Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="lg">
|
||||
<DialogHeader
|
||||
title="Large Dialog"
|
||||
subtitle="This dialog is wider for more complex content"
|
||||
/>
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
Large dialogs are useful when you need to display more content, such
|
||||
as detailed forms, tables, or multi-step processes.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithForm: Story = {
|
||||
render: function FormDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Create Account</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader
|
||||
title="Create Account"
|
||||
subtitle="Enter your details to create a new account"
|
||||
/>
|
||||
<DialogContentSection>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input title="Full Name" placeholder="Enter your full name" required />
|
||||
<Input
|
||||
title="Email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
title="Password"
|
||||
type="password"
|
||||
placeholder="Create a password"
|
||||
required
|
||||
note="Must be at least 8 characters"
|
||||
/>
|
||||
</div>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
confirmButtonText="Create Account"
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => {
|
||||
console.log('Account created!')
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithTooltip: Story = {
|
||||
render: function TooltipDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">With Tooltip</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader
|
||||
title="Settings"
|
||||
tooltip="Configure your application settings"
|
||||
showTooltip
|
||||
/>
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
Hover over the icon next to the title to see the tooltip.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText="Save"
|
||||
onConfirm={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithBackButton: Story = {
|
||||
render: function BackButtonDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Multi-step Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader
|
||||
title="Step 2 of 3"
|
||||
subtitle="Configure your preferences"
|
||||
showBackButton
|
||||
onBackClick={() => console.log('Back clicked')}
|
||||
/>
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
Click the back button to return to the previous step.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
cancelButtonText="Back"
|
||||
confirmButtonText="Next"
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => {
|
||||
console.log('Next step')
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const DestructiveAction: Story = {
|
||||
render: function DestructiveDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="warning">Delete Item</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader
|
||||
title="Delete Item"
|
||||
subtitle="This action cannot be undone"
|
||||
/>
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
Are you sure you want to delete this item? All associated data will
|
||||
be permanently removed.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonVariant="warning"
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => {
|
||||
console.log('Item deleted!')
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const NoCloseButton: Story = {
|
||||
render: function NoCloseDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">No Close Button</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent showCloseButton={false}>
|
||||
<DialogHeader title="Required Action" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
This dialog does not have a close button. User must interact with
|
||||
the footer buttons.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText="Acknowledge"
|
||||
onConfirm={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: function AllSizesDialog() {
|
||||
const [openSm, setOpenSm] = useState(false)
|
||||
const [openMd, setOpenMd] = useState(false)
|
||||
const [openLg, setOpenLg] = useState(false)
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<Dialog open={openSm} onOpenChange={setOpenSm}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Small (400px)</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader title="Small Dialog" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">Max width: 400px</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText="Close"
|
||||
onConfirm={() => setOpenSm(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={openMd} onOpenChange={setOpenMd}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Medium (600px)</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="md">
|
||||
<DialogHeader title="Medium Dialog" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">Max width: 600px (default)</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText="Close"
|
||||
onConfirm={() => setOpenMd(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={openLg} onOpenChange={setOpenLg}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Large (900px)</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="lg">
|
||||
<DialogHeader title="Large Dialog" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">Max width: 900px</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText="Close"
|
||||
onConfirm={() => setOpenLg(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// Interaction test stories
|
||||
export const OpenCloseInteraction: Story = {
|
||||
render: function OpenCloseDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader title="Interactive Dialog" subtitle="Test opening and closing" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
This dialog tests the open/close interaction.
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const body = within(document.body)
|
||||
|
||||
// Open the dialog
|
||||
await userEvent.click(canvas.getByRole('button', { name: /open dialog/i }))
|
||||
|
||||
// Verify dialog is visible (renders in portal)
|
||||
await expect(await body.findByText('Interactive Dialog')).toBeVisible()
|
||||
|
||||
// Close the dialog
|
||||
await userEvent.click(body.getByRole('button', { name: /cancel/i }))
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(body.queryByText('Interactive Dialog')).not.toBeInTheDocument()
|
||||
},
|
||||
}
|
||||
|
||||
export const FormInteraction: Story = {
|
||||
render: function FormInteractionDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary">Open Form</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader title="Sign Up" subtitle="Create your account" />
|
||||
<DialogContentSection>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input title="Name" placeholder="Enter your name" required />
|
||||
<Input
|
||||
title="Email"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
confirmButtonText="Sign Up"
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => {
|
||||
setSubmitted(true)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{submitted && <p data-testid="success-message">Form submitted successfully!</p>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const body = within(document.body)
|
||||
|
||||
// Open the dialog
|
||||
await userEvent.click(canvas.getByRole('button', { name: /open form/i }))
|
||||
|
||||
// Wait for dialog to appear (renders in portal)
|
||||
await expect(await body.findByText('Sign Up')).toBeVisible()
|
||||
|
||||
// Fill in the form
|
||||
const nameInput = body.getByPlaceholderText('Enter your name')
|
||||
const emailInput = body.getByPlaceholderText('Enter your email')
|
||||
|
||||
await userEvent.type(nameInput, 'John Doe')
|
||||
await userEvent.type(emailInput, 'john@example.com')
|
||||
|
||||
// Verify inputs have values
|
||||
await expect(nameInput).toHaveValue('John Doe')
|
||||
await expect(emailInput).toHaveValue('john@example.com')
|
||||
|
||||
// Submit the form
|
||||
await userEvent.click(body.getByRole('button', { name: /sign up/i }))
|
||||
|
||||
// Verify success message appears
|
||||
await expect(await canvas.findByTestId('success-message')).toBeVisible()
|
||||
},
|
||||
}
|
||||
|
||||
export const ConfirmDialogInteraction: Story = {
|
||||
render: function ConfirmInteractionDialog() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="warning">Delete Item</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader title="Confirm Delete" subtitle="This action cannot be undone" />
|
||||
<DialogContentSection>
|
||||
<p className="text-text-body">
|
||||
Are you sure you want to delete this item?
|
||||
</p>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showCancelButton
|
||||
showConfirmButton
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonVariant="warning"
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={() => {
|
||||
setConfirmed(true)
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{confirmed && <p data-testid="deleted-message">Item deleted!</p>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const body = within(document.body)
|
||||
|
||||
// Open the confirmation dialog
|
||||
await userEvent.click(canvas.getByRole('button', { name: /delete item/i }))
|
||||
|
||||
// Verify dialog is visible (renders in portal)
|
||||
await expect(await body.findByText('Confirm Delete')).toBeVisible()
|
||||
|
||||
// Click the confirm/delete button
|
||||
await userEvent.click(body.getByRole('button', { name: /^delete$/i }))
|
||||
|
||||
// Verify the deleted message appears
|
||||
const deletedMessage = await canvas.findByTestId('deleted-message')
|
||||
await expect(deletedMessage).toHaveTextContent('Item deleted!')
|
||||
},
|
||||
}
|
||||
291
src/components/ui/input.stories.tsx
Normal file
291
src/components/ui/input.stories.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { Input } from './input'
|
||||
import { Search, Eye, EyeOff } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { expect, fn, userEvent, within } from 'storybook/test'
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'UI/Input',
|
||||
component: Input,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['default', 'sm'],
|
||||
},
|
||||
state: {
|
||||
control: 'select',
|
||||
options: ['default', 'hover', 'input', 'error', 'success', 'disabled'],
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
required: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-80">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Input>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: 'Enter text...',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithTitle: Story = {
|
||||
args: {
|
||||
title: 'Email Address',
|
||||
placeholder: 'name@example.com',
|
||||
type: 'email',
|
||||
},
|
||||
}
|
||||
|
||||
export const Required: Story = {
|
||||
args: {
|
||||
title: 'Username',
|
||||
placeholder: 'Enter username',
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithTooltip: Story = {
|
||||
args: {
|
||||
title: 'API Key',
|
||||
placeholder: 'Enter your API key',
|
||||
tooltip: 'Your API key can be found in your account settings',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithNote: Story = {
|
||||
args: {
|
||||
title: 'Password',
|
||||
type: 'password',
|
||||
placeholder: 'Enter password',
|
||||
note: 'Must be at least 8 characters',
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorState: Story = {
|
||||
args: {
|
||||
title: 'Email',
|
||||
placeholder: 'name@example.com',
|
||||
state: 'error',
|
||||
note: 'Please enter a valid email address',
|
||||
defaultValue: 'invalid-email',
|
||||
},
|
||||
}
|
||||
|
||||
export const SuccessState: Story = {
|
||||
args: {
|
||||
title: 'Username',
|
||||
placeholder: 'Enter username',
|
||||
state: 'success',
|
||||
note: 'Username is available',
|
||||
defaultValue: 'johndoe',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
title: 'Locked Field',
|
||||
placeholder: 'This field is disabled',
|
||||
disabled: true,
|
||||
defaultValue: 'Cannot edit',
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallSize: Story = {
|
||||
args: {
|
||||
size: 'sm',
|
||||
placeholder: 'Small input',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithLeadingIcon: Story = {
|
||||
args: {
|
||||
placeholder: 'Search...',
|
||||
leadingIcon: <Search size={16} />,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithBackIcon: Story = {
|
||||
render: function PasswordInput() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
return (
|
||||
<Input
|
||||
title="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter password"
|
||||
backIcon={showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
onBackIconClick={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStates: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input title="Default" state="default" placeholder="Default state" />
|
||||
<Input title="Hover" state="hover" placeholder="Hover state" />
|
||||
<Input title="Input" state="input" placeholder="Input state" />
|
||||
<Input
|
||||
title="Error"
|
||||
state="error"
|
||||
placeholder="Error state"
|
||||
note="Error message"
|
||||
/>
|
||||
<Input
|
||||
title="Success"
|
||||
state="success"
|
||||
placeholder="Success state"
|
||||
note="Success message"
|
||||
/>
|
||||
<Input title="Disabled" disabled placeholder="Disabled state" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const FormExample: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-text-heading">Contact Form</h3>
|
||||
<Input title="Full Name" placeholder="John Doe" required />
|
||||
<Input
|
||||
title="Email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
title="Phone"
|
||||
type="tel"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
tooltip="We'll only use this for urgent matters"
|
||||
/>
|
||||
<Input
|
||||
title="Message"
|
||||
placeholder="How can we help you?"
|
||||
note="Maximum 500 characters"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-96">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
// Interaction test stories
|
||||
export const TypeInteraction: Story = {
|
||||
args: {
|
||||
placeholder: 'Type something...',
|
||||
onChange: fn(),
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const input = canvas.getByPlaceholderText('Type something...')
|
||||
|
||||
// Test that input is visible and enabled
|
||||
await expect(input).toBeVisible()
|
||||
await expect(input).toBeEnabled()
|
||||
|
||||
// Clear any existing value and type new text
|
||||
await userEvent.clear(input)
|
||||
await userEvent.type(input, 'Hello World')
|
||||
|
||||
// Verify the input value
|
||||
await expect(input).toHaveValue('Hello World')
|
||||
|
||||
// Verify onChange was called
|
||||
await expect(args.onChange).toHaveBeenCalled()
|
||||
},
|
||||
}
|
||||
|
||||
export const FocusInteraction: Story = {
|
||||
args: {
|
||||
title: 'Focus Test',
|
||||
placeholder: 'Click to focus...',
|
||||
onFocus: fn(),
|
||||
onBlur: fn(),
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const input = canvas.getByPlaceholderText('Click to focus...')
|
||||
|
||||
// Click to focus the input
|
||||
await userEvent.click(input)
|
||||
await expect(args.onFocus).toHaveBeenCalled()
|
||||
|
||||
// Tab away to blur
|
||||
await userEvent.tab()
|
||||
await expect(args.onBlur).toHaveBeenCalled()
|
||||
},
|
||||
}
|
||||
|
||||
export const ClearAndTypeInteraction: Story = {
|
||||
args: {
|
||||
title: 'Edit Field',
|
||||
placeholder: 'Edit this text',
|
||||
defaultValue: 'Initial value',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const input = canvas.getByPlaceholderText('Edit this text')
|
||||
|
||||
// Verify initial value
|
||||
await expect(input).toHaveValue('Initial value')
|
||||
|
||||
// Select all and replace
|
||||
await userEvent.tripleClick(input)
|
||||
await userEvent.type(input, 'Replaced text')
|
||||
|
||||
// Verify the new value
|
||||
await expect(input).toHaveValue('Replaced text')
|
||||
},
|
||||
}
|
||||
|
||||
export const PasswordToggleInteraction: Story = {
|
||||
render: function PasswordToggle() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
return (
|
||||
<Input
|
||||
title="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter password"
|
||||
defaultValue="secret123"
|
||||
backIcon={showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
onBackIconClick={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const input = canvas.getByPlaceholderText('Enter password')
|
||||
const toggleButton = canvas.getByRole('button')
|
||||
|
||||
// Initially password should be hidden (type="password")
|
||||
await expect(input).toHaveAttribute('type', 'password')
|
||||
|
||||
// Click toggle to show password
|
||||
await userEvent.click(toggleButton)
|
||||
await expect(input).toHaveAttribute('type', 'text')
|
||||
|
||||
// Click toggle again to hide password
|
||||
await userEvent.click(toggleButton)
|
||||
await expect(input).toHaveAttribute('type', 'password')
|
||||
},
|
||||
}
|
||||
|
|
@ -31,47 +31,47 @@ function resolveStateClasses(state: InputState | undefined) {
|
|||
if (state === "disabled") {
|
||||
return {
|
||||
container: "opacity-50 cursor-not-allowed",
|
||||
field:
|
||||
"border-input-border-default bg-input-bg-default text-input-text-default",
|
||||
field: "border-input-border-default bg-input-bg-default",
|
||||
input: "text-text-heading",
|
||||
placeholder: "placeholder-input-label-default",
|
||||
}
|
||||
}
|
||||
if (state === "hover") {
|
||||
return {
|
||||
container: "",
|
||||
field:
|
||||
"border-input-border-hover bg-input-bg-default text-input-text-default",
|
||||
field: "border-input-border-hover bg-input-bg-default",
|
||||
input: "text-text-heading",
|
||||
placeholder: "placeholder-input-label-default",
|
||||
}
|
||||
}
|
||||
if (state === "input") {
|
||||
return {
|
||||
container: "",
|
||||
field:
|
||||
"border-input-border-focus bg-input-bg-input text-input-text-focus",
|
||||
field: "border-input-border-focus bg-input-bg-input",
|
||||
input: "text-text-heading",
|
||||
placeholder: "placeholder-input-label-default",
|
||||
}
|
||||
}
|
||||
if (state === "error") {
|
||||
return {
|
||||
container: "",
|
||||
field:
|
||||
"border-input-border-cuation bg-input-bg-default text-text-body",
|
||||
field: "border-input-border-cuation bg-input-bg-default",
|
||||
input: "text-text-heading",
|
||||
placeholder: "placeholder-input-label-default",
|
||||
}
|
||||
}
|
||||
if (state === "success") {
|
||||
return {
|
||||
container: "",
|
||||
field:
|
||||
"border-input-border-success bg-input-bg-confirm text-text-body",
|
||||
field: "border-input-border-success bg-input-bg-confirm",
|
||||
input: "text-text-heading",
|
||||
placeholder: "placeholder-input-label-default",
|
||||
}
|
||||
}
|
||||
return {
|
||||
container: "",
|
||||
field:
|
||||
"border-input-border-default bg-input-bg-default text-input-text-default",
|
||||
field: "border-input-border-default bg-input-bg-default",
|
||||
input: "text-text-heading",
|
||||
placeholder: "placeholder-input-label-default/10",
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ const Input = React.forwardRef<HTMLInputElement, BaseInputProps>(
|
|||
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-center rounded-md border border-solid shadow-sm transition-colors",
|
||||
"relative flex items-center rounded-lg border border-solid shadow-sm transition-colors",
|
||||
// Only apply hover/focus visuals when not in error state
|
||||
state !== "error" &&
|
||||
"hover:bg-input-bg-hover hover:border-input-border-hover focus-within:border-input-border-focus focus-within:bg-input-bg-input",
|
||||
|
|
@ -140,6 +140,7 @@ const Input = React.forwardRef<HTMLInputElement, BaseInputProps>(
|
|||
placeholder={placeholder}
|
||||
className={cn(
|
||||
"peer w-full bg-transparent outline-none placeholder:transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
||||
stateCls.input,
|
||||
stateCls.placeholder,
|
||||
hasLeft ? "pl-9" : "pl-3",
|
||||
hasRight ? "pr-9" : "pr-3",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const SelectTrigger = React.forwardRef<
|
|||
disabled={disabled}
|
||||
className={cn(
|
||||
// Base styles
|
||||
"relative flex w-full items-center justify-between rounded-md border border-input-border-default border-solid outline-none transition-colors px-3 gap-2 text-text-body",
|
||||
"relative flex w-full items-center justify-between rounded-lg border border-input-border-default border-solid outline-none transition-colors px-3 gap-2 text-text-body",
|
||||
sizeClasses[size],
|
||||
"[&>span]:line-clamp-1 whitespace-nowrap",
|
||||
// Default state (when no error/success)
|
||||
|
|
@ -147,7 +147,7 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-input-bg-default relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-solid border-input-border-default text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
"bg-input-bg-default backdrop-blur-md relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-xl border border-solid border-input-border-default text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
|
|
@ -190,7 +190,7 @@ const SelectItem = React.forwardRef<
|
|||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -235,7 +235,7 @@ const SelectItemWithButton = React.forwardRef<
|
|||
value={value}
|
||||
disabled={!enabled}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 group",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-menutabs-fill-hover focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 group",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "وكيل متعدد الوسائط",
|
||||
"social-media-agent": "وكيل وسائل التواصل الاجتماعي",
|
||||
"no-projects-found": "لا توجد مشاريع"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "Multi-Modal-Agent",
|
||||
"social-media-agent": "Social-Media-Agent",
|
||||
"no-projects-found": "Keine Projekte gefunden."
|
||||
}
|
||||
}
|
||||
|
|
@ -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 incoming tickets 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 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.",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "Multi Modal Agent",
|
||||
"social-media-agent": "Social Media Agent",
|
||||
"no-projects-found": "No projects found."
|
||||
}
|
||||
}
|
||||
|
|
@ -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 incoming tickets 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 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.",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "Agente Multi Modal",
|
||||
"social-media-agent": "Agente de Redes Sociales",
|
||||
"no-projects-found": "No se encontraron proyectos."
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "Agent Multi Modal",
|
||||
"social-media-agent": "Agent de Médias Sociaux",
|
||||
"no-projects-found": "Aucun projet trouvé."
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "Agente Multi Modale",
|
||||
"social-media-agent": "Agente Social Media",
|
||||
"no-projects-found": "Nessun progetto trovato."
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "マルチモーダルエージェント",
|
||||
"social-media-agent": "ソーシャルメディアエージェント",
|
||||
"no-projects-found": "プロジェクトが見つかりませんでした。"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "멀티모달 에이전트",
|
||||
"social-media-agent": "소셜미디어 에이전트",
|
||||
"no-projects-found": "프로젝트를 찾을 수 없습니다."
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "Мультимодальный агент",
|
||||
"social-media-agent": "Агент социальных сетей",
|
||||
"no-projects-found": "Проекты не найдены"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "多模态智能体",
|
||||
"social-media-agent": "社交媒体智能体",
|
||||
"no-projects-found": "没有找到项目"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"multi-modal-agent": "多模態智能體",
|
||||
"social-media-agent": "社群媒體智能體",
|
||||
"no-projects-found": "沒有找到專案"
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +73,15 @@ export const INIT_PROVODERS: Provider[] = [
|
|||
is_valid: false,
|
||||
model_type: ""
|
||||
},
|
||||
{
|
||||
id: 'moonshot',
|
||||
name: 'Moonshot',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.moonshot.ai/v1',
|
||||
description: "Kimi model configuration.",
|
||||
is_valid: false,
|
||||
model_type: ""
|
||||
},
|
||||
{
|
||||
id: 'ModelArk',
|
||||
name: 'ModelArk',
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export default function Browser() {
|
|||
/>
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="flex w-full border-solid border-t-0 border-x-0 border-border-disabled">
|
||||
<div className="flex w-full">
|
||||
<div className="flex px-6 pt-8 pb-4 max-w-[900px] mx-auto w-full items-center justify-between">
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<div className="flex flex-col">
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export default function Home() {
|
|||
<div className="w-full h-[calc(100vh-104px)] flex-1 flex animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
<BrowserAgentWorkSpace />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.activeWorkSpace === "workflow" && (
|
||||
<div className="w-full h-full flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
|
|
|
|||
|
|
@ -590,7 +590,7 @@ export default function SettingMCP() {
|
|||
return (
|
||||
<div className="flex-1 h-auto m-auto">
|
||||
{/* Header Section */}
|
||||
<div className="flex w-full border-solid border-t-0 border-x-0 border-border-disabled">
|
||||
<div className="flex w-full">
|
||||
<div className="flex px-6 pt-8 pb-4 max-w-[900px] mx-auto w-full items-center justify-between">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{showMarket ? (
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ const resolveProcessTaskIdForToolkitEvent = (
|
|||
// Throttle streaming decompose text updates to prevent excessive re-renders
|
||||
const streamingDecomposeTextBuffer: Record<string, string> = {};
|
||||
const streamingDecomposeTextTimers: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||
// TTFT (Time to First Token) tracking for task decomposition
|
||||
const ttftTracking: Record<string, { confirmedAt: number; firstTokenLogged: boolean }> = {};
|
||||
|
||||
const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
||||
(set, get) => ({
|
||||
|
|
@ -213,11 +215,11 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
computedProgressValue(taskId: string) {
|
||||
const { tasks, setProgressValue, activeTaskId } = get()
|
||||
const taskRunning = [...tasks[taskId].taskRunning]
|
||||
const finshedTask = taskRunning?.filter(
|
||||
const finishedTask = taskRunning?.filter(
|
||||
(task) => task.status === "completed" || task.status === "failed"
|
||||
).length;
|
||||
const taskProgress = (
|
||||
((finshedTask || 0) / (taskRunning?.length || 0)) *
|
||||
((finishedTask || 0) / (taskRunning?.length || 0)) *
|
||||
100
|
||||
).toFixed(2);
|
||||
setProgressValue(
|
||||
|
|
@ -757,6 +759,11 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
|
||||
//Enable it for the rest of current SSE session
|
||||
skipFirstConfirm = false;
|
||||
|
||||
// Record confirmed time for TTFT tracking
|
||||
const ttftTaskId = getCurrentTaskId();
|
||||
ttftTracking[ttftTaskId] = { confirmedAt: performance.now(), firstTokenLogged: false };
|
||||
console.log(`[TTFT] Task ${ttftTaskId} confirmed at ${new Date().toISOString()}, starting TTFT measurement`);
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -796,6 +803,13 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
const text = content;
|
||||
const currentId = getCurrentTaskId();
|
||||
|
||||
// Log TTFT (Time to First Token) on first decompose_text event
|
||||
if (ttftTracking[currentId] && !ttftTracking[currentId].firstTokenLogged) {
|
||||
ttftTracking[currentId].firstTokenLogged = true;
|
||||
const ttft = performance.now() - ttftTracking[currentId].confirmedAt;
|
||||
console.log(`%c[TTFT] 🚀 Time to First Token: ${ttft.toFixed(2)}ms - First streaming token received for task ${currentId}`, 'color: #4CAF50; font-weight: bold');
|
||||
}
|
||||
|
||||
// Get current buffer or task state
|
||||
const currentContent = streamingDecomposeTextBuffer[currentId] ||
|
||||
getCurrentChatStore().tasks[currentId]?.streamingDecomposeText || "";
|
||||
|
|
@ -829,6 +843,8 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
if (agentMessages.step === "to_sub_tasks") {
|
||||
// Clear streaming decompose text when task splitting is done
|
||||
clearStreamingDecomposeText(currentTaskId);
|
||||
// Clean up TTFT tracking
|
||||
delete ttftTracking[currentTaskId];
|
||||
// Check if this is a multi-turn scenario after task completion
|
||||
const isMultiTurnAfterCompletion = tasks[currentTaskId].status === 'finished';
|
||||
|
||||
|
|
@ -1056,7 +1072,7 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
if (agentMessages.step === "new_task_state") {
|
||||
const { task_id, content, state, result, failure_count } = agentMessages.data;
|
||||
//new chatStore logic is handled along side "confirmed" event
|
||||
console.log(`Recieved new task: ${task_id} with content: ${content}`);
|
||||
console.log(`Received new task: ${task_id} with content: ${content}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@
|
|||
--text-developer: var(--colors-emerald-default);
|
||||
--text-multimodal: var(--colors-fuchsia-default);
|
||||
--text-on-hover: var(--colors-primary-2);
|
||||
--surface-primary: var(--colors-off-white-80);
|
||||
--surface-primary: var(--colors-off-white-50);
|
||||
--surface-secondary: var(--colors-off-white-50);
|
||||
--surface-success: var(--colors-green-50);
|
||||
--surface-information: var(--colors-blue-50);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./.storybook/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ describe('Integration Test: Case 2 - same session new chat', () => {
|
|||
console.log("Progress test - task status:", task?.status);
|
||||
}, { timeout: 1500 })
|
||||
|
||||
// Test 3: Rerender untill status is "finished"
|
||||
// Test 3: Rerender until status is "finished"
|
||||
await waitFor(() => {
|
||||
rerender()
|
||||
const {chatStore: newChatStore} = result.current;
|
||||
|
|
@ -392,7 +392,7 @@ describe('Integration Test: Case 2 - same session new chat', () => {
|
|||
})
|
||||
})
|
||||
|
||||
//TODO: Don't let new startTask untill newChatStore appended
|
||||
//TODO: Don't let new startTask until newChatStore appended
|
||||
it("Parallel startTask calls with separate chatStores (startTask -> wait for append -> startTask)", async () => {
|
||||
const { result, rerender } = renderHook(() => useChatStoreAdapter())
|
||||
|
||||
|
|
|
|||
|
|
@ -324,4 +324,4 @@ export const issue619SseSequence = [
|
|||
},
|
||||
delay: 1100
|
||||
}
|
||||
];
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue