Merge branch 'main' into feature/password-reset-884

This commit is contained in:
罗鹏铖 2026-01-21 19:26:53 +08:00 committed by GitHub
commit b73fcf147d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 4016 additions and 635 deletions

View file

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

View file

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

@ -59,3 +59,6 @@ __pycache__/
resources/prebuilt/bin/
resources/prebuilt/venv/
resources/prebuilt/cache/
*storybook.log
storybook-static

13
.storybook/main.ts Normal file
View 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
View 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
View file

@ -0,0 +1 @@
/* Storybook-specific CSS overrides - currently empty as component handles styling */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -1,7 +1,6 @@
---
title: "Models (Local Model)"
description: "Configure and deploy your preferred LLM models with Eigent."
icon: "server"
---
## **Self-Host Model**

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View 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();

View file

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

View file

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

View file

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

View file

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

View file

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

105
server/uv.lock generated
View file

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

Before After
Before After

View file

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

View file

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

View 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>
);
}

View file

@ -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()} */}

View file

@ -99,7 +99,6 @@ const Layout = () => {
onOpenChange={setNoticeOpen}
open={noticeOpen}
/>
<Halo />
</div>
</div>
);

View file

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

View file

@ -332,4 +332,4 @@ export default function TerminalAgentWrokSpace() {
</div>
</div>
);
}
}

View file

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

View file

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

View file

@ -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) {
/>
</>
);
}
}

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

View 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!')
},
}

View 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')
},
}

View file

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

View file

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

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "وكيل متعدد الوسائط",
"social-media-agent": "وكيل وسائل التواصل الاجتماعي",
"no-projects-found": "لا توجد مشاريع"
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "Multi-Modal-Agent",
"social-media-agent": "Social-Media-Agent",
"no-projects-found": "Keine Projekte gefunden."
}
}

View file

@ -15,7 +15,7 @@
"you-are-using-self-hosted-mode": "You're in Self-hosted mode. Cloud models can't be used here — set up your own local cloud model to keep things running.",
"you-are-using-self-hosted-mode-mcp": "You're using Self-hosted mode. Enter the Google Search Keys in “MCP and Tools” to ensure Eigent works properly.",
"it-ticket-creation": "Help me complete an online form",
"it-ticket-creation-message": "Access the ticket management system at https://eiti.eigent.ai/ and add all these new tickets into our system with Browser Agent:\n''\nAffected User: Alice Johnson\nAssignment Group: Software Services Team\nAssigned To: Michael Brown\nPriority: 4 Low | Urgency: 3 Medium | Impact: 4 Low\nAffected Service: Software Services\nIssue: Application Performance Degradation\nDescription:\nThe affected user reports slow response times and intermittent timeouts when accessing internal software applications during normal business hours.\n''\nOnce done, check the 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",

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "Multi Modal Agent",
"social-media-agent": "Social Media Agent",
"no-projects-found": "No projects found."
}
}

View file

@ -126,7 +126,7 @@
"terms-of-use": "Terms of Use",
"and": "and",
"it-ticket-creation": "Help me complete an online form",
"it-ticket-creation-message": "Access the ticket management system at https://eiti.eigent.ai/ and add all these new tickets into our system with Browser Agent:\n''\nAffected User: Alice Johnson\nAssignment Group: Software Services Team\nAssigned To: Michael Brown\nPriority: 4 Low | Urgency: 3 Medium | Impact: 4 Low\nAffected Service: Software Services\nIssue: Application Performance Degradation\nDescription:\nThe affected user reports slow response times and intermittent timeouts when accessing internal software applications during normal business hours.\n''\nOnce done, check the 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",

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "Agente Multi Modal",
"social-media-agent": "Agente de Redes Sociales",
"no-projects-found": "No se encontraron proyectos."
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "Agent Multi Modal",
"social-media-agent": "Agent de Médias Sociaux",
"no-projects-found": "Aucun projet trouvé."
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "Agente Multi Modale",
"social-media-agent": "Agente Social Media",
"no-projects-found": "Nessun progetto trovato."
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "マルチモーダルエージェント",
"social-media-agent": "ソーシャルメディアエージェント",
"no-projects-found": "プロジェクトが見つかりませんでした。"
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "멀티모달 에이전트",
"social-media-agent": "소셜미디어 에이전트",
"no-projects-found": "프로젝트를 찾을 수 없습니다."
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "Мультимодальный агент",
"social-media-agent": "Агент социальных сетей",
"no-projects-found": "Проекты не найдены"
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "多模态智能体",
"social-media-agent": "社交媒体智能体",
"no-projects-found": "没有找到项目"
}
}

View file

@ -24,4 +24,4 @@
"multi-modal-agent": "多模態智能體",
"social-media-agent": "社群媒體智能體",
"no-projects-found": "沒有找到專案"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -324,4 +324,4 @@ export const issue619SseSequence = [
},
delay: 1100
}
];
];