diff --git a/.gitattributes b/.gitattributes index 526c8a38..dfdb8b77 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -*.sh text eol=lf \ No newline at end of file +*.sh text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 770ded5f..bcf14c06 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: directory: "/" # Location of package manifests schedule: interval: "monthly" - + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/remove-old-artifacts.yml b/.github/workflows/remove-old-artifacts.yml index 1065bbd0..4a7aaf59 100644 --- a/.github/workflows/remove-old-artifacts.yml +++ b/.github/workflows/remove-old-artifacts.yml @@ -24,4 +24,4 @@ jobs: age: '2 days' # ' ', e.g. 5 days, 2 years, 90 seconds, parsed by Moment.js # Optional inputs # skip-tags: true - # skip-recent: 5 \ No newline at end of file + # skip-recent: 5 diff --git a/.husky/pre-commit b/.husky/pre-commit index f841cf21..33832262 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,14 @@ #!/usr/bin/env sh npx lint-staged + +# Run Python pre-commit checks if backend files are staged +if git diff --cached --name-only | grep -q "^backend/"; then + echo "Running backend pre-commit checks..." + cd backend + # Get staged backend Python files, remove 'backend/' prefix for pre-commit + staged_files=$(git diff --cached --name-only | grep '^backend/.*\.py$' | sed 's|^backend/||') + if [ -n "$staged_files" ]; then + SKIP=no-commit-to-branch uv run pre-commit run --files $staged_files + fi + cd .. +fi diff --git a/.vscode/.debug.script.mjs b/.vscode/.debug.script.mjs index 9ca93363..e3674cb0 100644 --- a/.vscode/.debug.script.mjs +++ b/.vscode/.debug.script.mjs @@ -20,4 +20,4 @@ spawn( stdio: 'inherit', env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), }, -) \ No newline at end of file +) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7277b491..adedecfa 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -67,4 +67,4 @@ "justMyCode": false } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 85d09cde..d8361f4a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,5 +1,5 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 + // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ @@ -28,4 +28,4 @@ } } ] -} \ No newline at end of file +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5d71860..aaf5c8c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,10 +39,6 @@ We're thrilled to have your support. opening your pull request; this will allow the PR to pass all tests that require [GitHub Secrets][gh-secrets]. -[fork-pr]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects -[checkout-pr]: https://dev.to/ceceliacreates/how-to-create-a-pull-request-on-github-16h1 -[gh-secrets]: https://docs.github.com/en/actions/security-guides/encrypted-secrets - Make sure to mention any related issues and tag the relevant maintainers too. 💪 @@ -105,8 +101,9 @@ our coding standards. - Readability: Is the code easy to read and understand? Is it well-commented where necessary? - Maintainability: Is the code structured in a way that makes future changes easy? - Style: Does the code follow the project’s style guidelines? - Currently we use Ruff for format check and take [Google Python Style Guide]("https://google.github.io/styleguide/pyguide.html") as reference. + Currently we use Ruff for format check and take [Google Python Style Guide](%22https://google.github.io/styleguide/pyguide.html%22) as reference. - Documentation: Are public methods, classes, and any complex logic well-documented? + - Design - Consistency: Does the code follow established design patterns and project architecture? - Modularity: Are the changes modular and self-contained? Does the code avoid unnecessary duplication? @@ -243,7 +240,7 @@ To run the application locally in developer mode: 1. Configure `.env.development`: - Set `VITE_USE_LOCAL_PROXY=true` - Set `VITE_PROXY_URL=http://localhost:3001` -2. Go to the settings to specify your model key and model type. +1. Go to the settings to specify your model key and model type. ## Common Actions 🔄 @@ -256,3 +253,7 @@ Whenever you add, update, or delete any dependencies in `pyproject.toml`, please If your contribution has been included in a release, we'd love to give you credit on Twitter, but only if you're comfortable with it! If you have a Twitter account that you would like us to mention, please let us know either in the pull request or through another communication method. We want to make sure you receive proper recognition for your valuable contributions. 😄 + +[checkout-pr]: https://dev.to/ceceliacreates/how-to-create-a-pull-request-on-github-16h1 +[fork-pr]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects +[gh-secrets]: https://docs.github.com/en/actions/security-guides/encrypted-secrets diff --git a/README.md b/README.md index 97458347..65b33bfe 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr - [🏠 Local Deployment (Recommended)](#-local-deployment-recommended) - [⚡ Quick Start (Cloud-Connected)](#-quick-start-cloud-connected) - [🏢 Enterprise](#-enterprise) - - [☁️ Cloud Version](#️-cloud-version) + - [☁️ Cloud Version](#%EF%B8%8F-cloud-version) - [✨ Key features - Open Source Cowork](#-key-features---open-source-Cowork) - [🏭 Workforce](#-workforce) - [🧠 Comprehensive Model Support](#-comprehensive-model-support) @@ -67,7 +67,7 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr - [Backend](#backend) - [Frontend](#frontend) - [🌟 Staying ahead - Open Source Cowork](#-staying-ahead---open-source-Cowork) -- [🗺️ Roadmap - Open Source Cowork](#️-roadmap---open-source-Cowork) +- [🗺️ Roadmap - Open Source Cowork](#%EF%B8%8F-roadmap---open-source-Cowork) - [📖 Contributing](#-contributing) - [Main Contributors](#main-contributors) - [Distinguished amabssador](#distinguished-amabssador) @@ -309,9 +309,9 @@ Eigent open-source Cowork desktop is built on modern, reliable technologies that ## 🌟 Staying ahead - Open Source Cowork -> \[!IMPORTANT] +> [!IMPORTANT] > -> **Star Eigent**, You will receive all release notifications from GitHub without any delay \~ ⭐️ +> **Star Eigent**, You will receive all release notifications from GitHub without any delay ~ ⭐️ ![][image-star-us] @@ -368,51 +368,42 @@ For more information please contact info@eigent.ai - -[discord-url]: https://discord.com/invite/CNcNpquyDc -[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb -[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= -[eigent-github]: https://github.com/eigent-ai/eigent -[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github -[camel-ai-org-github]: https://github.com/camel-ai -[camel-github]: https://github.com/camel-ai/camel -[eigent-github]: https://github.com/eigent-ai/eigent -[contribution-link]: https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md -[social-x-link]: https://x.com/Eigent_AI -[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic -[reddit-url]: https://www.reddit.com/r/CamelAI/ -[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white -[wechat-url]: https://ghli.org/camel/wechat.png -[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white -[sponsor-link]: https://github.com/sponsors/camel-ai -[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic -[eigent-download]: https://www.eigent.ai/download -[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic -[join-us]: https://eigent-ai.notion.site/eigent-ai-careers -[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic + -[camel-site]: https://www.camel-ai.org -[eigent-site]: https://www.eigent.ai -[docs-site]: https://docs.eigent.ai -[github-issue-link]: https://github.com/eigent-ai/eigent/issues - -[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png -[image-head]: https://eigent-ai.github.io/.github/assets/head.png -[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png -[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif -[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png -[image-wechat]: https://eigent-ai.github.io/.github/assets/wechat.png -[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png - -[image-workforce]: https://eigent-ai.github.io/.github/assets/feature_dynamic_workforce.gif -[image-human-in-the-loop]: https://eigent-ai.github.io/.github/assets/feature_human_in_the_loop.gif -[image-customise-workers]: https://eigent-ai.github.io/.github/assets/feature_customise_workers.gif -[image-add-mcps]: https://eigent-ai.github.io/.github/assets/feature_add_mcps.gif -[image-local-model]: https://eigent-ai.github.io/.github/assets/feature_local_model.gif +[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= +[camel-ai-org-github]: https://github.com/camel-ai +[camel-github]: https://github.com/camel-ai/camel +[camel-site]: https://www.camel-ai.org +[contribution-link]: https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md +[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb +[discord-url]: https://discord.com/invite/CNcNpquyDc +[docs-site]: https://docs.eigent.ai +[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic +[eigent-download]: https://www.eigent.ai/download +[eigent-github]: https://github.com/eigent-ai/eigent +[eigent-site]: https://www.eigent.ai +[github-issue-link]: https://github.com/eigent-ai/eigent/issues +[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github +[image-head]: https://eigent-ai.github.io/.github/assets/head.png +[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png +[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png +[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png +[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png +[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif +[join-us]: https://eigent-ai.notion.site/eigent-ai-careers +[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic +[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white +[reddit-url]: https://www.reddit.com/r/CamelAI/ +[social-x-link]: https://x.com/Eigent_AI +[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic +[sponsor-link]: https://github.com/sponsors/camel-ai +[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic +[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white +[wechat-url]: https://ghli.org/camel/wechat.png diff --git a/README_CN.md b/README_CN.md index f02302e4..a8eb2659 100644 --- a/README_CN.md +++ b/README_CN.md @@ -51,28 +51,28 @@ #### 目录 -- [🚀 快速开始 - 开源 Cowork](#-快速开始---开源 Cowork) - - [☁️ 云版本](#️-云版本) - - [🏠 自托管(社区版)](#-自托管社区版) - - [🏢 企业版](#-企业版) -- [✨ 核心功能 - 开源 Cowork](#-核心功能---开源 Cowork) - - [🏭 工作流](#-工作流) - - [🧠 全面模型支持](#-全面模型支持) - - [🔌 MCP 工具集成](#-mcp-工具集成) - - [✋ 人工介入](#-人工介入) - - [👐 100% 开源](#-100-开源) -- [🧩 使用案例 - 开源 Cowork](#-使用案例---开源 Cowork) -- [🛠️ 技术栈](#️-技术栈) - - [后端](#后端) - - [前端](#前端) -- [🌟 保持领先 - 开源 Cowork](#保持领先---开源 Cowork) -- [🗺️ 路线图 - 开源 Cowork](#️-路线图---开源 Cowork) -- [📖 贡献](#-贡献) - - [核心贡献者](#核心贡献者) - - [杰出大使](#杰出大使) -- [生态系统](#生态系统) -- [📄 开源许可证](#-开源许可证) -- [🌐 社区与联系](#-社区与联系) +- \[🚀 快速开始 - 开源 Cowork\](#-快速开始---开源 Cowork) + - [☁️ 云版本](#%EF%B8%8F-%E4%BA%91%E7%89%88%E6%9C%AC) + - [🏠 自托管(社区版)](#-%E8%87%AA%E6%89%98%E7%AE%A1%E7%A4%BE%E5%8C%BA%E7%89%88) + - [🏢 企业版](#-%E4%BC%81%E4%B8%9A%E7%89%88) +- \[✨ 核心功能 - 开源 Cowork\](#-核心功能---开源 Cowork) + - [🏭 工作流](#-%E5%B7%A5%E4%BD%9C%E6%B5%81) + - [🧠 全面模型支持](#-%E5%85%A8%E9%9D%A2%E6%A8%A1%E5%9E%8B%E6%94%AF%E6%8C%81) + - [🔌 MCP 工具集成](#-mcp-%E5%B7%A5%E5%85%B7%E9%9B%86%E6%88%90) + - [✋ 人工介入](#-%E4%BA%BA%E5%B7%A5%E4%BB%8B%E5%85%A5) + - [👐 100% 开源](#-100-%E5%BC%80%E6%BA%90) +- \[🧩 使用案例 - 开源 Cowork\](#-使用案例---开源 Cowork) +- [🛠️ 技术栈](#%EF%B8%8F-%E6%8A%80%E6%9C%AF%E6%A0%88) + - [后端](#%E5%90%8E%E7%AB%AF) + - [前端](#%E5%89%8D%E7%AB%AF) +- \[🌟 保持领先 - 开源 Cowork\](#保持领先---开源 Cowork) +- \[🗺️ 路线图 - 开源 Cowork\](#️-路线图---开源 Cowork) +- [📖 贡献](#-%E8%B4%A1%E7%8C%AE) + - [核心贡献者](#%E6%A0%B8%E5%BF%83%E8%B4%A1%E7%8C%AE%E8%80%85) + - [杰出大使](#%E6%9D%B0%E5%87%BA%E5%A4%A7%E4%BD%BF) +- [生态系统](#%E7%94%9F%E6%80%81%E7%B3%BB%E7%BB%9F) +- [📄 开源许可证](#-%E5%BC%80%E6%BA%90%E8%AE%B8%E5%8F%AF%E8%AF%81) +- [🌐 社区与联系](#-%E7%A4%BE%E5%8C%BA%E4%B8%8E%E8%81%94%E7%B3%BB) #### @@ -297,7 +297,7 @@ Eigent 开源 Cowork桌面应用基于现代、可靠的技术构建,确保可 ## 🌟 保持领先 - 开源 Cowork -> \[!重要] +> [!重要] > > **给 Eigent 加星标**,您将通过 GitHub 及时收到所有发布通知 ⭐️ @@ -356,51 +356,42 @@ Eigent 基于 [CAMEL-AI.org][camel-ai-org-github] 的研究和基础设施构建 - -[discord-url]: https://discord.com/invite/CNcNpquyDc -[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb -[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= -[eigent-github]: https://github.com/eigent-ai/eigent -[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github -[camel-ai-org-github]: https://github.com/camel-ai -[camel-github]: https://github.com/camel-ai/camel -[eigent-github]: https://github.com/eigent-ai/eigent -[contribution-link]: https:/github.com/eigent-ai/eigent/blob/master/CONTRIBUTING.md -[social-x-link]: https://x.com/Eigent_AI -[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic -[reddit-url]: https://www.reddit.com/r/CamelAI/ -[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white -[wechat-url]: https://ghli.org/camel/wechat.png -[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white -[sponsor-link]: https://github.com/sponsors/camel-ai -[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic -[eigent-download]: https://www.eigent.ai/download -[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic -[join-us]: https://eigent-ai.notion.site/eigent-ai-careers -[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic + -[camel-site]: https://www.camel-ai.org -[eigent-site]: https://www.eigent.ai -[docs-site]: https://docs.eigent.ai -[github-issue-link]: https://github.com/eigent-ai/eigent/issues - -[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png -[image-head]: https://eigent-ai.github.io/.github/assets/head.png -[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png -[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif -[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png -[image-wechat]: https://eigent-ai.github.io/.github/assets/wechat.png -[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png - -[image-workforce]: https://eigent-ai.github.io/.github/assets/feature_dynamic_workforce.gif -[image-human-in-the-loop]: https://eigent-ai.github.io/.github/assets/feature_human_in_the_loop.gif -[image-customise-workers]: https://eigent-ai.github.io/.github/assets/feature_customise_workers.gif -[image-add-mcps]: https://eigent-ai.github.io/.github/assets/feature_add_mcps.gif -[image-local-model]: https://eigent-ai.github.io/.github/assets/feature_local_model.gif +[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= +[camel-ai-org-github]: https://github.com/camel-ai +[camel-github]: https://github.com/camel-ai/camel +[camel-site]: https://www.camel-ai.org +[contribution-link]: https:/github.com/eigent-ai/eigent/blob/master/CONTRIBUTING.md +[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb +[discord-url]: https://discord.com/invite/CNcNpquyDc +[docs-site]: https://docs.eigent.ai +[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic +[eigent-download]: https://www.eigent.ai/download +[eigent-github]: https://github.com/eigent-ai/eigent +[eigent-site]: https://www.eigent.ai +[github-issue-link]: https://github.com/eigent-ai/eigent/issues +[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github +[image-head]: https://eigent-ai.github.io/.github/assets/head.png +[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png +[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png +[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png +[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png +[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif +[join-us]: https://eigent-ai.notion.site/eigent-ai-careers +[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic +[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white +[reddit-url]: https://www.reddit.com/r/CamelAI/ +[social-x-link]: https://x.com/Eigent_AI +[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic +[sponsor-link]: https://github.com/sponsors/camel-ai +[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic +[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white +[wechat-url]: https://ghli.org/camel/wechat.png diff --git a/README_JA.md b/README_JA.md index 15bb44fe..740cc4df 100644 --- a/README_JA.md +++ b/README_JA.md @@ -51,27 +51,27 @@ #### TOC -- [🚀 はじめに - オープンソース Cowork](#-はじめに---オープンソース Cowork) - - [🏠 ローカルデプロイメント(推奨)](#-ローカルデプロイメント推奨) - - [⚡ クイックスタート(クラウド接続)](#-クイックスタートクラウド接続) - - [🏢 エンタープライズ](#-エンタープライズ) - - [☁️ クラウドバージョン](#️-クラウドバージョン) -- [✨ 主な機能 - オープンソース Cowork](#-主な機能---オープンソース Cowork) - - [🏭 ワークフォース](#-ワークフォース) - - [🧠 包括的なモデルサポート](#-包括的なモデルサポート) - - [🔌 MCPツール統合](#-mcpツール統合) - - [✋ ヒューマンインザループ](#-ヒューマンインザループ) - - [👐 100%オープンソース](#-100オープンソース) -- [🧩 ユースケース - オープンソース Cowork](#-ユースケース---オープンソース Cowork) -- [🛠️ 技術スタック](#️-技術スタック) - - [バックエンド](#バックエンド) - - [フロントエンド](#フロントエンド) -- [🌟 最新情報を入手 - オープンソース Cowork](#最新情報を入手---オープンソース Cowork) -- [🗺️ ロードマップ - オープンソース Cowork](#️-ロードマップ---オープンソース Cowork) -- [📖 コントリビューション](#-コントリビューション) -- [エコシステム](#エコシステム) -- [📄 オープンソースライセンス](#-オープンソースライセンス) -- [🌐 コミュニティ & お問い合わせ](#-コミュニティ--お問い合わせ) +- \[🚀 はじめに - オープンソース Cowork\](#-はじめに---オープンソース Cowork) + - [🏠 ローカルデプロイメント(推奨)](#-%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%83%A1%E3%83%B3%E3%83%88%E6%8E%A8%E5%A5%A8) + - [⚡ クイックスタート(クラウド接続)](#-%E3%82%AF%E3%82%A4%E3%83%83%E3%82%AF%E3%82%B9%E3%82%BF%E3%83%BC%E3%83%88%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E6%8E%A5%E7%B6%9A) + - [🏢 エンタープライズ](#-%E3%82%A8%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%97%E3%83%A9%E3%82%A4%E3%82%BA) + - [☁️ クラウドバージョン](#%EF%B8%8F-%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3) +- \[✨ 主な機能 - オープンソース Cowork\](#-主な機能---オープンソース Cowork) + - [🏭 ワークフォース](#-%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B9) + - [🧠 包括的なモデルサポート](#-%E5%8C%85%E6%8B%AC%E7%9A%84%E3%81%AA%E3%83%A2%E3%83%87%E3%83%AB%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88) + - [🔌 MCPツール統合](#-mcp%E3%83%84%E3%83%BC%E3%83%AB%E7%B5%B1%E5%90%88) + - [✋ ヒューマンインザループ](#-%E3%83%92%E3%83%A5%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%A4%E3%83%B3%E3%82%B6%E3%83%AB%E3%83%BC%E3%83%97) + - [👐 100%オープンソース](#-100%E3%82%AA%E3%83%BC%E3%83%97%E3%83%B3%E3%82%BD%E3%83%BC%E3%82%B9) +- \[🧩 ユースケース - オープンソース Cowork\](#-ユースケース---オープンソース Cowork) +- [🛠️ 技術スタック](#%EF%B8%8F-%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF) + - [バックエンド](#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89) + - [フロントエンド](#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89) +- \[🌟 最新情報を入手 - オープンソース Cowork\](#最新情報を入手---オープンソース Cowork) +- \[🗺️ ロードマップ - オープンソース Cowork\](#️-ロードマップ---オープンソース Cowork) +- [📖 コントリビューション](#-%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3) +- [エコシステム](#%E3%82%A8%E3%82%B3%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0) +- [📄 オープンソースライセンス](#-%E3%82%AA%E3%83%BC%E3%83%97%E3%83%B3%E3%82%BD%E3%83%BC%E3%82%B9%E3%83%A9%E3%82%A4%E3%82%BB%E3%83%B3%E3%82%B9) +- [🌐 コミュニティ & お問い合わせ](#-%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3--%E3%81%8A%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B) #### @@ -113,7 +113,7 @@ npm install npm run dev ``` -> 注:このモードはEigentクラウドサービスに接続し、アカウント登録が必要です。完全にスタンドアロンで使用する場合は、代わりに[ローカルデプロイメント](#-ローカルデプロイメント推奨)を使用してください。 +> 注:このモードはEigentクラウドサービスに接続し、アカウント登録が必要です。完全にスタンドアロンで使用する場合は、代わりに[ローカルデプロイメント](#-%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%83%A1%E3%83%B3%E3%83%88%E6%8E%A8%E5%A5%A8)を使用してください。 #### 依存関係の更新 @@ -183,7 +183,7 @@ Eigentは以下のエージェントワーカーを事前定義しています ### 🔌 MCPツール統合 -Eigentには大規模な組み込み**Model Context Protocol(MCP)**ツール(ウェブブラウジング、コード実行、Notion、Google suite、Slackなど)が付属しており、**独自のツールをインストール**することもできます。エージェントにシナリオに適したツールを装備させ、内部APIやカスタム関数を統合して機能を強化できます。 +Eigentには大規模な組み込み\*\*Model Context Protocol(MCP)\*\*ツール(ウェブブラウジング、コード実行、Notion、Google suite、Slackなど)が付属しており、**独自のツールをインストール**することもできます。エージェントにシナリオに適したツールを装備させ、内部APIやカスタム関数を統合して機能を強化できます。 ![MCP](https://eigent-ai.github.io/.github/assets/gif/feature_add_mcps.gif) @@ -307,7 +307,7 @@ Eigent オープンソース Coworkデスクトップは、スケーラビリテ ## 🌟 最新情報を入手 - オープンソース Cowork -> \[!IMPORTANT] +> [!IMPORTANT] > > **Eigentにスター**を付けると、GitHubからすべてのリリース通知を遅延なく受け取れます ⭐️ @@ -366,51 +366,42 @@ Eigentは[CAMEL-AI.org][camel-ai-org-github]の研究とインフラストラク - -[discord-url]: https://discord.com/invite/CNcNpquyDc -[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb -[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= -[eigent-github]: https://github.com/eigent-ai/eigent -[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github -[camel-ai-org-github]: https://github.com/camel-ai -[camel-github]: https://github.com/camel-ai/camel -[eigent-github]: https://github.com/eigent-ai/eigent -[contribution-link]: https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md -[social-x-link]: https://x.com/Eigent_AI -[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic -[reddit-url]: https://www.reddit.com/r/CamelAI/ -[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white -[wechat-url]: https://ghli.org/camel/wechat.png -[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white -[sponsor-link]: https://github.com/sponsors/camel-ai -[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic -[eigent-download]: https://www.eigent.ai/download -[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic -[join-us]: https://eigent-ai.notion.site/eigent-ai-careers -[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic + -[camel-site]: https://www.camel-ai.org -[eigent-site]: https://www.eigent.ai -[docs-site]: https://docs.eigent.ai -[github-issue-link]: https://github.com/eigent-ai/eigent/issues - -[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png -[image-head]: https://eigent-ai.github.io/.github/assets/head.png -[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png -[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif -[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png -[image-wechat]: https://eigent-ai.github.io/.github/assets/wechat.png -[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png - -[image-workforce]: https://eigent-ai.github.io/.github/assets/feature_dynamic_workforce.gif -[image-human-in-the-loop]: https://eigent-ai.github.io/.github/assets/feature_human_in_the_loop.gif -[image-customise-workers]: https://eigent-ai.github.io/.github/assets/feature_customise_workers.gif -[image-add-mcps]: https://eigent-ai.github.io/.github/assets/feature_add_mcps.gif -[image-local-model]: https://eigent-ai.github.io/.github/assets/feature_local_model.gif +[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= +[camel-ai-org-github]: https://github.com/camel-ai +[camel-github]: https://github.com/camel-ai/camel +[camel-site]: https://www.camel-ai.org +[contribution-link]: https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md +[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb +[discord-url]: https://discord.com/invite/CNcNpquyDc +[docs-site]: https://docs.eigent.ai +[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic +[eigent-download]: https://www.eigent.ai/download +[eigent-github]: https://github.com/eigent-ai/eigent +[eigent-site]: https://www.eigent.ai +[github-issue-link]: https://github.com/eigent-ai/eigent/issues +[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github +[image-head]: https://eigent-ai.github.io/.github/assets/head.png +[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png +[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png +[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png +[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png +[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif +[join-us]: https://eigent-ai.notion.site/eigent-ai-careers +[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic +[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white +[reddit-url]: https://www.reddit.com/r/CamelAI/ +[social-x-link]: https://x.com/Eigent_AI +[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic +[sponsor-link]: https://github.com/sponsors/camel-ai +[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic +[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white +[wechat-url]: https://ghli.org/camel/wechat.png diff --git a/README_PT-BR.md b/README_PT-BR.md index b8521032..326d527e 100644 --- a/README_PT-BR.md +++ b/README_PT-BR.md @@ -52,26 +52,26 @@ Construído sobre o aclamado projeto open source da [CAMEL-AI][camel-site], noss #### TOC - [🚀 Primeiros Passos com Cowork Open Source](#-primeiros-passos-com-Cowork-open-source) - - [🏠 Implantação Local (Recomendado)](#-implantação-local-recomendado) - - [⚡ Início Rápido (Conectado à Nuvem)](#-início-rápido-conectado-à-nuvem) + - [🏠 Implantação Local (Recomendado)](#-implanta%C3%A7%C3%A3o-local-recomendado) + - [⚡ Início Rápido (Conectado à Nuvem)](#-in%C3%ADcio-r%C3%A1pido-conectado-%C3%A0-nuvem) - [🏢 Empresarial](#-empresarial) - - [☁️ Versão em Nuvem](#️-versão-em-nuvem) + - [☁️ Versão em Nuvem](#%EF%B8%8F-vers%C3%A3o-em-nuvem) - [✨ Principais Recursos - Cowork Open Source](#-principais-recursos---Cowork-open-source) - - [🏭 Força de Trabalho](#-força-de-trabalho) + - [🏭 Força de Trabalho](#-for%C3%A7a-de-trabalho) - [🧠 Suporte Abrangente a Modelos](#-suporte-abrangente-a-modelos) - - [🔌 Integração de Ferramentas MCP (MCP)](#-integração-de-ferramentas-mcp-mcp) + - [🔌 Integração de Ferramentas MCP (MCP)](#-integra%C3%A7%C3%A3o-de-ferramentas-mcp-mcp) - [✋ Humano no Circuito](#-humano-no-circuito) - - [👐 100% Código Aberto](#-100-código-aberto) + - [👐 100% Código Aberto](#-100-c%C3%B3digo-aberto) - [🧩 Casos de Uso - Cowork Open Source](#-casos-de-uso---Cowork-open-source) -- [🛠️ Stack Tecnológica](#-stack-tecnológica) +- [🛠️ Stack Tecnológica](#-stack-tecnol%C3%B3gica) - [Backend](#backend) - [Frontend](#frontend) -- [🌟 Mantendo-se à Frente - Cowork Open Source](#-mantendo-se-à-frente---Cowork-open-source) +- [🌟 Mantendo-se à Frente - Cowork Open Source](#-mantendo-se-%C3%A0-frente---Cowork-open-source) - [🗺️ Roadmap - Cowork Open Source](#-roadmap---Cowork-open-source) -- [🤝 Contribuição](#-contribuição) +- [🤝 Contribuição](#-contribui%C3%A7%C3%A3o) - [Contribuidores](#contribuidores) -- [❤️ Patrocínio](#-patrocínio) -- [📄 Licença Open Source](#-licença-open-source) +- [❤️ Patrocínio](#-patroc%C3%ADnio) +- [📄 Licença Open Source](#-licen%C3%A7a-open-source) - [🌐 Comunidade & Contato](#-comunidade--contato) #### @@ -114,7 +114,7 @@ npm install 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. +> 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%C3%A7%C3%A3o-local-recomendado) em vez disso. #### Atualizando Dependências @@ -214,69 +214,69 @@ Descubra como desenvolvedores em todo o mundo aproveitam as capacidades de Cowor
-Prompt: Somos dois fãs de tênis e queremos ir ver o torneio de tênis ... -
-Somos dois fãs de tênis e queremos ir ver o torneio de tênis em Palm Springs 2026. Eu moro em SF - por favor, prepare um itinerário detalhado com voos, hotéis, coisas para fazer por 3 dias - na época em que as semifinais/finais estão acontecendo. Gostamos de trilhas, comida vegana e spas. Nosso orçamento é de $5K. O itinerário deve ser uma linha do tempo detalhada de horário, atividade, custo, outros detalhes e, se aplicável, um link para comprar ingressos/fazer reservas etc. para o item. Algumas preferências. Acesso a spa seria bom, mas não necessário. Quando você terminar esta tarefa, por favor gere um relatório html sobre esta viagem; escreva um resumo deste plano e envie o resumo de texto e o link do relatório html para o canal slack #tennis-trip-sf. +Prompt: Somos dois fãs de tênis e queremos ir ver o torneio de tênis ... +
+Somos dois fãs de tênis e queremos ir ver o torneio de tênis em Palm Springs 2026. Eu moro em SF - por favor, prepare um itinerário detalhado com voos, hotéis, coisas para fazer por 3 dias - na época em que as semifinais/finais estão acontecendo. Gostamos de trilhas, comida vegana e spas. Nosso orçamento é de $5K. O itinerário deve ser uma linha do tempo detalhada de horário, atividade, custo, outros detalhes e, se aplicável, um link para comprar ingressos/fazer reservas etc. para o item. Algumas preferências. Acesso a spa seria bom, mas não necessário. Quando você terminar esta tarefa, por favor gere um relatório html sobre esta viagem; escreva um resumo deste plano e envie o resumo de texto e o link do relatório html para o canal slack #tennis-trip-sf.

### 2. Gerar Relatório do Q2 a partir de Dados Bancários em CSV [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTM1MjY4OTE4MDgtODczOSI.aIjJmQ.WTdoX9mATwrcBr_w53BmGEHPo8U__1753526891808-8739) -
-Prompt: Por favor, me ajude a preparar uma demonstração financeira do Q2 baseada no meu ... -
-Por favor, me ajude a preparar uma demonstração financeira do Q2 baseada no meu arquivo de registro de transferência bancária bank_transacation.csv na minha área de trabalho para um relatório html com gráfico para investidores sobre quanto gastamos. +
+Prompt: Por favor, me ajude a preparar uma demonstração financeira do Q2 baseada no meu ... +
+Por favor, me ajude a preparar uma demonstração financeira do Q2 baseada no meu arquivo de registro de transferência bancária bank_transacation.csv na minha área de trabalho para um relatório html com gráfico para investidores sobre quanto gastamos.

### 3. Automação de Relatório de Pesquisa de Mercado de Saúde do Reino Unido [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTMzOTM1NTg3OTctODcwNyI.aIey-Q.Jh9QXzYrRYarY0kz_qsgoj3ewX0__1753393558797-8707) -
-Prompt: Analise a indústria de saúde do Reino Unido para apoiar o planejamento ... -
-Analise a indústria de saúde do Reino Unido para apoiar o planejamento da minha próxima empresa. Forneça uma visão geral abrangente do mercado, incluindo tendências atuais, projeções de crescimento e regulamentações relevantes. Identifique as 5–10 principais oportunidades, lacunas ou segmentos mal atendidos dentro do mercado. Apresente todas as descobertas em um relatório HTML bem estruturado e profissional. Em seguida, envie uma mensagem para o canal slack #eigentr-product-test quando esta tarefa estiver concluída para alinhar o conteúdo do relatório com meus colegas de equipe. +
+Prompt: Analise a indústria de saúde do Reino Unido para apoiar o planejamento ... +
+Analise a indústria de saúde do Reino Unido para apoiar o planejamento da minha próxima empresa. Forneça uma visão geral abrangente do mercado, incluindo tendências atuais, projeções de crescimento e regulamentações relevantes. Identifique as 5–10 principais oportunidades, lacunas ou segmentos mal atendidos dentro do mercado. Apresente todas as descobertas em um relatório HTML bem estruturado e profissional. Em seguida, envie uma mensagem para o canal slack #eigentr-product-test quando esta tarefa estiver concluída para alinhar o conteúdo do relatório com meus colegas de equipe.

### 4. Viabilidade do Mercado Alemão de Skate Elétrico [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTM2NTI4MjY3ODctNjk2Ig.aIjGiA.t-qIXxk_BZ4ENqa-yVIm0wMVyXU__1753652826787-696) -
-Prompt: Somos uma empresa que produz skates elétricos de alto padrão ... -
-Somos uma empresa que produz skates elétricos de alto padrão e estamos considerando entrar no mercado alemão. Por favor, prepare um relatório detalhado de viabilidade de entrada no mercado. O relatório deve cobrir os seguintes aspectos: 1. Tamanho do Mercado & Regulamentações: Pesquise o tamanho do mercado, taxa de crescimento anual, principais players e participação de mercado de Veículos Elétricos Leves Pessoais (PLEVs) na Alemanha. Ao mesmo tempo, forneça um detalhamento e resumo das leis e regulamentações alemãs sobre o uso de skates elétricos em vias públicas, incluindo requisitos de certificação (como certificação ABE) e apólices de seguro. 2. Perfil do Consumidor: Analise o perfil dos potenciais consumidores alemães, incluindo idade, nível de renda, principais cenários de uso (deslocamento, lazer), fatores-chave de decisão de compra (preço, desempenho, marca, design) e os canais que normalmente utilizam para buscar informações (fóruns, redes sociais, lojas físicas). 3. Canais & Distribuição: Investigue as principais plataformas online de venda de eletrônicos na Alemanha (ex.: Amazon.de, MediaMarkt.de) e grandes redes físicas de artigos esportivos de alto padrão. Liste os 5 principais potenciais parceiros de distribuição online e offline e encontre, se possível, as informações de contato de seus departamentos de compras. 4. Custos & Precificação: Com base na estrutura de custos do produto no arquivo Product_Cost.csv na minha área de trabalho, e considerando taxas alfandegárias alemãs, Imposto sobre Valor Agregado (IVA), custos logísticos e de armazenagem, além de possíveis despesas de marketing, estime o Preço de Venda Sugerido ao Consumidor (MSRP) e analise sua competitividade no mercado. 5. Relatório Abrangente & Apresentação: Resuma todas as descobertas da pesquisa em um arquivo de relatório em HTML. O conteúdo deve incluir gráficos de dados, principais conclusões e uma recomendação final de estratégia de entrada no mercado (Recomendado / Não Recomendado / Recomendado com Condições). +
+Prompt: Somos uma empresa que produz skates elétricos de alto padrão ... +
+Somos uma empresa que produz skates elétricos de alto padrão e estamos considerando entrar no mercado alemão. Por favor, prepare um relatório detalhado de viabilidade de entrada no mercado. O relatório deve cobrir os seguintes aspectos: 1. Tamanho do Mercado & Regulamentações: Pesquise o tamanho do mercado, taxa de crescimento anual, principais players e participação de mercado de Veículos Elétricos Leves Pessoais (PLEVs) na Alemanha. Ao mesmo tempo, forneça um detalhamento e resumo das leis e regulamentações alemãs sobre o uso de skates elétricos em vias públicas, incluindo requisitos de certificação (como certificação ABE) e apólices de seguro. 2. Perfil do Consumidor: Analise o perfil dos potenciais consumidores alemães, incluindo idade, nível de renda, principais cenários de uso (deslocamento, lazer), fatores-chave de decisão de compra (preço, desempenho, marca, design) e os canais que normalmente utilizam para buscar informações (fóruns, redes sociais, lojas físicas). 3. Canais & Distribuição: Investigue as principais plataformas online de venda de eletrônicos na Alemanha (ex.: Amazon.de, MediaMarkt.de) e grandes redes físicas de artigos esportivos de alto padrão. Liste os 5 principais potenciais parceiros de distribuição online e offline e encontre, se possível, as informações de contato de seus departamentos de compras. 4. Custos & Precificação: Com base na estrutura de custos do produto no arquivo Product_Cost.csv na minha área de trabalho, e considerando taxas alfandegárias alemãs, Imposto sobre Valor Agregado (IVA), custos logísticos e de armazenagem, além de possíveis despesas de marketing, estime o Preço de Venda Sugerido ao Consumidor (MSRP) e analise sua competitividade no mercado. 5. Relatório Abrangente & Apresentação: Resuma todas as descobertas da pesquisa em um arquivo de relatório em HTML. O conteúdo deve incluir gráficos de dados, principais conclusões e uma recomendação final de estratégia de entrada no mercado (Recomendado / Não Recomendado / Recomendado com Condições).

### 5. Auditoria de SEO para Lançamento do Workforce Multiagent [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTM2OTk5NzExNDQtNTY5NiI.aIex0w.jc_NIPmfIf9e3zGt-oG9fbMi3K4__1753699971144-5696) -
-Prompt: Para apoiar o lançamento do nosso novo produto Workforce Multiagent ... -
-Para apoiar o lançamento do nosso novo produto Workforce Multiagent, por favor, execute uma auditoria completa de SEO no nosso site oficial (https://www.camel-ai.org/) e entregue um relatório detalhado de otimização com recomendações acionáveis. +
+Prompt: Para apoiar o lançamento do nosso novo produto Workforce Multiagent ... +
+Para apoiar o lançamento do nosso novo produto Workforce Multiagent, por favor, execute uma auditoria completa de SEO no nosso site oficial (https://www.camel-ai.org/) e entregue um relatório detalhado de otimização com recomendações acionáveis.

### 6. Identificar Arquivos Duplicados em Downloads [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTM3NjAzODgxNzEtMjQ4Ig.aIhKLQ.epOG--0Nj0o4Bqjtdqm9OZdaqRQ__1753760388171-248) -
-Prompt: Tenho uma pasta chamada mydocs dentro do diretório Documents ... -
-Tenho uma pasta chamada mydocs dentro do diretório Documents. Por favor, escaneie-a e identifique todos os arquivos que sejam duplicados exatos ou quase duplicados — incluindo aqueles com conteúdo, tamanho ou formato idênticos (mesmo que nomes ou extensões de arquivo sejam diferentes). Liste-os claramente, agrupados por similaridade. +
+Prompt: Tenho uma pasta chamada mydocs dentro do diretório Documents ... +
+Tenho uma pasta chamada mydocs dentro do diretório Documents. Por favor, escaneie-a e identifique todos os arquivos que sejam duplicados exatos ou quase duplicados — incluindo aqueles com conteúdo, tamanho ou formato idênticos (mesmo que nomes ou extensões de arquivo sejam diferentes). Liste-os claramente, agrupados por similaridade.

### 7. Adicionar Assinatura a PDF [Replay ▶️](https://www.eigent.ai/download?share_token=IjE3NTQwOTU0ODM0NTItNTY2MSI.aJCHrA.Mg5yPOFqj86H_GQvvRNditzepXc__1754095483452-5661) -
-Prompt: Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF ... -
-Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. Você pode instalar a ferramenta de linha de comando ‘tesseract’ (necessária para localização confiável das ‘Áreas de Assinatura’ via OCR) para ajudar a concluir esta tarefa. +
+Prompt: Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF ... +
+Por favor, adicione esta imagem de assinatura às áreas de assinatura no PDF. Você pode instalar a ferramenta de linha de comando ‘tesseract’ (necessária para localização confiável das ‘Áreas de Assinatura’ via OCR) para ajudar a concluir esta tarefa.

@@ -304,9 +304,9 @@ O desktop Eigent Cowork código aberto é construído com tecnologias modernas e ## 🌟 Mantendo-se à Frente - Cowork Open Source -> \[!IMPORTANT] +> [!IMPORTANT] > -> **Dê uma estrela no Eigent**, você receberá todas as notificações de lançamento do GitHub sem qualquer atraso \~ ⭐️ +> **Dê uma estrela no Eigent**, você receberá todas as notificações de lançamento do GitHub sem qualquer atraso ~ ⭐️ ![][image-star-us] @@ -363,50 +363,42 @@ Para mais informações, entre em contato pelo e-mail info@eigent.ai - -[discord-url]: https://discord.com/invite/CNcNpquyDc -[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb -[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= -[eigent-github]: https://github.com/eigent-ai/eigent -[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github -[camel-ai-org-github]: https://github.com/camel-ai -[camel-github]: https://github.com/camel-ai/camel -[contribution-link]: https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md -[social-x-link]: https://x.com/Eigent_AI -[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic -[reddit-url]: https://www.reddit.com/r/CamelAI/ -[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white -[wechat-url]: https://ghli.org/camel/wechat.png -[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white -[sponsor-link]: https://github.com/sponsors/camel-ai -[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic -[eigent-download]: https://www.eigent.ai/download -[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic -[join-us]: https://eigent-ai.notion.site/eigent-ai-careers -[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic + -[camel-site]: https://www.camel-ai.org -[eigent-site]: https://www.eigent.ai -[docs-site]: https://docs.eigent.ai -[github-issue-link]: https://github.com/eigent-ai/eigent/issues - -[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png -[image-head]: https://eigent-ai.github.io/.github/assets/head.png -[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png -[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif -[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png -[image-wechat]: https://eigent-ai.github.io/.github/assets/wechat.png -[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png - -[image-workforce]: https://eigent-ai.github.io/.github/assets/feature_dynamic_workforce.gif -[image-human-in-the-loop]: https://eigent-ai.github.io/.github/assets/feature_human_in_the_loop.gif -[image-customise-workers]: https://eigent-ai.github.io/.github/assets/feature_customise_workers.gif -[image-add-mcps]: https://eigent-ai.github.io/.github/assets/feature_add_mcps.gif -[image-local-model]: https://eigent-ai.github.io/.github/assets/feature_local_model.gif +[built-with-camel]: https://img.shields.io/badge/-Built--with--CAMEL-4C19E8.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQ4IiBoZWlnaHQ9IjI3MiIgdmlld0JveD0iMCAwIDI0OCAyNzIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik04LjgzMTE3IDE4LjU4NjVMMCAzMC44MjY3QzUuNDY2OTIgMzUuMDQzMiAxNS4xMzkxIDM4LjgyNTggMjQuODExNCAzNi4yOTU5QzMwLjY5ODggNDAuOTM0MSAzOS42NzAyIDQwLjIzMTMgNDQuMTU1OSA0MC4wOTA4QzQzLjQ1NSA0Ny4zOTk0IDQyLjQ3MzcgNzAuOTU1OCA0NC4xNTU5IDEwNi43MTJDNDUuODM4IDE0Mi40NjggNzEuNzcwOCAxNjYuODY4IDg0LjUyNjkgMTc0LjU5OEw3Ni4wMDAyIDIyMEw4NC41MjY5IDI3MkgxMDguOTE4TDk4LjAwMDIgMjIwTDEwOC45MTggMTc0LjU5OEwxMjkuOTQ0IDI3MkgxNTQuNzU2TDEzNC4xNSAxNzQuNTk4SDE4Ny4xMzdMMTY2LjUzMSAyNzJIMTkxLjc2M0wyMTIuMzY5IDE3NC41OThMMjI2IDIyMEwyMTIuMzY5IDI3MkgyMzcuNjAxTDI0OC4wMDEgMjIwTDIzNy4xOCAxNzQuNTk4QzIzOS4yODMgMTY5LjExNyAyNDAuNDAxIDE2Ni45NzYgMjQxLjgwNiAxNjEuMTA1QzI0OS4zNzUgMTI5LjQ4MSAyMzUuMDc3IDEwMy45MDEgMjI2LjY2NyA5NC40ODRMMjA2LjQ4MSA3My44MjNDMTk3LjY1IDY0Ljk2ODMgMTgyLjUxMSA2NC41NDY3IDE3Mi44MzkgNzIuNTU4MUMxNjUuNzI4IDc4LjQ0NzcgMTYxLjcwMSA3OC43NzI3IDE1NC43NTYgNzIuNTU4MUMxNTEuODEyIDcwLjAyODEgMTQ0LjUzNSA2MS40ODg5IDEzNC45OTEgNTMuNTgzN0MxMjUuMzE5IDQ1LjU3MjMgMTA4LjQ5NyA0OC45NDU1IDEwMi4xODkgNTUuNjkxOUw3My41OTMxIDg0LjM2NDRWNy42MjM0OUw3OS4xMjczIDBDNjAuOTA0MiAzLjY1NDMzIDIzLjgwMjEgOS41NjMwOSAxOS43NjUgMTAuNTc1MUMxNS43Mjc5IDExLjU4NyAxMC43OTM3IDE2LjMzNzcgOC44MzExNyAxOC41ODY1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTQzLjIwMzggMTguNzE4N0w0OS4wOTEyIDEzLjA0OTNMNTQuOTc4NyAxOC43MTg3TDQ5LjA5MTIgMjQuODI0Mkw0My4yMDM4IDE4LjcxODdaIiBmaWxsPSIjNEMxOUU4Ii8+Cjwvc3ZnPgo= +[camel-ai-org-github]: https://github.com/camel-ai +[camel-github]: https://github.com/camel-ai/camel +[camel-site]: https://www.camel-ai.org +[contribution-link]: https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md +[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb +[discord-url]: https://discord.com/invite/CNcNpquyDc +[docs-site]: https://docs.eigent.ai +[download-shield]: https://img.shields.io/badge/Download%20Eigent-363AF5?style=plastic +[eigent-download]: https://www.eigent.ai/download +[eigent-github]: https://github.com/eigent-ai/eigent +[eigent-site]: https://www.eigent.ai +[github-issue-link]: https://github.com/eigent-ai/eigent/issues +[github-star]: https://img.shields.io/github/stars/eigent-ai?color=F5F4F0&labelColor=gray&style=plastic&logo=github +[image-head]: https://eigent-ai.github.io/.github/assets/head.png +[image-join-us]: https://camel-ai.github.io/camel_asset/graphics/join_us.png +[image-opensource]: https://eigent-ai.github.io/.github/assets/opensource.png +[image-public-beta]: https://eigent-ai.github.io/.github/assets/banner.png +[image-seperator]: https://eigent-ai.github.io/.github/assets/seperator.png +[image-star-us]: https://eigent-ai.github.io/.github/assets/star-us.gif +[join-us]: https://eigent-ai.notion.site/eigent-ai-careers +[join-us-image]: https://img.shields.io/badge/Join%20Us-yellow?style=plastic +[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white +[reddit-url]: https://www.reddit.com/r/CamelAI/ +[social-x-link]: https://x.com/Eigent_AI +[social-x-shield]: https://img.shields.io/badge/-%40Eigent_AI-white?labelColor=gray&logo=x&logoColor=white&style=plastic +[sponsor-link]: https://github.com/sponsors/camel-ai +[sponsor-shield]: https://img.shields.io/badge/-Sponsor%20CAMEL--AI-1d1d1d?logo=github&logoColor=white&style=plastic +[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white +[wechat-url]: https://ghli.org/camel/wechat.png diff --git a/backend/.gitignore b/backend/.gitignore index cc8a07b5..e664232b 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -18,4 +18,4 @@ img uv_installing.lock -uv_installed.lock \ No newline at end of file +uv_installed.lock diff --git a/backend/.pre-commit-config.yaml b/backend/.pre-commit-config.yaml index ff741a85..c632b324 100644 --- a/backend/.pre-commit-config.yaml +++ b/backend/.pre-commit-config.yaml @@ -17,6 +17,7 @@ repos: args: [--fix, lf] - id: trailing-whitespace name: Remove trailing whitespaces + exclude: '\.md$' - id: check-toml name: Check toml - id: check-yaml @@ -27,54 +28,17 @@ repos: hooks: - id: yamllint name: Lint yaml - args: [-d, '{extends: default, rules: {line-length: disable, document-start: disable, truthy: {level: error}, braces: {max-spaces-inside: 1}}}'] - - - repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 - hooks: - - id: pyupgrade - name: Upgrade Python syntax - args: [--py38-plus] - - - repo: https://github.com/PyCQA/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - name: Remove unused imports and variables - args: [ - --remove-all-unused-imports, - --remove-unused-variables, - --remove-duplicate-keys, - --ignore-init-module-imports, - --in-place, - ] - - - repo: https://github.com/google/yapf - rev: v0.43.0 - hooks: - - id: yapf - name: Format code - additional_dependencies: [toml] - - - repo: https://github.com/pycqa/isort - rev: 7.0.0 - hooks: - - id: isort - name: Sort imports - - - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - - id: flake8 - name: Check PEP8 - additional_dependencies: [Flake8-pyproject] + args: [-d, '{extends: default, rules: {line-length: disable, document-start: disable, truthy: disable, braces: disable, brackets: disable, indentation: disable, comments: disable, comments-indentation: disable}}'] + # Ruff replaces: yapf, flake8, autoflake, pyupgrade, isort - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.3 hooks: - id: ruff - name: Ruff formatting - args: [--fix, --exit-non-zero-on-fix] + name: Ruff lint (auto-fix) + args: [--fix] + - id: ruff-format + name: Ruff format - repo: https://github.com/executablebooks/mdformat rev: 0.7.22 diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json index 18d80ff0..401d6326 100644 --- a/backend/.vscode/settings.json +++ b/backend/.vscode/settings.json @@ -7,4 +7,4 @@ "extendleft", "toolkits" ] -} \ No newline at end of file +} diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 4345ae6a..06f0364b 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -24,5 +24,5 @@ api.add_middleware( allow_origins=["*"], allow_credentials=True, allow_methods=["*"], - allow_headers=["*"] + allow_headers=["*"], ) diff --git a/backend/app/agent/agent_model.py b/backend/app/agent/agent_model.py index a203e319..1b1fe860 100644 --- a/backend/app/agent/agent_model.py +++ b/backend/app/agent/agent_model.py @@ -14,17 +14,18 @@ import logging import uuid -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from camel.messages import BaseMessage from camel.models import ModelFactory from camel.toolkits import FunctionTool, RegisteredAgentToolkit from camel.types import ModelPlatformType -from app.utils.event_loop_utils import _schedule_async_task from app.agent.listen_chat_agent import ListenChatAgent, logger from app.model.chat import AgentModelConfig, Chat from app.service.task import ActionCreateAgentData, Agents, get_task_lock +from app.utils.event_loop_utils import _schedule_async_task def agent_model( @@ -64,8 +65,9 @@ def agent_model( if custom_model_config and custom_model_config.has_custom_config(): for attr in config_attrs: - effective_config[attr] = getattr(custom_model_config, attr, - None) or getattr(options, attr) + effective_config[attr] = getattr( + custom_model_config, attr, None + ) or getattr(options, attr) extra_params = ( custom_model_config.extra_params or options.extra_params or {} ) diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index eed38e11..7fdff31c 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -42,8 +42,9 @@ def browser_agent(options: Chat): f"in directory: {working_directory}" ) message_integration = ToolkitMessageIntegration( - message_handler=HumanToolkit(options.project_id, Agents.browser_agent - ).send_message_to_user + message_handler=HumanToolkit( + options.project_id, Agents.browser_agent + ).send_message_to_user ) web_toolkit_custom = HybridBrowserToolkit( @@ -92,7 +93,7 @@ def browser_agent(options: Chat): note_toolkit = NoteTakingToolkit( options.project_id, Agents.browser_agent, - working_directory=working_directory + working_directory=working_directory, ) note_toolkit = message_integration.register_toolkits(note_toolkit) @@ -103,8 +104,9 @@ def browser_agent(options: Chat): search_tools = [] tools = [ - *HumanToolkit. - get_can_use_tools(options.project_id, Agents.browser_agent), + *HumanToolkit.get_can_use_tools( + options.project_id, Agents.browser_agent + ), *web_toolkit_custom.get_tools(), *terminal_toolkit, *note_toolkit.get_tools(), diff --git a/backend/app/agent/factory/developer.py b/backend/app/agent/factory/developer.py index 94a6c62f..c1012a19 100644 --- a/backend/app/agent/factory/developer.py +++ b/backend/app/agent/factory/developer.py @@ -71,8 +71,9 @@ async def developer_agent(options: Chat): terminal_toolkit = message_integration.register_toolkits(terminal_toolkit) tools = [ - *HumanToolkit. - get_can_use_tools(options.project_id, Agents.developer_agent), + *HumanToolkit.get_can_use_tools( + options.project_id, Agents.developer_agent + ), *note_toolkit.get_tools(), *web_deploy_toolkit.get_tools(), *terminal_toolkit.get_tools(), diff --git a/backend/app/agent/factory/document.py b/backend/app/agent/factory/document.py index b9b01c71..18a4705a 100644 --- a/backend/app/agent/factory/document.py +++ b/backend/app/agent/factory/document.py @@ -43,8 +43,9 @@ async def document_agent(options: Chat): ) message_integration = ToolkitMessageIntegration( - message_handler=HumanToolkit(options.project_id, Agents.task_agent - ).send_message_to_user + message_handler=HumanToolkit( + options.project_id, Agents.task_agent + ).send_message_to_user ) file_write_toolkit = FileToolkit( options.project_id, working_directory=working_directory @@ -64,7 +65,7 @@ async def document_agent(options: Chat): note_toolkit = NoteTakingToolkit( options.project_id, Agents.document_agent, - working_directory=working_directory + working_directory=working_directory, ) note_toolkit = message_integration.register_toolkits(note_toolkit) @@ -84,8 +85,9 @@ async def document_agent(options: Chat): tools = [ *file_write_toolkit.get_tools(), *pptx_toolkit.get_tools(), - *HumanToolkit. - get_can_use_tools(options.project_id, Agents.document_agent), + *HumanToolkit.get_can_use_tools( + options.project_id, Agents.document_agent + ), *mark_it_down_toolkit.get_tools(), *excel_toolkit.get_tools(), *note_toolkit.get_tools(), diff --git a/backend/app/agent/factory/mcp.py b/backend/app/agent/factory/mcp.py index c8f3c40e..c6139bec 100644 --- a/backend/app/agent/factory/mcp.py +++ b/backend/app/agent/factory/mcp.py @@ -43,8 +43,10 @@ async def mcp_agent(options: Chat): tool_names = [ ( tool.get_function_name() - if hasattr(tool, "get_function_name") else str(tool) - ) for tool in mcp_tools + if hasattr(tool, "get_function_name") + else str(tool) + ) + for tool in mcp_tools ] logger.debug(f"MCP tools: {tool_names}") tools = [*tools, *mcp_tools] @@ -61,10 +63,8 @@ async def mcp_agent(options: Chat): task_lock.put_queue( ActionCreateAgentData( data={ - "agent_name": - Agents.mcp_agent, - "agent_id": - agent_id, + "agent_name": Agents.mcp_agent, + "agent_id": agent_id, "tools": [ key for key in options.installed_mcp["mcpServers"].keys() @@ -85,7 +85,9 @@ async def mcp_agent(options: Chat): model_config_dict=( { "user": str(options.project_id), - } if options.is_cloud() else None + } + if options.is_cloud() + else None ), timeout=600, # 10 minutes **{ diff --git a/backend/app/agent/factory/multi_modal.py b/backend/app/agent/factory/multi_modal.py index 85bb973a..95e20103 100644 --- a/backend/app/agent/factory/multi_modal.py +++ b/backend/app/agent/factory/multi_modal.py @@ -78,8 +78,9 @@ def multi_modal_agent(options: Chat): tools = [ *video_download_toolkit.get_tools(), *image_analysis_toolkit.get_tools(), - *HumanToolkit. - get_can_use_tools(options.project_id, Agents.multi_modal_agent), + *HumanToolkit.get_can_use_tools( + options.project_id, Agents.multi_modal_agent + ), *terminal_toolkit.get_tools(), *note_toolkit.get_tools(), ] diff --git a/backend/app/agent/factory/social_media.py b/backend/app/agent/factory/social_media.py index 901cfb0f..4b32f423 100644 --- a/backend/app/agent/factory/social_media.py +++ b/backend/app/agent/factory/social_media.py @@ -52,11 +52,13 @@ async def social_media_agent(options: Chat): *RedditToolkit.get_can_use_tools(options.project_id), *await NotionMCPToolkit.get_can_use_tools(options.project_id), # *SlackToolkit.get_can_use_tools(options.project_id), - *await GoogleGmailMCPToolkit. - get_can_use_tools(options.project_id, options.get_bun_env()), + *await GoogleGmailMCPToolkit.get_can_use_tools( + options.project_id, options.get_bun_env() + ), *GoogleCalendarToolkit.get_can_use_tools(options.project_id), - *HumanToolkit. - get_can_use_tools(options.project_id, Agents.social_media_agent), + *HumanToolkit.get_can_use_tools( + options.project_id, Agents.social_media_agent + ), *TerminalToolkit( options.project_id, agent_name=Agents.social_media_agent, diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 3dffb595..137c7580 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -15,8 +15,9 @@ import asyncio import json import logging +from collections.abc import Callable from threading import Event -from typing import Any, Callable, Dict, List, Tuple +from typing import Any from camel.agents import ChatAgent from camel.agents._types import ToolCallRequest @@ -34,7 +35,6 @@ from camel.types import ModelPlatformType, ModelType from camel.types.agents import ToolCallingRecord from pydantic import BaseModel -from app.utils.event_loop_utils import _schedule_async_task from app.service.task import ( Action, ActionActivateAgentData, @@ -45,6 +45,7 @@ from app.service.task import ( get_task_lock, set_process_task, ) +from app.utils.event_loop_utils import _schedule_async_task # Logger for agent tracking logger = logging.getLogger("agent") @@ -59,27 +60,27 @@ class ListenChatAgent(ChatAgent): model: ( BaseModelBackend | ModelManager - | Tuple[str, str] + | tuple[str, str] | str | ModelType - | Tuple[ModelPlatformType, ModelType] - | List[BaseModelBackend] - | List[str] - | List[ModelType] - | List[Tuple[str, str]] - | List[Tuple[ModelPlatformType, ModelType]] + | tuple[ModelPlatformType, ModelType] + | list[BaseModelBackend] + | list[str] + | list[ModelType] + | list[tuple[str, str]] + | list[tuple[ModelPlatformType, ModelType]] | None ) = None, memory: AgentMemory | None = None, message_window_size: int | None = None, token_limit: int | None = None, output_language: str | None = None, - tools: List[FunctionTool | Callable[..., Any]] | None = None, - toolkits_to_register_agent: List[RegisteredAgentToolkit] | None = None, + tools: list[FunctionTool | Callable[..., Any]] | None = None, + toolkits_to_register_agent: list[RegisteredAgentToolkit] | None = None, external_tools: ( - List[FunctionTool | Callable[..., Any] | Dict[str, Any]] | None + list[FunctionTool | Callable[..., Any] | dict[str, Any]] | None ) = None, - response_terminators: List[ResponseTerminator] | None = None, + response_terminators: list[ResponseTerminator] | None = None, scheduling_strategy: str = "round_robin", max_iteration: int | None = None, agent_id: str | None = None, @@ -162,7 +163,9 @@ class ListenChatAgent(ChatAgent): return usage_info.get("total_tokens", 0) def _stream_chunks(self, response_gen): - """Generator that wraps a streaming response and sends chunks to frontend. + """Generator that wraps a streaming response. + + Sends chunks to frontend. Args: response_gen: The original streaming response generator @@ -171,7 +174,8 @@ class ListenChatAgent(ChatAgent): Each chunk from the original generator Returns: - Tuple of (accumulated_content, total_tokens) via StopIteration value + Tuple of (accumulated_content, total_tokens) via + StopIteration value """ accumulated_content = "" last_chunk = None @@ -187,7 +191,9 @@ class ListenChatAgent(ChatAgent): self._send_agent_deactivate(accumulated_content, total_tokens) async def _astream_chunks(self, response_gen): - """Async generator that wraps a streaming response and sends chunks to frontend. + """Async generator that wraps a streaming response. + + Sends chunks to frontend. Args: response_gen: The original async streaming response generator @@ -438,7 +444,7 @@ class ListenChatAgent(ChatAgent): task_lock = get_task_lock(self.api_task_id) toolkit_name = ( - getattr(tool, "_toolkit_name") + tool._toolkit_name if hasattr(tool, "_toolkit_name") else "mcp_toolkit" ) @@ -546,7 +552,8 @@ class ListenChatAgent(ChatAgent): if hasattr(tool, "_toolkit_name"): toolkit_name = tool._toolkit_name - # Method 2: For MCP tools, check if func has __self__ (the toolkit instance) + # Method 2: For MCP tools, check if func has __self__ + # (the toolkit instance) if ( not toolkit_name and hasattr(tool, "func") @@ -602,7 +609,7 @@ class ListenChatAgent(ChatAgent): # Try different invocation paths in order of preference if hasattr(tool, "func") and hasattr(tool.func, "async_call"): # Case: FunctionTool wrapping an MCP tool - # Check if the wrapped tool is sync to avoid run_in_executor + # Check if wrapped tool is sync to avoid run_in_executor if hasattr(tool, "is_async") and not tool.is_async: # Sync tool: call directly to preserve ContextVar result = tool(**args) @@ -620,7 +627,7 @@ class ListenChatAgent(ChatAgent): # Sync tool: call directly to preserve ContextVar # in same thread result = tool(**args) - # Handle case where synchronous call returns a coroutine + # Handle case where sync call returns a coroutine if asyncio.iscoroutine(result): result = await result else: @@ -638,7 +645,7 @@ class ListenChatAgent(ChatAgent): result = await tool(**args) else: - # Fallback: synchronous call - call directly in current context + # Fallback: sync call - call directly in current context # DO NOT use run_in_executor to preserve ContextVar result = tool(**args) # Handle case where synchronous call returns a coroutine diff --git a/backend/app/agent/tools.py b/backend/app/agent/tools.py index 957e06ed..4c0f7a1a 100644 --- a/backend/app/agent/tools.py +++ b/backend/app/agent/tools.py @@ -82,9 +82,11 @@ async def get_toolkits(tools: list[str], agent_name: str, api_task_id: str): toolkit: AbstractToolkit = toolkits[item] toolkit.agent_name = agent_name toolkit_tools = toolkit.get_can_use_tools(api_task_id) - toolkit_tools = await toolkit_tools if asyncio.iscoroutine( - toolkit_tools - ) else toolkit_tools + toolkit_tools = ( + await toolkit_tools + if asyncio.iscoroutine(toolkit_tools) + else toolkit_tools + ) res.extend(toolkit_tools) else: logger.warning(f"Toolkit {item} not found for agent {agent_name}") @@ -124,8 +126,10 @@ async def get_mcp_tools(mcp_server: McpServers): tool_names = [ ( tool.get_function_name() - if hasattr(tool, "get_function_name") else str(tool) - ) for tool in tools + if hasattr(tool, "get_function_name") + else str(tool) + ) + for tool in tools ] logging.debug(f"MCP tool names: {tool_names}") return tools diff --git a/backend/app/component/__init__.py b/backend/app/component/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/component/__init__.py +++ b/backend/app/component/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/component/babel.py b/backend/app/component/babel.py index 65584a2e..6dea770c 100644 --- a/backend/app/component/babel.py +++ b/backend/app/component/babel.py @@ -12,9 +12,10 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from fastapi_babel import BabelConfigs, Babel from pathlib import Path +from fastapi_babel import Babel, BabelConfigs + babel_configs = BabelConfigs( ROOT_DIR=Path(__file__).parent.parent, BABEL_DEFAULT_LOCALE="en_US", diff --git a/backend/app/component/code.py b/backend/app/component/code.py index 4d8c04ad..d4561371 100644 --- a/backend/app/component/code.py +++ b/backend/app/component/code.py @@ -22,7 +22,6 @@ token_expired = 12 # token expired token_invalid = 13 # token invalid token_blocked = 14 # token in block list - form_error = 100 # form error no_permission_error = 300 # no permission diff --git a/backend/app/component/environment.py b/backend/app/component/environment.py index 87482884..9b3666b4 100644 --- a/backend/app/component/environment.py +++ b/backend/app/component/environment.py @@ -1,12 +1,27 @@ -import logging -import importlib.util -import os -from pathlib import Path -from fastapi import APIRouter, FastAPI -from dotenv import load_dotenv +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + import importlib -from typing import Any, overload +import importlib.util +import logging +import os import threading +from pathlib import Path +from typing import Any, overload + +from dotenv import load_dotenv +from fastapi import APIRouter, FastAPI logger = logging.getLogger("env") @@ -25,7 +40,8 @@ def sanitize_env_path(env_path: str | None) -> str | None: """ Validate and sanitize user-provided environment file path. - Security: Ensures the path stays within ~/.eigent directory and ends with .env + Security: Ensures the path stays within ~/.eigent directory + and ends with .env to prevent path traversal attacks and unauthorized file access. Args: @@ -62,7 +78,7 @@ def sanitize_env_path(env_path: str | None) -> str | None: return None # Enforce .env file extension - if not resolved_path.name.endswith('.env'): + if not resolved_path.name.endswith(".env"): logger.warning( f"Security: Rejected env_path with invalid extension. " f"Path: {env_path}, must end with .env" @@ -90,10 +106,10 @@ def set_user_env_path(env_path: str | None = None): # Sanitize the path before any filesystem operations safe_env_path = sanitize_env_path(env_path) + exists_value = os.path.exists(safe_env_path) if safe_env_path else None logger.info( f"Setting user environment path: original={env_path}, " - f"sanitized={safe_env_path}, " - f"exists={safe_env_path and os.path.exists(safe_env_path) if safe_env_path else None}" + f"sanitized={safe_env_path}, exists={exists_value}" ) if safe_env_path and os.path.exists(safe_env_path): @@ -103,21 +119,27 @@ def set_user_env_path(env_path: str | None = None): logger.info(f"User-specific environment loaded: {safe_env_path}") else: # Clear thread-local env_path to fall back to global - if hasattr(_thread_local, 'env_path'): - delattr(_thread_local, 'env_path') + if hasattr(_thread_local, "env_path"): + delattr(_thread_local, "env_path") logger.info("Reset to default global environment") if env_path and not safe_env_path: - logger.warning(f"User environment path rejected by security validation: {env_path}") + logger.warning( + f"User environment path rejected by security " + f"validation: {env_path}" + ) elif safe_env_path and not os.path.exists(safe_env_path): - logger.warning(f"User environment path does not exist, falling back to global: {safe_env_path}") + logger.warning( + f"User environment path does not exist, " + f"falling back to global: {safe_env_path}" + ) def get_current_env_path() -> str: """ Get current environment path (either user-specific or default). """ - return getattr(_thread_local, 'env_path', default_env_path) + return getattr(_thread_local, "env_path", default_env_path) @overload @@ -141,34 +163,50 @@ def env(key: str, default=None): Security: Uses sanitized path stored in _thread_local.env_path which has already been validated by set_user_env_path. """ - # If we have a user-specific environment path, try to reload it to get latest values + # If we have a user-specific environment path, try to reload it + # to get latest values. # Note: _thread_local.env_path is already sanitized by set_user_env_path - if hasattr(_thread_local, 'env_path') and os.path.exists(_thread_local.env_path): + if hasattr(_thread_local, "env_path") and os.path.exists( + _thread_local.env_path + ): # Temporarily load user-specific env to get the latest value from dotenv import dotenv_values + user_env_values = dotenv_values(_thread_local.env_path) if key in user_env_values: value = user_env_values[key] or default - logger.debug(f"Environment variable retrieved from user-specific config: key={key}, env_path={_thread_local.env_path}, has_value={value is not None}") + logger.debug( + f"Environment variable retrieved from user-specific " + f"config: key={key}, env_path={_thread_local.env_path}, " + f"has_value={value is not None}" + ) return value # Fall back to global environment value = os.getenv(key, default) - logger.debug(f"Environment variable retrieved from global config: key={key}, has_value={value is not None}, using_default={value == default}") + logger.debug( + f"Environment variable retrieved from global config: key={key}, " + f"has_value={value is not None}, using_default={value == default}" + ) return value def env_or_fail(key: str): value = env(key) if value is None: - logger.warning(f"[ENVIRONMENT] can't get env config value for key: {key}") + logger.warning( + f"[ENVIRONMENT] can't get env config value for key: {key}" + ) raise Exception(f"can't get env config value for key: {key}") return value + def env_not_empty(key: str): value = env(key) if not value: - logger.warning(f"[ENVIRONMENT] env config value can't be empty for key: {key}") + logger.warning( + f"[ENVIRONMENT] env config value can't be empty for key: {key}" + ) raise Exception(f"env config value can't be empty for key: {key}") return value @@ -192,7 +230,8 @@ def auto_import(package: str): # Import all .py files in the folder for file in files: if file.endswith(".py") and not file.startswith("__"): - module_name = file[:-3] # Remove the .py extension from filename + # Remove the .py extension from filename + module_name = file[:-3] importlib.import_module(package + "." + module_name) @@ -210,7 +249,9 @@ def auto_include_routers(api: FastAPI, prefix: str, directory: str): # Traverse all .py files in the directory for root, _, files in os.walk(dir_path): for file_name in files: - if file_name.endswith("_controller.py") and not file_name.startswith("__"): + if file_name.endswith( + "_controller.py" + ) and not file_name.startswith("__"): # Construct complete file path file_path = Path(root) / file_name @@ -218,13 +259,16 @@ def auto_include_routers(api: FastAPI, prefix: str, directory: str): module_name = file_path.stem # Load module using importlib - spec = importlib.util.spec_from_file_location(module_name, file_path) + spec = importlib.util.spec_from_file_location( + module_name, file_path + ) if spec is None or spec.loader is None: continue module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - # Check if router attribute exists in module and is an APIRouter instance + # Check if router attribute exists in module + # and is an APIRouter instance router = getattr(module, "router", None) if isinstance(router, APIRouter): api.include_router(router, prefix=prefix) diff --git a/backend/app/component/error_format.py b/backend/app/component/error_format.py index e291fb3b..38b4cb21 100644 --- a/backend/app/component/error_format.py +++ b/backend/app/component/error_format.py @@ -16,7 +16,9 @@ import json import re -def normalize_error_to_openai_format(exception: Exception) -> tuple[str, str | None, dict | None]: +def normalize_error_to_openai_format( + exception: Exception, +) -> tuple[str, str | None, dict | None]: """ Normalize error to OpenAI-style error structure. @@ -55,7 +57,12 @@ def normalize_error_to_openai_format(exception: Exception) -> tuple[str, str | N # Heuristics if not parsed if error_obj is None: lower = raw_msg.lower() - if "invalid_api_key" in lower or "incorrect api key" in lower or "unauthorized" in lower or " 401" in lower: + if ( + "invalid_api_key" in lower + or "incorrect api key" in lower + or "unauthorized" in lower + or " 401" in lower + ): error_code = "invalid_api_key" message = "Invalid key. Validation failed." error_obj = { @@ -64,7 +71,11 @@ def normalize_error_to_openai_format(exception: Exception) -> tuple[str, str | N "param": None, "code": "invalid_api_key", } - elif "model_not_found" in lower or "does not exist" in lower or " 404" in lower: + elif ( + "model_not_found" in lower + or "does not exist" in lower + or " 404" in lower + ): error_code = "model_not_found" message = "Invalid model name. Validation failed." error_obj = { @@ -73,9 +84,16 @@ def normalize_error_to_openai_format(exception: Exception) -> tuple[str, str | N "param": None, "code": "model_not_found", } - elif "insufficient_quota" in lower or "quota" in lower or " 429" in lower: + elif ( + "insufficient_quota" in lower + or "quota" in lower + or " 429" in lower + ): error_code = "insufficient_quota" - message = "You exceeded your current quota, please check your plan and billing details." + message = ( + "You exceeded your current quota, " + "please check your plan and billing details." + ) error_obj = { "message": message, "type": "insufficient_quota", diff --git a/backend/app/component/model_validation.py b/backend/app/component/model_validation.py index 6c2b270a..fb855a94 100644 --- a/backend/app/component/model_validation.py +++ b/backend/app/component/model_validation.py @@ -14,7 +14,6 @@ from camel.agents import ChatAgent from camel.models import ModelFactory -from camel.types import ModelPlatformType, ModelType def get_website_content(url: str) -> str: @@ -26,11 +25,16 @@ def get_website_content(url: str) -> str: Returns: str: The content of the website. """ - return f"Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" + return "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" def create_agent( - model_platform: str, model_type: str, api_key: str = None, url: str = None, model_config_dict: dict = None, **kwargs + model_platform: str, + model_type: str, + api_key: str = None, + url: str = None, + model_config_dict: dict = None, + **kwargs, ) -> ChatAgent: platform = model_platform mtype = model_type diff --git a/backend/app/component/pydantic/__init__.py b/backend/app/component/pydantic/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/component/pydantic/__init__.py +++ b/backend/app/component/pydantic/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/component/pydantic/i18n.py b/backend/app/component/pydantic/i18n.py index 077bd12d..738a88fe 100644 --- a/backend/app/component/pydantic/i18n.py +++ b/backend/app/component/pydantic/i18n.py @@ -12,12 +12,15 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import os +import re from pathlib import Path -from app.component.babel import babel_configs, babel -import re, os -from fastapi_babel.middleware import Babel, LANGUAGES_PATTERN + +from fastapi_babel.middleware import LANGUAGES_PATTERN from pydantic_i18n import JsonLoader, PydanticI18n +from app.component.babel import babel, babel_configs + def get_language(lang_code: str | None = None): """Ported from fastapi_babel.middleware.BabelMiddleware.get_language @@ -40,7 +43,10 @@ def get_language(lang_code: str | None = None): matches = re.finditer(LANGUAGES_PATTERN, lang_code) languages = [ - (f"{m.group(1)}{f'_{m.group(2)}' if m.group(2) else ''}", m.group(3) or "") + ( + f"{m.group(1)}{f'_{m.group(2)}' if m.group(2) else ''}", + m.group(3) or "", + ) for m in matches ] languages = sorted( @@ -64,7 +70,9 @@ def get_language(lang_code: str | None = None): # Return language with explicit priority or default value return ( - explicit_priority if explicit_priority else babel_configs.BABEL_DEFAULT_LOCALE + explicit_priority + if explicit_priority + else babel_configs.BABEL_DEFAULT_LOCALE ) diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index a379a636..30cb13bd 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -78,7 +78,7 @@ async def _cleanup_task_lock_safe(task_lock, reason: str) -> bool: 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} + extra={"task_id": task_lock.id}, ) return False @@ -87,17 +87,14 @@ async def _cleanup_task_lock_safe(task_lock, reason: str) -> bool: await delete_task_lock(task_lock.id) chat_logger.info( f"[{reason}] Task lock cleanup completed", - extra={"task_id": task_lock.id} + 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 + extra={"task_id": task_lock.id, "error": str(e)}, + exc_info=True, ) return False @@ -105,7 +102,7 @@ async def _cleanup_task_lock_safe(task_lock, reason: str) -> bool: async def timeout_stream_wrapper( stream_generator, timeout_seconds: int = SSE_TIMEOUT_SECONDS, - task_lock=None + task_lock=None, ): """Wraps a stream generator with timeout handling. @@ -130,16 +127,16 @@ async def timeout_stream_wrapper( except asyncio.TimeoutError: chat_logger.warning( "SSE timeout: No data received, closing connection", - extra={"timeout_seconds": timeout_seconds} + extra={"timeout_seconds": timeout_seconds}, ) timeout_min = timeout_seconds // 60 yield sse_json( - "error", { - "message": - "Connection timeout: No data" + "error", + { + "message": "Connection timeout: No data" f" received for {timeout_min}" " minutes" - } + }, ) cleanup_triggered = await _cleanup_task_lock_safe( task_lock, "TIMEOUT" @@ -159,7 +156,7 @@ async def timeout_stream_wrapper( chat_logger.error( "[STREAM-ERROR] Unexpected error in stream wrapper", extra={"error": str(e)}, - exc_info=True + exc_info=True, ) if not cleanup_triggered: await _cleanup_task_lock_safe(task_lock, "ERROR") @@ -173,8 +170,8 @@ async def post(data: Chat, request: Request): extra={ "project_id": data.project_id, "task_id": data.task_id, - "user": data.email - } + "user": data.email, + }, ) task_lock = get_or_create_task_lock(data.project_id) @@ -189,8 +186,9 @@ async def post(data: Chat, request: Request): os.environ["file_save_path"] = data.file_save_path() os.environ["browser_port"] = str(data.browser_port) os.environ["OPENAI_API_KEY"] = data.api_key - os.environ["OPENAI_API_BASE_URL" - ] = data.api_url or "https://api.openai.com/v1" + os.environ["OPENAI_API_BASE_URL"] = ( + data.api_url or "https://api.openai.com/v1" + ) os.environ["CAMEL_MODEL_LOG_ENABLED"] = "true" # Set user-specific search engine configuration if provided @@ -200,15 +198,19 @@ async def post(data: Chat, request: Request): os.environ[key] = value chat_logger.debug( f"Set search config: {key}", - extra={"project_id": data.project_id} + extra={"project_id": data.project_id}, ) - email_sanitized = re.sub(r'[\\/*?:"<>|\s]', "_", - data.email.split("@")[0]).strip(".") + email_sanitized = re.sub( + r'[\\/*?:"<>|\s]', "_", data.email.split("@")[0] + ).strip(".") camel_log = ( - Path.home() / ".eigent" / email_sanitized / - ("project_" + data.project_id) / ("task_" + data.task_id) / - "camel_logs" + Path.home() + / ".eigent" + / email_sanitized + / ("project_" + data.project_id) + / ("task_" + data.task_id) + / "camel_logs" ) camel_log.mkdir(parents=True, exist_ok=True) @@ -230,14 +232,14 @@ async def post(data: Chat, request: Request): extra={ "project_id": data.project_id, "task_id": data.task_id, - "log_dir": str(camel_log) + "log_dir": str(camel_log), }, ) return StreamingResponse( timeout_stream_wrapper( step_solve(data, request, task_lock), task_lock=task_lock ), - media_type="text/event-stream" + media_type="text/event-stream", ) @@ -245,10 +247,7 @@ async def post(data: Chat, request: Request): def improve(id: str, data: SupplementChat): chat_logger.info( "Chat improvement requested", - extra={ - "task_id": id, - "question_length": len(data.question) - } + extra={"task_id": id, "question_length": len(data.question)}, ) task_lock = get_task_lock(id) @@ -266,14 +265,12 @@ def improve(id: str, data: SupplementChat): if hasattr(task_lock, "conversation_history"): hist_len = len(task_lock.conversation_history) chat_logger.info( - "[CONTEXT] Preserved" - f" {hist_len} conversation entries" + f"[CONTEXT] Preserved {hist_len} conversation entries" ) if hasattr(task_lock, "last_task_result"): result_len = len(task_lock.last_task_result) chat_logger.info( - "[CONTEXT] Preserved task" - f" result: {result_len} chars" + f"[CONTEXT] Preserved task result: {result_len} chars" ) # If task_id is provided, optimistically update @@ -301,8 +298,11 @@ def improve(id: str, data: SupplementChat): # Create new path using the existing # pattern: email/project_{id}/task_{id} new_folder_path = ( - Path.home() / "eigent" / current_email / f"project_{id}" / - f"task_{data.task_id}" + Path.home() + / "eigent" + / current_email + / f"project_{id}" + / f"task_{data.task_id}" ) new_folder_path.mkdir(parents=True, exist_ok=True) os.environ["file_save_path"] = str(new_folder_path) @@ -336,7 +336,7 @@ def improve(id: str, data: SupplementChat): ) chat_logger.info( "Improvement request queued with preserved context", - extra={"project_id": id} + extra={"project_id": id}, ) return Response(status_code=201) @@ -395,10 +395,7 @@ def stop(id: str): def human_reply(id: str, data: HumanReply): chat_logger.info( "Human reply received", - extra={ - "task_id": id, - "reply_length": len(data.reply) - } + extra={"task_id": id, "reply_length": len(data.reply)}, ) task_lock = get_task_lock(id) asyncio.run(task_lock.put_human_input(data.agent, data.reply)) @@ -412,8 +409,8 @@ def install_mcp(id: str, data: McpServers): "Installing MCP servers", extra={ "task_id": id, - "servers_count": len(data.get("mcpServers", {})) - } + "servers_count": len(data.get("mcpServers", {})), + }, ) task_lock = get_task_lock(id) asyncio.run( @@ -454,7 +451,7 @@ def add_task(id: str, data: AddTaskRequest): @router.delete( "/chat/{project_id}/remove-task/{task_id}", - name="remove task from workforce" + name="remove task from workforce", ) def remove_task(project_id: str, task_id: str): """Remove a task from the workforce""" diff --git a/backend/app/controller/health_controller.py b/backend/app/controller/health_controller.py index 83c0480a..1ee53719 100644 --- a/backend/app/controller/health_controller.py +++ b/backend/app/controller/health_controller.py @@ -35,9 +35,6 @@ async def health_check(): response = HealthResponse(status="ok", service="eigent") logger.debug( "Health check completed", - extra={ - "status": response.status, - "service": response.service - } + extra={"status": response.status, "service": response.service}, ) return response diff --git a/backend/app/controller/model_controller.py b/backend/app/controller/model_controller.py index d444f6d0..4a72ef2c 100644 --- a/backend/app/controller/model_controller.py +++ b/backend/app/controller/model_controller.py @@ -66,18 +66,15 @@ async def validate_model(request: ValidateModelRequest): "platform": platform, "model_type": model_type, "has_url": has_custom_url, - "has_config": has_config - } + "has_config": has_config, + }, ) # API key validation if request.api_key is not None and str(request.api_key).strip() == "": logger.warning( "Model validation failed: empty API key", - extra={ - "platform": platform, - "model_type": model_type - } + extra={"platform": platform, "model_type": model_type}, ) raise HTTPException( status_code=400, @@ -89,7 +86,7 @@ async def validate_model(request: ValidateModelRequest): "param": None, "code": "invalid_api_key", }, - } + }, ) try: @@ -97,10 +94,7 @@ async def validate_model(request: ValidateModelRequest): logger.debug( "Creating agent for validation", - extra={ - "platform": platform, - "model_type": model_type - } + extra={"platform": platform, "model_type": model_type}, ) agent = create_agent( platform, @@ -113,10 +107,7 @@ async def validate_model(request: ValidateModelRequest): logger.debug( "Agent created, executing test step", - extra={ - "platform": platform, - "model_type": model_type - } + extra={"platform": platform, "model_type": model_type}, ) response = agent.step( input_message=""" @@ -134,9 +125,9 @@ async def validate_model(request: ValidateModelRequest): extra={ "platform": platform, "model_type": model_type, - "error": str(e) + "error": str(e), }, - exc_info=True + exc_info=True, ) message, error_code, error_obj = normalize_error_to_openai_format(e) @@ -146,7 +137,7 @@ async def validate_model(request: ValidateModelRequest): "message": message, "error_code": error_code, "error": error_obj, - } + }, ) # Check validation results @@ -163,11 +154,10 @@ async def validate_model(request: ValidateModelRequest): " Website Content:" " Welcome to CAMEL AI!" ) - is_tool_calls = (tool_calls[0].result == expected) + is_tool_calls = tool_calls[0].result == expected no_tool_msg = ( - "This model doesn't support tool calls." - " please try with another model." + "This model doesn't support tool calls. please try with another model." ) result = ValidateModelResponse( is_valid=is_valid, @@ -183,8 +173,8 @@ async def validate_model(request: ValidateModelRequest): "platform": platform, "model_type": model_type, "is_valid": is_valid, - "is_tool_calls": is_tool_calls - } + "is_tool_calls": is_tool_calls, + }, ) return result diff --git a/backend/app/controller/task_controller.py b/backend/app/controller/task_controller.py index 90b4793b..8924218c 100644 --- a/backend/app/controller/task_controller.py +++ b/backend/app/controller/task_controller.py @@ -51,17 +51,11 @@ def start(id: str): def put(id: str, data: UpdateData): logger.info( "Updating task", - extra={ - "task_id": id, - "task_items_count": len(data.task) - } + extra={"task_id": id, "task_items_count": len(data.task)}, ) logger.debug( "Update task data", - extra={ - "task_id": id, - "data": data.model_dump_json() - } + extra={"task_id": id, "data": data.model_dump_json()}, ) task_lock = get_task_lock(id) asyncio.run( @@ -80,19 +74,13 @@ class TakeControl(BaseModel): @router.put("/task/{id}/take-control", name="take control pause or resume") def take_control(id: str, data: TakeControl): logger.info( - "Task control action", extra={ - "task_id": id, - "action": data.action - } + "Task control action", extra={"task_id": id, "action": data.action} ) task_lock = get_task_lock(id) asyncio.run(task_lock.put_queue(ActionTakeControl(action=data.action))) logger.info( "Task control action completed", - extra={ - "task_id": id, - "action": data.action - } + extra={"task_id": id, "action": data.action}, ) return Response(status_code=204) @@ -101,17 +89,11 @@ def take_control(id: str, data: TakeControl): def add_agent(id: str, data: NewAgent): logger.info( "Adding new agent to task", - extra={ - "task_id": id, - "agent_name": data.name - } + extra={"task_id": id, "agent_name": data.name}, ) logger.debug( "New agent data", - extra={ - "task_id": id, - "agent_data": data.model_dump_json() - } + extra={"task_id": id, "agent_data": data.model_dump_json()}, ) # Set user-specific environment path for this thread set_user_env_path(data.env_path) @@ -123,10 +105,7 @@ def add_agent(id: str, data: NewAgent): get_task_lock(id).put_queue(ActionNewAgent(**data.model_dump())) ) logger.info( - "Agent added to task", extra={ - "task_id": id, - "agent_name": data.name - } + "Agent added to task", extra={"task_id": id, "agent_name": data.name} ) return Response(status_code=204) diff --git a/backend/app/controller/tool_controller.py b/backend/app/controller/tool_controller.py index 573800b2..03d7baf6 100644 --- a/backend/app/controller/tool_controller.py +++ b/backend/app/controller/tool_controller.py @@ -15,7 +15,6 @@ import logging import os import time -from typing import Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel @@ -29,10 +28,11 @@ from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit class LinkedInTokenRequest(BaseModel): r"""Request model for saving LinkedIn OAuth token.""" + access_token: str - refresh_token: Optional[str] = None - expires_in: Optional[int] = None - scope: Optional[str] = None + refresh_token: str | None = None + expires_in: int | None = None + scope: str | None = None logger = logging.getLogger("tool_controller") @@ -77,10 +77,9 @@ async def install_tool(tool: str): return { "success": True, "tools": tools, - "message": - f"Successfully installed and authenticated {tool} toolkit", + "message": f"Successfully installed and authenticated {tool} toolkit", "count": len(tools), - "toolkit_name": "NotionMCPToolkit" + "toolkit_name": "NotionMCPToolkit", } except Exception as connect_error: logger.warning( @@ -89,22 +88,17 @@ async def install_tool(tool: str): # Even if connection fails, mark as # installed so user can use it later return { - "success": - True, + "success": True, "tools": [], - "message": - f"{tool} toolkit installed but" + "message": f"{tool} toolkit installed but" " not connected. Will connect" " when needed.", - "count": - 0, - "toolkit_name": - "NotionMCPToolkit", - "warning": - "Could not connect to Notion" + "count": 0, + "toolkit_name": "NotionMCPToolkit", + "warning": "Could not connect to Notion" " MCP server. You may need to" " authenticate when using" - " the tool." + " the tool.", } except Exception as e: logger.error(f"Failed to install {tool} toolkit: {e}") @@ -131,7 +125,7 @@ async def install_tool(tool: str): "tools": tools, "message": f"Successfully installed {tool} toolkit", "count": len(tools), - "toolkit_name": "GoogleCalendarToolkit" + "toolkit_name": "GoogleCalendarToolkit", } except ValueError as auth_error: # No credentials - need authorization @@ -147,19 +141,14 @@ async def install_tool(tool: str): GoogleCalendarToolkit.start_background_auth("install_auth") return { - "success": - False, - "status": - "authorizing", - "message": - "Authorization required. Browser" + "success": False, + "status": "authorizing", + "message": "Authorization required. Browser" " should open automatically." " Complete authorization and" " try installing again.", - "toolkit_name": - "GoogleCalendarToolkit", - "requires_auth": - True + "toolkit_name": "GoogleCalendarToolkit", + "requires_auth": True, } except Exception as e: logger.error(f"Failed to install {tool} toolkit: {e}") @@ -174,20 +163,14 @@ async def install_tool(tool: str): if LinkedInToolkit.is_token_expired(): logger.info("LinkedIn token has expired") return { - "success": - False, - "status": - "token_expired", - "message": - "LinkedIn token has expired." + "success": False, + "status": "token_expired", + "message": "LinkedIn token has expired." " Please re-authenticate" " via OAuth.", - "toolkit_name": - "LinkedInToolkit", - "requires_auth": - True, - "oauth_url": - "/api/oauth/linkedin/login" + "toolkit_name": "LinkedInToolkit", + "requires_auth": True, + "oauth_url": "/api/oauth/linkedin/login", } try: @@ -227,7 +210,7 @@ async def install_tool(tool: str): "message": f"Successfully installed {tool} toolkit", "count": len(tools), "toolkit_name": "LinkedInToolkit", - "profile": profile if "error" not in profile else None + "profile": profile if "error" not in profile else None, } if token_warning: result["warning"] = token_warning @@ -236,20 +219,14 @@ async def install_tool(tool: str): logger.warning(f"LinkedIn token may be invalid: {e}") # Token exists but may be expired/invalid return { - "success": - False, - "status": - "token_invalid", - "message": - "LinkedIn token may be expired" + "success": False, + "status": "token_invalid", + "message": "LinkedIn token may be expired" " or invalid. Please" " re-authenticate via OAuth.", - "toolkit_name": - "LinkedInToolkit", - "requires_auth": - True, - "oauth_url": - "/api/oauth/linkedin/login" + "toolkit_name": "LinkedInToolkit", + "requires_auth": True, + "oauth_url": "/api/oauth/linkedin/login", } else: # No credentials - need OAuth authorization @@ -257,11 +234,10 @@ async def install_tool(tool: str): return { "success": False, "status": "not_configured", - "message": - "LinkedIn OAuth required. Redirect user to OAuth login.", + "message": "LinkedIn OAuth required. Redirect user to OAuth login.", "toolkit_name": "LinkedInToolkit", "requires_auth": True, - "oauth_url": "/api/oauth/linkedin/login" + "oauth_url": "/api/oauth/linkedin/login", } except Exception as e: logger.error(f"Failed to install {tool} toolkit: {e}") @@ -277,7 +253,7 @@ async def install_tool(tool: str): " ['notion'," " 'google_calendar'," " 'linkedin']" - ) + ), ) @@ -292,48 +268,34 @@ async def list_available_tools(): return { "tools": [ { - "name": - "notion", - "display_name": - "Notion MCP", - "description": - "Notion workspace integration" + "name": "notion", + "display_name": "Notion MCP", + "description": "Notion workspace integration" " for reading and managing" " Notion pages", - "toolkit_class": - "NotionMCPToolkit", - "requires_auth": - True - }, { - "name": - "google_calendar", - "display_name": - "Google Calendar", - "description": - "Google Calendar integration" + "toolkit_class": "NotionMCPToolkit", + "requires_auth": True, + }, + { + "name": "google_calendar", + "display_name": "Google Calendar", + "description": "Google Calendar integration" " for managing events" " and schedules", - "toolkit_class": - "GoogleCalendarToolkit", - "requires_auth": - True - }, { - "name": - "linkedin", - "display_name": - "LinkedIn", - "description": - "LinkedIn integration for" + "toolkit_class": "GoogleCalendarToolkit", + "requires_auth": True, + }, + { + "name": "linkedin", + "display_name": "LinkedIn", + "description": "LinkedIn integration for" " creating posts, managing" " profile, and social media" " automation", - "toolkit_class": - "LinkedInToolkit", - "requires_auth": - True, - "oauth_url": - "/api/oauth/linkedin/login" - } + "toolkit_class": "LinkedInToolkit", + "requires_auth": True, + "oauth_url": "/api/oauth/linkedin/login", + }, ] } @@ -355,7 +317,7 @@ async def get_oauth_status(provider: str): return { "provider": provider, "status": "not_started", - "message": "No authorization in progress" + "message": "No authorization in progress", } return state.to_dict() @@ -377,13 +339,13 @@ async def cancel_oauth(provider: str): if not state: raise HTTPException( status_code=404, - detail=f"No authorization found for provider '{provider}'" + detail=f"No authorization found for provider '{provider}'", ) if state.status not in ["pending", "authorizing"]: raise HTTPException( status_code=400, - detail=f"Cannot cancel authorization with status '{state.status}'" + detail=f"Cannot cancel authorization with status '{state.status}'", ) state.cancel() @@ -392,7 +354,7 @@ async def cancel_oauth(provider: str): return { "success": True, "provider": provider, - "message": "Authorization cancelled successfully" + "message": "Authorization cancelled successfully", } @@ -456,13 +418,12 @@ async def uninstall_tool(tool: str): return { "success": True, "message": message, - "deleted_files": deleted_files + "deleted_files": deleted_files, } except Exception as e: logger.error(f"Failed to uninstall {tool}: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to uninstall {tool}: {str(e)}" + status_code=500, detail=f"Failed to uninstall {tool}: {str(e)}" ) elif tool == "google_calendar": @@ -484,8 +445,10 @@ async def uninstall_tool(tool: str): token_dirs.add( os.path.join( - os.path.expanduser("~"), ".eigent", "tokens", - "google_calendar" + os.path.expanduser("~"), + ".eigent", + "tokens", + "google_calendar", ) ) @@ -510,18 +473,15 @@ async def uninstall_tool(tool: str): logger.info("Cleared Google Calendar OAuth state cache") return { - "success": - True, - "message": - "Successfully uninstalled" + "success": True, + "message": "Successfully uninstalled" f" {tool} and cleaned up" - " authentication tokens" + " authentication tokens", } except Exception as e: logger.error(f"Failed to uninstall {tool}: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to uninstall {tool}: {str(e)}" + status_code=500, detail=f"Failed to uninstall {tool}: {str(e)}" ) elif tool == "linkedin": try: @@ -530,24 +490,20 @@ async def uninstall_tool(tool: str): if success: return { - "success": - True, - "message": - "Successfully uninstalled" + "success": True, + "message": "Successfully uninstalled" f" {tool} and cleaned up" - " authentication tokens" + " authentication tokens", } else: return { "success": True, - "message": - f"Uninstalled {tool} (no tokens found to clean up)" + "message": f"Uninstalled {tool} (no tokens found to clean up)", } except Exception as e: logger.error(f"Failed to uninstall {tool}: {e}") raise HTTPException( - status_code=500, - detail=f"Failed to uninstall {tool}: {str(e)}" + status_code=500, detail=f"Failed to uninstall {tool}: {str(e)}" ) else: raise HTTPException( @@ -558,7 +514,7 @@ async def uninstall_tool(tool: str): " ['notion'," " 'google_calendar'," " 'linkedin']" - ) + ), ) @@ -594,14 +550,14 @@ async def save_linkedin_token(token_request: LinkedInTokenRequest): "message": "LinkedIn token saved successfully", "tools": tools, "count": len(tools), - "profile": profile if "error" not in profile else None + "profile": profile if "error" not in profile else None, } except Exception as e: logger.warning(f"Token saved but verification failed: {e}") return { "success": True, "message": "LinkedIn token saved (verification pending)", - "warning": str(e) + "warning": str(e), } else: raise HTTPException( @@ -631,7 +587,7 @@ async def get_linkedin_status(): "authenticated": False, "status": "not_configured", "message": "LinkedIn not configured. OAuth required.", - "oauth_url": "/api/oauth/linkedin/login" + "oauth_url": "/api/oauth/linkedin/login", } token_info = LinkedInToolkit.get_token_info() @@ -639,11 +595,10 @@ async def get_linkedin_status(): is_expiring_soon = LinkedInToolkit.is_token_expiring_soon() result = { - "authenticated": - True, - "status": - "expired" if is_expired else - ("expiring_soon" if is_expiring_soon else "valid"), + "authenticated": True, + "status": "expired" + if is_expired + else ("expiring_soon" if is_expiring_soon else "valid"), } if token_info: @@ -662,11 +617,9 @@ async def get_linkedin_status(): result["message"] = "Token has expired. Please re-authenticate." result["oauth_url"] = "/api/oauth/linkedin/login" elif is_expiring_soon: - days = result.get('days_remaining', 'unknown') + days = result.get("days_remaining", "unknown") result["message"] = ( - f"Token expires in {days}" - " days. Consider" - " re-authenticating." + f"Token expires in {days} days. Consider re-authenticating." ) result["oauth_url"] = "/api/oauth/linkedin/login" else: @@ -719,7 +672,7 @@ async def open_browser_login(): # Check if browser is already running on this port def is_port_in_use(port): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(('localhost', port)) == 0 + return s.connect_ex(("localhost", port)) == 0 if is_port_in_use(cdp_port): logger.info(f"Browser already running on port {cdp_port}") @@ -728,9 +681,8 @@ async def open_browser_login(): "session_id": session_id, "user_data_dir": user_data_dir, "cdp_port": cdp_port, - "message": - "Browser already running. Use existing window to log in.", - "note": "Your login data will be saved in the profile." + "message": "Browser already running. Use existing window to log in.", + "note": "Your login data will be saved in the profile.", } # Use static Electron browser script @@ -746,8 +698,12 @@ async def open_browser_login(): electron_cmd = "npx" electron_args = [ - electron_cmd, "electron", electron_script_path, user_data_dir, - str(cdp_port), "https://www.google.com" + electron_cmd, + "electron", + electron_script_path, + user_data_dir, + str(cdp_port), + "https://www.google.com", ] # Get the app's directory to run npx in the right context @@ -771,22 +727,24 @@ async def open_browser_login(): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # Redirect stderr to stdout text=True, - encoding='utf-8', - errors='replace', # Replace undecodable chars instead of crashing - bufsize=1 # Line buffered + encoding="utf-8", + errors="replace", # Replace undecodable chars instead of crashing + bufsize=1, # Line buffered ) # Create async task to log Electron output async def log_electron_output(): - for line in iter(process.stdout.readline, ''): + for line in iter(process.stdout.readline, ""): if line: logger.info(f"[ELECTRON OUTPUT] {line.strip()}") import asyncio + asyncio.create_task(log_electron_output()) # Wait a bit for Electron to start import asyncio + await asyncio.sleep(3) logger.info( @@ -796,25 +754,17 @@ async def open_browser_login(): ) return { - "success": - True, - "session_id": - session_id, - "user_data_dir": - user_data_dir, - "cdp_port": - cdp_port, - "pid": - process.pid, - "chrome_version": - "130.0.6723.191", # Electron 33's Chrome version - "message": - "Electron browser opened successfully." + "success": True, + "session_id": session_id, + "user_data_dir": user_data_dir, + "cdp_port": cdp_port, + "pid": process.pid, + "chrome_version": "130.0.6723.191", # Electron 33's Chrome version + "message": "Electron browser opened successfully." " Please log in to your accounts.", - "note": - "The browser will remain open for" + "note": "The browser will remain open for" " you to log in. Your login data" - " will be saved in the profile." + " will be saved in the profile.", } except Exception as e: @@ -875,6 +825,7 @@ async def list_cookie_domains(search: str = None): # Try to read actual cookie count try: import sqlite3 + conn = sqlite3.connect(cookies_file) cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM cookies") @@ -890,13 +841,11 @@ async def list_cookie_domains(search: str = None): if not os.path.exists(user_data_dir): return { - "success": - True, + "success": True, "domains": [], - "message": - "No browser profile found." + "message": "No browser profile found." " Please login first" - " using /browser/login." + " using /browser/login.", } cookie_manager = CookieManager(user_data_dir) @@ -910,7 +859,7 @@ async def list_cookie_domains(search: str = None): "success": True, "domains": domains, "total": len(domains), - "user_data_dir": user_data_dir + "user_data_dir": user_data_dir, } except Exception as e: @@ -942,7 +891,7 @@ async def get_domain_cookies(domain: str): "No browser profile found." " Please login first using" " /browser/login." - ) + ), ) cookie_manager = CookieManager(user_data_dir) @@ -952,7 +901,7 @@ async def get_domain_cookies(domain: str): "success": True, "domain": domain, "cookies": cookies, - "count": len(cookies) + "count": len(cookies), } except HTTPException: @@ -986,7 +935,7 @@ async def delete_domain_cookies(domain: str): "No browser profile found." " Please login first using" " /browser/login." - ) + ), ) cookie_manager = CookieManager(user_data_dir) @@ -995,12 +944,12 @@ async def delete_domain_cookies(domain: str): if success: return { "success": True, - "message": f"Successfully deleted cookies for domain: {domain}" + "message": f"Successfully deleted cookies for domain: {domain}", } else: raise HTTPException( status_code=500, - detail=f"Failed to delete cookies for domain: {domain}" + detail=f"Failed to delete cookies for domain: {domain}", ) except HTTPException: @@ -1035,7 +984,7 @@ async def delete_all_cookies(): if success: return { "success": True, - "message": "Successfully deleted all cookies" + "message": "Successfully deleted all cookies", } else: raise HTTPException( diff --git a/backend/app/exception/__init__.py b/backend/app/exception/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/exception/__init__.py +++ b/backend/app/exception/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/exception/exception.py b/backend/app/exception/exception.py index 4c1137ab..f1916610 100644 --- a/backend/app/exception/exception.py +++ b/backend/app/exception/exception.py @@ -14,26 +14,22 @@ class UserException(Exception): - def __init__(self, code: int, description: str): self.code = code self.description = description class TokenException(Exception): - def __init__(self, code: int, text: str): self.code = code self.text = text class NoPermissionException(Exception): - def __init__(self, text: str): self.text = text class ProgramException(Exception): - def __init__(self, text: str): self.text = text diff --git a/backend/app/exception/handler.py b/backend/app/exception/handler.py index 12371519..aff0040f 100644 --- a/backend/app/exception/handler.py +++ b/backend/app/exception/handler.py @@ -40,10 +40,10 @@ async def request_exception(request: Request, e: RequestValidationError): return JSONResponse( content={ - "code": - code.form_error, - "error": - jsonable_encoder(trans.translate(list(e.errors()), locale=lang)), + "code": code.form_error, + "error": jsonable_encoder( + trans.translate(list(e.errors()), locale=lang) + ), } ) @@ -65,10 +65,7 @@ async def no_permission(request: Request, exception: NoPermissionException): logger.warning(f"No permission on {request.url.path}: {exception.text}") return JSONResponse( status_code=200, - content={ - "code": code.no_permission_error, - "text": exception.text - }, + content={"code": code.no_permission_error, "text": exception.text}, ) @@ -78,14 +75,11 @@ async def program_exception( ): logger.error( f"Program exception on {request.url.path}: {exception.text}", - exc_info=True + exc_info=True, ) return JSONResponse( status_code=200, - content={ - "code": code.program_error, - "text": exception.text - }, + content={"code": code.program_error, "text": exception.text}, ) @@ -99,7 +93,7 @@ async def global_exception_handler(request: Request, exc: Exception): "request_path": str(request.url.path), "request_query": str(request.url.query), "client_host": request.client.host if request.client else None, - } + }, ) return JSONResponse( diff --git a/backend/app/model/__init__.py b/backend/app/model/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/model/__init__.py +++ b/backend/app/model/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/model/chat.py b/backend/app/model/chat.py index d15ee597..52879391 100644 --- a/backend/app/model/chat.py +++ b/backend/app/model/chat.py @@ -21,7 +21,7 @@ from typing import Literal from camel.types import ModelType, RoleType from pydantic import BaseModel, Field, field_validator -from app.model.enums import DEFAULT_SUMMARY_PROMPT, Status +from app.model.enums import DEFAULT_SUMMARY_PROMPT logger = logging.getLogger("chat_model") @@ -38,7 +38,7 @@ class QuestionAnalysisResult(BaseModel): answer: str | None = Field( default=None, description="Direct answer for simple questions." - " None for complex tasks." + " None for complex tasks.", ) @@ -93,26 +93,34 @@ class Chat(BaseModel): return model_type def get_bun_env(self) -> dict[str, str]: - return { - "NPM_CONFIG_REGISTRY": self.bun_mirror - } if self.bun_mirror else {} + return ( + {"NPM_CONFIG_REGISTRY": self.bun_mirror} if self.bun_mirror else {} + ) def get_uvx_env(self) -> dict[str, str]: - return { - "UV_DEFAULT_INDEX": self.uvx_mirror, - "PIP_INDEX_URL": self.uvx_mirror - } if self.uvx_mirror else {} + return ( + { + "UV_DEFAULT_INDEX": self.uvx_mirror, + "PIP_INDEX_URL": self.uvx_mirror, + } + if self.uvx_mirror + else {} + ) def is_cloud(self): return self.api_url is not None and "44.247.171.124" in self.api_url def file_save_path(self, path: str | None = None): - email = re.sub(r'[\\/*?:"<>|\s]', "_", - self.email.split("@")[0]).strip(".") + email = re.sub(r'[\\/*?:"<>|\s]', "_", self.email.split("@")[0]).strip( + "." + ) # Use project-based structure: project_{project_id}/task_{task_id} save_path = ( - Path.home() / "eigent" / email / f"project_{self.project_id}" / - f"task_{self.task_id}" + Path.home() + / "eigent" + / email + / f"project_{self.project_id}" + / f"task_{self.task_id}" ) if path is not None: save_path = save_path / path @@ -143,6 +151,7 @@ class UpdateData(BaseModel): class AgentModelConfig(BaseModel): """Optional per-agent model configuration to override the default task model.""" + model_platform: str | None = None model_type: str | None = None api_key: str | None = None diff --git a/backend/app/router.py b/backend/app/router.py index b73ab2f2..f257dde0 100644 --- a/backend/app/router.py +++ b/backend/app/router.py @@ -16,6 +16,7 @@ Centralized router registration for the Eigent API. All routers are explicitly registered here for better visibility and maintainability. """ + import logging from fastapi import FastAPI @@ -48,31 +49,27 @@ def register_routers(app: FastAPI, prefix: str = "") -> None: { "router": health_controller.router, "tags": ["Health"], - "description": "Health check endpoint for service readiness" + "description": "Health check endpoint for service readiness", }, { - "router": - chat_controller.router, + "router": chat_controller.router, "tags": ["chat"], - "description": - "Chat session management, improvements, and human interactions" + "description": "Chat session management, improvements, and human interactions", }, { "router": model_controller.router, "tags": ["model"], - "description": "Model validation and configuration" + "description": "Model validation and configuration", }, { - "router": - task_controller.router, + "router": task_controller.router, "tags": ["task"], - "description": - "Task lifecycle management (start, stop, update, control)" + "description": "Task lifecycle management (start, stop, update, control)", }, { "router": tool_controller.router, "tags": ["tool"], - "description": "Tool installation and management" + "description": "Tool installation and management", }, ] diff --git a/backend/app/service/__init__.py b/backend/app/service/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/service/__init__.py +++ b/backend/app/service/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 399ae8ca..a69ba25d 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -66,9 +66,7 @@ logger = logging.getLogger("chat_service") def format_task_context( - task_data: dict, - seen_files: set | None = None, - skip_files: bool = False + task_data: dict, seen_files: set | None = None, skip_files: bool = False ) -> str: """Format structured task data into a readable context string. @@ -82,31 +80,33 @@ def format_task_context( """ context_parts = [] - if task_data.get('task_content'): + if task_data.get("task_content"): context_parts.append(f"Previous Task: {task_data['task_content']}") - if task_data.get('task_result'): + if task_data.get("task_result"): context_parts.append( f"Previous Task Result: {task_data['task_result']}" ) # Skip file listing if requested if not skip_files: - working_directory = task_data.get('working_directory') - skip_ext = ('.pyc', '.tmp') + working_directory = task_data.get("working_directory") + skip_ext = (".pyc", ".tmp") if working_directory: try: if os.path.exists(working_directory): generated_files = [] for root, dirs, files in os.walk(working_directory): dirs[:] = [ - d for d in dirs if not d.startswith('.') and d - not in ['node_modules', '__pycache__', 'venv'] + d + for d in dirs + if not d.startswith(".") + and d + not in ["node_modules", "__pycache__", "venv"] ] for file in files: - if ( - not file.startswith('.') - and not file.endswith(skip_ext) + if not file.startswith(".") and not file.endswith( + skip_ext ): file_path = os.path.join(root, file) absolute_path = os.path.abspath(file_path) @@ -136,7 +136,7 @@ def collect_previous_task_context( working_directory: str, previous_task_content: str, previous_task_result: str, - previous_summary: str = "" + previous_summary: str = "", ) -> str: """ Collect context from previous task including content, result, @@ -177,14 +177,15 @@ def collect_previous_task_context( generated_files = [] for root, dirs, files in os.walk(working_directory): dirs[:] = [ - d for d in dirs if not d.startswith('.') - and d not in ['node_modules', '__pycache__', 'venv'] + d + for d in dirs + if not d.startswith(".") + and d not in ["node_modules", "__pycache__", "venv"] ] - skip_ext = ('.pyc', '.tmp') + skip_ext = (".pyc", ".tmp") for file in files: - if ( - not file.startswith('.') - and not file.endswith(skip_ext) + if not file.startswith(".") and not file.endswith( + skip_ext ): file_path = os.path.join(root, file) absolute_path = os.path.abspath(file_path) @@ -212,14 +213,15 @@ def check_conversation_history_length( Returns: tuple: (is_exceeded, total_length) """ - if not hasattr( - task_lock, 'conversation_history' - ) or not task_lock.conversation_history: + if ( + not hasattr(task_lock, "conversation_history") + or not task_lock.conversation_history + ): return False, 0 total_length = 0 for entry in task_lock.conversation_history: - total_length += len(entry.get('content', '')) + total_length += len(entry.get("content", "")) is_exceeded = total_length > max_length @@ -253,19 +255,19 @@ def build_conversation_context( context = f"{header}\n" for entry in task_lock.conversation_history: - if entry['role'] == 'task_result': - if isinstance(entry['content'], dict): + if entry["role"] == "task_result": + if isinstance(entry["content"], dict): formatted_context = format_task_context( - entry['content'], skip_files=True + entry["content"], skip_files=True ) context += formatted_context + "\n\n" - if entry['content'].get('working_directory'): + if entry["content"].get("working_directory"): working_directories.add( - entry['content']['working_directory'] + entry["content"]["working_directory"] ) else: - context += entry['content'] + "\n" - elif entry['role'] == 'assistant': + context += entry["content"] + "\n" + elif entry["role"] == "assistant": context += f"Assistant: {entry['content']}\n\n" if working_directories: @@ -275,14 +277,16 @@ def build_conversation_context( if os.path.exists(working_directory): for root, dirs, files in os.walk(working_directory): dirs[:] = [ - d for d in dirs if not d.startswith('.') and d - not in ['node_modules', '__pycache__', 'venv'] + d + for d in dirs + if not d.startswith(".") + and d + not in ["node_modules", "__pycache__", "venv"] ] for file in files: - if not file.startswith('.' - ) and not file.endswith( - ('.pyc', '.tmp') - ): + if not file.startswith( + "." + ) and not file.endswith((".pyc", ".tmp")): file_path = os.path.join(root, file) absolute_path = os.path.abspath(file_path) all_generated_files.add(absolute_path) @@ -316,13 +320,13 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): start_event_loop = True # Initialize task_lock attributes - if not hasattr(task_lock, 'conversation_history'): + if not hasattr(task_lock, "conversation_history"): task_lock.conversation_history = [] - if not hasattr(task_lock, 'last_task_result'): + if not hasattr(task_lock, "last_task_result"): task_lock.last_task_result = "" - if not hasattr(task_lock, 'question_agent'): + if not hasattr(task_lock, "question_agent"): task_lock.question_agent = None - if not hasattr(task_lock, 'summary_generated'): + if not hasattr(task_lock, "summary_generated"): task_lock.summary_generated = False # Create or reuse persistent question_agent @@ -331,8 +335,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): else: hist_len = len(task_lock.conversation_history) logger.debug( - "Reusing existing question_agent " - f"with {hist_len} history entries" + f"Reusing existing question_agent with {hist_len} history entries" ) question_agent = task_lock.question_agent @@ -350,18 +353,15 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): logger.info("=" * 80) logger.info( "🚀 [LIFECYCLE] step_solve STARTED", - extra={ - "project_id": options.project_id, - "task_id": options.task_id - } + extra={"project_id": options.project_id, "task_id": options.task_id}, ) logger.info("=" * 80) logger.debug( "Step solve options", extra={ "task_id": options.task_id, - "model_platform": options.model_platform - } + "model_platform": options.model_platform, + }, ) while True: @@ -370,8 +370,8 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"[LIFECYCLE] step_solve loop iteration #{loop_iteration}", extra={ "project_id": options.project_id, - "task_id": options.task_id - } + "task_id": options.task_id, + }, ) if await request.is_disconnected(): @@ -418,9 +418,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): extra={ "project_id": options.project_id, "task_id": options.task_id, - "error": str(e) + "error": str(e), }, - exc_info=True + exc_info=True, ) # Continue waiting instead of breaking on queue error continue @@ -433,20 +433,22 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "received or start_event_loop", extra={ "project_id": options.project_id, - "start_event_loop": start_event_loop - } + "start_event_loop": start_event_loop, + }, ) wf_state = ( - 'None' - if workforce is None else f'exists(id={id(workforce)})' + "None" + if workforce is None + else f"exists(id={id(workforce)})" ) logger.info( "[NEW-QUESTION] Current workforce" f" state: workforce={wf_state}" ) ct_state = ( - 'None' - if camel_task is None else f'exists(id={camel_task.id})' + "None" + if camel_task is None + else f"exists(id={camel_task.id})" ) logger.info( "[NEW-QUESTION] Current " @@ -485,8 +487,8 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): extra={ "project_id": options.project_id, "current_length": total_length, - "max_length": 100000 - } + "max_length": 100000, + }, ) ctx_msg = ( "The conversation history " @@ -494,11 +496,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): " a new project to continue." ) yield sse_json( - "context_too_long", { + "context_too_long", + { "message": ctx_msg, "current_length": total_length, - "max_length": 100000 - } + "max_length": 100000, + }, ) continue @@ -528,8 +531,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "without workforce" ) conv_ctx = build_conversation_context( - task_lock, header='=== Previous ' - 'Conversation ===' + task_lock, header="=== Previous Conversation ===" ) simple_answer_prompt = ( f"{conv_ctx}" @@ -541,8 +543,8 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): try: simple_resp = question_agent.step(simple_answer_prompt) - if (simple_resp and simple_resp.msgs): - answer_content = (simple_resp.msgs[0].content) + if simple_resp and simple_resp.msgs: + answer_content = simple_resp.msgs[0].content else: answer_content = ( "I understand your " @@ -552,31 +554,29 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "right now." ) - task_lock.add_conversation('assistant', answer_content) + task_lock.add_conversation("assistant", answer_content) yield sse_json( - "wait_confirm", { - "content": answer_content, - "question": question - } + "wait_confirm", + {"content": answer_content, "question": question}, ) except Exception as e: logger.error(f"Error generating simple answer: {e}") yield sse_json( - "wait_confirm", { - "content": - "I encountered an error" + "wait_confirm", + { + "content": "I encountered an error" " while processing " "your question.", - "question": - question - } + "question": question, + }, ) # Clean up empty folder if it was created for this task - if hasattr( - task_lock, 'new_folder_path' - ) and task_lock.new_folder_path: + if ( + hasattr(task_lock, "new_folder_path") + and task_lock.new_folder_path + ): try: folder_path = Path(task_lock.new_folder_path) if folder_path.exists() and folder_path.is_dir(): @@ -618,7 +618,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "decomposing" ) # Update the sync_step with new task_id - if hasattr(item, 'new_task_id') and item.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 ) @@ -645,8 +645,8 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): (workforce, mcp) = await construct_workforce(options) for new_agent in options.new_agents: workforce.add_single_agent_worker( - format_agent_description(new_agent), await - new_agent_model(new_agent, options) + format_agent_description(new_agent), + await new_agent_model(new_agent, options), ) task_lock.status = Status.confirmed @@ -665,18 +665,19 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): stream_state = { "subtasks": [], "seen_ids": set(), - "last_content": "" + "last_content": "", } state_holder: dict[str, Any] = { "sub_tasks": [], - "summary_task": "" + "summary_task": "", } def on_stream_batch( new_tasks: list[Task], is_final: bool = False ): fresh_tasks = [ - t for t in new_tasks + t + for t in new_tasks if t.id not in stream_state["seen_ids"] ] for t in fresh_tasks: @@ -685,16 +686,19 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): def on_stream_text(chunk): try: - accumulated_content = chunk.msg.content if hasattr( - chunk, 'msg' - ) and chunk.msg else str(chunk) + accumulated_content = ( + chunk.msg.content + if hasattr(chunk, "msg") and chunk.msg + else str(chunk) + ) last_content = stream_state["last_content"] # Calculate delta: new content # not in the previous chunk if accumulated_content.startswith(last_content): delta_content = accumulated_content[ - len(last_content):] + len(last_content) : + ] else: delta_content = accumulated_content @@ -705,8 +709,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): task_lock.put_queue( ActionDecomposeTextData( data={ - "project_id": - options.project_id, + "project_id": options.project_id, "task_id": options.task_id, "content": delta_content, } @@ -738,9 +741,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"{len(sub_tasks)} subtasks" ) try: - setattr( - task_lock, "decompose_sub_tasks", sub_tasks - ) + task_lock.decompose_sub_tasks = sub_tasks except Exception: pass @@ -751,7 +752,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): summary_task( summary_task_agent, camel_task ), - timeout=10 + timeout=10, ) task_lock.summary_generated = True except asyncio.TimeoutError: @@ -759,21 +760,22 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "summary_task timeout", extra={ "project_id": options.project_id, - "task_id": options.task_id - } + "task_id": options.task_id, + }, ) task_lock.summary_generated = True content_preview = ( camel_task.content - if hasattr(camel_task, "content") else "" + if hasattr(camel_task, "content") + else "" ) if content_preview is None: content_preview = "" if len(content_preview) > 80: cp = content_preview[:80] - summary_task_content = (cp + "...") + summary_task_content = cp + "..." else: - summary_task_content = (content_preview) + summary_task_content = content_preview summary_task_content = ( f"Task|{summary_task_content}" ) @@ -781,23 +783,23 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): task_lock.summary_generated = True content_preview = ( camel_task.content - if hasattr(camel_task, "content") else "" + if hasattr(camel_task, "content") + else "" ) if content_preview is None: content_preview = "" if len(content_preview) > 80: cp = content_preview[:80] - summary_task_content = (cp + "...") + summary_task_content = cp + "..." else: - summary_task_content = (content_preview) + summary_task_content = content_preview summary_task_content = ( f"Task|{summary_task_content}" ) state_holder["summary_task"] = summary_task_content try: - setattr( - task_lock, "summary_task_content", + task_lock.summary_task_content = ( summary_task_content ) except Exception: @@ -806,8 +808,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): payload = { "project_id": options.project_id, "task_id": options.task_id, - "sub_tasks": - tree_sub_tasks(camel_task.subtasks), + "sub_tasks": tree_sub_tasks( + camel_task.subtasks + ), "delta_sub_tasks": tree_sub_tasks(sub_tasks), "is_final": True, "summary_task": summary_task_content, @@ -818,7 +821,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): except Exception as e: logger.error( f"Error in background decomposition: {e}", - exc_info=True + exc_info=True, ) bg_task = asyncio.create_task(run_decomposition()) @@ -844,13 +847,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): # Save updated sub_tasks back to # task_lock so Action.start uses # the correct list - setattr(task_lock, "decompose_sub_tasks", sub_tasks) + task_lock.decompose_sub_tasks = sub_tasks summary_task_content_local = getattr( task_lock, "summary_task_content", summary_task_content ) yield to_sub_tasks(camel_task, summary_task_content_local) elif item.action == Action.add_task: - # Check if this might be a misrouted second question if camel_task is None and workforce is None: logger.error( @@ -860,12 +862,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"{options.project_id}" ) yield sse_json( - "error", { - "message": - "Cannot add task: task not " + "error", + { + "message": "Cannot add task: task not " "initialized. Please start" " a task first." - } + }, ) continue @@ -878,12 +880,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"{options.project_id}" ) yield sse_json( - "error", { - "message": - "Workforce not initialized." + "error", + { + "message": "Workforce not initialized." " Please start the task " "first." - } + }, ) continue @@ -894,7 +896,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): returnData = { "project_id": item.project_id, - "task_id": item.task_id or (len(camel_task.subtasks) + 1) + "task_id": item.task_id or (len(camel_task.subtasks) + 1), } yield sse_json("add_task", returnData) elif item.action == Action.remove_task: @@ -906,19 +908,19 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"{options.project_id}" ) yield sse_json( - "error", { - "message": - "Workforce not initialized." + "error", + { + "message": "Workforce not initialized." " Please start the task " "first." - } + }, ) continue workforce.remove_task(item.task_id) returnData = { "project_id": item.project_id, - "task_id": item.task_id + "task_id": item.task_id, } yield sse_json("remove_task", returnData) elif item.action == Action.skip_task: @@ -929,8 +931,8 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "Stop button)", extra={ "project_id": options.project_id, - "item_project_id": item.project_id - } + "item_project_id": item.project_id, + }, ) logger.info("=" * 80) @@ -963,6 +965,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): from camel.societies.workforce.workforce import ( Workforce as BaseWorkforce, ) + BaseWorkforce.stop(workforce) logger.info( "[LIFECYCLE] " @@ -993,9 +996,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): # Mark task as done and preserve context (like Action.end does) task_lock.status = Status.done end_message = ( - "Task stopped" - "Task stopped " - "by user" + "Task stoppedTask stopped by user" ) task_lock.last_task_result = end_message @@ -1010,14 +1011,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): task_content: str = f"Task {options.task_id}" task_lock.add_conversation( - 'task_result', { - 'task_content': - task_content, - 'task_result': - end_message, - 'working_directory': - get_working_directory(options, task_lock) - } + "task_result", + { + "task_content": task_content, + "task_result": end_message, + "working_directory": get_working_directory( + options, task_lock + ), + }, ) # Clear camel_task as well @@ -1059,16 +1060,17 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): " a new project to continue." ) yield sse_json( - "context_too_long", { + "context_too_long", + { "message": ctx_msg, "current_length": total_length, - "max_length": 100000 - } + "max_length": 100000, + }, ) continue if workforce is not None: - if workforce._state.name == 'PAUSED': + if workforce._state.name == "PAUSED": # Resume paused workforce - # subtasks should already # be loaded @@ -1084,27 +1086,26 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): task_lock.add_background_task(task) elif item.action == Action.task_state: # Track completed task results for the end event - task_id = item.data.get('task_id', 'unknown') - task_state = item.data.get('state', 'unknown') - task_result = item.data.get('result', '') + task_id = item.data.get("task_id", "unknown") + task_state = item.data.get("state", "unknown") + task_result = item.data.get("result", "") - if task_state == 'DONE' and task_result: + if task_state == "DONE" and task_result: last_completed_task_result = task_result yield sse_json("task_state", item.data) elif item.action == Action.new_task_state: logger.info("=" * 80) logger.info( - "[LIFECYCLE] NEW_TASK_STATE " - "action received (Multi-turn)", - extra={"project_id": options.project_id} + "[LIFECYCLE] NEW_TASK_STATE action received (Multi-turn)", + extra={"project_id": options.project_id}, ) logger.info("=" * 80) # Log new task state details - new_task_id = item.data.get('task_id', 'unknown') - new_task_state = item.data.get('state', 'unknown') - new_task_result = item.data.get('result', '') + new_task_id = item.data.get("task_id", "unknown") + new_task_state = item.data.get("state", "unknown") + new_task_result = item.data.get("result", "") logger.info( "[LIFECYCLE] New task details" f": task_id={new_task_id}, " @@ -1120,17 +1121,17 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"task {new_task_id}" ) yield sse_json( - "error", { - "message": - "Cannot process new task " + "error", + { + "message": "Cannot process new task " "state: current task not " "initialized." - } + }, ) continue old_task_content: str = camel_task.content - get_result = (get_task_result_with_optional_summary) + get_result = get_task_result_with_optional_summary old_task_result: str = await get_result(camel_task, options) old_task_content_clean: str = old_task_content @@ -1140,27 +1141,29 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): )[-1].strip() task_lock.add_conversation( - 'task_result', { - 'task_content': - old_task_content_clean, - 'task_result': - old_task_result, - 'working_directory': - get_working_directory(options, task_lock) - } + "task_result", + { + "task_content": old_task_content_clean, + "task_result": old_task_result, + "working_directory": get_working_directory( + options, task_lock + ), + }, ) - new_task_content = item.data.get('content', '') + new_task_content = item.data.get("content", "") if new_task_content: import time + task_id = item.data.get( - 'task_id', f"{int(time.time() * 1000)}-multi" + "task_id", f"{int(time.time() * 1000)}-multi" ) new_camel_task = Task(content=new_task_content, id=task_id) - if hasattr( - camel_task, 'additional_info' - ) and camel_task.additional_info: + if ( + hasattr(camel_task, "additional_info") + and camel_task.additional_info + ): new_camel_task.additional_info = ( camel_task.additional_info ) @@ -1216,12 +1219,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): " direct answer without " "workforce" ) - conv_ctx = ( - build_conversation_context( - task_lock, - header='=== Previous ' - 'Conversation ===' - ) + conv_ctx = build_conversation_context( + task_lock, + header="=== Previous Conversation ===", ) simple_answer_prompt = ( f"{conv_ctx}" @@ -1236,10 +1236,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): simple_resp = question_agent.step( simple_answer_prompt ) - if (simple_resp and simple_resp.msgs): - answer_content = ( - simple_resp.msgs[0].content - ) + if simple_resp and simple_resp.msgs: + answer_content = simple_resp.msgs[ + 0 + ].content else: answer_content = ( "I understand your " @@ -1250,17 +1250,18 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): ) task_lock.add_conversation( - 'assistant', answer_content + "assistant", answer_content ) # Send response to user # (don't send confirmed # if simple response) yield sse_json( - "wait_confirm", { + "wait_confirm", + { "content": answer_content, - "question": new_task_content - } + "question": new_task_content, + }, ) except Exception as e: logger.error( @@ -1268,14 +1269,13 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"answer in multi-turn: {e}" ) yield sse_json( - "wait_confirm", { - "content": - "I encountered an error " + "wait_confirm", + { + "content": "I encountered an error " "while processing your " "question.", - "question": - new_task_content - } + "question": new_task_content, + }, ) logger.info( @@ -1321,14 +1321,15 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): stream_state = { "subtasks": [], "seen_ids": set(), - "last_content": "" + "last_content": "", } def on_stream_batch( new_tasks: list[Task], is_final: bool = False ): fresh_tasks = [ - t for t in new_tasks + t + for t in new_tasks if t.id not in stream_state["seen_ids"] ] for t in fresh_tasks: @@ -1337,10 +1338,11 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): def on_stream_text(chunk): try: - has_msg = (hasattr(chunk, 'msg') and chunk.msg) + has_msg = hasattr(chunk, "msg") and chunk.msg accumulated_content = ( chunk.msg.content - if has_msg else str(chunk) + if has_msg + else str(chunk) ) last_content = stream_state["last_content"] @@ -1348,20 +1350,21 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): last_content ): delta_content = accumulated_content[ - len(last_content):] + len(last_content) : + ] else: delta_content = accumulated_content - stream_state["last_content" - ] = accumulated_content + stream_state["last_content"] = ( + accumulated_content + ) if delta_content: asyncio.run_coroutine_threadsafe( task_lock.put_queue( ActionDecomposeTextData( data={ - "project_id": - options.project_id, + "project_id": options.project_id, "task_id": options.task_id, "content": delta_content, } @@ -1402,33 +1405,29 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): summary_task( multi_turn_summary_agent, camel_task ), - timeout=10 + timeout=10, ) logger.info( "Generated LLM summary for multi-turn task", - extra={"project_id": options.project_id} + extra={"project_id": options.project_id}, ) except asyncio.TimeoutError: logger.warning( "Multi-turn summary_task timeout", extra={ "project_id": options.project_id, - "task_id": task_id - } + "task_id": task_id, + }, ) # Fallback to descriptive but not generic summary task_content_for_summary = new_task_content tc = task_content_for_summary if len(tc) > 100: new_summary_content = ( - "Follow-up Task|" - f"{tc[:97]}..." + f"Follow-up Task|{tc[:97]}..." ) else: - new_summary_content = ( - "Follow-up Task|" - f"{tc}" - ) + new_summary_content = f"Follow-up Task|{tc}" except Exception as e: logger.error( "Error generating multi-turn " @@ -1439,14 +1438,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): tc = task_content_for_summary if len(tc) > 100: new_summary_content = ( - "Follow-up Task|" - f"{tc[:97]}..." + f"Follow-up Task|{tc[:97]}..." ) else: - new_summary_content = ( - "Follow-up Task|" - f"{tc}" - ) + new_summary_content = f"Follow-up Task|{tc}" # Emit final subtasks once when # decomposition is complete @@ -1468,13 +1463,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): except Exception as e: import traceback + logger.error( f"[TRACE] Traceback: {traceback.format_exc()}" ) # Continue with existing context if decomposition fails yield sse_json( "error", - {"message": f"Failed to process task: {str(e)}"} + {"message": f"Failed to process task: {str(e)}"}, ) else: if workforce is None: @@ -1501,7 +1497,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "write_file", { "file_path": item.data, - "process_task_id": item.process_task_id + "process_task_id": item.process_task_id, }, ) elif item.action == Action.ask: @@ -1511,7 +1507,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "notice", { "notice": item.data, - "process_task_id": item.process_task_id + "process_task_id": item.process_task_id, }, ) elif item.action == Action.search_mcp: @@ -1525,12 +1521,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"{options.project_id}" ) yield sse_json( - "error", { - "message": - "MCP agent not initialized." + "error", + { + "message": "MCP agent not initialized." " Please start a complex " "task first." - } + }, ) continue task = asyncio.create_task(install_mcp(mcp, item)) @@ -1540,16 +1536,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "terminal", { "output": item.data, - "process_task_id": item.process_task_id + "process_task_id": item.process_task_id, }, ) elif item.action == Action.pause: if workforce is not None: workforce.pause() logger.info( - "Workforce paused for " - "project " - f"{options.project_id}" + f"Workforce paused for project {options.project_id}" ) else: logger.warning( @@ -1561,9 +1555,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): if workforce is not None: workforce.resume() logger.info( - "Workforce resumed for " - "project " - f"{options.project_id}" + f"Workforce resumed for project {options.project_id}" ) else: logger.warning( @@ -1579,8 +1571,8 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): if workforce is not None: workforce.pause() workforce.add_single_agent_worker( - format_agent_description(item), await - new_agent_model(item, options) + format_agent_description(item), + await new_agent_model(item, options), ) workforce.resume() elif item.action == Action.timeout: @@ -1603,15 +1595,16 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): timeout_seconds = item.data.get("timeout_seconds", 0) yield sse_json( - "error", { + "error", + { "message": timeout_message, "type": "timeout", "details": { "in_flight_tasks": in_flight, "pending_tasks": pending, "timeout_seconds": timeout_seconds, - } - } + }, + }, ) elif item.action == Action.end: @@ -1662,9 +1655,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): ) # Use item data as final result # if camel_task is None - final_result: str = str( - item.data - ) if item.data else "Task completed" + final_result: str = ( + str(item.data) if item.data else "Task completed" + ) else: get_result = get_task_result_with_optional_summary final_result: str = await get_result(camel_task, options) @@ -1684,14 +1677,14 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): task_content: str = f"Task {options.task_id}" task_lock.add_conversation( - 'task_result', { - 'task_content': - task_content, - 'task_result': - final_result, - 'working_directory': - get_working_directory(options, task_lock) - } + "task_result", + { + "task_content": task_content, + "task_result": final_result, + "working_directory": get_working_directory( + options, task_lock + ), + }, ) yield sse_json("end", final_result) @@ -1732,7 +1725,6 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"{options.project_id}" ) elif item.action == Action.supplement: - # Check if this might be a misrouted second question if camel_task is None: logger.warning( @@ -1741,13 +1733,13 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): f"project {options.project_id}" ) yield sse_json( - "error", { - "message": - "Cannot supplement task: " + "error", + { + "message": "Cannot supplement task: " "task not initialized. " "Please start a task " "first." - } + }, ) continue else: @@ -1835,18 +1827,21 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): "ModelProcessingError for task " f"{options.task_id}, action " f"{item.action}: {e}", - exc_info=True + exc_info=True, ) yield sse_json("error", {"message": str(e)}) - if "workforce" in locals( - ) and workforce is not None and workforce._running: + if ( + "workforce" in locals() + and workforce is not None + and workforce._running + ): workforce.stop() except Exception as e: logger.error( "Unhandled exception for task " f"{options.task_id}, action " f"{item.action}: {e}", - exc_info=True + exc_info=True, ) yield sse_json("error", {"message": str(e)}) # Continue processing other items instead of breaking @@ -1856,7 +1851,7 @@ async def install_mcp( mcp: ListenChatAgent, install_mcp: ActionInstallMcpData, ): - mcp_keys = list(install_mcp.data.get('mcpServers', {}).keys()) + mcp_keys = list(install_mcp.data.get("mcpServers", {}).keys()) logger.info(f"Installing MCP tools: {mcp_keys}") try: mcp.add_tools(await get_mcp_tools(install_mcp.data)) @@ -1890,23 +1885,24 @@ def tree_sub_tasks(sub_tasks: list[Task], depth: int = 0): return [] result = ( - chain(sub_tasks).filter(lambda x: x.content != "").map( + chain(sub_tasks) + .filter(lambda x: x.content != "") + .map( lambda x: { "id": x.id, "content": x.content, "state": x.state, "subtasks": tree_sub_tasks(x.subtasks, depth + 1), } - ).value() + ) + .value() ) return result def update_sub_tasks( - sub_tasks: list[Task], - update_tasks: dict[str, TaskContent], - depth: int = 0 + sub_tasks: list[Task], update_tasks: dict[str, TaskContent], depth: int = 0 ): if depth > 5: # limit the depth of the recursion return [] @@ -1923,8 +1919,9 @@ def update_sub_tasks( return sub_tasks -def add_sub_tasks(camel_task: Task, - update_tasks: list[TaskContent]) -> list[Task]: +def add_sub_tasks( + camel_task: Task, update_tasks: list[TaskContent] +) -> list[Task]: """Add new tasks (with empty id) to camel_task and return the list of added tasks.""" added_tasks = [] @@ -1940,9 +1937,7 @@ def add_sub_tasks(camel_task: Task, async def question_confirm( - agent: ListenChatAgent, - prompt: str, - task_lock: TaskLock | None = None + agent: ListenChatAgent, prompt: str, task_lock: TaskLock | None = None ) -> bool: """Simple question confirmation - returns True for complex tasks, False for simple questions.""" @@ -1990,14 +1985,10 @@ Is this a complex task? (yes/no):""" normalized = content.strip().lower() is_complex = "yes" in normalized - result_str = ('complex task' if is_complex else 'simple question') + result_str = "complex task" if is_complex else "simple question" logger.info( - "Question confirm result: " - f"{result_str}", - extra={ - "response": content, - "is_complex": is_complex - } + f"Question confirm result: {result_str}", + extra={"response": content, "is_complex": is_complex}, ) return is_complex @@ -2030,7 +2021,7 @@ Do not include any other text or formatting. logger.error( "Error generating task summary", extra={"error": str(e)}, - exc_info=True + exc_info=True, ) raise @@ -2127,7 +2118,7 @@ async def get_task_result_with_optional_summary( async def construct_workforce( - options: Chat + options: Chat, ) -> tuple[Workforce, ListenChatAgent]: """Construct a workforce with all required agents. @@ -2137,10 +2128,7 @@ async def construct_workforce( """ logger.debug( "construct_workforce started", - extra={ - "project_id": options.project_id, - "task_id": options.task_id - } + extra={"project_id": options.project_id, "task_id": options.task_id}, ) # Store main event loop reference for thread-safe async task scheduling @@ -2170,14 +2158,14 @@ async def construct_workforce( ).register_toolkits( NoteTakingToolkit( options.project_id, - working_directory=working_directory + working_directory=working_directory, ) ) ).get_tools() ], - ) for key, prompt in { - Agents.coordinator_agent: - f""" + ) + 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 \ @@ -2190,8 +2178,7 @@ The current date is {datetime.date.today()}. \ For any date-related tasks, you MUST use this as \ the current date. """, - 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 \ @@ -2226,8 +2213,9 @@ the current date. """, options, [ - *HumanToolkit. - get_can_use_tools(options.project_id, Agents.new_worker_agent), + *HumanToolkit.get_can_use_tools( + options.project_id, Agents.new_worker_agent + ), *( ToolkitMessageIntegration( message_handler=HumanToolkit( @@ -2236,7 +2224,7 @@ the current date. ).register_toolkits( NoteTakingToolkit( options.project_id, - working_directory=working_directory + working_directory=working_directory, ) ) ).get_tools(), @@ -2308,7 +2296,8 @@ the current date. task_agent=task_agent, new_worker_agent=new_worker_agent, use_structured_output_handler=False - if model_platform_enum == ModelPlatformType.OPENAI else True, + if model_platform_enum == ModelPlatformType.OPENAI + else True, ) # Register workforce metrics callback @@ -2380,8 +2369,8 @@ async def new_agent_model(data: NewAgent | ActionNewAgent, options: Chat): extra={ "agent_name": data.name, "project_id": options.project_id, - "task_id": options.task_id - } + "task_id": options.task_id, + }, ) logger.debug( "New agent data", extra={"agent_data": data.model_dump_json()} @@ -2425,7 +2414,7 @@ the current date. """ # Pass per-agent custom model config if available - custom_model_config = getattr(data, 'custom_model_config', None) + custom_model_config = getattr(data, "custom_model_config", None) return agent_model( data.name, enhanced_description, diff --git a/backend/app/service/task.py b/backend/app/service/task.py index fc978525..6a6a7ae0 100644 --- a/backend/app/service/task.py +++ b/backend/app/service/task.py @@ -19,11 +19,11 @@ from contextlib import contextmanager from contextvars import ContextVar from datetime import datetime, timedelta from enum import Enum -from typing import Dict, List, Optional +from typing import Literal from camel.tasks import Task from pydantic import BaseModel -from typing_extensions import Any, Literal, TypedDict +from typing_extensions import Any, TypedDict from app.exception.exception import ProgramException from app.model.chat import ( @@ -88,8 +88,10 @@ class ActionUpdateTaskData(BaseModel): class ActionTaskStateData(BaseModel): action: Literal[Action.task_state] = Action.task_state - data: dict[Literal["task_id", "content", "state", "result", - "failure_count"], str | int] + data: dict[ + Literal["task_id", "content", "state", "result", "failure_count"], + str | int, + ] class ActionDecomposeProgressData(BaseModel): @@ -104,8 +106,10 @@ class ActionDecomposeTextData(BaseModel): class ActionNewTaskStateData(BaseModel): action: Literal[Action.new_task_state] = Action.new_task_state - data: dict[Literal["task_id", "content", "state", "result", - "failure_count"], str | int] + data: dict[ + Literal["task_id", "content", "state", "result", "failure_count"], + str | int, + ] class ActionAskData(BaseModel): @@ -126,8 +130,9 @@ class ActionCreateAgentData(BaseModel): class ActionActivateAgentData(BaseModel): action: Literal[Action.activate_agent] = Action.activate_agent - data: dict[Literal["agent_name", "process_task_id", "agent_id", "message"], - str] + data: dict[ + Literal["agent_name", "process_task_id", "agent_id", "message"], str + ] class DataDict(TypedDict): @@ -145,15 +150,22 @@ class ActionDeactivateAgentData(BaseModel): class ActionAssignTaskData(BaseModel): action: Literal[Action.assign_task] = Action.assign_task - data: dict[Literal["assignee_id", "task_id", "content", "state", - "failure_count"], str | int] + data: dict[ + Literal["assignee_id", "task_id", "content", "state", "failure_count"], + str | int, + ] class ActionActivateToolkitData(BaseModel): action: Literal[Action.activate_toolkit] = Action.activate_toolkit data: dict[ - Literal["agent_name", "toolkit_name", "process_task_id", "method_name", - "message"], + Literal[ + "agent_name", + "toolkit_name", + "process_task_id", + "method_name", + "message", + ], str, ] @@ -161,8 +173,13 @@ class ActionActivateToolkitData(BaseModel): class ActionDeactivateToolkitData(BaseModel): action: Literal[Action.deactivate_toolkit] = Action.deactivate_toolkit data: dict[ - Literal["agent_name", "toolkit_name", "process_task_id", "method_name", - "message"], + Literal[ + "agent_name", + "toolkit_name", + "process_task_id", + "method_name", + "message", + ], str, ] @@ -205,8 +222,12 @@ class ActionEndData(BaseModel): class ActionTimeoutData(BaseModel): action: Literal[Action.timeout] = Action.timeout - data: dict[Literal["message", "in_flight_tasks", "pending_tasks", - "timeout_seconds"], str | int] + data: dict[ + Literal[ + "message", "in_flight_tasks", "pending_tasks", "timeout_seconds" + ], + str | int, + ] class ActionSupplementData(BaseModel): @@ -313,15 +334,15 @@ class TaskLock: """Track toolkits for cleanup (e.g., TerminalToolkit venvs)""" # Context management fields - conversation_history: List[Dict[str, Any]] + conversation_history: list[dict[str, Any]] """Store conversation history for context""" last_task_result: str """Store the last task execution result""" - question_agent: Optional[Any] + question_agent: Any | None """Persistent question confirmation agent""" summary_generated: bool """Track if summary has been generated for this project""" - current_task_id: Optional[str] + current_task_id: str | None """Current task ID to be used in SSE responses""" def __init__( @@ -344,20 +365,14 @@ class TaskLock: logger.info( "Task lock initialized", - extra={ - "task_id": id, - "created_at": self.created_at.isoformat() - } + extra={"task_id": id, "created_at": self.created_at.isoformat()}, ) async def put_queue(self, data: ActionData): self.last_accessed = datetime.now() logger.debug( "Adding item to task queue", - extra={ - "task_id": self.id, - "action": data.action - } + extra={"task_id": self.id, "action": data.action}, ) await self.queue.put(data) @@ -374,27 +389,21 @@ class TaskLock: extra={ "task_id": self.id, "agent": agent, - "has_data": data is not None - } + "has_data": data is not None, + }, ) await self.human_input[agent].put(data) async def get_human_input(self, agent: str): logger.debug( - "Getting human input", extra={ - "task_id": self.id, - "agent": agent - } + "Getting human input", extra={"task_id": self.id, "agent": agent} ) return await self.human_input[agent].get() def add_human_input_listen(self, agent: str): logger.debug( "Adding human input listener", - extra={ - "task_id": self.id, - "agent": agent - } + extra={"task_id": self.id, "agent": agent}, ) self.human_input[agent] = asyncio.Queue(1) @@ -404,8 +413,8 @@ class TaskLock: "Adding background task", extra={ "task_id": self.id, - "background_tasks_count": len(self.background_tasks) - } + "background_tasks_count": len(self.background_tasks), + }, ) self.background_tasks.add(task) task.add_done_callback(lambda t: self.background_tasks.discard(t)) @@ -416,8 +425,8 @@ class TaskLock: "Starting task lock cleanup", extra={ "task_id": self.id, - "background_tasks_count": len(self.background_tasks) - } + "background_tasks_count": len(self.background_tasks), + }, ) for task in list(self.background_tasks): if not task.done(): @@ -431,22 +440,22 @@ class TaskLock: # Clean up registered toolkits (e.g., remove TerminalToolkit venvs) for toolkit in self.registered_toolkits: try: - if hasattr(toolkit, 'cleanup'): + if hasattr(toolkit, "cleanup"): toolkit.cleanup() logger.info( "Toolkit cleanup completed", extra={ "task_id": self.id, - "toolkit": type(toolkit).__name__ - } + "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__ - } + "toolkit": type(toolkit).__name__, + }, ) self.registered_toolkits.clear() @@ -464,10 +473,7 @@ class TaskLock: 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__ - } + extra={"task_id": self.id, "toolkit": type(toolkit).__name__}, ) return @@ -477,8 +483,8 @@ class TaskLock: extra={ "task_id": self.id, "toolkit": type(toolkit).__name__, - "total_registered": len(self.registered_toolkits) - } + "total_registered": len(self.registered_toolkits), + }, ) def add_conversation(self, role: str, content: str | dict): @@ -488,14 +494,14 @@ class TaskLock: extra={ "task_id": self.id, "role": role, - "content_length": len(str(content)) - } + "content_length": len(str(content)), + }, ) self.conversation_history.append( { - 'role': role, - 'content': content, - 'timestamp': datetime.now().isoformat() + "role": role, + "content": content, + "timestamp": datetime.now().isoformat(), } ) @@ -539,10 +545,7 @@ def set_current_task_id(project_id: str, task_id: str) -> None: task_lock.current_task_id = task_id logger.info( "Updated current task ID", - extra={ - "project_id": project_id, - "task_id": task_id - } + extra={"project_id": project_id, "task_id": task_id}, ) @@ -550,7 +553,7 @@ def create_task_lock(id: str) -> TaskLock: if id in task_locks: logger.warning( "Attempting to create task lock that already exists", - extra={"task_id": id} + extra={"task_id": id}, ) raise ProgramException("Task already exists") @@ -564,10 +567,7 @@ def create_task_lock(id: str) -> TaskLock: logger.info( "Task lock created successfully", - extra={ - "task_id": id, - "total_task_locks": len(task_locks) - } + extra={"task_id": id, "total_task_locks": len(task_locks)}, ) return task_locks[id] @@ -585,7 +585,7 @@ async def delete_task_lock(id: str): if id not in task_locks: logger.warning( "Attempting to delete non-existent task lock", - extra={"task_id": id} + extra={"task_id": id}, ) raise ProgramException("Task not found") @@ -595,18 +595,15 @@ async def delete_task_lock(id: str): "Cleaning up task lock", extra={ "task_id": id, - "background_tasks": len(task_lock.background_tasks) - } + "background_tasks": len(task_lock.background_tasks), + }, ) await task_lock.cleanup() del task_locks[id] logger.info( "Task lock deleted successfully", - extra={ - "task_id": id, - "remaining_task_locks": len(task_locks) - } + extra={"task_id": id, "remaining_task_locks": len(task_locks)}, ) diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/utils/__init__.py +++ b/backend/app/utils/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/utils/cookie_manager.py b/backend/app/utils/cookie_manager.py index 4111047a..8e72cf87 100644 --- a/backend/app/utils/cookie_manager.py +++ b/backend/app/utils/cookie_manager.py @@ -12,12 +12,12 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import sqlite3 -import os -from typing import Any, List, Dict, Optional import logging +import os import shutil +import sqlite3 from datetime import datetime +from typing import Any logger = logging.getLogger("cookie_manager") @@ -30,11 +30,15 @@ class CookieManager: self.user_data_dir = user_data_dir # Check for cookies in partition directory first (for persist:user_login) - partition_cookies_path = os.path.join(user_data_dir, "Partitions", "user_login", "Cookies") + partition_cookies_path = os.path.join( + user_data_dir, "Partitions", "user_login", "Cookies" + ) if os.path.exists(partition_cookies_path): self.cookies_db_path = partition_cookies_path - logger.info(f"Using partition cookies at: {partition_cookies_path}") + logger.info( + f"Using partition cookies at: {partition_cookies_path}" + ) else: # Fallback to default location self.cookies_db_path = os.path.join(user_data_dir, "Cookies") @@ -44,12 +48,16 @@ class CookieManager: if os.path.exists(alt_path): self.cookies_db_path = alt_path else: - logger.warning(f"Cookies database not found at {self.cookies_db_path} or {partition_cookies_path}") + logger.warning( + f"Cookies database not found at {self.cookies_db_path} or {partition_cookies_path}" + ) - def _get_cookies_connection(self) -> Optional[sqlite3.Connection]: + def _get_cookies_connection(self) -> sqlite3.Connection | None: """Get database connection using a temporary copy to avoid locks""" if not os.path.exists(self.cookies_db_path): - logger.warning(f"Cookies database not found: {self.cookies_db_path}") + logger.warning( + f"Cookies database not found: {self.cookies_db_path}" + ) return None temp_db_path = self.cookies_db_path + ".tmp" @@ -82,7 +90,7 @@ class CookieManager: except Exception as e: logger.debug(f"Error cleaning up temp database: {e}") - def get_cookie_domains(self) -> List[Dict[str, Any]]: + def get_cookie_domains(self) -> list[dict[str, Any]]: """Get list of all domains with cookies""" conn = self._get_cookies_connection() if not conn: @@ -105,21 +113,27 @@ class CookieManager: domains = [] for row in rows: try: - chrome_timestamp = row['last_access'] + chrome_timestamp = row["last_access"] if chrome_timestamp: - seconds_since_epoch = (chrome_timestamp / 1000000.0) - 11644473600 - last_access = datetime.fromtimestamp(seconds_since_epoch).strftime('%Y-%m-%d %H:%M:%S') + seconds_since_epoch = ( + chrome_timestamp / 1000000.0 + ) - 11644473600 + last_access = datetime.fromtimestamp( + seconds_since_epoch + ).strftime("%Y-%m-%d %H:%M:%S") else: last_access = "Never" except Exception as e: logger.debug(f"Error converting timestamp: {e}") last_access = "Unknown" - domains.append({ - 'domain': row['domain'], - 'cookie_count': row['cookie_count'], - 'last_access': last_access - }) + domains.append( + { + "domain": row["domain"], + "cookie_count": row["cookie_count"], + "last_access": last_access, + } + ) logger.info(f"Found {len(domains)} domains with cookies") return domains @@ -131,7 +145,7 @@ class CookieManager: conn.close() self._cleanup_temp_db() - def get_cookies_for_domain(self, domain: str) -> List[Dict[str, str]]: + def get_cookies_for_domain(self, domain: str) -> list[dict[str, str]]: """Get all cookies for a specific domain""" conn = self._get_cookies_connection() if not conn: @@ -152,26 +166,28 @@ class CookieManager: WHERE host_key = ? OR host_key LIKE ? ORDER BY name """ - cursor.execute(query, (domain, f'%.{domain}')) + cursor.execute(query, (domain, f"%.{domain}")) rows = cursor.fetchall() cookies = [] for row in rows: - raw_value = row['value'] + raw_value = row["value"] if raw_value is None: value_str = "" elif len(raw_value) > 50: value_str = raw_value[:50] + "..." else: value_str = raw_value - cookies.append({ - 'domain': row['host_key'], - 'name': row['name'], - 'value': value_str, - 'path': row['path'], - 'secure': bool(row['is_secure']), - 'httponly': bool(row['is_httponly']) - }) + cookies.append( + { + "domain": row["host_key"], + "name": row["name"], + "value": value_str, + "path": row["path"], + "secure": bool(row["is_secure"]), + "httponly": bool(row["is_httponly"]), + } + ) return cookies @@ -185,7 +201,9 @@ class CookieManager: def delete_cookies_for_domain(self, domain: str) -> bool: """Delete all cookies for a specific domain""" if not os.path.exists(self.cookies_db_path): - logger.warning(f"Cookies database not found: {self.cookies_db_path}") + logger.warning( + f"Cookies database not found: {self.cookies_db_path}" + ) return False try: @@ -195,7 +213,7 @@ class CookieManager: DELETE FROM cookies WHERE host_key = ? OR host_key LIKE ? """ - cursor.execute(delete_query, (domain, f'%.{domain}')) + cursor.execute(delete_query, (domain, f"%.{domain}")) deleted_count = cursor.rowcount conn.commit() @@ -218,9 +236,9 @@ class CookieManager: def _cleanup_wal_files(self): """Remove SQLite WAL and SHM files""" try: - wal_path = self.cookies_db_path + '-wal' - shm_path = self.cookies_db_path + '-shm' - journal_path = self.cookies_db_path + '-journal' + wal_path = self.cookies_db_path + "-wal" + shm_path = self.cookies_db_path + "-shm" + journal_path = self.cookies_db_path + "-journal" for path in [wal_path, shm_path, journal_path]: if os.path.exists(path): @@ -232,7 +250,9 @@ class CookieManager: def delete_all_cookies(self) -> bool: """Delete all cookies""" if not os.path.exists(self.cookies_db_path): - logger.warning(f"Cookies database not found: {self.cookies_db_path}") + logger.warning( + f"Cookies database not found: {self.cookies_db_path}" + ) return False try: @@ -258,11 +278,12 @@ class CookieManager: logger.error(f"Error deleting all cookies: {e}") return False - def search_cookies(self, keyword: str) -> List[Dict[str, Any]]: + def search_cookies(self, keyword: str) -> list[dict[str, Any]]: """Search cookies by domain keyword""" domains = self.get_cookie_domains() keyword_lower = keyword.lower() return [ - domain for domain in domains - if keyword_lower in (domain['domain'] or '').lower() + domain + for domain in domains + if keyword_lower in (domain["domain"] or "").lower() ] diff --git a/backend/app/utils/file_utils.py b/backend/app/utils/file_utils.py index db1d90c0..b5cc7879 100644 --- a/backend/app/utils/file_utils.py +++ b/backend/app/utils/file_utils.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - """File system utilities.""" from app.component.environment import env @@ -26,9 +25,14 @@ def get_working_directory(options: Chat, task_lock=None) -> str: """ if not task_lock: from app.service.task import get_task_lock_if_exists + task_lock = get_task_lock_if_exists(options.project_id) - - if task_lock and hasattr(task_lock, 'new_folder_path') and task_lock.new_folder_path: + + if ( + task_lock + and hasattr(task_lock, "new_folder_path") + and task_lock.new_folder_path + ): return str(task_lock.new_folder_path) else: - return env("file_save_path", options.file_save_path()) \ No newline at end of file + return env("file_save_path", options.file_save_path()) diff --git a/backend/app/utils/listen/__init__.py b/backend/app/utils/listen/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/utils/listen/__init__.py +++ b/backend/app/utils/listen/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/utils/listen/toolkit_listen.py b/backend/app/utils/listen/toolkit_listen.py index 071a1d6e..b71765ec 100644 --- a/backend/app/utils/listen/toolkit_listen.py +++ b/backend/app/utils/listen/toolkit_listen.py @@ -17,14 +17,18 @@ import json import logging import queue import threading +from collections.abc import Callable from datetime import datetime from functools import wraps from inspect import iscoroutinefunction, signature -from typing import Any, Callable, Type, TypeVar +from typing import Any, TypeVar -from app.service.task import (ActionActivateToolkitData, - ActionDeactivateToolkitData, get_task_lock, - process_task) +from app.service.task import ( + ActionActivateToolkitData, + ActionDeactivateToolkitData, + get_task_lock, + process_task, +) from app.utils.toolkit.abstract_toolkit import AbstractToolkit logger = logging.getLogger("toolkit_listen") @@ -35,8 +39,10 @@ MAX_LENGTH = 500 def _truncate(text: str, max_length: int = MAX_LENGTH) -> str: """Truncate text if it exceeds max_length.""" if len(text) > max_length: - return (f"{text[:max_length]}... " - f"(truncated, total length: {len(text)} chars)") + return ( + f"{text[:max_length]}... " + f"(truncated, total length: {len(text)} chars)" + ) return text @@ -98,11 +104,13 @@ def _get_context( # Multi-layer fallback to get process_task_id process_task_id = process_task.get("") if not process_task_id: - process_task_id = getattr(toolkit, 'api_task_id', "") + process_task_id = getattr(toolkit, "api_task_id", "") if not process_task_id: - logger.warning(f"[toolkit_listen] Both ContextVar process_task " - f"and toolkit.api_task_id are empty for " - f"{toolkit_name}.{method_name}") + logger.warning( + f"[toolkit_listen] Both ContextVar process_task " + f"and toolkit.api_task_id are empty for " + f"{toolkit_name}.{method_name}" + ) return toolkit_name, method_name, process_task_id, skip_workflow_display @@ -122,7 +130,8 @@ def _create_activate_data( "toolkit_name": toolkit_name, "method_name": method_name, "message": args_str, - }) + } + ) def _create_deactivate_data( @@ -140,7 +149,8 @@ def _create_deactivate_data( "toolkit_name": toolkit_name, "method_name": method_name, "message": res_msg, - }) + } + ) def _log_deactivate( @@ -153,10 +163,12 @@ def _log_deactivate( """Log toolkit deactivation.""" status = "ERROR" if error is not None else "SUCCESS" timestamp = datetime.now().isoformat() - logger.info(f"[TOOLKIT DEACTIVATE] Toolkit: {toolkit_name} | " - f"Method: {method_name} | Task ID: {process_task_id} | " - f"Agent: {agent_name} | Status: {status} | " - f"Timestamp: {timestamp}") + logger.info( + f"[TOOLKIT DEACTIVATE] Toolkit: {toolkit_name} | " + f"Method: {method_name} | Task ID: {process_task_id} | " + f"Agent: {agent_name} | Status: {status} | " + f"Timestamp: {timestamp}" + ) def _safe_put_queue(task_lock, data): @@ -209,10 +221,13 @@ def _safe_put_queue(task_lock, data): status, error = result_queue.get(timeout=1.0) if status == "error": logger.error( - f"[SAFE_PUT_QUEUE] Thread execution failed: {error}") + f"[SAFE_PUT_QUEUE] Thread execution failed: {error}" + ) except queue.Empty: - logger.warning(f"[SAFE_PUT_QUEUE] Thread timeout after 1s " - f"for {data.__class__.__name__}") + logger.warning( + f"[SAFE_PUT_QUEUE] Thread timeout after 1s " + f"for {data.__class__.__name__}" + ) except Exception as e: logger.error(f"[SAFE_PUT_QUEUE] Failed to send data to queue: {e}") @@ -268,10 +283,11 @@ def listen_toolkit( @wraps(wrap) async def async_wrapper(*args, **kwargs): toolkit: AbstractToolkit = args[0] - if not hasattr(toolkit, 'api_task_id'): + if not hasattr(toolkit, "api_task_id"): logger.warning( f"[listen_toolkit] {toolkit.__class__.__name__} " - f"missing api_task_id, calling method directly") + f"missing api_task_id, calling method directly" + ) return await func(*args, **kwargs) task_lock = get_task_lock(toolkit.api_task_id) @@ -281,8 +297,12 @@ def listen_toolkit( if not skip: activate_data = _create_activate_data( - toolkit, toolkit_name, method_name, process_task_id, - args_str) + toolkit, + toolkit_name, + method_name, + process_task_id, + args_str, + ) await task_lock.put_queue(activate_data) error = None @@ -293,13 +313,22 @@ def listen_toolkit( error = e res_msg = _format_result(res, error, return_msg) - _log_deactivate(toolkit_name, method_name, process_task_id, - toolkit.agent_name, error) + _log_deactivate( + toolkit_name, + method_name, + process_task_id, + toolkit.agent_name, + error, + ) if not skip: deactivate_data = _create_deactivate_data( - toolkit, toolkit_name, method_name, process_task_id, - res_msg) + toolkit, + toolkit_name, + method_name, + process_task_id, + res_msg, + ) await task_lock.put_queue(deactivate_data) if error is not None: @@ -314,10 +343,11 @@ def listen_toolkit( @wraps(wrap) def sync_wrapper(*args, **kwargs): toolkit: AbstractToolkit = args[0] - if not hasattr(toolkit, 'api_task_id'): + if not hasattr(toolkit, "api_task_id"): logger.warning( f"[listen_toolkit] {toolkit.__class__.__name__} " - f"missing api_task_id, calling method directly") + f"missing api_task_id, calling method directly" + ) return func(*args, **kwargs) task_lock = get_task_lock(toolkit.api_task_id) @@ -327,8 +357,12 @@ def listen_toolkit( if not skip: activate_data = _create_activate_data( - toolkit, toolkit_name, method_name, process_task_id, - args_str) + toolkit, + toolkit_name, + method_name, + process_task_id, + args_str, + ) _safe_put_queue(task_lock, activate_data) error = None @@ -342,7 +376,8 @@ def listen_toolkit( f"Async function {func.__name__} " f"was incorrectly called in sync context. " f"This is a bug - the function should be marked " - f"as async or should not return a coroutine.") + f"as async or should not return a coroutine." + ) logger.error(f"[listen_toolkit] {error_msg}") res.close() raise TypeError(error_msg) @@ -350,13 +385,22 @@ def listen_toolkit( error = e res_msg = _format_result(res, error, return_msg) - _log_deactivate(toolkit_name, method_name, process_task_id, - toolkit.agent_name, error) + _log_deactivate( + toolkit_name, + method_name, + process_task_id, + toolkit.agent_name, + error, + ) if not skip: deactivate_data = _create_deactivate_data( - toolkit, toolkit_name, method_name, process_task_id, - res_msg) + toolkit, + toolkit_name, + method_name, + process_task_id, + res_msg, + ) _safe_put_queue(task_lock, deactivate_data) if error is not None: @@ -369,26 +413,27 @@ def listen_toolkit( return decorator -T = TypeVar('T') +T = TypeVar("T") # Methods that should not be wrapped by auto_listen_toolkit # These are utility/helper methods that don't perform actual tool operations EXCLUDED_METHODS = { - 'get_tools', # Tool enumeration - 'get_can_use_tools', # Tool filtering - 'toolkit_name', # Metadata getter - 'run_mcp_server', # MCP server initialization - 'model_dump', # Pydantic model serialization - 'model_dump_json', # Pydantic model serialization - 'dict', # Pydantic legacy dict method - 'json', # Pydantic legacy json method - 'copy', # Object copying - 'update', # Object update + "get_tools", # Tool enumeration + "get_can_use_tools", # Tool filtering + "toolkit_name", # Metadata getter + "run_mcp_server", # MCP server initialization + "model_dump", # Pydantic model serialization + "model_dump_json", # Pydantic model serialization + "dict", # Pydantic legacy dict method + "json", # Pydantic legacy json method + "copy", # Object copying + "update", # Object update } def auto_listen_toolkit( - base_toolkit_class: Type[T]) -> Callable[[Type[T]], Type[T]]: + base_toolkit_class: type[T], +) -> Callable[[type[T]], type[T]]: """ Class decorator that automatically wraps all public methods from the base toolkit with the @listen_toolkit decorator. @@ -409,12 +454,11 @@ def auto_listen_toolkit( agent_name: str = Agents.document_agent """ - def class_decorator(cls: Type[T]) -> Type[T]: - + def class_decorator(cls: type[T]) -> type[T]: base_methods = {} for name in dir(base_toolkit_class): # Skip private methods and excluded helper methods - if not name.startswith('_') and name not in EXCLUDED_METHODS: + if not name.startswith("_") and name not in EXCLUDED_METHODS: attr = getattr(base_toolkit_class, name) if callable(attr): base_methods[name] = attr @@ -429,8 +473,9 @@ def auto_listen_toolkit( # 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) + is_already_decorated = getattr( + overridden_method, "__listen_toolkit__", False + ) if is_already_decorated: # Already has @listen_toolkit, skip @@ -438,25 +483,28 @@ def auto_listen_toolkit( # Not decorated, wrap the overridden method decorated_override = listen_toolkit(base_method)( - overridden_method) + overridden_method + ) setattr(cls, method_name, decorated_override) continue sig = signature(base_method) - def create_wrapper(method_name: str, - base_method: Callable) -> Callable: + def create_wrapper( + method_name: str, base_method: Callable + ) -> Callable: # Unwrap decorators to check the actual function unwrapped_method = base_method - while hasattr(unwrapped_method, '__wrapped__'): + while hasattr(unwrapped_method, "__wrapped__"): unwrapped_method = unwrapped_method.__wrapped__ # Check if the unwrapped method is a coroutine function if iscoroutinefunction(unwrapped_method): async def async_method_wrapper(self, *args, **kwargs): - return await getattr(super(cls, self), - method_name)(*args, **kwargs) + return await getattr(super(cls, self), method_name)( + *args, **kwargs + ) async_method_wrapper.__name__ = method_name async_method_wrapper.__signature__ = sig @@ -464,8 +512,9 @@ def auto_listen_toolkit( else: def sync_method_wrapper(self, *args, **kwargs): - return getattr(super(cls, self), method_name)(*args, - **kwargs) + return getattr(super(cls, self), method_name)( + *args, **kwargs + ) sync_method_wrapper.__name__ = method_name sync_method_wrapper.__signature__ = sig diff --git a/backend/app/utils/oauth_state_manager.py b/backend/app/utils/oauth_state_manager.py index 1798c865..1fe90af2 100644 --- a/backend/app/utils/oauth_state_manager.py +++ b/backend/app/utils/oauth_state_manager.py @@ -11,17 +11,20 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - """ OAuth authorization state manager for background authorization flows """ -import threading -from typing import Dict, Optional, Literal, Any -from datetime import datetime + import logging +import threading +from datetime import datetime +from typing import Any, Literal + logger = logging.getLogger("main") -AuthStatus = Literal["pending", "authorizing", "success", "failed", "cancelled"] +AuthStatus = Literal[ + "pending", "authorizing", "success", "failed", "cancelled" +] class OAuthState: @@ -30,42 +33,46 @@ class OAuthState: def __init__(self, provider: str): self.provider = provider self.status: AuthStatus = "pending" - self.error: Optional[str] = None - self.thread: Optional[threading.Thread] = None - self.result: Optional[Any] = None + self.error: str | None = None + self.thread: threading.Thread | None = None + self.result: Any | None = None self.started_at = datetime.now() - self.completed_at: Optional[datetime] = None + self.completed_at: datetime | None = None self._cancel_event = threading.Event() - self.server = None # Store the local server instance for forced shutdown - + self.server = ( + None # Store the local server instance for forced shutdown + ) + def is_cancelled(self) -> bool: """Check if cancellation has been requested""" return self._cancel_event.is_set() - + def cancel(self): """Request cancellation of the authorization flow""" self._cancel_event.set() self.status = "cancelled" self.completed_at = datetime.now() - - def to_dict(self) -> Dict: + + def to_dict(self) -> dict: """Convert state to dictionary for API response""" return { "provider": self.provider, "status": self.status, "error": self.error, "started_at": self.started_at.isoformat(), - "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "completed_at": self.completed_at.isoformat() + if self.completed_at + else None, } class OAuthStateManager: """Manager for tracking OAuth authorization flows""" - + def __init__(self): - self._states: Dict[str, OAuthState] = {} + self._states: dict[str, OAuthState] = {} self._lock = threading.Lock() - + def create_state(self, provider: str) -> OAuthState: """Create a new OAuth state for a provider""" with self._lock: @@ -75,22 +82,22 @@ class OAuthStateManager: if old_state.status in ["pending", "authorizing"]: old_state.cancel() logger.info(f"Cancelled previous {provider} authorization") - + state = OAuthState(provider) self._states[provider] = state return state - - def get_state(self, provider: str) -> Optional[OAuthState]: + + def get_state(self, provider: str) -> OAuthState | None: """Get the current state for a provider""" with self._lock: return self._states.get(provider) - + def update_status( self, provider: str, status: AuthStatus, - error: Optional[str] = None, - result: Optional[Any] = None + error: str | None = None, + result: Any | None = None, ): """Update the status of an authorization flow""" with self._lock: @@ -102,7 +109,7 @@ class OAuthStateManager: if status in ["success", "failed", "cancelled"]: state.completed_at = datetime.now() logger.info(f"Updated {provider} OAuth status to {status}") - + + # Global instance oauth_state_manager = OAuthStateManager() - diff --git a/backend/app/utils/server/__init__.py b/backend/app/utils/server/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/utils/server/__init__.py +++ b/backend/app/utils/server/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/utils/server/sync_step.py b/backend/app/utils/server/sync_step.py index 1078dc97..ec2d8f51 100644 --- a/backend/app/utils/server/sync_step.py +++ b/backend/app/utils/server/sync_step.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - """ Cloud sync step decorator. @@ -24,6 +23,7 @@ Config (~/.eigent/.env): import asyncio import json +import logging import time from functools import lru_cache @@ -31,11 +31,9 @@ import httpx from app.component.environment import env from app.service.task import get_task_lock_if_exists -import logging logger = logging.getLogger("sync_step") - # Batch config for decompose_text events BATCH_WORD_THRESHOLD = 5 @@ -46,26 +44,26 @@ _text_buffers: dict[str, str] = {} @lru_cache(maxsize=1) def _get_config(): server_url = env("SERVER_URL", "") - + if not server_url: return None - + return f"{server_url.rstrip('/')}/chat/steps" def sync_step(func): async def wrapper(*args, **kwargs): config = _get_config() - + if not config: async for value in func(*args, **kwargs): yield value return - + async for value in func(*args, **kwargs): _try_sync(args, value, config) yield value - + return wrapper @@ -73,31 +71,31 @@ def _try_sync(args, value, sync_url): data = _parse_value(value) if not data: return - + task_id = _get_task_id(args) if not task_id: return - + step = data.get("step") - + # Batch decompose_text events to reduce API calls if step == "decompose_text": _buffer_text(task_id, data["data"].get("content", "")) if _should_flush(task_id): _flush_buffer(task_id, sync_url) return - + # Flush any buffered text before sending other events (preserves order) if task_id in _text_buffers: _flush_buffer(task_id, sync_url) - + payload = { "task_id": task_id, "step": step, "data": data["data"], "timestamp": time.time_ns() / 1_000_000_000, } - + asyncio.create_task(_send(sync_url, payload)) @@ -120,28 +118,28 @@ def _flush_buffer(task_id: str, sync_url: str): text = _text_buffers.pop(task_id, "") if not text: return - + payload = { "task_id": task_id, "step": "decompose_text", "data": {"content": text}, "timestamp": time.time_ns() / 1_000_000_000, } - + asyncio.create_task(_send(sync_url, payload)) def _parse_value(value): if isinstance(value, str) and value.startswith("data: "): value = value[6:].strip() - + try: data = json.loads(value) if "step" in data and "data" in data: return data except (json.JSONDecodeError, TypeError): pass - + return None diff --git a/backend/app/utils/single_agent_worker.py b/backend/app/utils/single_agent_worker.py index 2e82a4a4..7a4ec481 100644 --- a/backend/app/utils/single_agent_worker.py +++ b/backend/app/utils/single_agent_worker.py @@ -13,18 +13,19 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import datetime +import logging + from camel.agents.chat_agent import AsyncStreamingChatAgentResponse +from camel.societies.workforce.prompts import PROCESS_TASK_PROMPT from camel.societies.workforce.single_agent_worker import ( SingleAgentWorker as BaseSingleAgentWorker, ) +from camel.societies.workforce.utils import TaskResult from camel.tasks.task import Task, TaskState, is_task_result_insufficient -import logging +from camel.utils.context_utils import ContextUtility +from colorama import Fore from app.agent.listen_chat_agent import ListenChatAgent -from camel.societies.workforce.prompts import PROCESS_TASK_PROMPT -from colorama import Fore -from camel.societies.workforce.utils import TaskResult -from camel.utils.context_utils import ContextUtility logger = logging.getLogger("single_agent_worker") diff --git a/backend/app/utils/telemetry/workforce_metrics.py b/backend/app/utils/telemetry/workforce_metrics.py index df7d2aae..15b099e2 100644 --- a/backend/app/utils/telemetry/workforce_metrics.py +++ b/backend/app/utils/telemetry/workforce_metrics.py @@ -17,21 +17,25 @@ import json import logging import os import re -from typing import Any, Dict +from typing import Any import camel -from camel.societies.workforce.events import (LogEvent, TaskAssignedEvent, - TaskCompletedEvent, - TaskCreatedEvent, - TaskDecomposedEvent, - TaskFailedEvent, - TaskStartedEvent, - TaskUpdatedEvent, - WorkerCreatedEvent) +from camel.societies.workforce.events import ( + LogEvent, + TaskAssignedEvent, + TaskCompletedEvent, + TaskCreatedEvent, + TaskDecomposedEvent, + TaskFailedEvent, + TaskStartedEvent, + TaskUpdatedEvent, + WorkerCreatedEvent, +) from camel.societies.workforce.workforce_metrics import WorkforceMetrics from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.http.trace_exporter import \ - OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter, +) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -122,8 +126,9 @@ def initialize_tracer_provider() -> None: # Get configuration from environment langfuse_public_key = os.getenv(ENV_LANGFUSE_PUBLIC_KEY) langfuse_secret_key = os.getenv(ENV_LANGFUSE_SECRET_KEY) - langfuse_base_url = os.getenv(ENV_LANGFUSE_BASE_URL, - DEFAULT_LANGFUSE_BASE_URL) + langfuse_base_url = os.getenv( + ENV_LANGFUSE_BASE_URL, DEFAULT_LANGFUSE_BASE_URL + ) # Create resource with service information resource = Resource(attributes={SERVICE_NAME: SERVICE_NAME_WORKFORCE}) @@ -137,8 +142,9 @@ def initialize_tracer_provider() -> None: # Set environment variables for OTLP exporter endpoint_url = _create_langfuse_endpoint(langfuse_base_url) os.environ[ENV_OTEL_EXPORTER_OTLP_ENDPOINT] = endpoint_url - auth_header = _create_basic_auth_header(langfuse_public_key, - langfuse_secret_key) + auth_header = _create_basic_auth_header( + langfuse_public_key, langfuse_secret_key + ) os.environ[ENV_OTEL_EXPORTER_OTLP_HEADERS] = auth_header # Create exporter using environment variables @@ -174,7 +180,8 @@ def get_tracer_provider() -> TracerProvider: if _GLOBAL_TRACER_PROVIDER is None: raise RuntimeError( "TracerProvider not initialized. " - "Call initialize_tracer_provider() during app startup.") + "Call initialize_tracer_provider() during app startup." + ) return _GLOBAL_TRACER_PROVIDER @@ -254,10 +261,12 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Get tracer from the shared provider # Use CAMEL version for instrumentation versioning - self.tracer = provider.get_tracer(TRACER_NAME_WORKFORCE, - camel.__version__) + self.tracer = provider.get_tracer( + TRACER_NAME_WORKFORCE, camel.__version__ + ) self.root_span = self.tracer.start_span( - f"{SPAN_WORKFORCE_EXECUTION}:{task_id}") + f"{SPAN_WORKFORCE_EXECUTION}:{task_id}" + ) # Langfuse-specific attributes self.root_span.set_attribute(ATTR_LANGFUSE_SESSION_ID, project_id) tags = json.dumps(DEFAULT_LANGFUSE_TAGS.copy()) @@ -272,11 +281,13 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Track quality scores (task_id -> quality_score) self.task_quality_scores = {} - def log_worker_created(self, - event: WorkerCreatedEvent, - agent_class: str = None, - model_type: str = None, - **kwargs) -> None: + def log_worker_created( + self, + event: WorkerCreatedEvent, + agent_class: str = None, + model_type: str = None, + **kwargs, + ) -> None: """Log worker creation as a span. Args: @@ -290,8 +301,9 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Create span as child of root span using context ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_WORKER_CREATED, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_WORKER_CREATED, context=ctx + ) as span: # Eigent-specific attributes span.set_attribute(ATTR_WORKER_ID, event.worker_id) span.set_attribute(ATTR_WORKER_TYPE, event.worker_type) @@ -314,8 +326,9 @@ class WorkforceMetricsCallback(WorkforceMetrics): return ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_TASK_CREATED, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_TASK_CREATED, context=ctx + ) as span: span.set_attribute(ATTR_TASK_ID, event.task_id) span.set_attribute(ATTR_TASK_DESCRIPTION, event.description) span.set_attribute(ATTR_PROJECT_ID, self.project_id) @@ -337,14 +350,16 @@ class WorkforceMetricsCallback(WorkforceMetrics): return ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_TASK_DECOMPOSED, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_TASK_DECOMPOSED, context=ctx + ) as span: span.set_attribute(ATTR_TASK_PARENT_ID, event.parent_task_id) span.set_attribute(ATTR_PROJECT_ID, self.project_id) if event.subtask_ids: - span.set_attribute(ATTR_TASK_SUBTASK_IDS, - json.dumps(event.subtask_ids)) + span.set_attribute( + ATTR_TASK_SUBTASK_IDS, json.dumps(event.subtask_ids) + ) span.set_status(Status(StatusCode.OK)) @@ -358,15 +373,17 @@ class WorkforceMetricsCallback(WorkforceMetrics): return ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_TASK_ASSIGNED, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_TASK_ASSIGNED, context=ctx + ) as span: span.set_attribute(ATTR_TASK_ID, event.task_id) span.set_attribute(ATTR_WORKER_ID, event.worker_id) span.set_attribute(ATTR_PROJECT_ID, self.project_id) if event.queue_time_seconds is not None: - span.set_attribute(ATTR_TASK_QUEUE_TIME_SECONDS, - event.queue_time_seconds) + span.set_attribute( + ATTR_TASK_QUEUE_TIME_SECONDS, event.queue_time_seconds + ) # Add dependencies as JSON array if event.dependencies: @@ -385,8 +402,9 @@ class WorkforceMetricsCallback(WorkforceMetrics): return ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_TASK_UPDATED, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_TASK_UPDATED, context=ctx + ) as span: span.set_attribute(ATTR_TASK_ID, event.task_id) span.set_attribute(ATTR_PROJECT_ID, self.project_id) span.set_attribute(ATTR_TASK_UPDATE_TYPE, event.update_type) @@ -396,17 +414,17 @@ class WorkforceMetricsCallback(WorkforceMetrics): if event.parent_task_id: span.set_attribute(ATTR_TASK_PARENT_ID, event.parent_task_id) if event.old_value is not None: - span.set_attribute(ATTR_TASK_UPDATE_OLD_VALUE, - event.old_value) + span.set_attribute(ATTR_TASK_UPDATE_OLD_VALUE, event.old_value) if event.new_value is not None: - span.set_attribute(ATTR_TASK_UPDATE_NEW_VALUE, - event.new_value) + span.set_attribute(ATTR_TASK_UPDATE_NEW_VALUE, event.new_value) if event.metadata: - span.set_attribute(ATTR_TASK_UPDATE_METADATA, - json.dumps(event.metadata)) - if hasattr(event, 'timestamp') and event.timestamp: - span.set_attribute(ATTR_TASK_TIMESTAMP, - event.timestamp.isoformat()) + span.set_attribute( + ATTR_TASK_UPDATE_METADATA, json.dumps(event.metadata) + ) + if hasattr(event, "timestamp") and event.timestamp: + span.set_attribute( + ATTR_TASK_TIMESTAMP, event.timestamp.isoformat() + ) span.set_status(Status(StatusCode.OK)) @@ -421,11 +439,13 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Start a long-running span for task execution as child of root span ctx = trace.set_span_in_context(self.root_span) - span = self.tracer.start_span(f"{SPAN_TASK_EXECUTION}:{event.task_id}", - context=ctx) + span = self.tracer.start_span( + f"{SPAN_TASK_EXECUTION}:{event.task_id}", context=ctx + ) span.set_attribute(ATTR_TASK_ID, event.task_id) - worker_id = event.worker_id if hasattr(event, - 'worker_id') else 'unknown' + worker_id = ( + event.worker_id if hasattr(event, "worker_id") else "unknown" + ) span.set_attribute(ATTR_WORKER_ID, worker_id) span.set_attribute(ATTR_PROJECT_ID, self.project_id) span.set_attribute(ATTR_TASK_STATUS, "started") @@ -449,30 +469,39 @@ class WorkforceMetricsCallback(WorkforceMetrics): span.set_attribute(ATTR_WORKER_ID, event.worker_id) # Add timestamp as ISO string - if hasattr(event, 'timestamp') and event.timestamp: - span.set_attribute(ATTR_TASK_TIMESTAMP, - event.timestamp.isoformat()) + if hasattr(event, "timestamp") and event.timestamp: + span.set_attribute( + ATTR_TASK_TIMESTAMP, event.timestamp.isoformat() + ) if event.parent_task_id: span.set_attribute(ATTR_TASK_PARENT_ID, event.parent_task_id) if event.processing_time_seconds is not None: - span.set_attribute(ATTR_TASK_PROCESSING_TIME_SECONDS, - event.processing_time_seconds) + span.set_attribute( + ATTR_TASK_PROCESSING_TIME_SECONDS, + event.processing_time_seconds, + ) # Check for quality score from parsed log messages first if event.task_id in self.task_quality_scores: quality_score = self.task_quality_scores.pop(event.task_id) span.set_attribute(ATTR_TASK_QUALITY_SCORE, quality_score) # Fallback to event attributes if available - elif hasattr(event, - 'quality_score') and event.quality_score is not None: - span.set_attribute(ATTR_TASK_QUALITY_SCORE, - event.quality_score) - elif hasattr( - event, 'metadata' - ) and event.metadata and 'quality_score' in event.metadata: - span.set_attribute(ATTR_TASK_QUALITY_SCORE, - event.metadata['quality_score']) + elif ( + hasattr(event, "quality_score") + and event.quality_score is not None + ): + span.set_attribute( + ATTR_TASK_QUALITY_SCORE, event.quality_score + ) + elif ( + hasattr(event, "metadata") + and event.metadata + and "quality_score" in event.metadata + ): + span.set_attribute( + ATTR_TASK_QUALITY_SCORE, event.metadata["quality_score"] + ) if event.token_usage: # Store all token usage as custom attributes @@ -519,8 +548,10 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Pattern: "Task completed successfully (quality score: X)." # TODO: add this from the camel if log_event.level == "info": - pattern = (r'Task\s+(\S+)\s+completed successfully' - r'.*quality score:\s*(\d+)') + pattern = ( + r"Task\s+(\S+)\s+completed successfully" + r".*quality score:\s*(\d+)" + ) match = re.search(pattern, log_event.message) if match: task_id = match.group(1) @@ -530,8 +561,9 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Only log errors and critical messages if log_event.level in ["error", "critical"]: ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_LOG_MESSAGE, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_LOG_MESSAGE, context=ctx + ) as span: span.set_attribute("log.level", log_event.level) span.set_attribute("log.message", log_event.message) span.set_attribute(ATTR_PROJECT_ID, self.project_id) @@ -543,8 +575,9 @@ class WorkforceMetricsCallback(WorkforceMetrics): # Set span status based on log level if log_event.level == "critical": - span.set_status(Status(StatusCode.ERROR, - log_event.message)) + span.set_status( + Status(StatusCode.ERROR, log_event.message) + ) def log_all_tasks_completed(self, event) -> None: """Log when all tasks in the workforce are completed. @@ -556,19 +589,22 @@ class WorkforceMetricsCallback(WorkforceMetrics): return ctx = trace.set_span_in_context(self.root_span) - with self.tracer.start_as_current_span(SPAN_ALL_TASKS_COMPLETED, - context=ctx) as span: + with self.tracer.start_as_current_span( + SPAN_ALL_TASKS_COMPLETED, context=ctx + ) as span: span.set_attribute(ATTR_PROJECT_ID, self.project_id) span.set_attribute(ATTR_TASK_ID, self.task_id) # Add timestamp as ISO string - if hasattr(event, 'timestamp') and event.timestamp: - span.set_attribute(ATTR_TASK_TIMESTAMP, - event.timestamp.isoformat()) + if hasattr(event, "timestamp") and event.timestamp: + span.set_attribute( + ATTR_TASK_TIMESTAMP, event.timestamp.isoformat() + ) - if hasattr(event, 'total_tasks'): - span.set_attribute(ATTR_WORKFORCE_TOTAL_TASKS, - event.total_tasks) + if hasattr(event, "total_tasks"): + span.set_attribute( + ATTR_WORKFORCE_TOTAL_TASKS, event.total_tasks + ) span.set_status(Status(StatusCode.OK)) # End the root span when all tasks are completed @@ -582,12 +618,14 @@ class WorkforceMetricsCallback(WorkforceMetrics): Returns: JSON string representation of metrics """ - return json.dumps({ - "project_id": self.project_id, - "task_id": self.task_id, - "otel_enabled": self.enabled, - "active_spans": len(self.task_spans), - }) + return json.dumps( + { + "project_id": self.project_id, + "task_id": self.task_id, + "otel_enabled": self.enabled, + "active_spans": len(self.task_spans), + } + ) def get_ascii_tree_representation(self) -> str: """Get ASCII tree representation of workforce metrics. @@ -596,10 +634,12 @@ class WorkforceMetricsCallback(WorkforceMetrics): ASCII tree string """ active_count = len(self.task_spans) - return (f"OpenTelemetry Metrics for project {self.project_id}, " - f"task {self.task_id} (active spans: {active_count})") + return ( + f"OpenTelemetry Metrics for project {self.project_id}, " + f"task {self.task_id} (active spans: {active_count})" + ) - def get_kpis(self) -> Dict[str, Any]: + def get_kpis(self) -> dict[str, Any]: """Get key performance indicators. Returns: diff --git a/backend/app/utils/toolkit/__init__.py b/backend/app/utils/toolkit/__init__.py index 3a4d90c0..fa7455a0 100644 --- a/backend/app/utils/toolkit/__init__.py +++ b/backend/app/utils/toolkit/__init__.py @@ -11,4 +11,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - diff --git a/backend/app/utils/toolkit/audio_analysis_toolkit.py b/backend/app/utils/toolkit/audio_analysis_toolkit.py index 512f8803..44d2ea01 100644 --- a/backend/app/utils/toolkit/audio_analysis_toolkit.py +++ b/backend/app/utils/toolkit/audio_analysis_toolkit.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import os + from camel.models import BaseAudioModel, BaseModelBackend from camel.toolkits import AudioAnalysisToolkit as BaseAudioAnalysisToolkit @@ -35,6 +36,10 @@ class AudioAnalysisToolkit(BaseAudioAnalysisToolkit, AbstractToolkit): timeout: float | None = None, ): if cache_dir is None: - cache_dir = env("file_save_path", os.path.expanduser("~/.eigent/tmp/")) - super().__init__(cache_dir, transcribe_model, audio_reasoning_model, timeout) + cache_dir = env( + "file_save_path", os.path.expanduser("~/.eigent/tmp/") + ) + super().__init__( + cache_dir, transcribe_model, audio_reasoning_model, timeout + ) self.api_task_id = api_task_id diff --git a/backend/app/utils/toolkit/code_execution_toolkit.py b/backend/app/utils/toolkit/code_execution_toolkit.py index d9d70e2e..5f108d62 100644 --- a/backend/app/utils/toolkit/code_execution_toolkit.py +++ b/backend/app/utils/toolkit/code_execution_toolkit.py @@ -12,8 +12,13 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import List, Literal -from camel.toolkits import CodeExecutionToolkit as BaseCodeExecutionToolkit, FunctionTool +from typing import Literal + +from camel.toolkits import ( + CodeExecutionToolkit as BaseCodeExecutionToolkit, + FunctionTool, +) + from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit @@ -26,17 +31,26 @@ class CodeExecutionToolkit(BaseCodeExecutionToolkit, AbstractToolkit): def __init__( self, api_task_id: str, - sandbox: Literal["internal_python", "jupyter", "docker", "subprocess", "e2b"] = "subprocess", + sandbox: Literal[ + "internal_python", "jupyter", "docker", "subprocess", "e2b" + ] = "subprocess", verbose: bool = False, unsafe_mode: bool = False, - import_white_list: List[str] | None = None, + import_white_list: list[str] | None = None, require_confirm: bool = False, timeout: float | None = None, ) -> None: self.api_task_id = api_task_id - super().__init__(sandbox, verbose, unsafe_mode, import_white_list, require_confirm, timeout) + super().__init__( + sandbox, + verbose, + unsafe_mode, + import_white_list, + require_confirm, + timeout, + ) - def get_tools(self) -> List[FunctionTool]: + def get_tools(self) -> list[FunctionTool]: return [ FunctionTool(self.execute_code), ] diff --git a/backend/app/utils/toolkit/excel_toolkit.py b/backend/app/utils/toolkit/excel_toolkit.py index b63163c3..d370f88b 100644 --- a/backend/app/utils/toolkit/excel_toolkit.py +++ b/backend/app/utils/toolkit/excel_toolkit.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import os + from camel.toolkits import ExcelToolkit as BaseExcelToolkit from app.component.environment import env @@ -33,5 +34,7 @@ class ExcelToolkit(BaseExcelToolkit, AbstractToolkit): ): self.api_task_id = api_task_id if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/Downloads")) + working_directory = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) super().__init__(timeout=timeout, working_directory=working_directory) diff --git a/backend/app/utils/toolkit/file_write_toolkit.py b/backend/app/utils/toolkit/file_write_toolkit.py index 23a61e8d..0c081b9d 100644 --- a/backend/app/utils/toolkit/file_write_toolkit.py +++ b/backend/app/utils/toolkit/file_write_toolkit.py @@ -12,14 +12,22 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import asyncio import os -from typing import List + from camel.toolkits import FileToolkit as BaseFileToolkit + from app.component.environment import env -from app.service.task import process_task -from app.service.task import ActionWriteFileData, Agents, get_task_lock -from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit, _safe_put_queue +from app.service.task import ( + ActionWriteFileData, + Agents, + get_task_lock, + process_task, +) +from app.utils.listen.toolkit_listen import ( + _safe_put_queue, + auto_listen_toolkit, + listen_toolkit, +) from app.utils.toolkit.abstract_toolkit import AbstractToolkit @@ -36,8 +44,12 @@ class FileToolkit(BaseFileToolkit, AbstractToolkit): backup_enabled: bool = True, ) -> None: if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/Downloads")) - super().__init__(working_directory, timeout, default_encoding, backup_enabled) + working_directory = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) + super().__init__( + working_directory, timeout, default_encoding, backup_enabled + ) self.api_task_id = api_task_id @listen_toolkit( @@ -52,12 +64,14 @@ class FileToolkit(BaseFileToolkit, AbstractToolkit): def write_to_file( self, title: str, - content: str | List[List[str]], + content: str | list[list[str]], filename: str, encoding: str | None = None, use_latex: bool = False, ) -> str: - res = super().write_to_file(title, content, filename, encoding, use_latex) + res = super().write_to_file( + title, content, filename, encoding, use_latex + ) if "Content successfully written to file: " in res: task_lock = get_task_lock(self.api_task_id) # Capture ContextVar value before creating async task @@ -68,7 +82,9 @@ class FileToolkit(BaseFileToolkit, AbstractToolkit): task_lock, ActionWriteFileData( process_task_id=current_process_task_id, - data=res.replace("Content successfully written to file: ", ""), - ) + data=res.replace( + "Content successfully written to file: ", "" + ), + ), ) return res diff --git a/backend/app/utils/toolkit/github_toolkit.py b/backend/app/utils/toolkit/github_toolkit.py index e6f4ccc2..55426fc4 100644 --- a/backend/app/utils/toolkit/github_toolkit.py +++ b/backend/app/utils/toolkit/github_toolkit.py @@ -12,9 +12,9 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import Literal from camel.toolkits import GithubToolkit as BaseGithubToolkit from camel.toolkits.function_tool import FunctionTool + from app.component.environment import env from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit diff --git a/backend/app/utils/toolkit/google_calendar_toolkit.py b/backend/app/utils/toolkit/google_calendar_toolkit.py index ea357e37..612fa094 100644 --- a/backend/app/utils/toolkit/google_calendar_toolkit.py +++ b/backend/app/utils/toolkit/google_calendar_toolkit.py @@ -26,7 +26,7 @@ from app.utils.toolkit.abstract_toolkit import AbstractToolkit logger = logging.getLogger("main") -SCOPES = ['https://www.googleapis.com/auth/calendar'] +SCOPES = ["https://www.googleapis.com/auth/calendar"] @auto_listen_toolkit(BaseGoogleCalendarToolkit) @@ -37,14 +37,12 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): self.api_task_id = api_task_id # Use a stable token file (no per-task suffix). # Can be overridden by env. - self._token_path = ( - env("GOOGLE_CALENDAR_TOKEN_PATH") or os.path.join( - os.path.expanduser("~"), - ".eigent", - "tokens", - "google_calendar", - "google_calendar_token.json", - ) + self._token_path = env("GOOGLE_CALENDAR_TOKEN_PATH") or os.path.join( + os.path.expanduser("~"), + ".eigent", + "tokens", + "google_calendar", + "google_calendar_token.json", ) super().__init__(timeout) @@ -69,8 +67,9 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): if os.path.exists(default_env_path): load_dotenv(dotenv_path=default_env_path, override=True) - if os.environ.get("GOOGLE_CLIENT_ID" - ) and os.environ.get("GOOGLE_CLIENT_SECRET"): + if os.environ.get("GOOGLE_CLIENT_ID") and os.environ.get( + "GOOGLE_CLIENT_SECRET" + ): return cls(api_task_id).get_tools() else: return [] @@ -120,12 +119,12 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): elif os.path.exists( self._token_path.replace( "google_calendar_token.json", - "google_calendar_token_install_auth.json" + "google_calendar_token_install_auth.json", ) ): legacy_path = self._token_path.replace( "google_calendar_token.json", - "google_calendar_token_install_auth.json" + "google_calendar_token_install_auth.json", ) logger.info( "Loading credentials from " @@ -146,9 +145,10 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): client_id = os.environ.get("GOOGLE_CLIENT_ID") client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") refresh_token = os.environ.get("GOOGLE_REFRESH_TOKEN") - token_uri = os.environ.get( - "GOOGLE_TOKEN_URI" - ) or "https://oauth2.googleapis.com/token" + token_uri = ( + os.environ.get("GOOGLE_TOKEN_URI") + or "https://oauth2.googleapis.com/token" + ) if refresh_token and client_id and client_secret: logger.info("Creating credentials from environment variables") @@ -223,7 +223,7 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): logger.info("Found existing authorization, forcing shutdown...") old_state.cancel() # Try to shutdown the old server if it exists - if hasattr(old_state, 'server') and old_state.server: + if hasattr(old_state, "server") and old_state.server: try: old_state.server.shutdown() logger.info("Old server shutdown successfully") @@ -242,6 +242,7 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): # Reload environment variables in this thread from dotenv import load_dotenv + default_env_path = os.path.join( os.path.expanduser("~"), ".eigent", ".env" ) @@ -250,9 +251,10 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): client_id = os.environ.get("GOOGLE_CLIENT_ID") client_secret = os.environ.get("GOOGLE_CLIENT_SECRET") - token_uri = os.environ.get( - "GOOGLE_TOKEN_URI" - ) or "https://oauth2.googleapis.com/token" + token_uri = ( + os.environ.get("GOOGLE_TOKEN_URI") + or "https://oauth2.googleapis.com/token" + ) logger.info( "Google Calendar auth - " @@ -274,8 +276,7 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): "installed": { "client_id": client_id, "client_secret": client_secret, - "auth_uri": - "https://accounts.google.com/o/oauth2/auth", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": token_uri, "redirect_uris": ["http://localhost"], } @@ -301,10 +302,7 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): " Starting local server for " "Google Calendar authorization" ) - logger.info( - "Browser should open automatically" - " in a moment..." - ) + logger.info("Browser should open automatically in a moment...") logger.info("=" * 80) # Run local server - this will block @@ -384,7 +382,7 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit): thread = threading.Thread( target=auth_flow, daemon=True, - name=f"GoogleCalendar-OAuth-{state.started_at.timestamp()}" + name=f"GoogleCalendar-OAuth-{state.started_at.timestamp()}", ) state.thread = thread thread.start() diff --git a/backend/app/utils/toolkit/google_drive_mcp_toolkit.py b/backend/app/utils/toolkit/google_drive_mcp_toolkit.py index 29f8e1da..1491e30d 100644 --- a/backend/app/utils/toolkit/google_drive_mcp_toolkit.py +++ b/backend/app/utils/toolkit/google_drive_mcp_toolkit.py @@ -12,12 +12,16 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from camel.toolkits import GoogleDriveMCPToolkit as BaseGoogleDriveMCPToolkit, MCPToolkit +from camel.toolkits import ( + GoogleDriveMCPToolkit as BaseGoogleDriveMCPToolkit, + MCPToolkit, +) +from camel.toolkits.function_tool import FunctionTool + from app.component.command import bun from app.component.environment import env from app.service.task import Agents from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from camel.toolkits.function_tool import FunctionTool class GoogleDriveMCPToolkit(BaseGoogleDriveMCPToolkit, AbstractToolkit): @@ -38,8 +42,15 @@ class GoogleDriveMCPToolkit(BaseGoogleDriveMCPToolkit, AbstractToolkit): "mcpServers": { "gdrive": { "command": bun(), - "args": ["x", "-y", "@modelcontextprotocol/server-gdrive"], - "env": {"GDRIVE_CREDENTIALS_PATH": credentials_path, **(input_env or {})}, + "args": [ + "x", + "-y", + "@modelcontextprotocol/server-gdrive", + ], + "env": { + "GDRIVE_CREDENTIALS_PATH": credentials_path, + **(input_env or {}), + }, } } }, @@ -47,13 +58,17 @@ class GoogleDriveMCPToolkit(BaseGoogleDriveMCPToolkit, AbstractToolkit): ) @classmethod - async def get_can_use_tools(cls, api_task_id: str, input_env: dict[str, str] | None = None) -> list[FunctionTool]: + async def get_can_use_tools( + cls, api_task_id: str, input_env: dict[str, str] | None = None + ) -> list[FunctionTool]: if env("GDRIVE_CREDENTIALS_PATH") is None: return [] - toolkit = cls(api_task_id, 180, env("GDRIVE_CREDENTIALS_PATH"), input_env) + toolkit = cls( + api_task_id, 180, env("GDRIVE_CREDENTIALS_PATH"), input_env + ) await toolkit.connect() tools = [] for item in toolkit.get_tools(): - setattr(item, "_toolkit_name", cls.__name__) + item._toolkit_name = cls.__name__ tools.append(item) return tools diff --git a/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py b/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py index c8bcc302..fef4c489 100644 --- a/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py +++ b/backend/app/utils/toolkit/google_gmail_mcp_toolkit.py @@ -38,11 +38,14 @@ class GoogleGmailMCPToolkit(BaseToolkit, AbstractToolkit): "mcpServers": { "gmail": { "command": bun(), - "args": - ["x", "-y", "@gongrzhe/server-gmail-autoauth-mcp"], + "args": [ + "x", + "-y", + "@gongrzhe/server-gmail-autoauth-mcp", + ], "env": { "GMAIL_CREDENTIALS_PATH": credentials_path, - **(input_env or {}) + **(input_env or {}), }, } } @@ -61,9 +64,7 @@ class GoogleGmailMCPToolkit(BaseToolkit, AbstractToolkit): @classmethod async def get_can_use_tools( - cls, - api_task_id: str, - input_env: dict[str, str] | None = None + cls, api_task_id: str, input_env: dict[str, str] | None = None ) -> list[FunctionTool]: if env("GMAIL_CREDENTIALS_PATH") is None: return [] @@ -73,6 +74,6 @@ class GoogleGmailMCPToolkit(BaseToolkit, AbstractToolkit): await toolkit.connect() tools = [] for item in toolkit.get_tools(): - setattr(item, "_toolkit_name", cls.__name__) + item._toolkit_name = cls.__name__ tools.append(item) return tools diff --git a/backend/app/utils/toolkit/human_toolkit.py b/backend/app/utils/toolkit/human_toolkit.py index d0bf03c8..53baeb2c 100644 --- a/backend/app/utils/toolkit/human_toolkit.py +++ b/backend/app/utils/toolkit/human_toolkit.py @@ -12,14 +12,20 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import asyncio +import logging + from camel.toolkits.base import BaseToolkit from camel.toolkits.function_tool import FunctionTool -from app.service.task import Action, ActionAskData, ActionNoticeData, get_task_lock + +from app.service.task import ( + Action, + ActionAskData, + ActionNoticeData, + get_task_lock, + process_task, +) from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from app.service.task import process_task -import logging logger = logging.getLogger("human_toolkit") @@ -34,7 +40,9 @@ class HumanToolkit(BaseToolkit, AbstractToolkit): agent_name: str - def __init__(self, api_task_id: str, agent_name: str, timeout: float | None = None): + def __init__( + self, api_task_id: str, agent_name: str, timeout: float | None = None + ): super().__init__(timeout) self.api_task_id = api_task_id self.agent_name = agent_name @@ -126,7 +134,9 @@ class HumanToolkit(BaseToolkit, AbstractToolkit): current_process_task_id = process_task.get("") if not current_process_task_id: current_process_task_id = self.api_task_id - logger.warning(f"[send_message_to_user] ContextVar process_task is empty, using api_task_id as fallback: '{current_process_task_id}'") + logger.warning( + f"[send_message_to_user] ContextVar process_task is empty, using api_task_id as fallback: '{current_process_task_id}'" + ) from app.utils.listen.toolkit_listen import _safe_put_queue @@ -136,7 +146,9 @@ class HumanToolkit(BaseToolkit, AbstractToolkit): ) _safe_put_queue(task_lock, notice_data) - attachment_info = f" {message_attachment}" if message_attachment else "" + attachment_info = ( + f" {message_attachment}" if message_attachment else "" + ) return f"Message successfully sent to user: '{message_title} {message_description}{attachment_info}'" def get_tools(self) -> list[FunctionTool]: @@ -153,7 +165,9 @@ class HumanToolkit(BaseToolkit, AbstractToolkit): ] @classmethod - def get_can_use_tools(cls, api_task_id: str, agent_name: str) -> list[FunctionTool]: + def get_can_use_tools( + cls, api_task_id: str, agent_name: str + ) -> list[FunctionTool]: human = cls(api_task_id, agent_name) return [ FunctionTool(human.ask_human_via_gui), diff --git a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py index 4c1d1667..00821d0e 100644 --- a/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_python_toolkit.py @@ -15,23 +15,28 @@ import asyncio import datetime import json +import logging import os -from typing import Any, Dict, List import uuid -from camel.models import BaseModelBackend -from camel.toolkits.hybrid_browser_toolkit_py import HybridBrowserToolkit as BaseHybridBrowserToolkit -from camel.toolkits.hybrid_browser_toolkit_py.config_loader import ConfigLoader -from camel.toolkits.hybrid_browser_toolkit_py.browser_session import HybridBrowserSession as BaseHybridBrowserSession -from camel.toolkits.hybrid_browser_toolkit_py.actions import ActionExecutor -from camel.toolkits.hybrid_browser_toolkit_py.snapshot import PageSnapshot -from camel.toolkits.hybrid_browser_toolkit_py.agent import PlaywrightLLMAgent +from typing import Any + from camel.toolkits.function_tool import FunctionTool +from camel.toolkits.hybrid_browser_toolkit_py import ( + HybridBrowserToolkit as BaseHybridBrowserToolkit, +) +from camel.toolkits.hybrid_browser_toolkit_py.actions import ActionExecutor +from camel.toolkits.hybrid_browser_toolkit_py.agent import PlaywrightLLMAgent +from camel.toolkits.hybrid_browser_toolkit_py.browser_session import ( + HybridBrowserSession as BaseHybridBrowserSession, +) +from camel.toolkits.hybrid_browser_toolkit_py.config_loader import ConfigLoader +from camel.toolkits.hybrid_browser_toolkit_py.snapshot import PageSnapshot + from app.component.environment import env from app.exception.exception import ProgramException from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -import logging logger = logging.getLogger("hybrid_browser_python_toolkit") @@ -46,15 +51,17 @@ class BrowserSession(BaseHybridBrowserSession): self._playwright = await async_playwright().start() # Prepare stealth options - launch_options: Dict[str, Any] = {"headless": self._headless} - context_options: Dict[str, Any] = {} + launch_options: dict[str, Any] = {"headless": self._headless} + context_options: dict[str, Any] = {} if self._stealth and self._stealth_config: # Use preloaded stealth configuration launch_options["args"] = self._stealth_config["launch_args"] context_options.update(self._stealth_config["context_options"]) if self._user_data_dir: - raise ProgramException("connect over cdp does not support set user_data_dir") + raise ProgramException( + "connect over cdp does not support set user_data_dir" + ) # Path(self._user_data_dir).mkdir(parents=True, exist_ok=True) # pl = self._playwright # assert pl is not None @@ -68,7 +75,9 @@ class BrowserSession(BaseHybridBrowserSession): assert pl is not None # self._browser = await pl.chromium.launch(headless=self._headless) port = env("browser_port", 9222) - self._browser = await pl.chromium.connect_over_cdp(f"http://localhost:{port}") + self._browser = await pl.chromium.connect_over_cdp( + f"http://localhost:{port}" + ) self._context = self._browser.contexts[0] # Reuse an already open page (persistent context may restore last @@ -84,7 +93,10 @@ class BrowserSession(BaseHybridBrowserSession): self._pages = {} for index, item in enumerate(self._context.pages): - if item.url.startswith("about:blank") and item.url != "about:blank": + if ( + item.url.startswith("about:blank") + and item.url != "about:blank" + ): tab_id = "tab-" + str(index) self._page = item self._pages[tab_id] = self._page @@ -94,10 +106,14 @@ class BrowserSession(BaseHybridBrowserSession): # If no suitable page found, create a new one if not self._page: - logger.debug(json.dumps([item.url for item in self._context.pages])) + logger.debug( + json.dumps([item.url for item in self._context.pages]) + ) await asyncio.sleep(3) # wait 3 sec, retry get new page await self.get_new_tab() - logger.debug(json.dumps([item.url for item in self._context.pages])) + logger.debug( + json.dumps([item.url for item in self._context.pages]) + ) if not self._page: raise ProgramException("Maximum Window Limit Reached.") @@ -131,7 +147,10 @@ class BrowserSession(BaseHybridBrowserSession): self._pages = {} for index, item in enumerate(self._context.pages): - if item.url.startswith("about:blank") and item.url != "about:blank": + if ( + item.url.startswith("about:blank") + and item.url != "about:blank" + ): tab_id = "tab-" + str(index) self._pages[tab_id] = item await item.goto("about:blank") @@ -152,7 +171,7 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit): user_data_dir: str | None = None, stealth: bool = False, cache_dir: str = os.path.expanduser("~/.eigent/tmp/"), - enabled_tools: List[str] | None = None, + enabled_tools: list[str] | None = None, browser_log_to_file: bool = False, session_id: str | None = None, default_start_url: str = "https://google.com/", @@ -176,11 +195,23 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit): # Store timeout configuration self._default_timeout = default_timeout self._short_timeout = short_timeout - self._navigation_timeout = ConfigLoader.get_navigation_timeout(navigation_timeout) - self._network_idle_timeout = ConfigLoader.get_network_idle_timeout(network_idle_timeout) - self._screenshot_timeout = ConfigLoader.get_screenshot_timeout(screenshot_timeout) - self._page_stability_timeout = ConfigLoader.get_page_stability_timeout(page_stability_timeout) - self._dom_content_loaded_timeout = ConfigLoader.get_dom_content_loaded_timeout(dom_content_loaded_timeout) + self._navigation_timeout = ConfigLoader.get_navigation_timeout( + navigation_timeout + ) + self._network_idle_timeout = ConfigLoader.get_network_idle_timeout( + network_idle_timeout + ) + self._screenshot_timeout = ConfigLoader.get_screenshot_timeout( + screenshot_timeout + ) + self._page_stability_timeout = ConfigLoader.get_page_stability_timeout( + page_stability_timeout + ) + self._dom_content_loaded_timeout = ( + ConfigLoader.get_dom_content_loaded_timeout( + dom_content_loaded_timeout + ) + ) # Logging configuration - fixed values for simplicity self.enable_action_logging = True @@ -204,23 +235,29 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit): self.log_file_path = None # Initialize log buffer for in-memory storage - self.log_buffer: List[Dict[str, Any]] = [] + self.log_buffer: list[dict[str, Any]] = [] # Configure enabled tools if enabled_tools is None: self.enabled_tools = self.DEFAULT_TOOLS.copy() else: # Validate enabled tools - invalid_tools = [tool for tool in enabled_tools if tool not in self.ALL_TOOLS] + invalid_tools = [ + tool for tool in enabled_tools if tool not in self.ALL_TOOLS + ] if invalid_tools: - raise ValueError(f"Invalid tools specified: {invalid_tools}. Available tools: {self.ALL_TOOLS}") + raise ValueError( + f"Invalid tools specified: {invalid_tools}. Available tools: {self.ALL_TOOLS}" + ) self.enabled_tools = enabled_tools.copy() logger.info(f"Enabled tools: {self.enabled_tools}") # Log initialization if file logging is enabled if self.log_to_file: - logger.info("HybridBrowserToolkit initialized with file logging enabled") + logger.info( + "HybridBrowserToolkit initialized with file logging enabled" + ) logger.info(f"Log file path: {self.log_file_path}") # Core components @@ -239,8 +276,10 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit): self._agent: PlaywrightLLMAgent | None = None self._unified_script = self._load_unified_analyzer() - @listen_toolkit(BaseHybridBrowserToolkit.browser_visit_page, lambda _, url: url) - async def browser_visit_page(self, url: str) -> Dict[str, Any]: + @listen_toolkit( + BaseHybridBrowserToolkit.browser_visit_page, lambda _, url: url + ) + async def browser_visit_page(self, url: str) -> dict[str, Any]: r"""Navigates to a URL. This method creates a new tab for the URL instead of navigating @@ -280,7 +319,9 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit): # Get snapshot snapshot = "" try: - snapshot = await session.get_snapshot(force_refresh=True, diff_only=False) + snapshot = await session.get_snapshot( + force_refresh=True, diff_only=False + ) except Exception as e: logger.warning(f"Failed to capture snapshot: {e}") @@ -314,14 +355,15 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit): # FunctionTool(browser.wait_user), ] - return base_tools @classmethod def toolkit_name(cls) -> str: return "Browser Toolkit" - def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBrowserPythonToolkit": + def clone_for_new_session( + self, new_session_id: str | None = None + ) -> "HybridBrowserPythonToolkit": if new_session_id is None: new_session_id = str(uuid.uuid4())[:8] diff --git a/backend/app/utils/toolkit/hybrid_browser_toolkit.py b/backend/app/utils/toolkit/hybrid_browser_toolkit.py index 1bde2cdb..610da3f5 100644 --- a/backend/app/utils/toolkit/hybrid_browser_toolkit.py +++ b/backend/app/utils/toolkit/hybrid_browser_toolkit.py @@ -12,25 +12,27 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import os import asyncio import json +import logging +import os import uuid -from typing import Any, Dict, List, Optional -from typing_extensions import TypedDict +from typing import Any + import websockets import websockets.exceptions - from camel.toolkits.hybrid_browser_toolkit.hybrid_browser_toolkit_ts import ( HybridBrowserToolkit as BaseHybridBrowserToolkit, ) -from camel.toolkits.hybrid_browser_toolkit.ws_wrapper import WebSocketBrowserWrapper as BaseWebSocketBrowserWrapper -from app.component.command import bun, uv +from camel.toolkits.hybrid_browser_toolkit.ws_wrapper import ( + WebSocketBrowserWrapper as BaseWebSocketBrowserWrapper, +) +from typing_extensions import TypedDict + from app.component.environment import env from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -import logging logger = logging.getLogger("hybrid_browser_toolkit") @@ -39,7 +41,7 @@ logger = logging.getLogger("hybrid_browser_toolkit") _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: dict[str, str] = {} _global_tab_registry_lock = asyncio.Lock() @@ -50,7 +52,7 @@ class SheetCell(TypedDict): class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): - def __init__(self, config: Optional[Dict[str, Any]] = None): + def __init__(self, config: dict[str, Any] | None = None): """Initialize wrapper.""" super().__init__(config) logger.info(f"WebSocketBrowserWrapper using ts_dir: {self.ts_dir}") @@ -65,7 +67,9 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): if not current: os.environ[key] = ",".join(local_hosts) continue - parts = [item.strip() for item in current.split(",") if item.strip()] + parts = [ + item.strip() for item in current.split(",") if item.strip() + ] updated = False for host in local_hosts: if host not in parts: @@ -91,34 +95,53 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): future = self._pending_responses.pop(message_id) if not future.done(): future.set_result(response) - logger.debug(f"Processed response for message {message_id}") + logger.debug( + f"Processed response for message {message_id}" + ) else: message_summary = { "id": response.get("id"), "success": response.get("success"), "has_result": "result" in response, - "result_type": type(response.get("result")).__name__ if "result" in response else None + "result_type": type( + response.get("result") + ).__name__ + if "result" in response + else None, } - logger.debug(f"Received unexpected message: {message_summary}") + logger.debug( + f"Received unexpected message: {message_summary}" + ) except asyncio.CancelledError: disconnect_reason = "Receive loop cancelled" logger.info(f"WebSocket disconnect: {disconnect_reason}") break except websockets.exceptions.ConnectionClosed as e: - disconnect_reason = f"WebSocket closed: code={e.code}, reason={e.reason}" - logger.warning(f"WebSocket disconnect: {disconnect_reason}") + disconnect_reason = ( + f"WebSocket closed: code={e.code}, reason={e.reason}" + ) + logger.warning( + f"WebSocket disconnect: {disconnect_reason}" + ) break except websockets.exceptions.WebSocketException as e: - disconnect_reason = f"WebSocket error: {type(e).__name__}: {e}" + disconnect_reason = ( + f"WebSocket error: {type(e).__name__}: {e}" + ) logger.error(f"WebSocket disconnect: {disconnect_reason}") break except json.JSONDecodeError as e: logger.error(f"Failed to decode WebSocket message: {e}") continue # Try to continue on JSON errors except Exception as e: - disconnect_reason = f"Unexpected error: {type(e).__name__}: {e}" - logger.error(f"WebSocket disconnect: {disconnect_reason}", exc_info=True) + disconnect_reason = ( + f"Unexpected error: {type(e).__name__}: {e}" + ) + logger.error( + f"WebSocket disconnect: {disconnect_reason}", + exc_info=True, + ) # Notify all pending futures of the error for future in self._pending_responses.values(): if not future.done(): @@ -126,17 +149,23 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): self._pending_responses.clear() break finally: - logger.info(f"WebSocket receive loop terminated. Reason: {disconnect_reason or 'Normal shutdown'}") + logger.info( + f"WebSocket receive loop terminated. Reason: {disconnect_reason or 'Normal shutdown'}" + ) # Mark the websocket as None to indicate disconnection self.websocket = None async def start(self): # Simply use the parent implementation which uses system npm/node self._ensure_local_no_proxy() - logger.info("Starting WebSocket server using parent implementation (system npm/node)") + logger.info( + "Starting WebSocket server using parent implementation (system npm/node)" + ) await super().start() - async def _send_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]: + async def _send_command( + self, command: str, params: dict[str, Any] + ) -> dict[str, Any]: """Send a command to the WebSocket server with enhanced error handling.""" try: # First ensure we have a valid connection @@ -148,7 +177,9 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): import websockets.protocol if self.websocket.state != websockets.protocol.State.OPEN: - raise RuntimeError(f"WebSocket is in {self.websocket.state} state, not OPEN") + raise RuntimeError( + f"WebSocket is in {self.websocket.state} state, not OPEN" + ) logger.debug(f"Sending command '{command}' with params: {params}") @@ -166,10 +197,12 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): self.websocket = None raise except Exception as e: - logger.error(f"Unexpected error sending command '{command}': {type(e).__name__}: {e}") + logger.error( + f"Unexpected error sending command '{command}': {type(e).__name__}: {e}" + ) raise - async def visit_page(self, url: str) -> Dict[str, Any]: + 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 @@ -180,16 +213,20 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): global _global_navigation_lock async with _global_navigation_lock: - logger.debug(f"[visit_page] Acquired navigation lock, navigating to {url}") + 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") + logger.debug( + "[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]]: + 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 @@ -202,30 +239,34 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): 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'] + 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}") + 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] + 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}") + 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]: + 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 @@ -238,7 +279,8 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): 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}") + f"[Session Tab Tracking] Removed closed tab: {tab_id}, session now has tabs: {self._session_tab_ids}" + ) return result @@ -261,7 +303,8 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper): # 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}") + f"[Session Tab Tracking] Cleaned up {cleaned_count} tabs for session {self._wrapper_session_id}" + ) # WebSocket connection pool @@ -269,10 +312,12 @@ class WebSocketConnectionPool: """Manage WebSocket browser connections with session-based pooling.""" def __init__(self): - self._connections: Dict[str, WebSocketBrowserWrapper] = {} + self._connections: dict[str, WebSocketBrowserWrapper] = {} self._lock = asyncio.Lock() - async def get_connection(self, session_id: str, config: Dict[str, Any]) -> WebSocketBrowserWrapper: + async def get_connection( + self, session_id: str, config: dict[str, Any] + ) -> WebSocketBrowserWrapper: """Get or create a connection for the given session ID.""" async with self._lock: # Check if we have an existing connection for this session @@ -287,28 +332,41 @@ class WebSocketConnectionPool: if hasattr(wrapper.websocket, "state"): import websockets.protocol - is_healthy = wrapper.websocket.state == websockets.protocol.State.OPEN + is_healthy = ( + wrapper.websocket.state + == websockets.protocol.State.OPEN + ) if not is_healthy: - logger.debug(f"Session {session_id} WebSocket state: {wrapper.websocket.state}") + logger.debug( + f"Session {session_id} WebSocket state: {wrapper.websocket.state}" + ) elif hasattr(wrapper.websocket, "open"): is_healthy = wrapper.websocket.open else: # Try ping as last resort try: - await asyncio.wait_for(wrapper.websocket.ping(), timeout=1.0) + await asyncio.wait_for( + wrapper.websocket.ping(), timeout=1.0 + ) is_healthy = True - except: + except Exception: is_healthy = False except Exception as e: - logger.debug(f"Health check failed for session {session_id}: {e}") + logger.debug( + f"Health check failed for session {session_id}: {e}" + ) is_healthy = False if is_healthy: - logger.debug(f"Reusing healthy WebSocket connection for session {session_id}") + logger.debug( + f"Reusing healthy WebSocket connection for session {session_id}" + ) return wrapper else: # Connection is unhealthy, clean it up - logger.info(f"Removing unhealthy WebSocket connection for session {session_id}") + logger.info( + f"Removing unhealthy WebSocket connection for session {session_id}" + ) try: await wrapper.cleanup_tab_tracking() await wrapper.stop() @@ -317,11 +375,15 @@ class WebSocketConnectionPool: del self._connections[session_id] # Create a new connection - logger.info(f"Creating new WebSocket connection for session {session_id}") + logger.info( + f"Creating new WebSocket connection for session {session_id}" + ) wrapper = WebSocketBrowserWrapper(config) await wrapper.start() self._connections[session_id] = wrapper - logger.info(f"Successfully created WebSocket connection for session {session_id}") + logger.info( + f"Successfully created WebSocket connection for session {session_id}" + ) return wrapper async def close_connection(self, session_id: str): @@ -333,9 +395,13 @@ class WebSocketConnectionPool: await wrapper.cleanup_tab_tracking() await wrapper.stop() except Exception as e: - logger.error(f"Error closing WebSocket connection for session {session_id}: {e}") + logger.error( + f"Error closing WebSocket connection for session {session_id}: {e}" + ) del self._connections[session_id] - logger.info(f"Closed WebSocket connection for session {session_id}") + logger.info( + f"Closed WebSocket connection for session {session_id}" + ) async def _close_connection_unlocked(self, session_id: str): """Close connection without acquiring lock (for internal use).""" @@ -345,9 +411,13 @@ class WebSocketConnectionPool: await wrapper.cleanup_tab_tracking() await wrapper.stop() except Exception as e: - logger.error(f"Error closing WebSocket connection for session {session_id}: {e}") + logger.error( + f"Error closing WebSocket connection for session {session_id}: {e}" + ) del self._connections[session_id] - logger.info(f"Closed WebSocket connection for session {session_id}") + logger.info( + f"Closed WebSocket connection for session {session_id}" + ) async def close_all(self): """Close all connections in the pool.""" @@ -372,12 +442,12 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): headless: bool = False, user_data_dir: str | None = None, stealth: bool = True, - cache_dir: Optional[str] = None, - enabled_tools: List[str] | None = None, + cache_dir: str | None = None, + enabled_tools: list[str] | None = None, browser_log_to_file: bool = False, - log_dir: Optional[str] = None, + log_dir: str | None = None, session_id: str | None = None, - default_start_url: Optional[str] = None, + default_start_url: str | None = None, default_timeout: int | None = None, short_timeout: int | None = None, navigation_timeout: int | None = None, @@ -391,22 +461,34 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): cdp_keep_current_page: bool = False, full_visual_mode: bool = False, ) -> None: - logger.info(f"[HybridBrowserToolkit] Initializing with api_task_id: {api_task_id}") + logger.info( + f"[HybridBrowserToolkit] Initializing with api_task_id: {api_task_id}" + ) self.api_task_id = api_task_id - logger.debug(f"[HybridBrowserToolkit] api_task_id set to: {self.api_task_id}") - + logger.debug( + f"[HybridBrowserToolkit] api_task_id set to: {self.api_task_id}" + ) + # Set default user_data_dir if not provided if user_data_dir is None: # Use browser port to determine profile directory - browser_port = env('browser_port', '9222') + browser_port = env("browser_port", "9222") user_data_base = os.path.expanduser("~/.eigent/browser_profiles") - user_data_dir = os.path.join(user_data_base, f"profile_{browser_port}") + user_data_dir = os.path.join( + user_data_base, f"profile_{browser_port}" + ) os.makedirs(user_data_dir, exist_ok=True) - logger.info(f"[HybridBrowserToolkit] Using port-based user_data_dir: {user_data_dir} (port: {browser_port})") + logger.info( + f"[HybridBrowserToolkit] Using port-based user_data_dir: {user_data_dir} (port: {browser_port})" + ) else: - logger.info(f"[HybridBrowserToolkit] Using provided user_data_dir: {user_data_dir}") + logger.info( + f"[HybridBrowserToolkit] Using provided user_data_dir: {user_data_dir}" + ) - logger.debug(f"[HybridBrowserToolkit] Calling super().__init__ with session_id: {session_id}") + logger.debug( + f"[HybridBrowserToolkit] Calling super().__init__ with session_id: {session_id}" + ) super().__init__( headless=headless, user_data_dir=user_data_dir, @@ -429,11 +511,15 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): cdp_keep_current_page=cdp_keep_current_page, full_visual_mode=full_visual_mode, ) - logger.info(f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}") + logger.info( + f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}" + ) async def _ensure_ws_wrapper(self): """Ensure WebSocket wrapper is initialized using connection pool.""" - logger.debug(f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}") + logger.debug( + f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}" + ) global websocket_connection_pool # Get session ID from config or use default @@ -441,20 +527,34 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): logger.debug(f"[HybridBrowserToolkit] Using session_id: {session_id}") # Log when connecting to browser - cdp_url = self._ws_config.get("cdp_url", f"http://localhost:{env('browser_port', '9222')}") - logger.info(f"[PROJECT BROWSER] Connecting to browser via CDP at {cdp_url}") + cdp_url = self._ws_config.get( + "cdp_url", f"http://localhost:{env('browser_port', '9222')}" + ) + logger.info( + f"[PROJECT BROWSER] Connecting to browser via CDP at {cdp_url}" + ) # Get or create connection from pool - self._ws_wrapper = await websocket_connection_pool.get_connection(session_id, self._ws_config) - logger.info(f"[HybridBrowserToolkit] WebSocket wrapper initialized for session: {session_id}") + self._ws_wrapper = await websocket_connection_pool.get_connection( + session_id, self._ws_config + ) + logger.info( + f"[HybridBrowserToolkit] WebSocket wrapper initialized for session: {session_id}" + ) # Additional health check if self._ws_wrapper.websocket is None: - logger.warning(f"WebSocket connection for session {session_id} is None after pool retrieval, recreating...") + logger.warning( + f"WebSocket connection for session {session_id} is None after pool retrieval, recreating..." + ) await websocket_connection_pool.close_connection(session_id) - self._ws_wrapper = await websocket_connection_pool.get_connection(session_id, self._ws_config) + self._ws_wrapper = await websocket_connection_pool.get_connection( + session_id, self._ws_config + ) - def clone_for_new_session(self, new_session_id: str | None = None) -> "HybridBrowserToolkit": + def clone_for_new_session( + self, new_session_id: str | None = None + ) -> "HybridBrowserToolkit": import uuid if new_session_id is None: @@ -462,7 +562,9 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): # For cloned sessions, use the same user_data_dir to share login state # This allows multiple agents to use the same browser profile without conflicts - logger.info(f"Cloning session {new_session_id} with shared user_data_dir: {self._user_data_dir}") + logger.info( + f"Cloning session {new_session_id} with shared user_data_dir: {self._user_data_dir}" + ) # Use the same session_id to share the same browser instance # This ensures all clones use the same WebSocket connection and browser @@ -491,7 +593,9 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): full_visual_mode=self._full_visual_mode, ) - async def browser_sheet_input(self, *, cells: List[SheetCell]) -> Dict[str, Any]: + 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) @@ -517,4 +621,6 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit): """Cleanup when object is garbage collected.""" if hasattr(self, "_ws_wrapper") and self._ws_wrapper: session_id = self._ws_config.get("session_id", "default") - logger.debug(f"HybridBrowserToolkit for session {session_id} is being garbage collected") + logger.debug( + f"HybridBrowserToolkit for session {session_id} is being garbage collected" + ) diff --git a/backend/app/utils/toolkit/lark_toolkit.py b/backend/app/utils/toolkit/lark_toolkit.py index 492b3c3b..10df4c56 100644 --- a/backend/app/utils/toolkit/lark_toolkit.py +++ b/backend/app/utils/toolkit/lark_toolkit.py @@ -22,7 +22,6 @@ from app.utils.toolkit.abstract_toolkit import AbstractToolkit @auto_listen_toolkit(BaseLarkToolkit) class LarkToolkit(BaseLarkToolkit, AbstractToolkit): - def __init__(self, api_task_id: str, timeout: float | None = None): super().__init__(timeout=timeout) self.api_task_id = api_task_id diff --git a/backend/app/utils/toolkit/linkedin_toolkit.py b/backend/app/utils/toolkit/linkedin_toolkit.py index 02a9aea3..53339a92 100644 --- a/backend/app/utils/toolkit/linkedin_toolkit.py +++ b/backend/app/utils/toolkit/linkedin_toolkit.py @@ -16,7 +16,6 @@ import json import logging import os import time -from typing import Optional from camel.toolkits import LinkedInToolkit as BaseLinkedInToolkit from camel.toolkits.function_tool import FunctionTool @@ -155,12 +154,14 @@ class LinkedInToolkit(BaseLinkedInToolkit, AbstractToolkit): # Calculate expiration time if expires_in is provided if "expires_in" in token_data and "expires_at" not in token_data: - token_data["expires_at"] = token_data["saved_at"] + token_data[ - "expires_in"] + token_data["expires_at"] = ( + token_data["saved_at"] + token_data["expires_in"] + ) elif "expires_at" not in token_data: # Default to 60 days if no expiration info provided - token_data["expires_at"] = token_data[ - "saved_at"] + LINKEDIN_TOKEN_LIFETIME_SECONDS + token_data["expires_at"] = ( + token_data["saved_at"] + LINKEDIN_TOKEN_LIFETIME_SECONDS + ) os.makedirs(os.path.dirname(token_path), exist_ok=True) with open(token_path, "w") as f: @@ -169,8 +170,9 @@ class LinkedInToolkit(BaseLinkedInToolkit, AbstractToolkit): # Also update environment variable if token_data.get("access_token"): - os.environ["LINKEDIN_ACCESS_TOKEN"] = token_data["access_token" - ] + os.environ["LINKEDIN_ACCESS_TOKEN"] = token_data[ + "access_token" + ] return True except Exception as e: @@ -238,7 +240,7 @@ class LinkedInToolkit(BaseLinkedInToolkit, AbstractToolkit): return bool(env("LINKEDIN_ACCESS_TOKEN")) @classmethod - def get_token_info(cls) -> Optional[dict]: + def get_token_info(cls) -> dict | None: r"""Get stored token information including expiration. Returns: diff --git a/backend/app/utils/toolkit/markitdown_toolkit.py b/backend/app/utils/toolkit/markitdown_toolkit.py index b17f03c8..48d1745f 100644 --- a/backend/app/utils/toolkit/markitdown_toolkit.py +++ b/backend/app/utils/toolkit/markitdown_toolkit.py @@ -12,7 +12,6 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import Dict, List from camel.toolkits import MarkItDownToolkit as BaseMarkItDownToolkit from app.service.task import Agents diff --git a/backend/app/utils/toolkit/mcp_search_toolkit.py b/backend/app/utils/toolkit/mcp_search_toolkit.py index ddb4e4ae..daf8687b 100644 --- a/backend/app/utils/toolkit/mcp_search_toolkit.py +++ b/backend/app/utils/toolkit/mcp_search_toolkit.py @@ -12,11 +12,13 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import Any, List -from camel.toolkits import BaseToolkit, FunctionTool +from typing import Any + import httpx -from app.service.task import Action, ActionSearchMcpData, Agents, get_task_lock +from camel.toolkits import BaseToolkit, FunctionTool + from app.component.environment import env_not_empty +from app.service.task import Action, ActionSearchMcpData, Agents, get_task_lock from app.utils.listen.toolkit_listen import listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit @@ -65,9 +67,11 @@ class McpSearchToolkit(BaseToolkit, AbstractToolkit): data = response.json() task_lock = get_task_lock(self.api_task_id) await task_lock.put_queue( - ActionSearchMcpData(action=Action.search_mcp, data=data["items"]) + ActionSearchMcpData( + action=Action.search_mcp, data=data["items"] + ) ) return data - def get_tools(self) -> List[FunctionTool]: + def get_tools(self) -> list[FunctionTool]: return [FunctionTool(self.search_mcp_from_url)] diff --git a/backend/app/utils/toolkit/note_taking_toolkit.py b/backend/app/utils/toolkit/note_taking_toolkit.py index ef6e61e1..31455498 100644 --- a/backend/app/utils/toolkit/note_taking_toolkit.py +++ b/backend/app/utils/toolkit/note_taking_toolkit.py @@ -12,10 +12,11 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +from camel.toolkits import NoteTakingToolkit as BaseNoteTakingToolkit + from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from camel.toolkits import NoteTakingToolkit as BaseNoteTakingToolkit @auto_listen_toolkit(BaseNoteTakingToolkit) @@ -35,7 +36,8 @@ class NoteTakingToolkit(BaseNoteTakingToolkit, AbstractToolkit): if working_directory is None: raise ValueError( "working_directory is required for NoteTakingToolkit. " - "Notes must be stored in a task-specific directory.") + "Notes must be stored in a task-specific directory." + ) self.api_task_id = api_task_id if agent_name is not None: self.agent_name = agent_name diff --git a/backend/app/utils/toolkit/notion_mcp_toolkit.py b/backend/app/utils/toolkit/notion_mcp_toolkit.py index 75a07743..fee5a361 100644 --- a/backend/app/utils/toolkit/notion_mcp_toolkit.py +++ b/backend/app/utils/toolkit/notion_mcp_toolkit.py @@ -12,42 +12,50 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import os -import json import asyncio -from textwrap import indent -from typing import Any, Dict, List +import logging +import os +from typing import Any + from camel.toolkits import FunctionTool +from camel.toolkits.mcp_toolkit import MCPToolkit + from app.component.environment import env from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from camel.toolkits.mcp_toolkit import MCPToolkit -import logging logger = logging.getLogger("notion_mcp_toolkit") -def _customize_function_parameters(schema: Dict[str, Any]) -> None: - r"""Customize function parameters for specific functions. - This method allows modifying parameter descriptions or other schema - attributes for specific functions. - """ - function_info = schema.get("function", {}) - function_name = function_info.get("name", "") - parameters = function_info.get("parameters", {}) - properties = parameters.get("properties", {}) - required = parameters.get("required", []) - - help_description = "If you need use parent, you can use `notion-search` for the information" - # Modify the notion-create-pages function to make parent optional - if function_name == "notion-create-pages" or function_name == "notion-create-database": - required.remove("parent") - parameters["required"] = required - if "parent" in properties: - # Update the parent parameter description - properties["parent"]["description"] = "Optional. " + properties["parent"]["description"] + help_description +def _customize_function_parameters(schema: dict[str, Any]) -> None: + r"""Customize function parameters for specific functions. + + This method allows modifying parameter descriptions or other schema + attributes for specific functions. + """ + function_info = schema.get("function", {}) + function_name = function_info.get("name", "") + parameters = function_info.get("parameters", {}) + properties = parameters.get("properties", {}) + required = parameters.get("required", []) + + help_description = "If you need use parent, you can use `notion-search` for the information" + # Modify the notion-create-pages function to make parent optional + if ( + function_name == "notion-create-pages" + or function_name == "notion-create-database" + ): + required.remove("parent") + parameters["required"] = required + if "parent" in properties: + # Update the parent parameter description + properties["parent"]["description"] = ( + "Optional. " + + properties["parent"]["description"] + + help_description + ) + class NotionMCPToolkit(MCPToolkit, AbstractToolkit): - def __init__( self, api_task_id: str, @@ -56,8 +64,8 @@ class NotionMCPToolkit(MCPToolkit, AbstractToolkit): self.api_task_id = api_task_id if timeout is None: timeout = 120.0 - - config_dict={ + + config_dict = { "mcpServers": { "notionMCP": { "command": "npx", @@ -67,62 +75,77 @@ class NotionMCPToolkit(MCPToolkit, AbstractToolkit): "https://mcp.notion.com/mcp", ], "env": { - "MCP_REMOTE_CONFIG_DIR": env("MCP_REMOTE_CONFIG_DIR", os.path.expanduser("~/.mcp-auth")), + "MCP_REMOTE_CONFIG_DIR": env( + "MCP_REMOTE_CONFIG_DIR", + os.path.expanduser("~/.mcp-auth"), + ), }, } } } - super().__init__(config_dict=config_dict, timeout=timeout) + super().__init__(config_dict=config_dict, timeout=timeout) @classmethod async def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: # Retry mechanism for remote MCP connection max_retries = 3 retry_delay = 2 # seconds - + for attempt in range(max_retries): tools = [] toolkit = None - + try: # Create a fresh toolkit instance for each retry toolkit = cls(api_task_id) - logger.info(f"Attempting to connect to Notion MCP server (attempt {attempt + 1}/{max_retries})") - + logger.info( + f"Attempting to connect to Notion MCP server (attempt {attempt + 1}/{max_retries})" + ) + await toolkit.connect() - + # Get tools from the connected toolkit all_tools = toolkit.get_tools() tool_schema = [ item.get_openai_tool_schema() for item in all_tools ] - + # Adjust tool schema for item in tool_schema: _customize_function_parameters(item) - + for item in all_tools: - setattr(item, "_toolkit_name", cls.__name__) + item._toolkit_name = cls.__name__ tools.append(item) - + # Check if we actually got tools if len(tools) == 0: - logger.warning(f"Connected to Notion MCP server but got 0 tools (attempt {attempt + 1}/{max_retries})") - raise Exception("No tools retrieved from Notion MCP server") - + logger.warning( + f"Connected to Notion MCP server but got 0 tools (attempt {attempt + 1}/{max_retries})" + ) + raise Exception( + "No tools retrieved from Notion MCP server" + ) + # Success! Got tools - logger.info(f"Successfully connected to Notion MCP server and loaded {len(tools)} tools") - + logger.info( + f"Successfully connected to Notion MCP server and loaded {len(tools)} tools" + ) + return tools - + except Exception as e: - logger.warning(f"Failed to connect to Notion MCP server (attempt {attempt + 1}/{max_retries}): {e}") - + logger.warning( + f"Failed to connect to Notion MCP server (attempt {attempt + 1}/{max_retries}): {e}" + ) + # If not the last attempt, wait and retry if attempt < max_retries - 1: logger.info(f"Retrying in {retry_delay} seconds...") await asyncio.sleep(retry_delay) else: # Last attempt failed - logger.error(f"All {max_retries} connection attempts to Notion MCP server failed. Notion tools will not be available for this task.") + logger.error( + f"All {max_retries} connection attempts to Notion MCP server failed. Notion tools will not be available for this task." + ) return [] diff --git a/backend/app/utils/toolkit/notion_toolkit.py b/backend/app/utils/toolkit/notion_toolkit.py index 7cd72e97..42772c58 100644 --- a/backend/app/utils/toolkit/notion_toolkit.py +++ b/backend/app/utils/toolkit/notion_toolkit.py @@ -12,9 +12,10 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import List + from camel.toolkits import NotionToolkit as BaseNotionToolkit from camel.toolkits.function_tool import FunctionTool + from app.component.environment import env from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit @@ -35,7 +36,7 @@ class NotionToolkit(BaseNotionToolkit, AbstractToolkit): self.api_task_id = api_task_id @classmethod - def get_can_use_tools(cls, api_task_id: str) -> List[FunctionTool]: + def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: if env("NOTION_TOKEN"): return NotionToolkit(api_task_id).get_tools() else: diff --git a/backend/app/utils/toolkit/openai_image_toolkit.py b/backend/app/utils/toolkit/openai_image_toolkit.py index 6a52ef3f..f7d376ca 100644 --- a/backend/app/utils/toolkit/openai_image_toolkit.py +++ b/backend/app/utils/toolkit/openai_image_toolkit.py @@ -12,14 +12,13 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import os +from typing import Literal + from camel.toolkits import OpenAIImageToolkit as BaseOpenAIImageToolkit -from app.component.environment import env from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from typing import Literal, Optional, Union, List @auto_listen_toolkit(BaseOpenAIImageToolkit) @@ -29,7 +28,10 @@ class OpenAIImageToolkit(BaseOpenAIImageToolkit, AbstractToolkit): def __init__( self, api_task_id: str, - model: None | Literal["gpt-image-1"] | Literal["dall-e-3"] | Literal["dall-e-2"] = "gpt-image-1", + model: None + | Literal["gpt-image-1"] + | Literal["dall-e-3"] + | Literal["dall-e-2"] = "gpt-image-1", timeout: float | None = None, api_key: str | None = None, url: str | None = None, @@ -49,30 +51,51 @@ class OpenAIImageToolkit(BaseOpenAIImageToolkit, AbstractToolkit): | Literal["high"] | Literal["standard"] | Literal["hd"] = "standard", - response_format: None | Literal["url"] | Literal["b64_json"] = "b64_json", - background: None | Literal["transparent"] | Literal["opaque"] | Literal["auto"] = "auto", + response_format: None + | Literal["url"] + | Literal["b64_json"] = "b64_json", + background: None + | Literal["transparent"] + | Literal["opaque"] + | Literal["auto"] = "auto", style: None | Literal["vivid"] | Literal["natural"] = None, working_directory: str | None = None, ): self.api_task_id = api_task_id super().__init__( - model, timeout, api_key, url, size, quality, response_format, background, style, working_directory + model, + timeout, + api_key, + url, + size, + quality, + response_format, + background, + style, + working_directory, ) @listen_toolkit(BaseOpenAIImageToolkit.generate_image) - def generate_image(self, prompt: str, image_name: Union[str, List[str]] = "image.png", n: int = 1,) -> str: + def generate_image( + self, + prompt: str, + image_name: str | list[str] = "image.png", + n: int = 1, + ) -> str: # Validate image_name ends with .png if isinstance(image_name, str): - if not image_name.endswith('.png'): - return f"Error: Image name must end with .png, got: {image_name}" + if not image_name.endswith(".png"): + return ( + f"Error: Image name must end with .png, got: {image_name}" + ) elif isinstance(image_name, list): for name in image_name: - if not name.endswith('.png'): + if not name.endswith(".png"): return f"Error: All image names must end with .png, got: {name}" - + return super().generate_image(prompt, image_name, n) - def _build_base_params(self, prompt: str, n: Optional[int] = None) -> dict: + def _build_base_params(self, prompt: str, n: int | None = None) -> dict: params = super()._build_base_params(prompt, n) params["user"] = self.api_task_id # support cloud key billing return params diff --git a/backend/app/utils/toolkit/pptx_toolkit.py b/backend/app/utils/toolkit/pptx_toolkit.py index 35209ef8..6f895322 100644 --- a/backend/app/utils/toolkit/pptx_toolkit.py +++ b/backend/app/utils/toolkit/pptx_toolkit.py @@ -12,15 +12,23 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import asyncio import os + from camel.toolkits import PPTXToolkit as BasePPTXToolkit from app.component.environment import env -from app.service.task import ActionWriteFileData, Agents, get_task_lock -from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit, _safe_put_queue +from app.service.task import ( + ActionWriteFileData, + Agents, + get_task_lock, + process_task, +) +from app.utils.listen.toolkit_listen import ( + _safe_put_queue, + auto_listen_toolkit, + listen_toolkit, +) from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from app.service.task import process_task @auto_listen_toolkit(BasePPTXToolkit) @@ -35,7 +43,9 @@ class PPTXToolkit(BasePPTXToolkit, AbstractToolkit): ) -> None: self.api_task_id = api_task_id if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/Downloads")) + working_directory = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) super().__init__(working_directory, timeout) @listen_toolkit( @@ -45,7 +55,9 @@ class PPTXToolkit(BasePPTXToolkit, AbstractToolkit): filename, template=None: f"create presentation with content: {content}, filename: {filename}, template: {template}", ) - def create_presentation(self, content: str, filename: str, template: str | None = None) -> str: + def create_presentation( + self, content: str, filename: str, template: str | None = None + ) -> str: if not filename.lower().endswith(".pptx"): filename += ".pptx" @@ -59,6 +71,9 @@ class PPTXToolkit(BasePPTXToolkit, AbstractToolkit): # Use _safe_put_queue to handle both sync and async contexts _safe_put_queue( task_lock, - ActionWriteFileData(process_task_id=current_process_task_id, data=str(file_path)) + ActionWriteFileData( + process_task_id=current_process_task_id, + data=str(file_path), + ), ) return res diff --git a/backend/app/utils/toolkit/pyautogui_toolkit.py b/backend/app/utils/toolkit/pyautogui_toolkit.py index d6e985ba..ca03ac69 100644 --- a/backend/app/utils/toolkit/pyautogui_toolkit.py +++ b/backend/app/utils/toolkit/pyautogui_toolkit.py @@ -13,7 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import os -from typing import List, Literal + from camel.toolkits import PyAutoGUIToolkit as BasePyAutoGUIToolkit from app.component.environment import env @@ -33,6 +33,8 @@ class PyAutoGUIToolkit(BasePyAutoGUIToolkit, AbstractToolkit): screenshots_dir: str | None = None, ): if screenshots_dir is None: - screenshots_dir = env("file_save_path", os.path.expanduser("~/Downloads")) + screenshots_dir = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) super().__init__(timeout, screenshots_dir) self.api_task_id = api_task_id diff --git a/backend/app/utils/toolkit/reddit_toolkit.py b/backend/app/utils/toolkit/reddit_toolkit.py index 9ecf0b99..865c3971 100644 --- a/backend/app/utils/toolkit/reddit_toolkit.py +++ b/backend/app/utils/toolkit/reddit_toolkit.py @@ -37,8 +37,11 @@ class RedditToolkit(BaseRedditToolkit, AbstractToolkit): @classmethod def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: - if env("REDDIT_CLIENT_ID") and env("REDDIT_CLIENT_SECRET" - ) and env("REDDIT_USER_AGENT"): + if ( + env("REDDIT_CLIENT_ID") + and env("REDDIT_CLIENT_SECRET") + and env("REDDIT_USER_AGENT") + ): return RedditToolkit(api_task_id).get_tools() else: return [] diff --git a/backend/app/utils/toolkit/screenshot_toolkit.py b/backend/app/utils/toolkit/screenshot_toolkit.py index 536a5c18..584cffba 100644 --- a/backend/app/utils/toolkit/screenshot_toolkit.py +++ b/backend/app/utils/toolkit/screenshot_toolkit.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import os + from camel.toolkits import ScreenshotToolkit as BaseScreenshotToolkit from app.component.environment import env @@ -25,8 +26,15 @@ from app.utils.toolkit.abstract_toolkit import AbstractToolkit class ScreenshotToolkit(BaseScreenshotToolkit, AbstractToolkit): agent_name: str = Agents.developer_agent - def __init__(self, api_task_id, working_directory: str | None = None, timeout: float | None = None): + def __init__( + self, + api_task_id, + working_directory: str | None = None, + timeout: float | None = None, + ): self.api_task_id = api_task_id if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/Downloads")) + working_directory = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) super().__init__(working_directory, timeout) diff --git a/backend/app/utils/toolkit/search_toolkit.py b/backend/app/utils/toolkit/search_toolkit.py index 4824a94f..2b4f15a1 100644 --- a/backend/app/utils/toolkit/search_toolkit.py +++ b/backend/app/utils/toolkit/search_toolkit.py @@ -12,16 +12,18 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import Any, Dict, List, Literal +import logging +import os +from typing import Any + +import httpx from camel.toolkits import SearchToolkit as BaseSearchToolkit from camel.toolkits.function_tool import FunctionTool -import httpx -import os + from app.component.environment import env, env_not_empty from app.service.task import Agents from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -import logging logger = logging.getLogger("search_toolkit") @@ -35,14 +37,12 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): api_task_id: str, agent_name: str | None = None, timeout: float | None = None, - exclude_domains: List[str] | None = None, + exclude_domains: list[str] | None = None, ): self.api_task_id = api_task_id if agent_name is not None: self.agent_name = agent_name - super().__init__( - timeout=timeout, exclude_domains=exclude_domains - ) + super().__init__(timeout=timeout, exclude_domains=exclude_domains) # Cache for user-specific search configurations self._user_google_api_key = None self._user_search_engine_id = None @@ -68,7 +68,9 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): self._user_search_engine_id = search_engine_id logger.info("Loaded user-specific Google Search configuration") else: - logger.debug("No user-specific Google Search configuration found, will use cloud search") + logger.debug( + "No user-specific Google Search configuration found, will use cloud search" + ) # @listen_toolkit(BaseSearchToolkit.search_wiki) # def search_wiki(self, entity: str) -> str: @@ -94,14 +96,18 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): @listen_toolkit( BaseSearchToolkit.search_google, - lambda _, query, search_type="web", number_of_result_pages=10, start_page=1: f"with query '{query}', {search_type} type, {number_of_result_pages} result pages starting from page {start_page}", + lambda _, + query, + search_type="web", + number_of_result_pages=10, + start_page=1: f"with query '{query}', {search_type} type, {number_of_result_pages} result pages starting from page {start_page}", ) def search_google( self, query: str, search_type: str = "web", number_of_result_pages: int = 10, - start_page: int = 1 + start_page: int = 1, ) -> list[dict[str, Any]]: # Load user-specific configuration self._load_user_search_config() @@ -116,7 +122,9 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): try: os.environ["GOOGLE_API_KEY"] = self._user_google_api_key os.environ["SEARCH_ENGINE_ID"] = self._user_search_engine_id - return super().search_google(query, search_type, number_of_result_pages, start_page) + return super().search_google( + query, search_type, number_of_result_pages, start_page + ) finally: # Restore original environment variables if old_google_key is not None: @@ -130,15 +138,19 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): del os.environ["SEARCH_ENGINE_ID"] else: # Fallback to cloud search - logger.info("Using cloud Google Search (no user configuration found)") - return self.cloud_search_google(query, search_type, number_of_result_pages, start_page) + logger.info( + "Using cloud Google Search (no user configuration found)" + ) + return self.cloud_search_google( + query, search_type, number_of_result_pages, start_page + ) def cloud_search_google( self, query: str, search_type: str = "web", number_of_result_pages: int = 10, - start_page: int = 1 + start_page: int = 1, ): url = env_not_empty("SERVER_URL") res = httpx.get( @@ -147,7 +159,7 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): "query": query, "search_type": search_type, "number_of_result_pages": number_of_result_pages, - "start_page": start_page + "start_page": start_page, }, headers={"api-key": env_not_empty("cloud_api_key")}, ) @@ -366,7 +378,9 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit): # if env("BRAVE_API_KEY"): # tools.append(FunctionTool(search_toolkit.search_brave)) - if (env("GOOGLE_API_KEY") and env("SEARCH_ENGINE_ID")) or env("cloud_api_key"): + if (env("GOOGLE_API_KEY") and env("SEARCH_ENGINE_ID")) or env( + "cloud_api_key" + ): tools.append(FunctionTool(search_toolkit.search_google)) # if env("TAVILY_API_KEY"): diff --git a/backend/app/utils/toolkit/terminal_toolkit.py b/backend/app/utils/toolkit/terminal_toolkit.py index 353b2680..1736d3fa 100644 --- a/backend/app/utils/toolkit/terminal_toolkit.py +++ b/backend/app/utils/toolkit/terminal_toolkit.py @@ -19,17 +19,23 @@ 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 + +from camel.toolkits.terminal_toolkit import ( + TerminalToolkit as BaseTerminalToolkit, +) from camel.toolkits.terminal_toolkit.terminal_toolkit import _to_plain + from app.component.environment import env -from app.service.task import Action, ActionTerminalData, Agents, get_task_lock +from app.service.task import ( + Action, + ActionTerminalData, + Agents, + get_task_lock, + process_task, +) from app.utils.listen.toolkit_listen import auto_listen_toolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit -from app.service.task import process_task -import logging logger = logging.getLogger("terminal_toolkit") @@ -44,14 +50,14 @@ def get_terminal_base_venv_path() -> str: os.path.expanduser("~"), ".eigent", "venvs", - f"terminal_base-{APP_VERSION}" + f"terminal_base-{APP_VERSION}", ) @auto_listen_toolkit(BaseTerminalToolkit) class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): agent_name: str = Agents.developer_agent - _thread_pool: Optional[ThreadPoolExecutor] = None + _thread_pool: ThreadPoolExecutor | None = None _thread_local = threading.local() def __init__( @@ -72,22 +78,26 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): self.agent_name = agent_name # Get base directory from environment - base_dir = env("file_save_path", os.path.expanduser("~/.eigent/terminal/")) + 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, - "working_directory": working_directory, - "agent_venv_dir": self._agent_venv_dir, - }) + logger.debug( + f"Initializing TerminalToolkit for agent={self.agent_name}", + extra={ + "api_task_id": api_task_id, + "working_directory": working_directory, + "agent_venv_dir": self._agent_venv_dir, + }, + ) if TerminalToolkit._thread_pool is None: TerminalToolkit._thread_pool = ThreadPoolExecutor( - max_workers=1, - thread_name_prefix="terminal_toolkit" + max_workers=1, thread_name_prefix="terminal_toolkit" ) super().__init__( @@ -104,13 +114,17 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): # 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 - }) + 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. @@ -122,8 +136,10 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): 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") + 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") @@ -135,13 +151,17 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): return # Check if cloned env already exists - if platform.system() == 'Windows': - cloned_python = os.path.join(self.cloned_env_path, "Scripts", "python.exe") + 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}") + logger.info( + f"Using existing cloned environment: {self.cloned_env_path}" + ) self.python_executable = cloned_python return @@ -153,13 +173,19 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): # 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._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}") + 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) + 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) @@ -167,7 +193,7 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): def _get_venv_path(self): """Return the cloned venv path for shell activation.""" - cloned_env_path = getattr(self, 'cloned_env_path', None) + 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 @@ -177,20 +203,22 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): Creates the structure needed: pyvenv.cfg, bin/python, lib symlink, and activate scripts. """ - is_windows = platform.system() == 'Windows' + 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', encoding='utf-8') as f: + with open(source_cfg, encoding="utf-8") as f: for line in f: - if line.startswith('home = '): - python_home = line.split('=', 1)[1].strip() + 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}") + 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")) @@ -208,17 +236,20 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): 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: + with open(src, 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: + 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) + 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") @@ -236,12 +267,12 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): 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: + with open(src) 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: + with open(dst, "w") as f: f.write(content) # Symlink lib directory @@ -257,11 +288,14 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): """ # Convert ANSI escape sequences to plain text super()._write_to_log(log_file, content) - logger.debug("Terminal output logged", extra={ - "api_task_id": self.api_task_id, - "log_file": log_file, - "content_length": len(content) - }) + logger.debug( + "Terminal output logged", + extra={ + "api_task_id": self.api_task_id, + "log_file": log_file, + "content_length": len(content), + }, + ) self._update_terminal_output(_to_plain(content)) def _update_terminal_output(self, output: str): @@ -285,10 +319,10 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): if hasattr(task_lock, "add_background_task"): task_lock.add_background_task(task) except RuntimeError: - self._thread_pool.submit(self._run_coro_in_thread, coro,task_lock) + self._thread_pool.submit(self._run_coro_in_thread, coro, task_lock) @staticmethod - def _run_coro_in_thread(coro,task_lock): + def _run_coro_in_thread(coro, task_lock): """ Execute coro in the thread pool, with each thread bound to a long-term event loop """ @@ -312,7 +346,7 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): except Exception as e: logging.error( f"Failed to execute coroutine in thread pool: {str(e)}", - exc_info=True + exc_info=True, ) def shell_exec( @@ -337,9 +371,12 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): # Auto-generate ID if not provided if id is None: import time + id = f"auto_{int(time.time() * 1000)}" - result = super().shell_exec(id=id, command=command, block=block, timeout=timeout) + result = super().shell_exec( + id=id, command=command, block=block, timeout=timeout + ) # If the command executed successfully but returned empty output, # provide a clear success message to help the AI agent understand @@ -363,36 +400,48 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit): return # Remove cloned env (.venv) if it exists - cloned_env_path = getattr(self, 'cloned_env_path', None) + 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 - }) + 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) - }) + 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) + 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 - }) + 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) - }) + 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): diff --git a/backend/app/utils/toolkit/thinking_toolkit.py b/backend/app/utils/toolkit/thinking_toolkit.py index 9cc5cf23..9989b16e 100644 --- a/backend/app/utils/toolkit/thinking_toolkit.py +++ b/backend/app/utils/toolkit/thinking_toolkit.py @@ -20,8 +20,9 @@ from app.utils.toolkit.abstract_toolkit import AbstractToolkit @auto_listen_toolkit(BaseThinkingToolkit) class ThinkingToolkit(BaseThinkingToolkit, AbstractToolkit): - - def __init__(self, api_task_id: str, agent_name: str, timeout: float | None = None): + def __init__( + self, api_task_id: str, agent_name: str, timeout: float | None = None + ): super().__init__(timeout) self.api_task_id = api_task_id self.agent_name = agent_name diff --git a/backend/app/utils/toolkit/twitter_toolkit.py b/backend/app/utils/toolkit/twitter_toolkit.py index 8ff53a21..29b14c7f 100644 --- a/backend/app/utils/toolkit/twitter_toolkit.py +++ b/backend/app/utils/toolkit/twitter_toolkit.py @@ -12,10 +12,8 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from typing import List -from camel.toolkits import FunctionTool -from camel.toolkits import TwitterToolkit as BaseTwitterToolkit +from camel.toolkits import FunctionTool, TwitterToolkit as BaseTwitterToolkit from camel.toolkits.twitter_toolkit import ( create_tweet, delete_tweet, @@ -39,8 +37,9 @@ class TwitterToolkit(BaseTwitterToolkit, AbstractToolkit): @listen_toolkit( create_tweet, - lambda _, text, **kwargs: - f"create tweet with text: {text} and options: {kwargs}", + lambda _, + text, + **kwargs: f"create tweet with text: {text} and options: {kwargs}", ) def create_tweet( self, @@ -74,7 +73,7 @@ class TwitterToolkit(BaseTwitterToolkit, AbstractToolkit): def get_user_by_username(self, username: str) -> str: return get_user_by_username(username) - def get_tools(self) -> List[FunctionTool]: + def get_tools(self) -> list[FunctionTool]: return [ FunctionTool(self.create_tweet), FunctionTool(self.delete_tweet), @@ -83,9 +82,10 @@ class TwitterToolkit(BaseTwitterToolkit, AbstractToolkit): ] @classmethod - def get_can_use_tools(cls, api_task_id: str) -> List[FunctionTool]: + def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]: if ( - env("TWITTER_CONSUMER_KEY") and env("TWITTER_CONSUMER_SECRET") + env("TWITTER_CONSUMER_KEY") + and env("TWITTER_CONSUMER_SECRET") and env("TWITTER_ACCESS_TOKEN") and env("TWITTER_ACCESS_TOKEN_SECRET") ): diff --git a/backend/app/utils/toolkit/video_analysis_toolkit.py b/backend/app/utils/toolkit/video_analysis_toolkit.py index 35f59da7..2a76f1bd 100644 --- a/backend/app/utils/toolkit/video_analysis_toolkit.py +++ b/backend/app/utils/toolkit/video_analysis_toolkit.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import os + from camel.models import BaseModelBackend from camel.toolkits import VideoAnalysisToolkit as BaseVideoAnalysisToolkit @@ -40,7 +41,9 @@ class VideoAnalysisToolkit(BaseVideoAnalysisToolkit, AbstractToolkit): ) -> None: self.api_task_id = api_task_id if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/Downloads")) + working_directory = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) super().__init__( working_directory, model, diff --git a/backend/app/utils/toolkit/video_download_toolkit.py b/backend/app/utils/toolkit/video_download_toolkit.py index 843a0e27..395634cd 100644 --- a/backend/app/utils/toolkit/video_download_toolkit.py +++ b/backend/app/utils/toolkit/video_download_toolkit.py @@ -13,8 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import os -from typing import List -from PIL.Image import Image + from camel.toolkits import VideoDownloaderToolkit as BaseVideoDownloaderToolkit from app.component.environment import env @@ -35,6 +34,8 @@ class VideoDownloaderToolkit(BaseVideoDownloaderToolkit, AbstractToolkit): timeout: float | None = None, ) -> None: if working_directory is None: - working_directory = env("file_save_path", os.path.expanduser("~/Downloads")) + working_directory = env( + "file_save_path", os.path.expanduser("~/Downloads") + ) super().__init__(working_directory, cookies_path, timeout) self.api_task_id = api_task_id diff --git a/backend/app/utils/toolkit/web_deploy_toolkit.py b/backend/app/utils/toolkit/web_deploy_toolkit.py index 4b3af64f..c7d2a5ff 100644 --- a/backend/app/utils/toolkit/web_deploy_toolkit.py +++ b/backend/app/utils/toolkit/web_deploy_toolkit.py @@ -13,7 +13,8 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import uuid -from typing import Any, Dict +from typing import Any + from camel.toolkits import WebDeployToolkit as BaseWebDeployToolkit from app.service.task import Agents @@ -37,7 +38,15 @@ class WebDeployToolkit(BaseWebDeployToolkit, AbstractToolkit): remote_server_port: int = 8080, ): self.api_task_id = api_task_id - super().__init__(timeout, add_branding_tag, logo_path, tag_text, tag_url, remote_server_ip, remote_server_port) + super().__init__( + timeout, + add_branding_tag, + logo_path, + tag_text, + tag_url, + remote_server_ip, + remote_server_port, + ) @listen_toolkit(BaseWebDeployToolkit.deploy_html_content) def deploy_html_content( @@ -48,13 +57,19 @@ class WebDeployToolkit(BaseWebDeployToolkit, AbstractToolkit): port: int = 8080, domain: str | None = None, subdirectory: str | None = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: subdirectory = str(uuid.uuid4()) - return super().deploy_html_content(html_content, html_file_path, file_name, port, domain, subdirectory) + return super().deploy_html_content( + html_content, html_file_path, file_name, port, domain, subdirectory + ) @listen_toolkit(BaseWebDeployToolkit.deploy_folder) def deploy_folder( - self, folder_path: str, port: int = 8080, domain: str | None = None, subdirectory: str | None = None - ) -> Dict[str, Any]: + self, + folder_path: str, + port: int = 8080, + domain: str | None = None, + subdirectory: str | None = None, + ) -> dict[str, Any]: subdirectory = str(uuid.uuid4()) return super().deploy_folder(folder_path, port, domain, subdirectory) diff --git a/backend/app/utils/workforce.py b/backend/app/utils/workforce.py index 03c82b86..ff1c7784 100644 --- a/backend/app/utils/workforce.py +++ b/backend/app/utils/workforce.py @@ -14,43 +14,55 @@ import asyncio import logging -from typing import Generator, List, Optional +from collections.abc import Generator -from app.component import code -from app.exception.exception import UserException -from app.service.task import (Action, ActionAssignTaskData, ActionEndData, - ActionTaskStateData, ActionTimeoutData, - get_camel_task, get_task_lock) -from app.agent.listen_chat_agent import ListenChatAgent -from app.utils.single_agent_worker import SingleAgentWorker -from app.utils.telemetry.workforce_metrics import WorkforceMetricsCallback from camel.agents import ChatAgent from camel.societies.workforce.base import BaseNode -from camel.societies.workforce.events import (TaskAssignedEvent, - TaskCompletedEvent, - TaskCreatedEvent, - TaskFailedEvent, - WorkerCreatedEvent) +from camel.societies.workforce.events import ( + TaskAssignedEvent, + TaskCompletedEvent, + TaskCreatedEvent, + TaskFailedEvent, + WorkerCreatedEvent, +) from camel.societies.workforce.prompts import TASK_DECOMPOSE_PROMPT from camel.societies.workforce.task_channel import TaskChannel -from camel.societies.workforce.utils import (FailureHandlingConfig, - TaskAssignResult) -from camel.societies.workforce.workforce import DEFAULT_WORKER_POOL_SIZE -from camel.societies.workforce.workforce import Workforce as BaseWorkforce -from camel.societies.workforce.workforce import WorkforceState +from camel.societies.workforce.utils import ( + FailureHandlingConfig, + TaskAssignResult, +) +from camel.societies.workforce.workforce import ( + DEFAULT_WORKER_POOL_SIZE, + Workforce as BaseWorkforce, + WorkforceState, +) from camel.societies.workforce.workforce_metrics import WorkforceMetrics from camel.tasks.task import Task, TaskState, validate_task_content +from app.agent.listen_chat_agent import ListenChatAgent +from app.component import code +from app.exception.exception import UserException +from app.service.task import ( + Action, + ActionAssignTaskData, + ActionEndData, + ActionTaskStateData, + ActionTimeoutData, + get_camel_task, + get_task_lock, +) +from app.utils.single_agent_worker import SingleAgentWorker +from app.utils.telemetry.workforce_metrics import WorkforceMetricsCallback + logger = logging.getLogger("workforce") class Workforce(BaseWorkforce): - def __init__( self, api_task_id: str, description: str, - children: List[BaseNode] | None = None, + children: list[BaseNode] | None = None, coordinator_agent: ChatAgent | None = None, task_agent: ChatAgent | None = None, new_worker_agent: ChatAgent | None = None, @@ -60,12 +72,15 @@ class Workforce(BaseWorkforce): ) -> None: self.api_task_id = api_task_id logger.info("=" * 80) - logger.info("🏭 [WF-LIFECYCLE] Workforce.__init__ STARTED", - extra={"api_task_id": api_task_id}) + logger.info( + "🏭 [WF-LIFECYCLE] Workforce.__init__ STARTED", + extra={"api_task_id": api_task_id}, + ) logger.info(f"[WF-LIFECYCLE] Workforce id will be: {id(self)}") logger.info( f"[WF-LIFECYCLE] Init params: graceful_shutdown_timeout=" - f"{graceful_shutdown_timeout}, share_memory={share_memory}") + f"{graceful_shutdown_timeout}, share_memory={share_memory}" + ) logger.info("=" * 80) super().__init__( description=description, @@ -78,12 +93,14 @@ class Workforce(BaseWorkforce): use_structured_output_handler=use_structured_output_handler, task_timeout_seconds=3600, # 60 minutes failure_handling_config=FailureHandlingConfig( - enabled_strategies=["retry", "replan"], ), + enabled_strategies=["retry", "replan"], + ), ) self.task_agent.stream_accumulate = True self.task_agent._stream_accumulate_explicit = True logger.info( - f"[WF-LIFECYCLE] ✅ Workforce.__init__ COMPLETED, id={id(self)}") + f"[WF-LIFECYCLE] ✅ Workforce.__init__ COMPLETED, id={id(self)}" + ) def eigent_make_sub_tasks( self, @@ -105,11 +122,10 @@ class Workforce(BaseWorkforce): on_stream_text: Optional callback for raw streaming text chunks """ - logger.debug("[DECOMPOSE] eigent_make_sub_tasks called", - extra={ - "api_task_id": self.api_task_id, - "task_id": task.id - }) + logger.debug( + "[DECOMPOSE] eigent_make_sub_tasks called", + extra={"api_task_id": self.api_task_id, "task_id": task.id}, + ) if not validate_task_content(task.content, task.id): task.state = TaskState.FAILED @@ -117,12 +133,12 @@ class Workforce(BaseWorkforce): 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 - }) + "task_id": task.id, + "content_preview": task.content[:50] + "..." + if len(task.content) > 50 + else task.content, + }, + ) raise UserException(code.error, task.result) self.reset() @@ -136,21 +152,29 @@ class Workforce(BaseWorkforce): reset=False, coordinator_context=coordinator_context, on_stream_batch=on_stream_batch, - on_stream_text=on_stream_text)) + on_stream_text=on_stream_text, + ) + ) - logger.info("[DECOMPOSE] Task decomposition completed", - extra={ - "api_task_id": self.api_task_id, - "task_id": task.id, - "subtasks_count": len(subtasks) - }) + logger.info( + "[DECOMPOSE] Task decomposition completed", + extra={ + "api_task_id": self.api_task_id, + "task_id": task.id, + "subtasks_count": len(subtasks), + }, + ) return subtasks async def eigent_start(self, subtasks: list[Task]): """start the workforce""" - logger.debug((f"[WF-LIFECYCLE] eigent_start called with " - f"{len(subtasks)} subtasks"), - extra={"api_task_id": self.api_task_id}) + logger.debug( + ( + f"[WF-LIFECYCLE] eigent_start called with " + f"{len(subtasks)} subtasks" + ), + extra={"api_task_id": self.api_task_id}, + ) # Clear existing pending tasks to use the user-edited task list # (tasks may have been added during decomposition before user edits) self._pending_tasks.clear() @@ -161,12 +185,11 @@ class Workforce(BaseWorkforce): try: await self.start() except Exception as e: - logger.error(f"[WF-LIFECYCLE] Error in workforce execution: {e}", - extra={ - "api_task_id": self.api_task_id, - "error": str(e) - }, - exc_info=True) + 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 raise finally: @@ -180,12 +203,13 @@ class Workforce(BaseWorkforce): content=task.content, child_nodes_info=self._get_child_nodes_info(), additional_info=task.additional_info, - )) + ) + ) self.task_agent.reset() - result = task.decompose(self.task_agent, - decompose_prompt, - stream_callback=stream_callback) + result = task.decompose( + self.task_agent, decompose_prompt, stream_callback=stream_callback + ) if isinstance(result, Generator): @@ -195,7 +219,8 @@ class Workforce(BaseWorkforce): all_subtasks.extend(new_tasks) if new_tasks: self._update_dependencies_for_decomposition( - task, all_subtasks) + task, all_subtasks + ) yield new_tasks return streaming_with_dependencies() @@ -212,7 +237,7 @@ class Workforce(BaseWorkforce): coordinator_context: str = "", on_stream_batch=None, on_stream_text=None, - ) -> List[Task]: + ) -> list[Task]: """Override to support coordinator_context parameter. Handle task decomposition and validation, then append to pending tasks. @@ -230,15 +255,19 @@ class Workforce(BaseWorkforce): Returns: List[Task]: The decomposed subtasks or the original task """ - logger.debug(f"[DECOMPOSE] handle_decompose_append_task called, " - f"task_id={task.id}, reset={reset}") + logger.debug( + f"[DECOMPOSE] handle_decompose_append_task called, " + f"task_id={task.id}, reset={reset}" + ) if not validate_task_content(task.content, task.id): task.state = TaskState.FAILED task.result = "Task failed: Invalid or empty content provided" - logger.warning(f"[DECOMPOSE] Task {task.id} rejected: " - f"Invalid or empty content. " - f"Content preview: '{task.content}'") + logger.warning( + f"[DECOMPOSE] Task {task.id} rejected: " + f"Invalid or empty content. " + f"Content preview: '{task.content}'" + ) return [task] if reset and self._state != WorkforceState.RUNNING: @@ -249,15 +278,20 @@ class Workforce(BaseWorkforce): if coordinator_context: original_content = task.content - task_with_context = (coordinator_context + - "\n=== CURRENT TASK ===\n" + original_content) + task_with_context = ( + coordinator_context + + "\n=== CURRENT TASK ===\n" + + original_content + ) task.content = task_with_context subtasks_result = self._decompose_task( - task, stream_callback=on_stream_text) + task, stream_callback=on_stream_text + ) task.content = original_content else: subtasks_result = self._decompose_task( - task, stream_callback=on_stream_text) + task, stream_callback=on_stream_text + ) if isinstance(subtasks_result, Generator): subtasks = [] @@ -280,7 +314,8 @@ class Workforce(BaseWorkforce): self._pending_tasks.extendleft(reversed(subtasks)) # Log task created events metrics_callbacks = [ - cb for cb in self._callbacks + cb + for cb in self._callbacks if isinstance(cb, WorkforceMetrics) ] if metrics_callbacks: @@ -295,7 +330,8 @@ class Workforce(BaseWorkforce): if not subtasks: logger.warning( - "[DECOMPOSE] No subtasks returned, creating fallback task") + "[DECOMPOSE] No subtasks returned, creating fallback task" + ) fallback_task = Task( content=task.content, id=f"{task.id}.1", @@ -306,7 +342,8 @@ class Workforce(BaseWorkforce): # Log fallback task created event metrics_callbacks = [ - cb for cb in self._callbacks + cb + for cb in self._callbacks if isinstance(cb, WorkforceMetrics) ] if metrics_callbacks: @@ -324,8 +361,10 @@ class Workforce(BaseWorkforce): except Exception as e: logger.warning(f"Final streaming callback failed: {e}") - logger.debug(f"[DECOMPOSE] handle_decompose_append_task completed, " - f"returned {len(subtasks)} subtasks") + logger.debug( + f"[DECOMPOSE] handle_decompose_append_task completed, " + f"returned {len(subtasks)} subtasks" + ) return subtasks def _get_agent_id_from_node_id(self, node_id: str) -> str | None: @@ -337,13 +376,14 @@ class Workforce(BaseWorkforce): This method provides the mapping. """ for child in self._children: - if hasattr(child, 'node_id') and child.node_id == node_id: - if hasattr(child, 'worker') and hasattr( - child.worker, 'agent_id'): + if hasattr(child, "node_id") and child.node_id == node_id: + if hasattr(child, "worker") and hasattr( + child.worker, "agent_id" + ): return child.worker.agent_id return None - def _extract_model_type(self, agent: ChatAgent) -> Optional[str]: + def _extract_model_type(self, agent: ChatAgent) -> str | None: """Extract model type from agent's model_backend. Handles both ModelManager (multiple models) and single model cases. @@ -354,24 +394,27 @@ class Workforce(BaseWorkforce): Returns: Model type as string, or None if not found """ - if not hasattr(agent, 'model_backend') or not agent.model_backend: + if not hasattr(agent, "model_backend") or not agent.model_backend: return None model_obj = agent.model_backend # Handle ModelManager case (multiple models) - if hasattr(model_obj, 'models') and model_obj.models: + if hasattr(model_obj, "models") and model_obj.models: first_model = model_obj.models[0] if model_obj.models else None if first_model: - mt = getattr(first_model, 'model_type', None) - return str( - mt.value if hasattr(mt, 'value') else mt) if mt else None + mt = getattr(first_model, "model_type", None) + return ( + str(mt.value if hasattr(mt, "value") else mt) + if mt + else None + ) # Handle single model case - mt = getattr(model_obj, 'model_type', None) - return str(mt.value if hasattr(mt, 'value') else mt) if mt else None + mt = getattr(model_obj, "model_type", None) + return str(mt.value if hasattr(mt, "value") else mt) if mt else None - async def _find_assignee(self, tasks: List[Task]) -> TaskAssignResult: + async def _find_assignee(self, tasks: list[Task]) -> TaskAssignResult: # Task assignment phase: send "waiting for execution" notification # to the frontend, and send "start execution" notification when the # task actually begins execution @@ -381,8 +424,10 @@ class Workforce(BaseWorkforce): for item in assigned.assignments: # DEBUG ▶ Task has been assigned to which worker # and its dependencies - logger.debug(f"[WF] ASSIGN {item.task_id} -> {item.assignee_id} " - f"deps={item.dependencies}") + logger.debug( + f"[WF] ASSIGN {item.task_id} -> {item.assignee_id} " + f"deps={item.dependencies}" + ) # The main task itself does not need notification if self._task and item.task_id == self._task.id: continue @@ -392,7 +437,8 @@ class Workforce(BaseWorkforce): logger.warning( f"[WF] WARN: Task {item.task_id} not found in " f"tasks list during ASSIGN phase. This may indicate " - f"a task tree inconsistency.") + f"a task tree inconsistency." + ) content = "" else: content = task_obj.content @@ -407,7 +453,8 @@ class Workforce(BaseWorkforce): f"[WF] ASSIGN Skip notification for task {item.task_id}: " f"already has assigned_worker_id=" f"{task_obj.assigned_worker_id}, " - f"new assignee={item.assignee_id} (retry/replan scenario)") + f"new assignee={item.assignee_id} (retry/replan scenario)" + ) continue # Map node_id to agent_id for frontend communication @@ -416,13 +463,14 @@ class Workforce(BaseWorkforce): agent_id = self._get_agent_id_from_node_id(item.assignee_id) if agent_id is None: workers = [ - c.node_id for c in self._children if hasattr(c, 'node_id') + c.node_id for c in self._children if hasattr(c, "node_id") ] logger.error( f"[WF] ERROR: Could not find agent_id for " f"node_id={item.assignee_id}. Task {item.task_id} " f"will not be properly tracked on frontend. " - f"Available workers: {workers}") + f"Available workers: {workers}" + ) continue # Skip sending notification for unmapped worker # Asynchronously send waiting notification @@ -437,12 +485,15 @@ class Workforce(BaseWorkforce): "state": "waiting", # Mark as waiting state "failure_count": 0, }, - ))) + ) + ) + ) # Track the task for cleanup task_lock.add_background_task(task) metrics_callbacks = [ - cb for cb in self._callbacks + cb + for cb in self._callbacks if isinstance(cb, WorkforceMetrics) ] if metrics_callbacks: @@ -469,15 +520,17 @@ class Workforce(BaseWorkforce): # Map node_id to agent_id for frontend communication agent_id = self._get_agent_id_from_node_id(assignee_id) workers = [ - c.node_id for c in self._children if hasattr(c, 'node_id') + c.node_id for c in self._children if hasattr(c, "node_id") ] if agent_id is None: - logger.error(f"[WF] ERROR: Could not find agent_id " - f"for node_id={assignee_id}. " - f"Task {task.id} will not be properly " - f"tracked on frontend. " - f"Available workers: " - f"{workers}") + logger.error( + f"[WF] ERROR: Could not find agent_id " + f"for node_id={assignee_id}. " + f"Task {task.id} will not be properly " + f"tracked on frontend. " + f"Available workers: " + f"{workers}" + ) else: await task_lock.put_queue( ActionAssignTaskData( @@ -489,7 +542,8 @@ class Workforce(BaseWorkforce): "state": "running", # running state "failure_count": task.failure_count, }, - )) + ) + ) # Call the parent class method to continue the # normal task publishing process await super()._post_task(task, assignee_id) @@ -504,7 +558,8 @@ class Workforce(BaseWorkforce): if self._state == WorkforceState.RUNNING: raise RuntimeError( "Cannot add workers while workforce is running. " - "Pause the workforce first.") + "Pause the workforce first." + ) # Validate worker agent compatibility self._validate_agent_compatibility(worker, "Worker agent") @@ -535,8 +590,9 @@ class Workforce(BaseWorkforce): ] if metrics_callbacks: # Collect agent metadata for telemetry - agent_class_name = getattr(worker, 'agent_name', - worker.__class__.__name__) + agent_class_name = getattr( + worker, "agent_name", worker.__class__.__name__ + ) model_type = self._extract_model_type(worker) # Log worker created event @@ -580,12 +636,13 @@ class Workforce(BaseWorkforce): sub.state = task.state logger.debug( f"[SYNC] Synced subtask {task.id} " - f"result to parent.subtasks") + f"result to parent.subtasks" + ) return logger.warning( - f"[SYNC] Subtask {task.id} not " - f"found in parent.subtasks") + f"[SYNC] Subtask {task.id} not found in parent.subtasks" + ) async def _notify_task_completion(self, task: Task) -> None: """Send task completion notification to frontend. @@ -596,7 +653,7 @@ class Workforce(BaseWorkforce): task_lock = get_task_lock(self.api_task_id) # Log task completion - is_main_task = (self._task and task.id == self._task.id) + is_main_task = self._task and task.id == self._task.id task_type = "MAIN TASK" if is_main_task else "SUB-TASK" logger.info(f"[TASK-RESULT] {task_type} COMPLETED: {task.id}") @@ -630,7 +687,7 @@ class Workforce(BaseWorkforce): ] if metrics_callbacks: # worker_id is required and cannot be None - worker_id = getattr(task, 'assigned_worker_id', None) or 'unknown' + worker_id = getattr(task, "assigned_worker_id", None) or "unknown" event = TaskCompletedEvent( task_id=task.id, worker_id=worker_id, @@ -671,8 +728,10 @@ class Workforce(BaseWorkforce): ] if metrics_callbacks and hasattr(metrics_callbacks[0], "log_entries"): for entry in reversed(metrics_callbacks[0].log_entries): - if entry.get("event_type") == "task_failed" and entry.get( - "task_id") == task.id: + if ( + entry.get("event_type") == "task_failed" + and entry.get("task_id") == task.id + ): error_message = entry.get("error_message") break @@ -685,7 +744,9 @@ class Workforce(BaseWorkforce): "state": task.state, "failure_count": task.failure_count, "result": str(error_message), - })) + } + ) + ) if metrics_callbacks: error_msg = error_message or str(task.result or "Unknown error") @@ -694,14 +755,14 @@ class Workforce(BaseWorkforce): error_message=error_msg, ) # Add failure details if available - if hasattr(task, 'assigned_worker_id'): + if hasattr(task, "assigned_worker_id"): event.worker_id = task.assigned_worker_id event.failure_count = task.failure_count metrics_callbacks[0].log_task_failed(event) return result - async def _get_returned_task(self) -> Optional[Task]: + async def _get_returned_task(self) -> Task | None: r"""Override to handle timeout and send notification to frontend. Get the task that's published by this node and just get returned @@ -722,7 +783,8 @@ class Workforce(BaseWorkforce): f"⏰ [WF-TIMEOUT] Task timeout in workforce {self.node_id}. " f"Timeout: {self.task_timeout_seconds}s, " f"Pending tasks: {len(self._pending_tasks)}, " - f"In-flight tasks: {self._in_flight_tasks}") + f"In-flight tasks: {self._in_flight_tasks}" + ) # Try to notify frontend, but don't let # notification failure mask the timeout @@ -732,40 +794,46 @@ class Workforce(BaseWorkforce): await task_lock.put_queue( ActionTimeoutData( data={ - "message": - (f"Task execution timeout: No response received " - f"for {timeout_minutes} minutes"), - "in_flight_tasks": - self._in_flight_tasks, - "pending_tasks": - len(self._pending_tasks), - "timeout_seconds": - self.task_timeout_seconds, - })) + "message": ( + f"Task execution timeout: No response received " + f"for {timeout_minutes} minutes" + ), + "in_flight_tasks": self._in_flight_tasks, + "pending_tasks": len(self._pending_tasks), + "timeout_seconds": self.task_timeout_seconds, + } + ) + ) except Exception as notify_err: logger.error( - f"Failed to send timeout notification: {notify_err}") + f"Failed to send timeout notification: {notify_err}" + ) raise except Exception as e: - logger.error(f"Error getting returned task {e} in " - f"workforce {self.node_id}. " - f"Current pending tasks: {len(self._pending_tasks)}, " - f"In-flight tasks: {self._in_flight_tasks}") + logger.error( + f"Error getting returned task {e} in " + f"workforce {self.node_id}. " + f"Current pending tasks: {len(self._pending_tasks)}, " + f"In-flight tasks: {self._in_flight_tasks}" + ) raise def stop(self) -> None: logger.info("=" * 80) - logger.info("⏹️ [WF-LIFECYCLE] stop() CALLED", - extra={ - "api_task_id": self.api_task_id, - "workforce_id": id(self) - }) - logger.info(f"[WF-LIFECYCLE] Current state before stop: " - f"{self._state.name}, _running: {self._running}") + logger.info( + "⏹️ [WF-LIFECYCLE] stop() CALLED", + extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}, + ) + logger.info( + f"[WF-LIFECYCLE] Current state before stop: " + f"{self._state.name}, _running: {self._running}" + ) logger.info("=" * 80) super().stop() - logger.info(f"[WF-LIFECYCLE] super().stop() completed, " - f"new state: {self._state.name}") + logger.info( + f"[WF-LIFECYCLE] super().stop() completed, " + f"new state: {self._state.name}" + ) task_lock = get_task_lock(self.api_task_id) task = asyncio.create_task(task_lock.put_queue(ActionEndData())) task_lock.add_background_task(task) @@ -773,63 +841,71 @@ class Workforce(BaseWorkforce): def stop_gracefully(self) -> None: logger.info("=" * 80) - logger.info("🛑 [WF-LIFECYCLE] stop_gracefully() CALLED", - extra={ - "api_task_id": self.api_task_id, - "workforce_id": id(self) - }) - logger.info(f"[WF-LIFECYCLE] Current state before stop_gracefully: " - f"{self._state.name}, _running: {self._running}") + logger.info( + "🛑 [WF-LIFECYCLE] stop_gracefully() CALLED", + extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}, + ) + logger.info( + f"[WF-LIFECYCLE] Current state before stop_gracefully: " + f"{self._state.name}, _running: {self._running}" + ) logger.info("=" * 80) super().stop_gracefully() logger.info( f"[WF-LIFECYCLE] ✅ super().stop_gracefully() completed, " - f"new state: {self._state.name}, _running: {self._running}") + f"new state: {self._state.name}, _running: {self._running}" + ) def skip_gracefully(self) -> None: logger.info("=" * 80) - logger.info("⏭️ [WF-LIFECYCLE] skip_gracefully() CALLED", - extra={ - "api_task_id": self.api_task_id, - "workforce_id": id(self) - }) - logger.info(f"[WF-LIFECYCLE] Current state before skip_gracefully: " - f"{self._state.name}, _running: {self._running}") + logger.info( + "⏭️ [WF-LIFECYCLE] skip_gracefully() CALLED", + extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}, + ) + logger.info( + f"[WF-LIFECYCLE] Current state before skip_gracefully: " + f"{self._state.name}, _running: {self._running}" + ) logger.info("=" * 80) super().skip_gracefully() logger.info( f"[WF-LIFECYCLE] ✅ super().skip_gracefully() completed, " - f"new state: {self._state.name}, _running: {self._running}") + f"new state: {self._state.name}, _running: {self._running}" + ) def pause(self) -> None: logger.info("=" * 80) - logger.info("⏸️ [WF-LIFECYCLE] pause() CALLED", - extra={ - "api_task_id": self.api_task_id, - "workforce_id": id(self) - }) - logger.info(f"[WF-LIFECYCLE] Current state before pause: " - f"{self._state.name}, _running: {self._running}") + logger.info( + "⏸️ [WF-LIFECYCLE] pause() CALLED", + extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}, + ) + logger.info( + f"[WF-LIFECYCLE] Current state before pause: " + f"{self._state.name}, _running: {self._running}" + ) logger.info("=" * 80) super().pause() logger.info( f"[WF-LIFECYCLE] ✅ super().pause() completed, " - f"new state: {self._state.name}, _running: {self._running}") + f"new state: {self._state.name}, _running: {self._running}" + ) def resume(self) -> None: logger.info("=" * 80) - logger.info("▶️ [WF-LIFECYCLE] resume() CALLED", - extra={ - "api_task_id": self.api_task_id, - "workforce_id": id(self) - }) - logger.info(f"[WF-LIFECYCLE] Current state before resume: " - f"{self._state.name}, _running: {self._running}") + logger.info( + "▶️ [WF-LIFECYCLE] resume() CALLED", + extra={"api_task_id": self.api_task_id, "workforce_id": id(self)}, + ) + logger.info( + f"[WF-LIFECYCLE] Current state before resume: " + f"{self._state.name}, _running: {self._running}" + ) logger.info("=" * 80) super().resume() logger.info( f"[WF-LIFECYCLE] ✅ super().resume() completed, " - f"new state: {self._state.name}, _running: {self._running}") + f"new state: {self._state.name}, _running: {self._running}" + ) async def cleanup(self) -> None: r"""Clean up resources when workforce is done""" diff --git a/backend/babel.cfg b/backend/babel.cfg index 1d15bb36..efceab81 100644 --- a/backend/babel.cfg +++ b/backend/babel.cfg @@ -1 +1 @@ -[python: **.py] \ No newline at end of file +[python: **.py] diff --git a/backend/cli.py b/backend/cli.py index 2eefa1f6..707b6dd9 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -12,12 +12,10 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from app.component.environment import auto_import from app.command import cli - +from app.component.environment import auto_import auto_import("app.command") - if __name__ == "__main__": cli() diff --git a/backend/lang/en_US/LC_MESSAGES/messages.po b/backend/lang/en_US/LC_MESSAGES/messages.po index c7f0fc45..ae59e232 100644 --- a/backend/lang/en_US/LC_MESSAGES/messages.po +++ b/backend/lang/en_US/LC_MESSAGES/messages.po @@ -31,4 +31,3 @@ msgstr "" #~ msgid "hello" #~ msgstr "" - diff --git a/backend/lang/zh_CN/LC_MESSAGES/messages.po b/backend/lang/zh_CN/LC_MESSAGES/messages.po index cd7a07dd..2108124b 100644 --- a/backend/lang/zh_CN/LC_MESSAGES/messages.po +++ b/backend/lang/zh_CN/LC_MESSAGES/messages.po @@ -33,4 +33,3 @@ msgstr "" #: app/service/task.py:275 msgid "Task already exists" msgstr "" - diff --git a/backend/main.py b/backend/main.py index 1f1c0dee..92843bf1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,12 +12,12 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import os -import sys -import pathlib -import signal import asyncio import atexit +import os +import pathlib +import signal +import sys # Add project root to Python path to import shared utils _project_root = pathlib.Path(__file__).parent.parent @@ -29,7 +29,7 @@ import logging # Setup logging logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) # Disable verbose CAMEL logs @@ -57,21 +57,27 @@ register_routers(api, prefix) app_logger.info("All routers loaded successfully") # Check if debug mode is enabled via environment variable -if os.environ.get('ENABLE_PYTHON_DEBUG') == 'true': +if os.environ.get("ENABLE_PYTHON_DEBUG") == "true": try: import debugpy - DEBUG_PORT = int(os.environ.get('DEBUG_PORT', '5678')) - app_logger.info(f"Debug mode enabled - Starting debugpy server on port {DEBUG_PORT}") + + DEBUG_PORT = int(os.environ.get("DEBUG_PORT", "5678")) + app_logger.info( + f"Debug mode enabled - Starting debugpy server on port {DEBUG_PORT}" + ) debugpy.listen(("localhost", DEBUG_PORT)) - app_logger.info(f"Debugger ready for attachment on localhost:{DEBUG_PORT}") - #📝 In VS Code: Run 'Debug Python Backend (Attach)' configuration + app_logger.info( + f"Debugger ready for attachment on localhost:{DEBUG_PORT}" + ) + # 📝 In VS Code: Run 'Debug Python Backend (Attach)' configuration # Don't wait for client automatically - let it attach when ready except ImportError: - app_logger.warning("debugpy not available, install with: uv add debugpy") + app_logger.warning( + "debugpy not available, install with: uv add debugpy" + ) except Exception as e: app_logger.error(f"Failed to start debugpy: {e}") - dir = pathlib.Path(__file__).parent / "runtime" dir.mkdir(parents=True, exist_ok=True) @@ -89,6 +95,7 @@ async def write_pid_file(): # PID task will be created on startup pid_task = None + @api.on_event("startup") async def startup_event(): global pid_task @@ -96,10 +103,14 @@ async def startup_event(): app_logger.info("PID write task created") # Initialize telemetry tracer provider - from app.utils.telemetry.workforce_metrics import initialize_tracer_provider + from app.utils.telemetry.workforce_metrics import ( + initialize_tracer_provider, + ) + initialize_tracer_provider() app_logger.info("Telemetry tracer provider initialized") + # Graceful shutdown handler shutdown_event = asyncio.Event() @@ -108,7 +119,7 @@ async def cleanup_resources(): r"""Cleanup all resources on shutdown""" app_logger.info("Starting graceful shutdown process") - from app.service.task import task_locks, _cleanup_task + from app.service.task import _cleanup_task, task_locks if _cleanup_task and not _cleanup_task.done(): _cleanup_task.cancel() @@ -143,6 +154,7 @@ def signal_handler(signum, frame): signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) + # Register cleanup on exit with safe synchronous wrapper def sync_cleanup(): """Synchronous cleanup for atexit - handles PID file removal""" @@ -155,6 +167,7 @@ def sync_cleanup(): except Exception as e: app_logger.error(f"Error during atexit cleanup: {e}") + atexit.register(sync_cleanup) # Log successful initialization diff --git a/backend/messages.pot b/backend/messages.pot index 64da8a40..448f971f 100644 --- a/backend/messages.pot +++ b/backend/messages.pot @@ -32,4 +32,3 @@ msgstr "" #: app/service/task.py:275 msgid "Task already exists" msgstr "" - diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e1d7a8f7..800cca33 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -34,31 +34,39 @@ dev = [ "pytest-asyncio>=1.1.0", ] -[tool.isort] -profile = "black" -line_length = 79 -force_single_line = false -multi_line_output = 3 -include_trailing_comma = true -use_parentheses = true - -[tool.yapf] -based_on_style = "pep8" -column_limit = 79 -blank_line_before_nested_class_or_def = true -split_before_named_assigns = true -dedent_closing_brackets = true - [tool.ruff] line-length = 79 +target-version = "py310" [tool.ruff.lint] -extend-select = [ - "B006", # forbid def demo(mutation = []) +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes (unused imports, variables, etc.) + "I", # isort (import sorting) + "UP", # pyupgrade (Python syntax upgrades) + "B", # flake8-bugbear +] +ignore = [ + "E402", # module level import not at top of file (needed for path setup) + "E501", # line too long (handled by formatter) + "E712", # == False comparisons (needed for SQLAlchemy) + "B904", # raise from - too many existing violations + "B023", # closure captures loop variable - existing pattern + "B017", # blind except in tests - acceptable + "B905", # zip strict - not needed for Python 3.10 ] -[tool.flake8] -max-line-length = 79 +[tool.ruff.lint.isort] +force-single-line = false +combine-as-imports = true + +[tool.ruff.lint.per-file-ignores] +"**/tests/**" = ["F841"] # unused variables OK in tests + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/backend/tests/app/agent/factory/test_browser.py b/backend/tests/app/agent/factory/test_browser.py index c5cb62a1..e52a5a46 100644 --- a/backend/tests/app/agent/factory/test_browser.py +++ b/backend/tests/app/agent/factory/test_browser.py @@ -28,20 +28,22 @@ def test_browser_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.browser' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'), \ - patch(f'{_mod}.HumanToolkit') as mock_human_toolkit, \ - patch(f'{_mod}.HybridBrowserToolkit') as mock_browser_toolkit, \ - patch(f'{_mod}.TerminalToolkit') as mock_terminal_toolkit, \ - patch(f'{_mod}.NoteTakingToolkit') as mock_note_toolkit, \ - patch(f'{_mod}.SearchToolkit') as mock_search_toolkit, \ - patch(f'{_mod}.ToolkitMessageIntegration'), \ - patch('uuid.uuid4') as mock_uuid: - + _mod = "app.agent.factory.browser" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + patch(f"{_mod}.HumanToolkit") as mock_human_toolkit, + patch(f"{_mod}.HybridBrowserToolkit") as mock_browser_toolkit, + patch(f"{_mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit, + patch(f"{_mod}.SearchToolkit") as mock_search_toolkit, + patch(f"{_mod}.ToolkitMessageIntegration"), + patch("uuid.uuid4") as mock_uuid, + ): # Mock all toolkit instances mock_human_toolkit.get_can_use_tools.return_value = [] mock_browser_toolkit.return_value.get_tools.return_value = [] @@ -72,8 +74,9 @@ def test_browser_agent_creation(sample_chat_data): ) # agent_name (enum contains this value) # The system_prompt is a BaseMessage, so check its content attribute system_message = call_args[0][1] - if hasattr(system_message, 'content'): + if hasattr(system_message, "content"): assert "search" in system_message.content.lower() else: - assert "search" in str(system_message).lower( + assert ( + "search" in str(system_message).lower() ) # system_prompt contains search diff --git a/backend/tests/app/agent/factory/test_developer.py b/backend/tests/app/agent/factory/test_developer.py index ead3403e..0a4e55ce 100644 --- a/backend/tests/app/agent/factory/test_developer.py +++ b/backend/tests/app/agent/factory/test_developer.py @@ -29,19 +29,21 @@ async def test_developer_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.developer' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'), \ - patch(f'{_mod}.HumanToolkit') as mock_human_toolkit, \ - patch(f'{_mod}.NoteTakingToolkit') as mock_note_toolkit, \ - patch(f'{_mod}.WebDeployToolkit') as mock_web_toolkit, \ - patch(f'{_mod}.ScreenshotToolkit') as mock_screenshot_toolkit, \ - patch(f'{_mod}.TerminalToolkit') as mock_terminal_toolkit, \ - patch(f'{_mod}.ToolkitMessageIntegration'): - + _mod = "app.agent.factory.developer" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + patch(f"{_mod}.HumanToolkit") as mock_human_toolkit, + patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit, + patch(f"{_mod}.WebDeployToolkit") as mock_web_toolkit, + patch(f"{_mod}.ScreenshotToolkit") as mock_screenshot_toolkit, + patch(f"{_mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{_mod}.ToolkitMessageIntegration"), + ): # Mock all toolkit instances mock_human_toolkit.get_can_use_tools.return_value = [] mock_note_toolkit.return_value.get_tools.return_value = [] @@ -73,19 +75,21 @@ async def test_developer_agent_with_multiple_toolkits(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.developer' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'), \ - patch(f'{_mod}.HumanToolkit') as mock_human_toolkit, \ - patch(f'{_mod}.NoteTakingToolkit') as mock_note_toolkit, \ - patch(f'{_mod}.WebDeployToolkit') as mock_web_toolkit, \ - patch(f'{_mod}.ScreenshotToolkit') as mock_screenshot_toolkit, \ - patch(f'{_mod}.TerminalToolkit') as mock_terminal_toolkit, \ - patch(f'{_mod}.ToolkitMessageIntegration'): - + _mod = "app.agent.factory.developer" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + patch(f"{_mod}.HumanToolkit") as mock_human_toolkit, + patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit, + patch(f"{_mod}.WebDeployToolkit") as mock_web_toolkit, + patch(f"{_mod}.ScreenshotToolkit") as mock_screenshot_toolkit, + patch(f"{_mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{_mod}.ToolkitMessageIntegration"), + ): # Mock all toolkit instances mock_human_toolkit.get_can_use_tools.return_value = [] mock_note_toolkit.return_value.get_tools.return_value = [] diff --git a/backend/tests/app/agent/factory/test_document.py b/backend/tests/app/agent/factory/test_document.py index 0ebd36ec..034a1696 100644 --- a/backend/tests/app/agent/factory/test_document.py +++ b/backend/tests/app/agent/factory/test_document.py @@ -29,22 +29,24 @@ async def test_document_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.document' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'), \ - patch(f'{_mod}.HumanToolkit') as mock_human_toolkit, \ - patch(f'{_mod}.FileToolkit') as mock_file_toolkit, \ - patch(f'{_mod}.PPTXToolkit') as mock_pptx_toolkit, \ - patch(f'{_mod}.MarkItDownToolkit') as mock_markdown_toolkit, \ - patch(f'{_mod}.ExcelToolkit') as mock_excel_toolkit, \ - patch(f'{_mod}.NoteTakingToolkit') as mock_note_toolkit, \ - patch(f'{_mod}.TerminalToolkit') as mock_terminal_toolkit, \ - patch(f'{_mod}.GoogleDriveMCPToolkit') as mock_gdrive_toolkit, \ - patch(f'{_mod}.ToolkitMessageIntegration'): - + _mod = "app.agent.factory.document" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + patch(f"{_mod}.HumanToolkit") as mock_human_toolkit, + patch(f"{_mod}.FileToolkit") as mock_file_toolkit, + patch(f"{_mod}.PPTXToolkit") as mock_pptx_toolkit, + patch(f"{_mod}.MarkItDownToolkit") as mock_markdown_toolkit, + patch(f"{_mod}.ExcelToolkit") as mock_excel_toolkit, + patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit, + patch(f"{_mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{_mod}.GoogleDriveMCPToolkit") as mock_gdrive_toolkit, + patch(f"{_mod}.ToolkitMessageIntegration"), + ): # Mock all toolkit instances mock_human_toolkit.get_can_use_tools.return_value = [] mock_file_toolkit.return_value.get_tools.return_value = [] diff --git a/backend/tests/app/agent/factory/test_mcp.py b/backend/tests/app/agent/factory/test_mcp.py index ceb7e82c..c773fff7 100644 --- a/backend/tests/app/agent/factory/test_mcp.py +++ b/backend/tests/app/agent/factory/test_mcp.py @@ -27,14 +27,15 @@ async def test_mcp_agent_creation(sample_chat_data): """Test mcp_agent creates agent with MCP tools.""" options = Chat(**sample_chat_data) - _mod = 'app.agent.factory.mcp' - with patch(f'{_mod}.ListenChatAgent') as mock_listen_agent, \ - patch(f'{_mod}.ModelFactory.create') as mock_model_factory, \ - patch('asyncio.create_task'), \ - patch(f'{_mod}.McpSearchToolkit') as mock_mcp_search_toolkit, \ - patch(f'{_mod}.get_mcp_tools') as mock_get_mcp_tools, \ - patch(f'{_mod}.get_task_lock'): - + _mod = "app.agent.factory.mcp" + with ( + patch(f"{_mod}.ListenChatAgent") as mock_listen_agent, + patch(f"{_mod}.ModelFactory.create") as mock_model_factory, + patch("asyncio.create_task"), + patch(f"{_mod}.McpSearchToolkit") as mock_mcp_search_toolkit, + patch(f"{_mod}.get_mcp_tools") as mock_get_mcp_tools, + patch(f"{_mod}.get_task_lock"), + ): # Mock toolkit instances mock_mcp_search_toolkit.return_value.get_tools.return_value = [] mock_get_mcp_tools.return_value = [] diff --git a/backend/tests/app/agent/factory/test_multi_modal.py b/backend/tests/app/agent/factory/test_multi_modal.py index 47a62442..308def0a 100644 --- a/backend/tests/app/agent/factory/test_multi_modal.py +++ b/backend/tests/app/agent/factory/test_multi_modal.py @@ -28,22 +28,24 @@ def test_multi_modal_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.multi_modal' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'), \ - patch(f'{_mod}.HumanToolkit') as mock_human_toolkit, \ - patch(f'{_mod}.VideoDownloaderToolkit') as mock_video_toolkit, \ - patch(f'{_mod}.ImageAnalysisToolkit') as mock_image_toolkit, \ - patch(f'{_mod}.OpenAIImageToolkit') as mock_openai_image_toolkit, \ - patch(f'{_mod}.AudioAnalysisToolkit') as mock_audio_toolkit, \ - patch(f'{_mod}.TerminalToolkit') as mock_terminal_toolkit, \ - patch(f'{_mod}.NoteTakingToolkit') as mock_note_toolkit, \ - patch(f'{_mod}.SearchToolkit') as mock_search_toolkit, \ - patch(f'{_mod}.ToolkitMessageIntegration'): - + _mod = "app.agent.factory.multi_modal" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + patch(f"{_mod}.HumanToolkit") as mock_human_toolkit, + patch(f"{_mod}.VideoDownloaderToolkit") as mock_video_toolkit, + patch(f"{_mod}.ImageAnalysisToolkit") as mock_image_toolkit, + patch(f"{_mod}.OpenAIImageToolkit") as mock_openai_image_toolkit, + patch(f"{_mod}.AudioAnalysisToolkit") as mock_audio_toolkit, + patch(f"{_mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit, + patch(f"{_mod}.SearchToolkit") as mock_search_toolkit, + patch(f"{_mod}.ToolkitMessageIntegration"), + ): # Mock all toolkit instances mock_human_toolkit.get_can_use_tools.return_value = [] mock_video_toolkit.return_value.get_tools.return_value = [] diff --git a/backend/tests/app/agent/factory/test_question_confirm.py b/backend/tests/app/agent/factory/test_question_confirm.py index 84ec1a25..77a9bbeb 100644 --- a/backend/tests/app/agent/factory/test_question_confirm.py +++ b/backend/tests/app/agent/factory/test_question_confirm.py @@ -28,12 +28,15 @@ def test_question_confirm_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.question_confirm' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'): + _mod = "app.agent.factory.question_confirm" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + ): mock_agent = MagicMock() mock_agent_model.return_value = mock_agent diff --git a/backend/tests/app/agent/factory/test_social_media.py b/backend/tests/app/agent/factory/test_social_media.py index 057ab318..56b3ee42 100644 --- a/backend/tests/app/agent/factory/test_social_media.py +++ b/backend/tests/app/agent/factory/test_social_media.py @@ -29,25 +29,25 @@ async def test_social_media_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - mod = 'app.agent.factory.social_media' + mod = "app.agent.factory.social_media" with ( - patch(f'{mod}.agent_model') as mock_agent_model, - patch('asyncio.create_task'), - patch(f'{mod}.WhatsAppToolkit') as mock_whatsapp_toolkit, - patch(f'{mod}.TwitterToolkit') as mock_twitter_toolkit, - patch(f'{mod}.LinkedInToolkit') as mock_linkedin_toolkit, - patch(f'{mod}.RedditToolkit') as mock_reddit_toolkit, - patch(f'{mod}.NotionMCPToolkit') as mock_notion_mcp_toolkit, - patch(f'{mod}.GoogleGmailMCPToolkit') as mock_gmail_toolkit, - patch(f'{mod}.GoogleCalendarToolkit') as mock_calendar_toolkit, - patch(f'{mod}.HumanToolkit') as mock_human_toolkit, - patch(f'{mod}.TerminalToolkit') as mock_terminal_toolkit, - patch(f'{mod}.NoteTakingToolkit') as mock_note_toolkit, + patch(f"{mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + patch(f"{mod}.WhatsAppToolkit") as mock_whatsapp_toolkit, + patch(f"{mod}.TwitterToolkit") as mock_twitter_toolkit, + patch(f"{mod}.LinkedInToolkit") as mock_linkedin_toolkit, + patch(f"{mod}.RedditToolkit") as mock_reddit_toolkit, + patch(f"{mod}.NotionMCPToolkit") as mock_notion_mcp_toolkit, + patch(f"{mod}.GoogleGmailMCPToolkit") as mock_gmail_toolkit, + patch(f"{mod}.GoogleCalendarToolkit") as mock_calendar_toolkit, + patch(f"{mod}.HumanToolkit") as mock_human_toolkit, + patch(f"{mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{mod}.NoteTakingToolkit") as mock_note_toolkit, ): - # Mock all toolkit instances mock_whatsapp_toolkit.get_can_use_tools.return_value = [] mock_twitter_toolkit.get_can_use_tools.return_value = [] diff --git a/backend/tests/app/agent/factory/test_task_summary.py b/backend/tests/app/agent/factory/test_task_summary.py index 7f63d509..b6f05811 100644 --- a/backend/tests/app/agent/factory/test_task_summary.py +++ b/backend/tests/app/agent/factory/test_task_summary.py @@ -28,12 +28,15 @@ def test_task_summary_agent_creation(sample_chat_data): # Setup task lock in the registry before calling agent function from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _mod = 'app.agent.factory.task_summary' - with patch(f'{_mod}.agent_model') as mock_agent_model, \ - patch('asyncio.create_task'): + _mod = "app.agent.factory.task_summary" + with ( + patch(f"{_mod}.agent_model") as mock_agent_model, + patch("asyncio.create_task"), + ): mock_agent = MagicMock() mock_agent_model.return_value = mock_agent diff --git a/backend/tests/app/agent/test_agent_model.py b/backend/tests/app/agent/test_agent_model.py index 3bcfa42a..f58343d7 100644 --- a/backend/tests/app/agent/test_agent_model.py +++ b/backend/tests/app/agent/test_agent_model.py @@ -34,15 +34,17 @@ class TestAgentFactoryFunctions: # Setup task lock in the registry before calling agent_model from app.service.task import task_locks + mock_task_lock = MagicMock() task_locks[options.task_id] = mock_task_lock - _m = sys.modules['app.agent.agent_model'] - with patch.object(_m, 'ListenChatAgent') as mock_listen_agent, \ - patch.object(_m, 'ModelFactory') as mock_model_factory, \ - patch.object(_m, 'get_task_lock', return_value=mock_task_lock), \ - patch('asyncio.create_task'): - + _m = sys.modules["app.agent.agent_model"] + with ( + patch.object(_m, "ListenChatAgent") as mock_listen_agent, + patch.object(_m, "ModelFactory") as mock_model_factory, + patch.object(_m, "get_task_lock", return_value=mock_task_lock), + patch("asyncio.create_task"), + ): mock_agent = MagicMock() mock_listen_agent.return_value = mock_agent mock_model_factory.create.return_value = MagicMock() @@ -69,6 +71,7 @@ class TestAgentIntegration: def setup_method(self): """Clean up before each test.""" from app.service.task import task_locks + task_locks.clear() @pytest.mark.asyncio @@ -84,11 +87,13 @@ class TestAgentIntegration: task_locks[api_task_id] = mock_task_lock # Create agent - _m = sys.modules['app.agent.agent_model'] - with patch.object(_m, 'ModelFactory') as mock_model_factory, \ - patch.object(_m, '_schedule_async_task'), \ - patch.object(_m, 'ListenChatAgent') as mock_listen_agent, \ - patch.object(_m, 'get_task_lock', return_value=mock_task_lock): + _m = sys.modules["app.agent.agent_model"] + with ( + patch.object(_m, "ModelFactory") as mock_model_factory, + patch.object(_m, "_schedule_async_task"), + patch.object(_m, "ListenChatAgent") as mock_listen_agent, + patch.object(_m, "get_task_lock", return_value=mock_task_lock), + ): mock_model = MagicMock() mock_model_factory.return_value = mock_model diff --git a/backend/tests/app/agent/test_listen_chat_agent.py b/backend/tests/app/agent/test_listen_chat_agent.py index 9e8e8426..3b576fcc 100644 --- a/backend/tests/app/agent/test_listen_chat_agent.py +++ b/backend/tests/app/agent/test_listen_chat_agent.py @@ -26,7 +26,7 @@ from camel.types.agents import ToolCallingRecord from app.agent.listen_chat_agent import ListenChatAgent from app.model.chat import Chat -_LCA = 'app.agent.listen_chat_agent' +_LCA = "app.agent.listen_chat_agent" pytestmark = pytest.mark.unit @@ -39,8 +39,10 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock') as mock_get_lock, \ - patch('camel.models.ModelFactory.create') as mock_create_model: + with ( + patch(f"{_LCA}.get_task_lock") as mock_get_lock, + patch("camel.models.ModelFactory.create") as mock_create_model, + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock @@ -57,7 +59,7 @@ class TestListenChatAgent: model="gpt-4", # Use string instead of mock system_message="You are a helpful assistant", tools=[], - agent_id="test_agent_123" + agent_id="test_agent_123", ) assert agent.api_task_id == api_task_id @@ -69,10 +71,11 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model, \ - patch('asyncio.create_task'): - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + patch("asyncio.create_task"), + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -92,7 +95,7 @@ class TestListenChatAgent: mock_response.info = {"usage": {"total_tokens": 100}} with patch.object( - ChatAgent, 'step', return_value=mock_response + ChatAgent, "step", return_value=mock_response ) as mock_parent_step: result = agent.step("Test input message") @@ -113,10 +116,11 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model, \ - patch('asyncio.create_task'): - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + patch("asyncio.create_task"), + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -141,7 +145,7 @@ class TestListenChatAgent: mock_response.info = {"usage": {"total_tokens": 100}} with patch.object( - ChatAgent, 'step', return_value=mock_response + ChatAgent, "step", return_value=mock_response ) as mock_parent_step: result = agent.step(mock_message) @@ -165,10 +169,11 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model, \ - patch('asyncio.create_task'): - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + patch("asyncio.create_task"), + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -188,7 +193,7 @@ class TestListenChatAgent: mock_response.info = {"usage": {"total_tokens": 100}} with patch.object( - ChatAgent, 'astep', return_value=mock_response + ChatAgent, "astep", return_value=mock_response ) as mock_parent_astep: result = await agent.astep("Test async input") @@ -208,10 +213,11 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model, \ - patch('asyncio.create_task'): - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + patch("asyncio.create_task"), + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -241,7 +247,7 @@ class TestListenChatAgent: mock_record = MagicMock(spec=ToolCallingRecord) with patch.object( - agent, '_record_tool_calling', return_value=mock_record + agent, "_record_tool_calling", return_value=mock_record ) as mock_record_func: result = agent._execute_tool(tool_call_request) @@ -258,9 +264,10 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model: - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -288,7 +295,7 @@ class TestListenChatAgent: mock_record = MagicMock(spec=ToolCallingRecord) with patch.object( - agent, '_record_tool_calling', return_value=mock_record + agent, "_record_tool_calling", return_value=mock_record ) as mock_record_func: result = await agent._aexecute_tool(tool_call_request) @@ -304,9 +311,10 @@ class TestListenChatAgent: api_task_id = "test_api_task_123" agent_name = "TestAgent" - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model: - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -347,14 +355,12 @@ class TestListenChatAgent: agent.prune_tool_calls_from_memory = False # Now mock the constructor for the clone call - with patch( - f'{_LCA}.ListenChatAgent', - return_value=cloned_agent - ) as mock_clone_constructor, \ - patch.object( - agent, '_clone_tools', - return_value=([], [])): - + with ( + patch( + f"{_LCA}.ListenChatAgent", return_value=cloned_agent + ) as mock_clone_constructor, + patch.object(agent, "_clone_tools", return_value=([], [])), + ): result = agent.clone(with_memory=True) assert result is cloned_agent @@ -369,9 +375,10 @@ class TestListenChatAgent: mock_tool = MagicMock(spec=FunctionTool) tools = [mock_tool] - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model: - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -383,7 +390,7 @@ class TestListenChatAgent: api_task_id=api_task_id, agent_name=agent_name, model="gpt-4", - tools=tools + tools=tools, ) # Mock function_list attribute that is expected to exist @@ -391,8 +398,7 @@ class TestListenChatAgent: assert len(agent.function_list) == 1 # Should have the tool # Check that tools were passed to parent class - mock_task_lock.put_queue.assert_not_called( - ) # No immediate action for tool setup + mock_task_lock.put_queue.assert_not_called() # No immediate action for tool setup def test_listen_chat_agent_with_pause_event(self, mock_task_lock): """Test ListenChatAgent with pause event.""" @@ -401,9 +407,10 @@ class TestListenChatAgent: pause_event = asyncio.Event() - with patch(f'{_LCA}.get_task_lock', return_value=mock_task_lock), \ - patch('camel.models.ModelFactory.create') as mock_create_model: - + with ( + patch(f"{_LCA}.get_task_lock", return_value=mock_task_lock), + patch("camel.models.ModelFactory.create") as mock_create_model, + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" @@ -415,7 +422,7 @@ class TestListenChatAgent: api_task_id=api_task_id, agent_name=agent_name, model="gpt-4", - pause_event=pause_event + pause_event=pause_event, ) assert agent.pause_event is pause_event @@ -425,13 +432,13 @@ class TestListenChatAgent: api_task_id = "error_test_123" agent_name = "ErrorAgent" - with patch(f'{_LCA}.get_task_lock') as mock_get_lock, \ - patch( - 'camel.models.ModelFactory.create', - side_effect=ValueError( - "Invalid model" - ) - ): + with ( + patch(f"{_LCA}.get_task_lock") as mock_get_lock, + patch( + "camel.models.ModelFactory.create", + side_effect=ValueError("Invalid model"), + ), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock @@ -442,7 +449,7 @@ class TestListenChatAgent: ListenChatAgent( api_task_id=api_task_id, agent_name=agent_name, - model="invalid_model_string" # Invalid model type + model="invalid_model_string", # Invalid model type ) def test_listen_chat_agent_step_with_task_lock_error(self): @@ -450,15 +457,13 @@ class TestListenChatAgent: api_task_id = "error_test_123" agent_name = "ErrorAgent" - with patch( - f'{_LCA}.get_task_lock', - side_effect=Exception( - "Task lock not found" - ) - ), \ + with ( patch( - 'camel.models.ModelFactory.create') as mock_create_model: - + f"{_LCA}.get_task_lock", + side_effect=Exception("Task lock not found"), + ), + patch("camel.models.ModelFactory.create") as mock_create_model, + ): # Mock the model backend creation mock_backend = MagicMock() mock_backend.model_type = "gpt-4" diff --git a/backend/tests/app/agent/test_tools.py b/backend/tests/app/agent/test_tools.py index ea5fd695..4d65c823 100644 --- a/backend/tests/app/agent/test_tools.py +++ b/backend/tests/app/agent/test_tools.py @@ -32,28 +32,29 @@ class TestToolkitFunctions: agent_name = "TestAgent" api_task_id = "test_task_123" - _mod = 'app.agent.tools' - with patch(f'{_mod}.SearchToolkit') as mock_search_toolkit, \ - patch(f'{_mod}.TerminalToolkit') as mock_terminal_toolkit, \ - patch(f'{_mod}.FileToolkit') as mock_file_toolkit: - + _mod = "app.agent.tools" + with ( + patch(f"{_mod}.SearchToolkit") as mock_search_toolkit, + patch(f"{_mod}.TerminalToolkit") as mock_terminal_toolkit, + patch(f"{_mod}.FileToolkit") as mock_file_toolkit, + ): # Mock toolkit instances - these should # return tools directly # from get_can_use_tools mock_search_instance = MagicMock() mock_search_instance.agent_name = agent_name mock_search_tools = [MagicMock(), MagicMock()] - mock_search_instance\ - .get_can_use_tools\ - .return_value = mock_search_tools + mock_search_instance.get_can_use_tools.return_value = ( + mock_search_tools + ) mock_search_toolkit.return_value = mock_search_instance mock_terminal_instance = MagicMock() mock_terminal_instance.agent_name = agent_name mock_terminal_tools = [MagicMock()] - mock_terminal_instance\ - .get_can_use_tools\ - .return_value = mock_terminal_tools + mock_terminal_instance.get_can_use_tools.return_value = ( + mock_terminal_tools + ) mock_terminal_toolkit.return_value = mock_terminal_instance mock_file_instance = MagicMock() @@ -115,8 +116,8 @@ class TestToolkitFunctions: api_task_id = "error_test_123" with patch( - 'app.agent.tools.SearchToolkit', - side_effect=Exception("Toolkit init failed") + "app.agent.tools.SearchToolkit", + side_effect=Exception("Toolkit init failed"), ): # Should handle toolkit initialization errors result = await get_toolkits(tools, agent_name, api_task_id) @@ -134,14 +135,14 @@ class TestMcpTools: "mcpServers": { "notion": { "command": "npx", - "args": ["@modelcontextprotocol/server-notion"] + "args": ["@modelcontextprotocol/server-notion"], } } } mock_tools = [MagicMock(), MagicMock()] - with patch('app.agent.tools.MCPToolkit') as mock_mcp_toolkit: + with patch("app.agent.tools.MCPToolkit") as mock_mcp_toolkit: mock_toolkit_instance = MagicMock() mock_toolkit_instance.connect = AsyncMock() mock_toolkit_instance.get_tools.return_value = mock_tools @@ -167,16 +168,12 @@ class TestMcpTools: async def test_get_mcp_tools_connection_failure(self): """Test get_mcp_tools when MCP connection fails.""" mcp_servers: McpServers = { - "mcpServers": { - "failing_server": { - "command": "invalid_command" - } - } + "mcpServers": {"failing_server": {"command": "invalid_command"}} } with patch( - 'app.agent.tools.MCPToolkit', - side_effect=Exception("Connection failed") + "app.agent.tools.MCPToolkit", + side_effect=Exception("Connection failed"), ): result = await get_mcp_tools(mcp_servers) assert result == [] diff --git a/backend/tests/app/utils/listen/test_toolkit_listen.py b/backend/tests/app/utils/listen/test_toolkit_listen.py index 09be998b..cf7150dd 100644 --- a/backend/tests/app/utils/listen/test_toolkit_listen.py +++ b/backend/tests/app/utils/listen/test_toolkit_listen.py @@ -67,7 +67,7 @@ def test_format_args_with_positional_args(): @pytest.mark.unit def test_format_args_with_kwargs(): """Format keyword arguments.""" - args = ("self", ) + args = ("self",) kwargs = {"key1": "value1", "key2": 42} result = _format_args(args, kwargs, None) assert "key1='value1'" in result @@ -204,7 +204,7 @@ def test_listen_toolkit_sync_returns_result(): with patch( "app.utils.listen.toolkit_listen.get_task_lock", - return_value=mock_task_lock + return_value=mock_task_lock, ): @listen_toolkit() @@ -224,7 +224,7 @@ def test_listen_toolkit_sync_raises_exception(): with patch( "app.utils.listen.toolkit_listen.get_task_lock", - return_value=mock_task_lock + return_value=mock_task_lock, ): @listen_toolkit() @@ -276,7 +276,7 @@ async def test_listen_toolkit_async_returns_result(): with patch( "app.utils.listen.toolkit_listen.get_task_lock", - return_value=mock_task_lock + return_value=mock_task_lock, ): @listen_toolkit() @@ -297,7 +297,7 @@ async def test_listen_toolkit_async_raises_exception(): with patch( "app.utils.listen.toolkit_listen.get_task_lock", - return_value=mock_task_lock + return_value=mock_task_lock, ): @listen_toolkit() @@ -337,7 +337,7 @@ def test_listen_toolkit_with_custom_inputs_formatter(): with patch( "app.utils.listen.toolkit_listen.get_task_lock", - return_value=mock_task_lock + return_value=mock_task_lock, ): @listen_toolkit(inputs=custom_inputs) @@ -360,10 +360,13 @@ def test_listen_toolkit_with_custom_return_msg_formatter(): def custom_return_msg(res): return f"formatted: {res}" - with patch( - "app.utils.listen.toolkit_listen.get_task_lock", - return_value=mock_task_lock - ), patch("app.utils.listen.toolkit_listen._format_result") as mock_format: + with ( + patch( + "app.utils.listen.toolkit_listen.get_task_lock", + return_value=mock_task_lock, + ), + patch("app.utils.listen.toolkit_listen._format_result") as mock_format, + ): mock_format.return_value = "formatted: test_result" @listen_toolkit(return_msg=custom_return_msg) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 56df4c8a..dece55a0 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -15,8 +15,8 @@ import asyncio import os import tempfile +from collections.abc import AsyncGenerator, Generator from pathlib import Path -from typing import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -288,7 +288,7 @@ def mock_environment_variables(): "CAMEL_MODEL_LOG_ENABLED": "true", "CAMEL_LOG_DIR": "/tmp/test_logs", "file_save_path": "/tmp/test_files", - "browser_port": "8080" + "browser_port": "8080", } with patch.dict(os.environ, env_vars, clear=False): @@ -311,7 +311,7 @@ def sample_chat_data(): "new_agents": [], "env_path": ".env", "browser_port": 8080, - "summary_prompt": "" + "summary_prompt": "", } @@ -321,7 +321,7 @@ def sample_task_content(): return { "id": "test_task_123", "content": "Test task content", - "state": "OPEN" # Changed from CREATED to OPEN + "state": "OPEN", # Changed from CREATED to OPEN } @@ -355,7 +355,7 @@ def pytest_configure(config): ) config.addinivalue_line( "markers", - "very_slow: mark test as very slow (requires full test mode)" + "very_slow: mark test as very slow (requires full test mode)", ) config.addinivalue_line( "markers", "optional: mark test as optional (skipped in fast mode)" diff --git a/backend/tests/unit/component/test_environment_security.py b/backend/tests/unit/component/test_environment_security.py index 6bfbddc6..bb7202e1 100644 --- a/backend/tests/unit/component/test_environment_security.py +++ b/backend/tests/unit/component/test_environment_security.py @@ -1,8 +1,23 @@ +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + import os import tempfile from pathlib import Path import pytest + from app.component.environment import env_base_dir, sanitize_env_path @@ -44,8 +59,9 @@ def test_path_traversal_attack_rejected(): # Path traversal should either be rejected # or normalized within base_dir if result: - assert result.startswith(env_base_dir), \ + assert result.startswith(env_base_dir), ( f"Path traversal not blocked: {path} -> {result}" + ) def test_absolute_path_outside_base_dir_rejected(): @@ -58,8 +74,9 @@ def test_absolute_path_outside_base_dir_rejected(): ] for path in malicious_paths: result = sanitize_env_path(path) - assert result is None, \ + assert result is None, ( f"Absolute path outside base dir not rejected: {path}" + ) def test_non_env_extension_rejected(): @@ -73,8 +90,7 @@ def test_non_env_extension_rejected(): ] for path in invalid_paths: result = sanitize_env_path(path) - assert result is None, \ - f"Non-.env file not rejected: {path}" + assert result is None, f"Non-.env file not rejected: {path}" def test_nested_valid_path(): @@ -146,8 +162,9 @@ def test_special_characters_in_path(): ] for path in valid_special_chars: result = sanitize_env_path(path) - assert result is not None, (f"Valid path with special " - f"chars rejected: {path}") + assert result is not None, ( + f"Valid path with special chars rejected: {path}" + ) assert result.startswith(env_base_dir) diff --git a/backend/tests/unit/controller/test_chat_controller.py b/backend/tests/unit/controller/test_chat_controller.py index e59868fb..0cf3f3c7 100644 --- a/backend/tests/unit/controller/test_chat_controller.py +++ b/backend/tests/unit/controller/test_chat_controller.py @@ -19,9 +19,16 @@ import pytest from fastapi import Response from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient - -from app.controller.chat_controller import improve, post, stop, supplement, human_reply, install_mcp from pydantic import ValidationError + +from app.controller.chat_controller import ( + human_reply, + improve, + install_mcp, + post, + stop, + supplement, +) from app.exception.exception import UserException from app.model.chat import Chat, HumanReply, McpServers, Status, SupplementChat @@ -29,53 +36,79 @@ from app.model.chat import Chat, HumanReply, McpServers, Status, SupplementChat @pytest.mark.unit class TestChatController: """Test cases for chat controller endpoints.""" - + @pytest.mark.asyncio - async def test_post_chat_endpoint_success(self, sample_chat_data, mock_request, mock_task_lock, mock_environment_variables): + async def test_post_chat_endpoint_success( + self, + sample_chat_data, + mock_request, + mock_task_lock, + mock_environment_variables, + ): """Test successful chat initialization.""" chat_data = Chat(**sample_chat_data) - - with patch("app.controller.chat_controller.create_task_lock", return_value=mock_task_lock), \ - patch("app.controller.chat_controller.step_solve") as mock_step_solve, \ - patch("app.controller.chat_controller.load_dotenv"), \ - patch("pathlib.Path.mkdir"), \ - patch("pathlib.Path.home", return_value=MagicMock()): - + + with ( + patch( + "app.controller.chat_controller.create_task_lock", + return_value=mock_task_lock, + ), + patch( + "app.controller.chat_controller.step_solve" + ) as mock_step_solve, + patch("app.controller.chat_controller.load_dotenv"), + patch("pathlib.Path.mkdir"), + patch("pathlib.Path.home", return_value=MagicMock()), + ): # Mock async generator async def mock_generator(): yield "data: test_response\n\n" yield "data: test_response_2\n\n" - + mock_step_solve.return_value = mock_generator() - + response = await post(chat_data, mock_request) - + assert isinstance(response, StreamingResponse) assert response.media_type == "text/event-stream" - mock_step_solve.assert_called_once_with(chat_data, mock_request, mock_task_lock) + mock_step_solve.assert_called_once_with( + chat_data, mock_request, mock_task_lock + ) @pytest.mark.asyncio - async def test_post_chat_sets_environment_variables(self, sample_chat_data, mock_request, mock_task_lock): + async def test_post_chat_sets_environment_variables( + self, sample_chat_data, mock_request, mock_task_lock + ): """Test that environment variables are properly set.""" chat_data = Chat(**sample_chat_data) - - with patch("app.controller.chat_controller.create_task_lock", return_value=mock_task_lock), \ - patch("app.controller.chat_controller.step_solve") as mock_step_solve, \ - patch("app.controller.chat_controller.load_dotenv"), \ - patch("pathlib.Path.mkdir"), \ - patch("pathlib.Path.home", return_value=MagicMock()), \ - patch.dict(os.environ, {}, clear=True): - + + with ( + patch( + "app.controller.chat_controller.create_task_lock", + return_value=mock_task_lock, + ), + patch( + "app.controller.chat_controller.step_solve" + ) as mock_step_solve, + patch("app.controller.chat_controller.load_dotenv"), + patch("pathlib.Path.mkdir"), + patch("pathlib.Path.home", return_value=MagicMock()), + patch.dict(os.environ, {}, clear=True), + ): + async def mock_generator(): yield "data: test_response\n\n" - + mock_step_solve.return_value = mock_generator() - + await post(chat_data, mock_request) - + # Check environment variables were set assert os.environ.get("OPENAI_API_KEY") == "test_key" - assert os.environ.get("OPENAI_API_BASE_URL") == "https://api.openai.com/v1" + assert ( + os.environ.get("OPENAI_API_BASE_URL") + == "https://api.openai.com/v1" + ) assert os.environ.get("CAMEL_MODEL_LOG_ENABLED") == "true" assert os.environ.get("browser_port") == "8080" @@ -84,12 +117,16 @@ class TestChatController: task_id = "test_task_123" supplement_data = SupplementChat(question="Improve this code") mock_task_lock.status = Status.processing - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = improve(task_id, supplement_data) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -101,8 +138,11 @@ class TestChatController: task_id = "test_task_123" supplement_data = SupplementChat(question="Improve this code") mock_task_lock.status = Status.done - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock): + + with patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ): with pytest.raises(UserException): improve(task_id, supplement_data) @@ -111,12 +151,16 @@ class TestChatController: task_id = "test_task_123" supplement_data = SupplementChat(question="Add more details") mock_task_lock.status = Status.done - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = supplement(task_id, supplement_data) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -126,20 +170,27 @@ class TestChatController: task_id = "test_task_123" supplement_data = SupplementChat(question="Add more details") mock_task_lock.status = Status.processing - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock): + + with patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ): with pytest.raises(UserException): supplement(task_id, supplement_data) def test_stop_chat_success(self, mock_task_lock): """Test successful chat stopping.""" task_id = "test_task_123" - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = stop(task_id) - + assert isinstance(response, Response) assert response.status_code == 204 mock_run.assert_called_once() @@ -148,12 +199,16 @@ class TestChatController: """Test successful human reply.""" task_id = "test_task_123" reply_data = HumanReply(agent="test_agent", reply="This is my reply") - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = human_reply(task_id, reply_data) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -161,13 +216,19 @@ class TestChatController: def test_install_mcp_success(self, mock_task_lock): """Test successful MCP installation.""" task_id = "test_task_123" - mcp_data: McpServers = {"mcpServers": {"test_server": {"config": "test"}}} - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + mcp_data: McpServers = { + "mcpServers": {"test_server": {"config": "test"}} + } + + with ( + patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = install_mcp(task_id, mcp_data) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -176,121 +237,154 @@ class TestChatController: @pytest.mark.integration class TestChatControllerIntegration: """Integration tests for chat controller.""" - - def test_chat_endpoint_integration(self, client: TestClient, sample_chat_data): + + def test_chat_endpoint_integration( + self, client: TestClient, sample_chat_data + ): """Test chat endpoint through FastAPI test client.""" - with patch("app.controller.chat_controller.create_task_lock") as mock_create_lock, \ - patch("app.controller.chat_controller.step_solve") as mock_step_solve, \ - patch("app.controller.chat_controller.load_dotenv"), \ - patch("pathlib.Path.mkdir"), \ - patch("pathlib.Path.home", return_value=MagicMock()): - + with ( + patch( + "app.controller.chat_controller.create_task_lock" + ) as mock_create_lock, + patch( + "app.controller.chat_controller.step_solve" + ) as mock_step_solve, + patch("app.controller.chat_controller.load_dotenv"), + patch("pathlib.Path.mkdir"), + patch("pathlib.Path.home", return_value=MagicMock()), + ): mock_task_lock = MagicMock() mock_create_lock.return_value = mock_task_lock - + async def mock_generator(): yield "data: test_response\n\n" - + mock_step_solve.return_value = mock_generator() - + response = client.post("/chat", json=sample_chat_data) - + assert response.status_code == 200 - assert response.headers["content-type"] == "text/event-stream; charset=utf-8" + assert ( + response.headers["content-type"] + == "text/event-stream; charset=utf-8" + ) def test_improve_chat_endpoint_integration(self, client: TestClient): """Test improve chat endpoint through FastAPI test client.""" task_id = "test_task_123" supplement_data = {"question": "Improve this code"} - - with patch("app.controller.chat_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.chat_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_task_lock.status = Status.processing mock_get_lock.return_value = mock_task_lock - + response = client.post(f"/chat/{task_id}", json=supplement_data) - + assert response.status_code == 201 def test_supplement_chat_endpoint_integration(self, client: TestClient): """Test supplement chat endpoint through FastAPI test client.""" task_id = "test_task_123" supplement_data = {"question": "Add more details"} - - with patch("app.controller.chat_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.chat_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_task_lock.status = Status.done mock_get_lock.return_value = mock_task_lock - + response = client.put(f"/chat/{task_id}", json=supplement_data) - + assert response.status_code == 201 def test_stop_chat_endpoint_integration(self, client: TestClient): """Test stop chat endpoint through FastAPI test client.""" task_id = "test_task_123" - - with patch("app.controller.chat_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.chat_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - + response = client.delete(f"/chat/{task_id}") - + assert response.status_code == 204 def test_human_reply_endpoint_integration(self, client: TestClient): """Test human reply endpoint through FastAPI test client.""" task_id = "test_task_123" reply_data = {"agent": "test_agent", "reply": "This is my reply"} - - with patch("app.controller.chat_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.chat_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - - response = client.post(f"/chat/{task_id}/human-reply", json=reply_data) - + + response = client.post( + f"/chat/{task_id}/human-reply", json=reply_data + ) + assert response.status_code == 201 def test_install_mcp_endpoint_integration(self, client: TestClient): """Test install MCP endpoint through FastAPI test client.""" task_id = "test_task_123" mcp_data = {"mcpServers": {"test_server": {"config": "test"}}} - - with patch("app.controller.chat_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.chat_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - - response = client.post(f"/chat/{task_id}/install-mcp", json=mcp_data) - + + response = client.post( + f"/chat/{task_id}/install-mcp", json=mcp_data + ) + assert response.status_code == 201 @pytest.mark.model_backend class TestChatControllerWithLLM: """Tests that require LLM backend (marked for selective running).""" - + @pytest.mark.asyncio - async def test_post_with_real_llm_model(self, sample_chat_data, mock_request): + async def test_post_with_real_llm_model( + self, sample_chat_data, mock_request + ): """Test chat endpoint with real LLM model (slow test).""" # This test would use actual LLM models and should be marked accordingly - chat_data = Chat(**sample_chat_data) - + Chat(**sample_chat_data) + # Test implementation would involve real model calls # This is marked as model_backend test for selective execution assert True # Placeholder @pytest.mark.very_slow - async def test_full_chat_workflow_with_llm(self, sample_chat_data, mock_request): + async def test_full_chat_workflow_with_llm( + self, sample_chat_data, mock_request + ): """Test complete chat workflow with LLM (very slow test).""" # This test would run the complete workflow including actual agent interactions # Marked as very_slow for execution only in full test mode @@ -300,7 +394,7 @@ class TestChatControllerWithLLM: @pytest.mark.unit class TestChatControllerErrorCases: """Test error cases and edge conditions.""" - + @pytest.mark.asyncio async def test_post_with_invalid_data(self, mock_request): """Test chat endpoint with invalid data.""" @@ -318,7 +412,7 @@ class TestChatControllerErrorCases: new_agents=[], env_path="nonexistent.env", browser_port=-1, # Invalid port - summary_prompt="" + summary_prompt="", ) # If future validation moves to endpoint level, keep logic placeholder below. # (Intentionally not calling post with invalid Chat object since creation fails.) @@ -327,8 +421,11 @@ class TestChatControllerErrorCases: """Test improve endpoint with nonexistent task.""" task_id = "nonexistent_task" supplement_data = SupplementChat(question="Improve this code") - - with patch("app.controller.chat_controller.get_task_lock", side_effect=KeyError("Task not found")): + + with patch( + "app.controller.chat_controller.get_task_lock", + side_effect=KeyError("Task not found"), + ): with pytest.raises(KeyError): improve(task_id, supplement_data) @@ -337,26 +434,41 @@ class TestChatControllerErrorCases: task_id = "test_task_123" supplement_data = SupplementChat(question="") mock_task_lock.status = Status.done - - with patch("app.controller.chat_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.chat_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run"), + ): # Should handle empty question gracefully or raise appropriate error response = supplement(task_id, supplement_data) assert response.status_code == 201 # Or should it be an error? @pytest.mark.asyncio - async def test_post_environment_setup_failure(self, sample_chat_data, mock_request): + async def test_post_environment_setup_failure( + self, sample_chat_data, mock_request + ): """Test chat endpoint when environment setup fails.""" chat_data = Chat(**sample_chat_data) - - with patch("app.controller.chat_controller.create_task_lock") as mock_create_lock, \ - patch("app.controller.chat_controller.load_dotenv", side_effect=Exception("Env load failed")), \ - patch("pathlib.Path.mkdir", side_effect=Exception("Directory creation failed")): - + + with ( + patch( + "app.controller.chat_controller.create_task_lock" + ) as mock_create_lock, + patch( + "app.controller.chat_controller.load_dotenv", + side_effect=Exception("Env load failed"), + ), + patch( + "pathlib.Path.mkdir", + side_effect=Exception("Directory creation failed"), + ), + ): mock_task_lock = MagicMock() mock_create_lock.return_value = mock_task_lock - + # Should handle environment setup failures gracefully with pytest.raises(Exception): await post(chat_data, mock_request) diff --git a/backend/tests/unit/controller/test_model_controller.py b/backend/tests/unit/controller/test_model_controller.py index ba1a1fd4..1d59d88d 100644 --- a/backend/tests/unit/controller/test_model_controller.py +++ b/backend/tests/unit/controller/test_model_controller.py @@ -13,10 +13,15 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from unittest.mock import MagicMock, patch + import pytest from fastapi.testclient import TestClient -from app.controller.model_controller import validate_model, ValidateModelRequest, ValidateModelResponse +from app.controller.model_controller import ( + ValidateModelRequest, + ValidateModelResponse, + validate_model, +) @pytest.mark.unit @@ -38,13 +43,14 @@ class TestModelController: mock_agent = MagicMock() mock_response = MagicMock() tool_call = MagicMock() - tool_call.result = ( - "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" - ) + tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" mock_response.info = {"tool_calls": [tool_call]} mock_agent.step.return_value = mock_response - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): response = await validate_model(request_data) assert isinstance(response, ValidateModelResponse) @@ -57,10 +63,15 @@ class TestModelController: @pytest.mark.asyncio async def test_validate_model_creation_failure(self): """Test model validation when agent creation fails.""" - request_data = ValidateModelRequest(model_platform="INVALID", model_type="INVALID_MODEL", api_key="invalid_key") + request_data = ValidateModelRequest( + model_platform="INVALID", + model_type="INVALID_MODEL", + api_key="invalid_key", + ) with patch( - "app.controller.model_controller.create_agent", side_effect=Exception("Invalid model configuration") + "app.controller.model_controller.create_agent", + side_effect=Exception("Invalid model configuration"), ): response = await validate_model(request_data) assert isinstance(response, ValidateModelResponse) @@ -71,12 +82,17 @@ class TestModelController: @pytest.mark.asyncio async def test_validate_model_step_failure(self): """Test model validation when agent step fails.""" - request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", api_key="test_key") + request_data = ValidateModelRequest( + model_platform="openai", model_type="gpt-4o", api_key="test_key" + ) mock_agent = MagicMock() mock_agent.step.side_effect = Exception("API call failed") - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): response = await validate_model(request_data) assert isinstance(response, ValidateModelResponse) @@ -87,7 +103,9 @@ class TestModelController: @pytest.mark.asyncio async def test_validate_model_tool_calls_false(self): """Test model validation when tool calls fail.""" - request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", api_key="test_key") + request_data = ValidateModelRequest( + model_platform="openai", model_type="gpt-4o", api_key="test_key" + ) mock_agent = MagicMock() mock_response = MagicMock() @@ -96,13 +114,19 @@ class TestModelController: mock_response.info = {"tool_calls": [tool_call]} mock_agent.step.return_value = mock_response - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): response = await validate_model(request_data) assert isinstance(response, ValidateModelResponse) assert response.is_valid is True assert response.is_tool_calls is False - assert response.message == "This model doesn't support tool calls. please try with another model." + assert ( + response.message + == "This model doesn't support tool calls. please try with another model." + ) @pytest.mark.asyncio async def test_validate_model_with_minimal_parameters(self): @@ -112,13 +136,14 @@ class TestModelController: mock_agent = MagicMock() mock_response = MagicMock() tool_call = MagicMock() - tool_call.result = ( - "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" - ) + tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" mock_response.info = {"tool_calls": [tool_call]} mock_agent.step.return_value = mock_response - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): response = await validate_model(request_data) assert isinstance(response, ValidateModelResponse) assert response.is_valid is False @@ -129,13 +154,18 @@ class TestModelController: @pytest.mark.asyncio async def test_validate_model_no_response(self): """Test model validation when no response is returned.""" - request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o") + request_data = ValidateModelRequest( + model_platform="openai", model_type="gpt-4o" + ) mock_agent = MagicMock() mock_agent.step.return_value = None # When response is None, should return False - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): result = await validate_model(request_data) assert result.is_valid is False assert result.is_tool_calls is False @@ -161,13 +191,14 @@ class TestModelControllerIntegration: mock_agent = MagicMock() mock_response = MagicMock() tool_call = MagicMock() - tool_call.result = ( - "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" - ) + tool_call.result = "Tool execution completed successfully for https://www.camel-ai.org, Website Content: Welcome to CAMEL AI!" mock_response.info = {"tool_calls": [tool_call]} mock_agent.step.return_value = mock_response - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): response = client.post("/model/validate", json=request_data) assert response.status_code == 200 @@ -176,14 +207,24 @@ class TestModelControllerIntegration: assert response_data["is_tool_calls"] is True assert response_data["message"] == "Validation Success" - def test_validate_model_endpoint_error_integration(self, client: TestClient): + def test_validate_model_endpoint_error_integration( + self, client: TestClient + ): """Test validate model endpoint error handling through FastAPI test client.""" - request_data = {"model_platform": "INVALID", "model_type": "INVALID_MODEL"} + request_data = { + "model_platform": "INVALID", + "model_type": "INVALID_MODEL", + } - with patch("app.controller.model_controller.create_agent", side_effect=Exception("Test error")): + with patch( + "app.controller.model_controller.create_agent", + side_effect=Exception("Test error"), + ): response = client.post("/model/validate", json=request_data) - assert response.status_code == 200 # Returns 200 with error in response body + assert ( + response.status_code == 200 + ) # Returns 200 with error in response body response_data = response.json() assert response_data["is_valid"] is False assert response_data["is_tool_calls"] is False @@ -222,7 +263,10 @@ class TestModelControllerErrorCases: model_config_dict={"invalid": float("inf")}, # Invalid JSON value ) - with patch("app.controller.model_controller.create_agent", side_effect=ValueError("Invalid configuration")): + with patch( + "app.controller.model_controller.create_agent", + side_effect=ValueError("Invalid configuration"), + ): response = await validate_model(request_data) assert response.is_valid is False @@ -231,12 +275,19 @@ class TestModelControllerErrorCases: @pytest.mark.asyncio async def test_validate_model_with_network_error(self): """Test model validation with network connectivity issues.""" - request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o", url="https://invalid-url.com") + request_data = ValidateModelRequest( + model_platform="openai", + model_type="gpt-4o", + url="https://invalid-url.com", + ) mock_agent = MagicMock() mock_agent.step.side_effect = ConnectionError("Network unreachable") - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): response = await validate_model(request_data) assert response.is_valid is False @@ -245,7 +296,9 @@ class TestModelControllerErrorCases: @pytest.mark.asyncio async def test_validate_model_with_malformed_tool_calls_response(self): """Test model validation with malformed tool calls in response.""" - request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o") + request_data = ValidateModelRequest( + model_platform="openai", model_type="gpt-4o" + ) mock_agent = MagicMock() mock_response = MagicMock() @@ -254,7 +307,10 @@ class TestModelControllerErrorCases: } mock_agent.step.return_value = mock_response - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): # Should handle empty tool calls gracefully result = await validate_model(request_data) assert result.is_valid is True # Response exists @@ -263,14 +319,19 @@ class TestModelControllerErrorCases: @pytest.mark.asyncio async def test_validate_model_with_missing_info_field(self): """Test model validation with missing info field in response.""" - request_data = ValidateModelRequest(model_platform="openai", model_type="gpt-4o") + request_data = ValidateModelRequest( + model_platform="openai", model_type="gpt-4o" + ) mock_agent = MagicMock() mock_response = MagicMock() mock_response.info = {} # Missing tool_calls mock_agent.step.return_value = mock_response - with patch("app.controller.model_controller.create_agent", return_value=mock_agent): + with patch( + "app.controller.model_controller.create_agent", + return_value=mock_agent, + ): # Should handle missing tool_calls key gracefully result = await validate_model(request_data) assert result.is_valid is True # Response exists @@ -300,7 +361,9 @@ class TestModelControllerErrorCases: async def test_validate_model_invalid_model_type(self): """Test model validation with invalid model type.""" request_data = ValidateModelRequest( - model_platform="openai", model_type="INVALID_MODEL_TYPE", api_key="test_key" + model_platform="openai", + model_type="INVALID_MODEL_TYPE", + api_key="test_key", ) response = await validate_model(request_data) @@ -310,6 +373,9 @@ class TestModelControllerErrorCases: assert response.error_code is not None assert "model_not_found" in response.error_code assert response.error is not None - assert response.error["message"] == "Invalid model name. Validation failed." + assert ( + response.error["message"] + == "Invalid model name. Validation failed." + ) assert response.error["type"] == "invalid_request_error" assert response.error["code"] == "model_not_found" diff --git a/backend/tests/unit/controller/test_task_controller.py b/backend/tests/unit/controller/test_task_controller.py index 50544278..3da13c80 100644 --- a/backend/tests/unit/controller/test_task_controller.py +++ b/backend/tests/unit/controller/test_task_controller.py @@ -13,28 +13,39 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from unittest.mock import MagicMock, patch + import pytest from fastapi import Response from fastapi.testclient import TestClient -from app.controller.task_controller import start, put, take_control, add_agent, TakeControl -from app.model.chat import NewAgent, UpdateData, TaskContent +from app.controller.task_controller import ( + TakeControl, + add_agent, + put, + start, + take_control, +) +from app.model.chat import NewAgent, TaskContent, UpdateData from app.service.task import Action @pytest.mark.unit class TestTaskController: """Test cases for task controller endpoints.""" - + def test_start_task_success(self, mock_task_lock): """Test successful task start.""" task_id = "test_task_123" - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = start(task_id) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -45,15 +56,19 @@ class TestTaskController: update_data = UpdateData( task=[ TaskContent(id="subtask_1", content="Updated content 1"), - TaskContent(id="subtask_2", content="Updated content 2") + TaskContent(id="subtask_2", content="Updated content 2"), ] ) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = put(task_id, update_data) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -62,12 +77,16 @@ class TestTaskController: """Test successful task pause control.""" task_id = "test_task_123" control_data = TakeControl(action=Action.pause) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = take_control(task_id, control_data) - + assert isinstance(response, Response) assert response.status_code == 204 mock_run.assert_called_once() @@ -76,12 +95,16 @@ class TestTaskController: """Test successful task resume control.""" task_id = "test_task_123" control_data = TakeControl(action=Action.resume) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = take_control(task_id, control_data) - + assert isinstance(response, Response) assert response.status_code == 204 mock_run.assert_called_once() @@ -94,15 +117,19 @@ class TestTaskController: description="A test agent", tools=["search", "code"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("app.controller.task_controller.load_dotenv"), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("app.controller.task_controller.load_dotenv"), + patch("asyncio.run") as mock_run, + ): response = add_agent(task_id, new_agent) - + assert isinstance(response, Response) assert response.status_code == 204 mock_run.assert_called_once() @@ -110,8 +137,11 @@ class TestTaskController: def test_start_task_nonexistent_task(self): """Test start task with nonexistent task ID.""" task_id = "nonexistent_task" - - with patch("app.controller.task_controller.get_task_lock", side_effect=KeyError("Task not found")): + + with patch( + "app.controller.task_controller.get_task_lock", + side_effect=KeyError("Task not found"), + ): with pytest.raises(KeyError): start(task_id) @@ -119,12 +149,16 @@ class TestTaskController: """Test update task with empty task list.""" task_id = "test_task_123" update_data = UpdateData(task=[]) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = put(task_id, update_data) - + assert isinstance(response, Response) assert response.status_code == 201 mock_run.assert_called_once() @@ -137,15 +171,19 @@ class TestTaskController: description="An agent with MCP tools", tools=["search"], mcp_tools={"mcpServers": {"notion": {"config": "test"}}}, - env_path=".env" + env_path=".env", ) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("app.controller.task_controller.load_dotenv"), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("app.controller.task_controller.load_dotenv"), + patch("asyncio.run") as mock_run, + ): response = add_agent(task_id, new_agent) - + assert isinstance(response, Response) assert response.status_code == 204 mock_run.assert_called_once() @@ -154,19 +192,22 @@ class TestTaskController: @pytest.mark.integration class TestTaskControllerIntegration: """Integration tests for task controller.""" - + def test_start_task_endpoint_integration(self, client: TestClient): """Test start task endpoint through FastAPI test client.""" task_id = "test_task_123" - - with patch("app.controller.task_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - + response = client.post(f"/task/{task_id}/start") - + assert response.status_code == 201 def test_update_task_endpoint_integration(self, client: TestClient): @@ -175,48 +216,63 @@ class TestTaskControllerIntegration: update_data = { "task": [ {"id": "subtask_1", "content": "Updated content 1"}, - {"id": "subtask_2", "content": "Updated content 2"} + {"id": "subtask_2", "content": "Updated content 2"}, ] } - - with patch("app.controller.task_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - + response = client.put(f"/task/{task_id}", json=update_data) - + assert response.status_code == 201 def test_take_control_pause_endpoint_integration(self, client: TestClient): """Test take control pause endpoint through FastAPI test client.""" task_id = "test_task_123" control_data = {"action": "pause"} - - with patch("app.controller.task_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - - response = client.put(f"/task/{task_id}/take-control", json=control_data) - + + response = client.put( + f"/task/{task_id}/take-control", json=control_data + ) + assert response.status_code == 204 - def test_take_control_resume_endpoint_integration(self, client: TestClient): + def test_take_control_resume_endpoint_integration( + self, client: TestClient + ): """Test take control resume endpoint through FastAPI test client.""" task_id = "test_task_123" control_data = {"action": "resume"} - - with patch("app.controller.task_controller.get_task_lock") as mock_get_lock, \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock" + ) as mock_get_lock, + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - - response = client.put(f"/task/{task_id}/take-control", json=control_data) - + + response = client.put( + f"/task/{task_id}/take-control", json=control_data + ) + assert response.status_code == 204 def test_add_agent_endpoint_integration(self, client: TestClient): @@ -227,32 +283,41 @@ class TestTaskControllerIntegration: "description": "A test agent", "tools": ["search", "code"], "mcp_tools": None, - "env_path": ".env" + "env_path": ".env", } - - with patch("app.controller.task_controller.get_task_lock") as mock_get_lock, \ - patch("app.controller.task_controller.load_dotenv"), \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock" + ) as mock_get_lock, + patch("app.controller.task_controller.load_dotenv"), + patch("asyncio.run"), + ): mock_task_lock = MagicMock() mock_get_lock.return_value = mock_task_lock - - response = client.post(f"/task/{task_id}/add-agent", json=agent_data) - + + response = client.post( + f"/task/{task_id}/add-agent", json=agent_data + ) + assert response.status_code == 204 @pytest.mark.unit class TestTaskControllerErrorCases: """Test error cases and edge conditions for task controller.""" - + def test_start_task_async_error(self, mock_task_lock): """Test start task when async operation fails.""" task_id = "test_task_123" - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run", side_effect=Exception("Async error")): - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run", side_effect=Exception("Async error")), + ): with pytest.raises(Exception, match="Async error"): start(task_id) @@ -260,22 +325,27 @@ class TestTaskControllerErrorCases: """Test update task with invalid task content.""" task_id = "test_task_123" # Create invalid update data that might cause validation errors - update_data = UpdateData(task=[ - TaskContent(id="", content=""), # Empty ID and content - TaskContent(id="valid_id", content="Valid content") - ]) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + update_data = UpdateData( + task=[ + TaskContent(id="", content=""), # Empty ID and content + TaskContent(id="valid_id", content="Valid content"), + ] + ) + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): # Should handle invalid data gracefully or raise appropriate error response = put(task_id, update_data) assert response.status_code == 201 def test_take_control_invalid_action(self): """Test take control with invalid action value.""" - task_id = "test_task_123" - + # This should be caught by Pydantic validation with pytest.raises((ValueError, TypeError)): TakeControl(action="invalid_action") @@ -288,13 +358,20 @@ class TestTaskControllerErrorCases: description="A test agent", tools=["search"], mcp_tools=None, - env_path="nonexistent.env" + env_path="nonexistent.env", ) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("app.controller.task_controller.load_dotenv", side_effect=Exception("Env load failed")), \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch( + "app.controller.task_controller.load_dotenv", + side_effect=Exception("Env load failed"), + ), + patch("asyncio.run"), + ): # Should handle environment load failure gracefully or raise error with pytest.raises(Exception, match="Env load failed"): add_agent(task_id, new_agent) @@ -307,13 +384,17 @@ class TestTaskControllerErrorCases: description="A test agent", tools=["search"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("app.controller.task_controller.load_dotenv"), \ - patch("asyncio.run"): - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("app.controller.task_controller.load_dotenv"), + patch("asyncio.run"), + ): # Should handle empty name appropriately response = add_agent(task_id, new_agent) assert response.status_code == 204 @@ -321,17 +402,21 @@ class TestTaskControllerErrorCases: def test_task_operations_with_concurrent_access(self, mock_task_lock): """Test task operations with concurrent access scenarios.""" task_id = "test_task_123" - + # Simulate concurrent access by having the task lock be modified during operation def side_effect(): mock_task_lock.status = "modified_during_operation" return None - + mock_task_lock.put_queue.side_effect = side_effect - - with patch("app.controller.task_controller.get_task_lock", return_value=mock_task_lock), \ - patch("asyncio.run") as mock_run: - + + with ( + patch( + "app.controller.task_controller.get_task_lock", + return_value=mock_task_lock, + ), + patch("asyncio.run") as mock_run, + ): response = start(task_id) assert response.status_code == 201 @@ -339,18 +424,17 @@ class TestTaskControllerErrorCases: @pytest.mark.model_backend class TestTaskControllerWithLLM: """Tests that require LLM backend (marked for selective running).""" - + def test_add_agent_with_real_model_integration(self, mock_task_lock): """Test adding an agent that requires real model integration.""" - task_id = "test_task_123" new_agent = NewAgent( name="Real Model Agent", description="An agent that uses real models", tools=["search", "code"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - + # This test would involve real model creation and configuration # Marked as model_backend test for selective execution assert True # Placeholder diff --git a/backend/tests/unit/controller/test_tool_controller.py b/backend/tests/unit/controller/test_tool_controller.py index de932d0b..b30acfdf 100644 --- a/backend/tests/unit/controller/test_tool_controller.py +++ b/backend/tests/unit/controller/test_tool_controller.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from unittest.mock import AsyncMock, MagicMock, patch + import pytest from fastapi.testclient import TestClient @@ -22,7 +23,7 @@ from app.controller.tool_controller import install_tool @pytest.mark.unit class TestToolController: """Test cases for tool controller endpoints.""" - + @pytest.mark.asyncio async def test_install_notion_tool_success(self): tool_name = "notion" @@ -31,7 +32,10 @@ class TestToolController: for tool, name in zip(mock_tools, ["create_page", "update_page"]): tool.func.__name__ = name mock_toolkit.get_tools = MagicMock(return_value=mock_tools) - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): result = await install_tool(tool_name) assert result == ["create_page", "update_page"] mock_toolkit.connect.assert_called_once() @@ -46,15 +50,23 @@ class TestToolController: async def test_install_notion_tool_connection_failure(self): mock_toolkit = AsyncMock() mock_toolkit.connect.side_effect = Exception("Connection failed") - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): with pytest.raises(Exception, match="Connection failed"): await install_tool("notion") @pytest.mark.asyncio async def test_install_notion_tool_get_tools_failure(self): mock_toolkit = AsyncMock() - mock_toolkit.get_tools = MagicMock(side_effect=Exception("Failed to get tools")) - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + mock_toolkit.get_tools = MagicMock( + side_effect=Exception("Failed to get tools") + ) + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): with pytest.raises(Exception, match="Failed to get tools"): await install_tool("notion") @@ -65,7 +77,10 @@ class TestToolController: mock_tools[0].func.__name__ = "test_tool" mock_toolkit.get_tools = MagicMock(return_value=mock_tools) mock_toolkit.disconnect.side_effect = Exception("Disconnect failed") - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): with pytest.raises(Exception, match="Disconnect failed"): await install_tool("notion") @@ -73,7 +88,10 @@ class TestToolController: async def test_install_notion_tool_empty_tools(self): mock_toolkit = AsyncMock() mock_toolkit.get_tools = MagicMock(return_value=[]) - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): result = await install_tool("notion") assert result == [] mock_toolkit.connect.assert_called_once() @@ -82,14 +100,22 @@ class TestToolController: @pytest.mark.asyncio async def test_install_notion_tool_with_complex_tools(self): mock_toolkit = AsyncMock() - names = ["create_database", "query_database", "update_block", "delete_page"] + names = [ + "create_database", + "query_database", + "update_block", + "delete_page", + ] mock_tools = [] for name in names: mt = MagicMock() mt.func.__name__ = name mock_tools.append(mt) mock_toolkit.get_tools = MagicMock(return_value=mock_tools) - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): result = await install_tool("notion") assert result == names mock_toolkit.connect.assert_called_once() @@ -99,54 +125,65 @@ class TestToolController: @pytest.mark.integration class TestToolControllerIntegration: """Integration tests for tool controller.""" - - def test_install_notion_tool_endpoint_integration(self, client: TestClient): + + def test_install_notion_tool_endpoint_integration( + self, client: TestClient + ): """Test install Notion tool endpoint through FastAPI test client.""" tool_name = "notion" - + mock_toolkit = AsyncMock() mock_tools = [MagicMock(), MagicMock()] mock_tools[0].func.__name__ = "create_page" mock_tools[1].func.__name__ = "update_page" mock_toolkit.get_tools = MagicMock(return_value=mock_tools) - - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): response = client.post(f"/install/tool/{tool_name}") - + assert response.status_code == 200 assert response.json() == ["create_page", "update_page"] - def test_install_unknown_tool_endpoint_integration(self, client: TestClient): + def test_install_unknown_tool_endpoint_integration( + self, client: TestClient + ): """Test install unknown tool endpoint through FastAPI test client.""" tool_name = "unknown_tool" - + response = client.post(f"/install/tool/{tool_name}") - + assert response.status_code == 200 assert response.json() == {"error": "Tool not found"} - def test_install_notion_tool_endpoint_with_connection_error(self, client: TestClient): + def test_install_notion_tool_endpoint_with_connection_error( + self, client: TestClient + ): """Test install Notion tool endpoint when connection fails.""" tool_name = "notion" - + mock_toolkit = AsyncMock() mock_toolkit.connect.side_effect = Exception("Connection failed") - - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): # The exception should be raised by the endpoint since there's no error handling with pytest.raises(Exception, match="Connection failed"): - response = client.post(f"/install/tool/{tool_name}") + client.post(f"/install/tool/{tool_name}") @pytest.mark.model_backend class TestToolControllerWithRealMCP: """Tests that require real MCP connections (marked for selective running).""" - + @pytest.mark.asyncio async def test_install_notion_tool_with_real_connection(self): """Test Notion tool installation with real MCP connection.""" - tool_name = "notion" - + # This test would connect to real Notion MCP server # Requires actual MCP server setup and credentials # Marked as model_backend test for selective execution @@ -170,13 +207,19 @@ class TestToolControllerErrorCases: tools = [MagicMock(), object()] # Second item lacks func tools[0].func.__name__ = "valid_tool" mock_toolkit.get_tools = MagicMock(return_value=tools) - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): with pytest.raises(AttributeError): await install_tool("notion") @pytest.mark.asyncio async def test_install_tool_with_none_toolkit(self): - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=None): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=None, + ): with pytest.raises(AttributeError): await install_tool("notion") @@ -205,6 +248,9 @@ class TestToolControllerErrorCases: tools[2].func = None mock_toolkit.get_tools = MagicMock(return_value=tools) mock_toolkit.disconnect.return_value = None - with patch("app.controller.tool_controller.NotionMCPToolkit", return_value=mock_toolkit): + with patch( + "app.controller.tool_controller.NotionMCPToolkit", + return_value=mock_toolkit, + ): with pytest.raises(AttributeError): await install_tool("notion") diff --git a/backend/tests/unit/model/test_agent_model_config.py b/backend/tests/unit/model/test_agent_model_config.py index 855dbcc4..ac216a63 100644 --- a/backend/tests/unit/model/test_agent_model_config.py +++ b/backend/tests/unit/model/test_agent_model_config.py @@ -11,8 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - """Unit tests for AgentModelConfig and per-agent model configuration.""" + from app.model.chat import AgentModelConfig, NewAgent @@ -35,7 +35,7 @@ class TestAgentModelConfig: model_type="gpt-4", api_key="test-key", api_url="https://api.openai.com/v1", - extra_params={"temperature": 0.7} + extra_params={"temperature": 0.7}, ) assert config.model_platform == "openai" assert config.model_type == "gpt-4" @@ -61,8 +61,7 @@ class TestAgentModelConfig: def test_has_custom_config_true_with_both(self): """Test has_custom_config returns True when both are set.""" config = AgentModelConfig( - model_platform="anthropic", - model_type="claude-3-opus" + model_platform="anthropic", model_type="claude-3-opus" ) assert config.has_custom_config() is True @@ -81,7 +80,7 @@ class TestNewAgentWithModelConfig: name="TestAgent", description="A test agent", tools=[], - mcp_tools=None + mcp_tools=None, ) assert agent.name == "TestAgent" assert agent.custom_model_config is None @@ -89,15 +88,14 @@ class TestNewAgentWithModelConfig: def test_new_agent_with_model_config(self): """Test NewAgent creation with custom model config.""" model_config = AgentModelConfig( - model_platform="openai", - model_type="gpt-4-turbo" + model_platform="openai", model_type="gpt-4-turbo" ) agent = NewAgent( name="CustomModelAgent", description="An agent with custom model", tools=[], mcp_tools=None, - custom_model_config=model_config + custom_model_config=model_config, ) assert agent.name == "CustomModelAgent" assert agent.custom_model_config is not None @@ -107,15 +105,14 @@ class TestNewAgentWithModelConfig: def test_new_agent_serialization_with_model_config(self): """Test NewAgent serialization includes model config.""" model_config = AgentModelConfig( - model_platform="anthropic", - model_type="claude-3-sonnet" + model_platform="anthropic", model_type="claude-3-sonnet" ) agent = NewAgent( name="SerializationTest", description="Test serialization", tools=[], mcp_tools=None, - custom_model_config=model_config + custom_model_config=model_config, ) data = agent.model_dump() assert "custom_model_config" in data diff --git a/backend/tests/unit/service/test_chat_service.py b/backend/tests/unit/service/test_chat_service.py index 89f54097..9c405743 100644 --- a/backend/tests/unit/service/test_chat_service.py +++ b/backend/tests/unit/service/test_chat_service.py @@ -13,30 +13,34 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from unittest.mock import AsyncMock, MagicMock, patch -import pytest -import os -import tempfile -from pathlib import Path +import pytest +from camel.tasks import Task +from camel.tasks.task import TaskState + +from app.model.chat import Chat, NewAgent from app.service.chat_service import ( - step_solve, + add_sub_tasks, + build_context_for_workforce, + collect_previous_task_context, + construct_workforce, + format_agent_description, install_mcp, + new_agent_model, + question_confirm, + step_solve, + summary_task, to_sub_tasks, tree_sub_tasks, update_sub_tasks, - add_sub_tasks, - question_confirm, - summary_task, - construct_workforce, - format_agent_description, - new_agent_model, - collect_previous_task_context, - build_context_for_workforce ) -from app.model.chat import Chat, NewAgent -from app.service.task import Action, ActionImproveData, ActionEndData, ActionInstallMcpData, TaskLock -from camel.tasks import Task -from camel.tasks.task import TaskState +from app.service.task import ( + Action, + ActionEndData, + ActionImproveData, + ActionInstallMcpData, + TaskLock, +) @pytest.mark.unit @@ -49,14 +53,14 @@ class TestCollectPreviousTaskContext: previous_task_content = "Create a Python script" previous_task_result = "Successfully created script.py" previous_summary = "Python Script Creation Task" - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content=previous_task_content, previous_task_result=previous_task_result, - previous_summary=previous_summary + previous_summary=previous_summary, ) - + # Check that all sections are included assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "Previous Task:" in result @@ -68,70 +72,78 @@ class TestCollectPreviousTaskContext: assert "=== END OF PREVIOUS TASK CONTEXT ===" in result assert "=== NEW TASK ===" in result - def test_collect_previous_task_context_with_generated_files(self, temp_dir): + def test_collect_previous_task_context_with_generated_files( + self, temp_dir + ): """Test collect_previous_task_context with generated files in working directory.""" working_directory = str(temp_dir) - + # Create some test files (temp_dir / "script.py").write_text("print('Hello World')") (temp_dir / "config.json").write_text('{"test": true}') (temp_dir / "README.md").write_text("# Test Project") - + # Create a subdirectory with files sub_dir = temp_dir / "utils" sub_dir.mkdir() (sub_dir / "helper.py").write_text("def helper(): pass") - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Create project files", previous_task_result="Files created successfully", - previous_summary="" + previous_summary="", ) - + # Check that generated files are listed assert "Generated Files from Previous Task:" in result assert "script.py" in result assert "config.json" in result assert "README.md" in result - assert "utils/helper.py" in result or "utils\\helper.py" in result # Handle Windows paths - + assert ( + "utils/helper.py" in result or "utils\\helper.py" in result + ) # Handle Windows paths + # Files should be sorted - lines = result.split('\n') - file_lines = [line.strip() for line in lines if line.strip().startswith('- ')] + lines = result.split("\n") + file_lines = [ + line.strip() for line in lines if line.strip().startswith("- ") + ] assert len(file_lines) == 4 - def test_collect_previous_task_context_filters_hidden_files(self, temp_dir): + def test_collect_previous_task_context_filters_hidden_files( + self, temp_dir + ): """Test that hidden files and directories are filtered out.""" working_directory = str(temp_dir) - + # Create regular files (temp_dir / "visible.py").write_text("# Visible file") - + # Create hidden files and directories (temp_dir / ".hidden_file").write_text("hidden content") (temp_dir / ".env").write_text("SECRET=hidden") - + hidden_dir = temp_dir / ".hidden_dir" hidden_dir.mkdir() (hidden_dir / "file.txt").write_text("in hidden dir") - + # Create cache directories cache_dir = temp_dir / "__pycache__" cache_dir.mkdir() (cache_dir / "module.pyc").write_text("compiled") - + node_modules = temp_dir / "node_modules" node_modules.mkdir() (node_modules / "package").mkdir() - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test filtering", previous_task_result="Files filtered", - previous_summary="" + previous_summary="", ) - + # Should only include visible files assert "visible.py" in result assert ".hidden_file" not in result @@ -143,21 +155,21 @@ class TestCollectPreviousTaskContext: def test_collect_previous_task_context_filters_temp_files(self, temp_dir): """Test that temporary files are filtered out.""" working_directory = str(temp_dir) - + # Create regular files (temp_dir / "main.py").write_text("# Main file") - + # Create temporary files (temp_dir / "temp.tmp").write_text("temporary") (temp_dir / "compiled.pyc").write_text("compiled python") - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test temp filtering", previous_task_result="Temp files filtered", - previous_summary="" + previous_summary="", ) - + # Should only include regular files assert "main.py" in result assert "temp.tmp" not in result @@ -166,14 +178,14 @@ class TestCollectPreviousTaskContext: def test_collect_previous_task_context_nonexistent_directory(self): """Test collect_previous_task_context with non-existent working directory.""" working_directory = "/nonexistent/directory" - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test task", previous_task_result="Test result", - previous_summary="Test summary" + previous_summary="Test summary", ) - + # Should not crash and should not include file listing assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "Test task" in result @@ -184,19 +196,19 @@ class TestCollectPreviousTaskContext: def test_collect_previous_task_context_empty_inputs(self, temp_dir): """Test collect_previous_task_context with empty string inputs.""" working_directory = str(temp_dir) - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="", previous_task_result="", - previous_summary="" + previous_summary="", ) - + # Should still have the structural elements assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "=== END OF PREVIOUS TASK CONTEXT ===" in result assert "=== NEW TASK ===" in result - + # Should not have content sections for empty inputs assert "Previous Task:" not in result assert "Previous Task Summary:" not in result @@ -205,62 +217,64 @@ class TestCollectPreviousTaskContext: def test_collect_previous_task_context_only_summary(self, temp_dir): """Test collect_previous_task_context with only summary provided.""" working_directory = str(temp_dir) - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="", previous_task_result="", - previous_summary="Only summary provided" + previous_summary="Only summary provided", ) - + # Should include summary section only assert "Previous Task Summary:" in result assert "Only summary provided" in result assert "Previous Task:" not in result assert "Previous Task Result:" not in result - @patch('app.service.chat_service.logger') - def test_collect_previous_task_context_file_system_error(self, mock_logger, temp_dir): + @patch("app.service.chat_service.logger") + def test_collect_previous_task_context_file_system_error( + self, mock_logger, temp_dir + ): """Test collect_previous_task_context handles file system errors gracefully.""" working_directory = str(temp_dir) - + # Mock os.walk to raise an exception - with patch('os.walk', side_effect=PermissionError("Access denied")): + with patch("os.walk", side_effect=PermissionError("Access denied")): result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test task", previous_task_result="Test result", - previous_summary="Test summary" + previous_summary="Test summary", ) - + # Should still return result without files assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "Test task" in result assert "Generated Files from Previous Task:" not in result - + # Should log warning mock_logger.warning.assert_called_once() def test_collect_previous_task_context_relative_paths(self, temp_dir): """Test that file paths are correctly converted to relative paths.""" working_directory = str(temp_dir) - + # Create nested directory structure deep_dir = temp_dir / "level1" / "level2" / "level3" deep_dir.mkdir(parents=True) (deep_dir / "deep_file.txt").write_text("deep content") - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test relative paths", previous_task_result="Paths converted", - previous_summary="" + previous_summary="", ) - + # Check that the path is relative to working directory expected_path = "level1/level2/level3/deep_file.txt" windows_path = "level1\\level2\\level3\\deep_file.txt" - + # Should contain relative path (handle both Unix and Windows separators) assert expected_path in result or windows_path in result @@ -274,23 +288,26 @@ class TestBuildContextForWorkforce: # Create mock TaskLock task_lock = MagicMock(spec=TaskLock) task_lock.conversation_history = [ - {'role': 'user', 'content': 'Create a Python script'}, - {'role': 'assistant', 'content': 'I will create a Python script for you'} + {"role": "user", "content": "Create a Python script"}, + { + "role": "assistant", + "content": "I will create a Python script for you", + }, ] task_lock.last_task_result = "Script created successfully" task_lock.last_task_summary = "Python Script Creation" - + # Create mock Chat options options = MagicMock() options.file_save_path.return_value = str(temp_dir) - + result = build_context_for_workforce(task_lock, options) - + # Should include conversation history assert "=== CONVERSATION HISTORY ===" in result assert "user: Create a Python script" in result assert "assistant: I will create a Python script for you" in result - + # Should include previous task context assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "Script created successfully" in result @@ -301,12 +318,12 @@ class TestBuildContextForWorkforce: task_lock.conversation_history = [] task_lock.last_task_result = "" task_lock.last_task_summary = "" - + options = MagicMock() options.file_save_path.return_value = str(temp_dir) - + result = build_context_for_workforce(task_lock, options) - + # Should return empty string for no context assert result == "" @@ -314,21 +331,26 @@ class TestBuildContextForWorkforce: """Test build_context_for_workforce handles 'task_result' role specially.""" task_lock = MagicMock(spec=TaskLock) task_lock.conversation_history = [ - {'role': 'user', 'content': 'First question'}, - {'role': 'task_result', 'content': 'Full task context from previous task'}, - {'role': 'user', 'content': 'Second question'} + {"role": "user", "content": "First question"}, + { + "role": "task_result", + "content": "Full task context from previous task", + }, + {"role": "user", "content": "Second question"}, ] task_lock.last_task_result = "Final result" task_lock.last_task_summary = "Task summary" - + options = MagicMock() options.file_save_path.return_value = str(temp_dir) - + result = build_context_for_workforce(task_lock, options) - + # Should simplify task_result display assert "[Previous Task Completed]" in result - assert "Full task context from previous task" not in result # Should not show full content + assert ( + "Full task context from previous task" not in result + ) # Should not show full content assert "user: First question" in result assert "user: Second question" in result @@ -336,19 +358,19 @@ class TestBuildContextForWorkforce: """Test build_context_for_workforce includes last task result context.""" # Create some files in temp directory (temp_dir / "output.txt").write_text("Task output") - + task_lock = MagicMock(spec=TaskLock) task_lock.conversation_history = [ - {'role': 'user', 'content': 'Test question'} + {"role": "user", "content": "Test question"} ] task_lock.last_task_result = "Task completed with output.txt" task_lock.last_task_summary = "File creation task" - + options = MagicMock() options.file_save_path.return_value = str(temp_dir) - + result = build_context_for_workforce(task_lock, options) - + # Should include conversation history and task context assert "=== CONVERSATION HISTORY ===" in result assert "user: Test question" in result @@ -361,17 +383,17 @@ class TestBuildContextForWorkforce: @pytest.mark.unit class TestChatServiceUtilities: """Test cases for chat service utility functions.""" - + def test_tree_sub_tasks_simple(self): """Test tree_sub_tasks with simple task structure.""" task1 = Task(content="Task 1", id="task_1") task1.state = TaskState.OPEN task2 = Task(content="Task 2", id="task_2") task2.state = TaskState.RUNNING - + sub_tasks = [task1, task2] result = tree_sub_tasks(sub_tasks) - + assert len(result) == 2 assert result[0]["id"] == "task_1" assert result[0]["content"] == "Task 1" @@ -384,13 +406,13 @@ class TestChatServiceUtilities: """Test tree_sub_tasks with nested subtask structure.""" parent_task = Task(content="Parent Task", id="parent") parent_task.state = TaskState.RUNNING - + child_task = Task(content="Child Task", id="child") child_task.state = TaskState.OPEN parent_task.add_subtask(child_task) - + result = tree_sub_tasks([parent_task]) - + assert len(result) == 1 assert result[0]["id"] == "parent" assert result[0]["content"] == "Parent Task" @@ -404,9 +426,9 @@ class TestChatServiceUtilities: task1.state = TaskState.OPEN task2 = Task(content="", id="task_2") # Empty content task2.state = TaskState.OPEN - + result = tree_sub_tasks([task1, task2]) - + assert len(result) == 1 assert result[0]["id"] == "task_1" @@ -414,34 +436,34 @@ class TestChatServiceUtilities: """Test tree_sub_tasks respects depth limit.""" # Create deeply nested structure current_task = Task(content="Root", id="root") - + for i in range(10): - child_task = Task(content=f"Level {i+1}", id=f"level_{i+1}") + child_task = Task(content=f"Level {i + 1}", id=f"level_{i + 1}") current_task.add_subtask(child_task) current_task = child_task - + result = tree_sub_tasks([Task(content="Root", id="root")]) - + # Should not exceed depth limit (function should handle deep nesting gracefully) assert isinstance(result, list) def test_update_sub_tasks_success(self): """Test update_sub_tasks updates existing tasks correctly.""" from app.model.chat import TaskContent - + task1 = Task(content="Original Content 1", id="task_1") task2 = Task(content="Original Content 2", id="task_2") task3 = Task(content="Original Content 3", id="task_3") - + sub_tasks = [task1, task2, task3] - + update_tasks = { "task_2": TaskContent(id="task_2", content="Updated Content 2"), - "task_3": TaskContent(id="task_3", content="Updated Content 3") + "task_3": TaskContent(id="task_3", content="Updated Content 3"), } - + result = update_sub_tasks(sub_tasks, update_tasks) - + assert len(result) == 2 # Only updated tasks remain assert result[0].content == "Updated Content 2" assert result[1].content == "Updated Content 3" @@ -449,19 +471,21 @@ class TestChatServiceUtilities: def test_update_sub_tasks_with_nested_tasks(self): """Test update_sub_tasks handles nested task updates.""" from app.model.chat import TaskContent - + parent_task = Task(content="Parent", id="parent") child_task = Task(content="Original Child", id="child") parent_task.add_subtask(child_task) - + sub_tasks = [parent_task] update_tasks = { - "parent": TaskContent(id="parent", content="Parent"), # Include parent to keep it - "child": TaskContent(id="child", content="Updated Child") + "parent": TaskContent( + id="parent", content="Parent" + ), # Include parent to keep it + "child": TaskContent(id="child", content="Updated Child"), } - + result = update_sub_tasks(sub_tasks, update_tasks, depth=0) - + # Parent task should remain with updated child assert len(result) == 1 # Note: The actual behavior depends on the implementation details @@ -469,19 +493,19 @@ class TestChatServiceUtilities: def test_add_sub_tasks_to_camel_task(self): """Test add_sub_tasks adds new tasks to CAMEL task.""" from app.model.chat import TaskContent - + camel_task = Task(content="Main Task", id="main") - + new_tasks = [ TaskContent(id="", content="New Task 1"), - TaskContent(id="", content="New Task 2") + TaskContent(id="", content="New Task 2"), ] - + initial_subtask_count = len(camel_task.subtasks) add_sub_tasks(camel_task, new_tasks) - + assert len(camel_task.subtasks) == initial_subtask_count + 2 - + # Check that new subtasks were added with proper IDs new_subtasks = camel_task.subtasks[-2:] assert new_subtasks[0].content == "New Task 1" @@ -495,11 +519,11 @@ class TestChatServiceUtilities: subtask = Task(content="Sub Task", id="sub") subtask.state = TaskState.OPEN task.add_subtask(subtask) - + summary_content = "Task Summary" - + result = to_sub_tasks(task, summary_content) - + # Should be a JSON string formatted for SSE assert "to_sub_tasks" in result assert "summary_task" in result @@ -512,11 +536,11 @@ class TestChatServiceUtilities: description="A test agent for testing", tools=["search", "code"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - + result = format_agent_description(agent_data) - + assert "TestAgent:" in result assert "A test agent for testing" in result assert "Search" in result # Should titleize tool names @@ -529,11 +553,11 @@ class TestChatServiceUtilities: description="An agent with MCP tools", tools=["search"], mcp_tools={"mcpServers": {"notion": {}, "slack": {}}}, - env_path=".env" + env_path=".env", ) - + result = format_agent_description(agent_data) - + assert "MCPAgent:" in result assert "An agent with MCP tools" in result assert "Notion" in result @@ -546,11 +570,11 @@ class TestChatServiceUtilities: description="", tools=["search"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - + result = format_agent_description(agent_data) - + assert "SimpleAgent:" in result assert "A specialized agent" in result # Default description @@ -558,15 +582,17 @@ class TestChatServiceUtilities: @pytest.mark.unit class TestChatServiceAgentOperations: """Test cases for agent-related chat service operations.""" - + @pytest.mark.asyncio async def test_question_confirm_simple_query(self, mock_camel_agent): """Test question_confirm with simple query that gets direct response.""" - mock_camel_agent.step.return_value.msgs[0].content = "Hello! How can I help you today?" + mock_camel_agent.step.return_value.msgs[ + 0 + ].content = "Hello! How can I help you today?" mock_camel_agent.chat_history = [] - + result = await question_confirm(mock_camel_agent, "hello") - + # Should return SSE formatted response for simple queries assert "wait_confirm" in result assert "Hello! How can I help you today?" in result @@ -576,22 +602,32 @@ class TestChatServiceAgentOperations: """Test question_confirm with complex task that should proceed.""" mock_camel_agent.step.return_value.msgs[0].content = "yes" mock_camel_agent.chat_history = [] - - result = await question_confirm(mock_camel_agent, "Create a web application with authentication") - + + result = await question_confirm( + mock_camel_agent, "Create a web application with authentication" + ) + # Should return True for complex tasks assert result is True @pytest.mark.asyncio async def test_summary_task(self, mock_camel_agent): """Test summary_task creates proper task summary.""" - mock_camel_agent.step.return_value.msgs[0].content = "Web App Creation|Create a modern web application with user authentication and dashboard" - - task = Task(content="Create a web application with user authentication", id="web_app_task") - + mock_camel_agent.step.return_value.msgs[ + 0 + ].content = "Web App Creation|Create a modern web application with user authentication and dashboard" + + task = Task( + content="Create a web application with user authentication", + id="web_app_task", + ) + result = await summary_task(mock_camel_agent, task) - - assert result == "Web App Creation|Create a modern web application with user authentication and dashboard" + + assert ( + result + == "Web App Creation|Create a modern web application with user authentication and dashboard" + ) mock_camel_agent.step.assert_called_once() @pytest.mark.asyncio @@ -603,43 +639,56 @@ class TestChatServiceAgentOperations: description="A test agent", tools=["search", "code"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - + mock_agent = MagicMock() - - with patch("app.service.chat_service.get_toolkits", return_value=[]), \ - patch("app.service.chat_service.get_mcp_tools", return_value=[]), \ - patch("app.service.chat_service.agent_model", return_value=mock_agent): - + + with ( + patch("app.service.chat_service.get_toolkits", return_value=[]), + patch("app.service.chat_service.get_mcp_tools", return_value=[]), + patch( + "app.service.chat_service.agent_model", return_value=mock_agent + ), + ): result = await new_agent_model(agent_data, options) - + assert result is mock_agent @pytest.mark.asyncio async def test_construct_workforce(self, sample_chat_data, mock_task_lock): """Test construct_workforce creates workforce with proper agents.""" options = Chat(**sample_chat_data) - + mock_workforce = MagicMock() mock_mcp_agent = MagicMock() - - with patch("app.service.chat_service.agent_model") as mock_agent_model, \ - patch("app.service.chat_service.Workforce", return_value=mock_workforce), \ - patch("app.service.chat_service.browser_agent"), \ - patch("app.service.chat_service.developer_agent"), \ - patch("app.service.chat_service.document_agent"), \ - patch("app.service.chat_service.multi_modal_agent"), \ - patch("app.service.chat_service.mcp_agent", return_value=mock_mcp_agent), \ - patch("app.utils.toolkit.human_toolkit.get_task_lock", return_value=mock_task_lock): - + + with ( + patch("app.service.chat_service.agent_model") as mock_agent_model, + patch( + "app.service.chat_service.Workforce", + return_value=mock_workforce, + ), + patch("app.service.chat_service.browser_agent"), + patch("app.service.chat_service.developer_agent"), + patch("app.service.chat_service.document_agent"), + patch("app.service.chat_service.multi_modal_agent"), + patch( + "app.service.chat_service.mcp_agent", + return_value=mock_mcp_agent, + ), + patch( + "app.utils.toolkit.human_toolkit.get_task_lock", + return_value=mock_task_lock, + ), + ): mock_agent_model.return_value = MagicMock() - + workforce, mcp = await construct_workforce(options) - + assert workforce is mock_workforce assert mcp is mock_mcp_agent - + # Should add multiple agent workers assert mock_workforce.add_single_agent_worker.call_count >= 4 @@ -650,51 +699,56 @@ class TestChatServiceAgentOperations: install_data = ActionInstallMcpData( data={"mcpServers": {"notion": {"config": "test"}}} ) - - with patch("app.service.chat_service.get_mcp_tools", return_value=mock_tools): + + with patch( + "app.service.chat_service.get_mcp_tools", return_value=mock_tools + ): await install_mcp(mock_camel_agent, install_data) - + mock_camel_agent.add_tools.assert_called_once_with(mock_tools) @pytest.mark.integration class TestChatServiceIntegration: """Integration tests for chat service.""" - + @pytest.mark.asyncio - async def test_step_solve_context_building_workflow(self, sample_chat_data, mock_request, temp_dir): + async def test_step_solve_context_building_workflow( + self, sample_chat_data, mock_request, temp_dir + ): """Test step_solve builds context correctly using collect_previous_task_context.""" options = Chat(**sample_chat_data) - + # Create actual TaskLock with context data task_lock = TaskLock( - id="test_task_123", - queue=AsyncMock(), - human_input={} + id="test_task_123", queue=AsyncMock(), human_input={} ) task_lock.conversation_history = [ - {'role': 'user', 'content': 'Create a Python script'}, - {'role': 'assistant', 'content': 'Script created successfully'} + {"role": "user", "content": "Create a Python script"}, + {"role": "assistant", "content": "Script created successfully"}, ] task_lock.last_task_result = "def hello(): print('Hello World')" task_lock.last_task_summary = "Python Hello World Script" - + # Create some files in working directory working_dir = temp_dir / "test_project" working_dir.mkdir() - (working_dir / "script.py").write_text("def hello(): print('Hello World')") - + (working_dir / "script.py").write_text( + "def hello(): print('Hello World')" + ) + # Mock file_save_path method to return our temp directory - with patch.object(Chat, 'file_save_path', return_value=str(working_dir)): - + with patch.object( + Chat, "file_save_path", return_value=str(working_dir) + ): # Test the context building directly context = build_context_for_workforce(task_lock, options) - + # Verify context includes conversation history assert "=== CONVERSATION HISTORY ===" in context assert "user: Create a Python script" in context assert "assistant: Script created successfully" in context - + # Verify context includes task context with files assert "=== CONTEXT FROM PREVIOUS TASK ===" in context assert "def hello(): print('Hello World')" in context @@ -702,27 +756,30 @@ class TestChatServiceIntegration: assert "script.py" in context @pytest.mark.asyncio - async def test_step_solve_new_task_state_context_collection(self, sample_chat_data, mock_request, temp_dir): + async def test_step_solve_new_task_state_context_collection( + self, sample_chat_data, mock_request, temp_dir + ): """Test step_solve correctly collects context in new_task_state action.""" - options = Chat(**sample_chat_data) + Chat(**sample_chat_data) working_dir = temp_dir / "project" working_dir.mkdir() - + # Create files that should be included in context (working_dir / "main.py").write_text("print('main')") (working_dir / "config.json").write_text('{"version": "1.0"}') - + # Mock file_save_path to return our temp directory - with patch.object(Chat, 'file_save_path', return_value=str(working_dir)): - + with patch.object( + Chat, "file_save_path", return_value=str(working_dir) + ): # Test collect_previous_task_context directly with the scenario result = collect_previous_task_context( working_directory=str(working_dir), previous_task_content="Create project structure", previous_task_result="Project files created successfully", - previous_summary="Project Setup Task" + previous_summary="Project Setup Task", ) - + # Verify all expected elements are present assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "Previous Task:" in result @@ -738,38 +795,39 @@ class TestChatServiceIntegration: assert "=== NEW TASK ===" in result @pytest.mark.asyncio - async def test_step_solve_end_action_context_collection(self, sample_chat_data, mock_request, temp_dir): + async def test_step_solve_end_action_context_collection( + self, sample_chat_data, mock_request, temp_dir + ): """Test step_solve correctly collects and saves context in end action.""" - options = Chat(**sample_chat_data) + Chat(**sample_chat_data) working_dir = temp_dir / "finished_project" working_dir.mkdir() - + # Create output files (working_dir / "output.txt").write_text("Final output") (working_dir / "report.md").write_text("# Task Report") - + # Create actual TaskLock task_lock = TaskLock( - id="test_end_task", - queue=AsyncMock(), - human_input={} + id="test_end_task", queue=AsyncMock(), human_input={} ) task_lock.last_task_summary = "Final Task Summary" - + # Mock file_save_path - with patch.object(Chat, 'file_save_path', return_value=str(working_dir)): - + with patch.object( + Chat, "file_save_path", return_value=str(working_dir) + ): # Test the context collection for end action scenario task_content = "Generate final report" task_result = "Report generated successfully with output files" - + context = collect_previous_task_context( working_directory=str(working_dir), previous_task_content=task_content, previous_task_result=task_result, - previous_summary=task_lock.last_task_summary + previous_summary=task_lock.last_task_summary, ) - + # Verify context structure for end action assert "=== CONTEXT FROM PREVIOUS TASK ===" in context assert "Generate final report" in context @@ -777,83 +835,115 @@ class TestChatServiceIntegration: assert "Final Task Summary" in context assert "output.txt" in context assert "report.md" in context - + # Test that context can be added to conversation history - task_lock.add_conversation('task_result', context) + task_lock.add_conversation("task_result", context) assert len(task_lock.conversation_history) == 1 - assert task_lock.conversation_history[0]['role'] == 'task_result' - assert task_lock.conversation_history[0]['content'] == context + assert task_lock.conversation_history[0]["role"] == "task_result" + assert task_lock.conversation_history[0]["content"] == context @pytest.mark.asyncio @pytest.mark.skip(reason="Gets Stuck for some reason.") - async def test_step_solve_basic_workflow(self, sample_chat_data, mock_request, mock_task_lock): + async def test_step_solve_basic_workflow( + self, sample_chat_data, mock_request, mock_task_lock + ): """Test step_solve basic workflow integration.""" options = Chat(**sample_chat_data) - + # Mock the action queue to return improve action first, then end - mock_task_lock.get_queue = AsyncMock(side_effect=[ - # First call returns improve action - ActionImproveData(action=Action.improve, data="Test question"), - # Second call returns end action - ActionEndData(action=Action.end) - ]) - + mock_task_lock.get_queue = AsyncMock( + side_effect=[ + # First call returns improve action + ActionImproveData(action=Action.improve, data="Test question"), + # Second call returns end action + ActionEndData(action=Action.end), + ] + ) + mock_workforce = MagicMock() mock_mcp = MagicMock() - - with patch("app.service.chat_service.construct_workforce", return_value=(mock_workforce, mock_mcp)), \ - patch("app.service.chat_service.question_confirm_agent") as mock_question_agent, \ - patch("app.service.chat_service.task_summary_agent") as mock_summary_agent, \ - patch("app.service.chat_service.question_confirm", return_value=True), \ - patch("app.service.chat_service.summary_task", return_value="Test Summary"): - + + with ( + patch( + "app.service.chat_service.construct_workforce", + return_value=(mock_workforce, mock_mcp), + ), + patch( + "app.service.chat_service.question_confirm_agent" + ) as mock_question_agent, + patch( + "app.service.chat_service.task_summary_agent" + ) as mock_summary_agent, + patch( + "app.service.chat_service.question_confirm", return_value=True + ), + patch( + "app.service.chat_service.summary_task", + return_value="Test Summary", + ), + ): mock_question_agent.return_value = MagicMock() mock_summary_agent.return_value = MagicMock() mock_workforce.eigent_make_sub_tasks.return_value = [] - + # Convert async generator to list responses = [] - async for response in step_solve(options, mock_request, mock_task_lock): + async for response in step_solve( + options, mock_request, mock_task_lock + ): responses.append(response) # Break after a few responses to avoid infinite loop if len(responses) > 10: break - + # Should have received some responses assert len(responses) > 0 - @pytest.mark.asyncio - async def test_step_solve_with_disconnected_request(self, sample_chat_data, mock_request, mock_task_lock): + @pytest.mark.asyncio + async def test_step_solve_with_disconnected_request( + self, sample_chat_data, mock_request, mock_task_lock + ): """Test step_solve handles disconnected request.""" options = Chat(**sample_chat_data) mock_request.is_disconnected = AsyncMock(return_value=True) - + mock_workforce = MagicMock() - - with patch("app.service.chat_service.construct_workforce", return_value=(mock_workforce, MagicMock())): + + with patch( + "app.service.chat_service.construct_workforce", + return_value=(mock_workforce, MagicMock()), + ): # Should exit immediately if request is disconnected responses = [] - async for response in step_solve(options, mock_request, mock_task_lock): + async for response in step_solve( + options, mock_request, mock_task_lock + ): responses.append(response) - + # Should not have any responses due to immediate disconnection assert len(responses) == 0 # Note: Workforce might not be created/stopped if request is immediately disconnected @pytest.mark.asyncio @pytest.mark.skip(reason="Gets Stuck for some reason.") - async def test_step_solve_error_handling(self, sample_chat_data, mock_request, mock_task_lock): + async def test_step_solve_error_handling( + self, sample_chat_data, mock_request, mock_task_lock + ): """Test step_solve handles errors gracefully.""" options = Chat(**sample_chat_data) - + # Mock get_queue to raise an exception - mock_task_lock.get_queue = AsyncMock(side_effect=Exception("Queue error")) - + mock_task_lock.get_queue = AsyncMock( + side_effect=Exception("Queue error") + ) + responses = [] - async for response in step_solve(options, mock_request, mock_task_lock): + async for response in step_solve( + options, mock_request, mock_task_lock + ): responses.append(response) break # Exit after first iteration - + # Should handle the error and exit gracefully assert len(responses) == 0 @@ -861,21 +951,25 @@ class TestChatServiceIntegration: @pytest.mark.model_backend class TestChatServiceWithLLM: """Tests that require LLM backend (marked for selective running).""" - + @pytest.mark.asyncio - async def test_construct_workforce_with_real_agents(self, sample_chat_data): + async def test_construct_workforce_with_real_agents( + self, sample_chat_data + ): """Test construct_workforce with real agent creation.""" - options = Chat(**sample_chat_data) - + Chat(**sample_chat_data) + # This test would create real agents and workforce # Marked as model_backend test for selective execution assert True # Placeholder @pytest.mark.very_slow - async def test_full_chat_workflow_integration(self, sample_chat_data, mock_request): + async def test_full_chat_workflow_integration( + self, sample_chat_data, mock_request + ): """Test complete chat workflow with real components (very slow test).""" - options = Chat(**sample_chat_data) - + Chat(**sample_chat_data) + # This test would run the complete chat workflow # Marked as very_slow for execution only in full test mode assert True # Placeholder @@ -888,44 +982,44 @@ class TestChatServiceErrorCases: def test_collect_previous_task_context_os_walk_exception(self, temp_dir): """Test collect_previous_task_context handles os.walk exceptions.""" working_directory = str(temp_dir) - - with patch('os.walk', side_effect=OSError("Permission denied")): - with patch('app.service.chat_service.logger') as mock_logger: + + with patch("os.walk", side_effect=OSError("Permission denied")): + with patch("app.service.chat_service.logger") as mock_logger: result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test task", previous_task_result="Test result", - previous_summary="Test summary" + previous_summary="Test summary", ) - + # Should still include basic context assert "=== CONTEXT FROM PREVIOUS TASK ===" in result assert "Test task" in result assert "Test result" in result assert "Test summary" in result - + # Should not include file listing assert "Generated Files from Previous Task:" not in result - + # Should log warning mock_logger.warning.assert_called_once() def test_collect_previous_task_context_relpath_exception(self, temp_dir): """Test collect_previous_task_context handles os.path.relpath exceptions.""" working_directory = str(temp_dir) - + # Create a test file (temp_dir / "test.txt").write_text("test content") - - with patch('os.path.relpath', side_effect=ValueError("Invalid path")): - with patch('app.service.chat_service.logger') as mock_logger: + + with patch("os.path.relpath", side_effect=ValueError("Invalid path")): + with patch("app.service.chat_service.logger") as mock_logger: result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test task", previous_task_result="Test result", - previous_summary="Test summary" + previous_summary="Test summary", ) - + # Should handle the exception gracefully assert "=== CONTEXT FROM PREVIOUS TASK ===" in result # Should log warning about file collection failure @@ -938,12 +1032,12 @@ class TestChatServiceErrorCases: task_lock.conversation_history = None # Missing attribute task_lock.last_task_result = None # Missing attribute task_lock.last_task_summary = None # Missing attribute - + options = MagicMock() options.file_save_path.return_value = str(temp_dir) - + result = build_context_for_workforce(task_lock, options) - + # Should handle missing attributes gracefully assert result == "" @@ -953,11 +1047,11 @@ class TestChatServiceErrorCases: task_lock.conversation_history = [] task_lock.last_task_result = "Test result" task_lock.last_task_summary = "Test summary" - + options = MagicMock() options.file_save_path.side_effect = Exception("Path error") - - with patch('app.service.chat_service.logger') as mock_logger: + + with patch("app.service.chat_service.logger") as mock_logger: # Should handle exception when getting file path with pytest.raises(Exception, match="Path error"): build_context_for_workforce(task_lock, options) @@ -965,21 +1059,25 @@ class TestChatServiceErrorCases: def test_collect_previous_task_context_unicode_handling(self, temp_dir): """Test collect_previous_task_context handles unicode content correctly.""" working_directory = str(temp_dir) - + # Create files with unicode content - (temp_dir / "unicode_file.txt").write_text("Unicode content: 🐍 Python ñáéíóú", encoding='utf-8') - - unicode_task_content = "Create files with unicode: 🔥 emojis and ñáéíóú accents" + (temp_dir / "unicode_file.txt").write_text( + "Unicode content: 🐍 Python ñáéíóú", encoding="utf-8" + ) + + unicode_task_content = ( + "Create files with unicode: 🔥 emojis and ñáéíóú accents" + ) unicode_result = "Files created successfully with unicode: ✅ done" unicode_summary = "Unicode Task: 📝 file creation" - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content=unicode_task_content, previous_task_result=unicode_result, - previous_summary=unicode_summary + previous_summary=unicode_summary, ) - + # Should handle unicode correctly assert "🔥 emojis" in result assert "ñáéíóú accents" in result @@ -990,19 +1088,19 @@ class TestChatServiceErrorCases: def test_collect_previous_task_context_very_long_content(self, temp_dir): """Test collect_previous_task_context handles very long content.""" working_directory = str(temp_dir) - + # Create very long content strings long_content = "Very long task content. " * 1000 # ~25KB - long_result = "Very long task result. " * 1000 # ~23KB - long_summary = "Very long summary. " * 100 # ~1.8KB - + long_result = "Very long task result. " * 1000 # ~23KB + long_summary = "Very long summary. " * 100 # ~1.8KB + result = collect_previous_task_context( working_directory=working_directory, previous_task_content=long_content, previous_task_result=long_result, - previous_summary=long_summary + previous_summary=long_summary, ) - + # Should handle long content without issues assert len(result) > 49000 # Should be quite long assert "Very long task content." in result @@ -1012,44 +1110,49 @@ class TestChatServiceErrorCases: def test_collect_previous_task_context_many_files(self, temp_dir): """Test collect_previous_task_context performance with many files.""" working_directory = str(temp_dir) - + # Create many files to test performance for i in range(100): (temp_dir / f"file_{i:03d}.txt").write_text(f"Content {i}") - + # Create subdirectories with files for dir_i in range(10): sub_dir = temp_dir / f"subdir_{dir_i}" sub_dir.mkdir() for file_i in range(10): - (sub_dir / f"subfile_{file_i}.txt").write_text(f"Sub content {dir_i}-{file_i}") - + (sub_dir / f"subfile_{file_i}.txt").write_text( + f"Sub content {dir_i}-{file_i}" + ) + import time + start_time = time.time() - + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test many files", previous_task_result="Many files processed", - previous_summary="Performance test" + previous_summary="Performance test", ) - + end_time = time.time() execution_time = end_time - start_time - + # Should complete in reasonable time (less than 1 second for 200 files) assert execution_time < 1.0 - + # Should list all files assert "Generated Files from Previous Task:" in result # Count number of file entries - file_lines = [line for line in result.split('\n') if ' - ' in line] + file_lines = [line for line in result.split("\n") if " - " in line] assert len(file_lines) == 200 # 100 main files + 100 subfiles - def test_collect_previous_task_context_special_characters_in_filenames(self, temp_dir): + def test_collect_previous_task_context_special_characters_in_filenames( + self, temp_dir + ): """Test collect_previous_task_context handles special characters in filenames.""" working_directory = str(temp_dir) - + # Create files with special characters (that are valid in filenames) try: (temp_dir / "file with spaces.txt").write_text("content") @@ -1058,15 +1161,17 @@ class TestChatServiceErrorCases: (temp_dir / "file.with.dots.txt").write_text("content") except OSError: # Skip if filesystem doesn't support these characters - pytest.skip("Filesystem doesn't support special characters in filenames") - + pytest.skip( + "Filesystem doesn't support special characters in filenames" + ) + result = collect_previous_task_context( working_directory=working_directory, previous_task_content="Test special chars", previous_task_result="Files created", - previous_summary="" + previous_summary="", ) - + # Should list files with special characters assert "file with spaces.txt" in result assert "file-with-dashes.txt" in result @@ -1077,7 +1182,7 @@ class TestChatServiceErrorCases: async def test_question_confirm_agent_error(self, mock_camel_agent): """Test question_confirm when agent raises error.""" mock_camel_agent.step.side_effect = Exception("Agent error") - + with pytest.raises(Exception, match="Agent error"): await question_confirm(mock_camel_agent, "test question") @@ -1085,19 +1190,29 @@ class TestChatServiceErrorCases: async def test_summary_task_agent_error(self, mock_camel_agent): """Test summary_task when agent raises error.""" mock_camel_agent.step.side_effect = Exception("Summary error") - + task = Task(content="Test task", id="test") - + with pytest.raises(Exception, match="Summary error"): await summary_task(mock_camel_agent, task) @pytest.mark.asyncio - async def test_construct_workforce_agent_creation_error(self, sample_chat_data, mock_task_lock): + async def test_construct_workforce_agent_creation_error( + self, sample_chat_data, mock_task_lock + ): """Test construct_workforce when agent creation fails.""" options = Chat(**sample_chat_data) - - with patch("app.utils.toolkit.human_toolkit.get_task_lock", return_value=mock_task_lock), \ - patch("app.service.chat_service.agent_model", side_effect=Exception("Agent creation failed")): + + with ( + patch( + "app.utils.toolkit.human_toolkit.get_task_lock", + return_value=mock_task_lock, + ), + patch( + "app.service.chat_service.agent_model", + side_effect=Exception("Agent creation failed"), + ), + ): with pytest.raises(Exception, match="Agent creation failed"): await construct_workforce(options) @@ -1110,27 +1225,30 @@ class TestChatServiceErrorCases: description="Agent with invalid tools", tools=["nonexistent_tool"], mcp_tools=None, - env_path=".env" + env_path=".env", ) - - with patch("app.service.chat_service.get_toolkits", side_effect=Exception("Invalid tool")): + + with patch( + "app.service.chat_service.get_toolkits", + side_effect=Exception("Invalid tool"), + ): with pytest.raises(Exception, match="Invalid tool"): await new_agent_model(agent_data, options) def test_format_agent_description_with_none_values(self): """Test format_agent_description handles empty values gracefully.""" from app.service.task import ActionNewAgent - + # Test with ActionNewAgent that might have empty values agent_data = ActionNewAgent( name="TestAgent", description="", # Empty string instead of None tools=[], - mcp_tools=None # Should be None instead of empty list + mcp_tools=None, # Should be None instead of empty list ) - + result = format_agent_description(agent_data) - + assert "TestAgent:" in result assert "A specialized agent" in result # Default description @@ -1138,13 +1256,13 @@ class TestChatServiceErrorCases: """Test tree_sub_tasks handles tasks with empty content.""" task1 = Task(content="Valid Task", id="task_1") task1.state = TaskState.OPEN - + # Create task with empty content (edge case) task2 = Task(content="", id="task_2") # Empty string instead of None task2.state = TaskState.OPEN - + # Should handle empty content gracefully result = tree_sub_tasks([task1, task2]) - + # Should filter out empty content tasks assert len(result) <= 1 diff --git a/backend/tests/unit/service/test_task.py b/backend/tests/unit/service/test_task.py index d5e1cca2..21952338 100644 --- a/backend/tests/unit/service/test_task.py +++ b/backend/tests/unit/service/test_task.py @@ -92,7 +92,7 @@ class TestTaskServiceModels: "content": "Test content", "state": "RUNNING", "result": "In progress", - "failure_count": 0 + "failure_count": 0, } data = ActionTaskStateData(data=state_data) @@ -104,7 +104,7 @@ class TestTaskServiceModels: """Test ActionAskData model creation.""" ask_data = { "question": "What should I do next?", - "agent": "test_agent" + "agent": "test_agent", } data = ActionAskData(data=ask_data) @@ -117,7 +117,7 @@ class TestTaskServiceModels: agent_data = { "agent_name": "TestAgent", "agent_id": "agent_123", - "tools": ["search", "code"] + "tools": ["search", "code"], } data = ActionCreateAgentData(data=agent_data) @@ -149,11 +149,7 @@ class TestTaskServiceModels: name="New Agent", description="A new agent", tools=["search", "code"], - mcp_tools={"mcpServers": { - "test": { - "config": "value" - } - }} + mcp_tools={"mcpServers": {"test": {"config": "value"}}}, ) assert data.action == Action.new_agent @@ -165,9 +161,15 @@ class TestTaskServiceModels: def test_agents_enum_values(self): """Test Agents enum contains expected values.""" expected_agents = [ - "task_agent", "coordinator_agent", "new_worker_agent", - "developer_agent", "browser_agent", "document_agent", - "multi_modal_agent", "social_media_agent", "mcp_agent" + "task_agent", + "coordinator_agent", + "new_worker_agent", + "developer_agent", + "browser_agent", + "document_agent", + "multi_modal_agent", + "social_media_agent", + "mcp_agent", ] for agent in expected_agents: @@ -520,17 +522,22 @@ class TestPeriodicCleanup: task_lock.last_accessed = datetime.now() - timedelta(hours=3) # Mock delete_task_lock to raise exception - with patch( - 'app.service.task.delete_task_lock', - side_effect=Exception("Test error"), - ), patch('app.service.task.logger.error', ) as mock_logger: - + with ( + patch( + "app.service.task.delete_task_lock", + side_effect=Exception("Test error"), + ), + patch( + "app.service.task.logger.error", + ) as mock_logger, + ): # Directly call the cleanup logic # that should trigger the exception try: await delete_task_lock("test_task") except Exception as e: import logging + task_logger = logging.getLogger("task_service") task_logger.error(f"Error during task cleanup: {e}") diff --git a/backend/tests/unit/utils/telemetry/test_workforce_metrics.py b/backend/tests/unit/utils/telemetry/test_workforce_metrics.py index d30415b5..47963759 100644 --- a/backend/tests/unit/utils/telemetry/test_workforce_metrics.py +++ b/backend/tests/unit/utils/telemetry/test_workforce_metrics.py @@ -11,22 +11,26 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - """Tests for workforce metrics telemetry.""" + from datetime import datetime from unittest.mock import MagicMock, Mock, patch -import app.utils.telemetry.workforce_metrics as wm_module import pytest +from camel.societies.workforce.events import ( + LogEvent, + TaskAssignedEvent, + TaskCompletedEvent, + TaskCreatedEvent, + TaskDecomposedEvent, + TaskFailedEvent, + TaskStartedEvent, + TaskUpdatedEvent, + WorkerCreatedEvent, +) + +import app.utils.telemetry.workforce_metrics as wm_module from app.utils.telemetry.workforce_metrics import WorkforceMetricsCallback -from camel.societies.workforce.events import (LogEvent, TaskAssignedEvent, - TaskCompletedEvent, - TaskCreatedEvent, - TaskDecomposedEvent, - TaskFailedEvent, - TaskStartedEvent, - TaskUpdatedEvent, - WorkerCreatedEvent) @pytest.fixture(autouse=True) @@ -46,8 +50,8 @@ def mock_env_vars(): "LANGFUSE_BASE_URL": "https://test.langfuse.com", } with patch.dict( - "os.environ", - envs, + "os.environ", + envs, ): yield @@ -59,8 +63,9 @@ def metrics_callback(mock_env_vars): # Initialize the tracer provider first wm_module.initialize_tracer_provider() - callback = WorkforceMetricsCallback(project_id="test_project", - task_id="test_task") + callback = WorkforceMetricsCallback( + project_id="test_project", task_id="test_task" + ) # Mock the tracer and spans callback.tracer = Mock() callback.root_span = Mock() @@ -69,17 +74,20 @@ def metrics_callback(mock_env_vars): def test_log_worker_created(metrics_callback): """Test log_worker_created function.""" - event = WorkerCreatedEvent(worker_id="worker_1", - worker_type="test_worker", - role="test_role") + event = WorkerCreatedEvent( + worker_id="worker_1", worker_type="test_worker", role="test_role" + ) mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) - metrics_callback.log_worker_created(event, - agent_class="TestAgent", - model_type="gpt-4") + metrics_callback.log_worker_created( + event, agent_class="TestAgent", model_type="gpt-4" + ) # Verify span attributes were set assert mock_span.set_attribute.called @@ -96,8 +104,11 @@ def test_log_task_created(metrics_callback): ) mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) metrics_callback.log_task_created(event) @@ -114,8 +125,11 @@ def test_log_task_decomposed(metrics_callback): ) mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) metrics_callback.log_task_decomposed(event) @@ -134,8 +148,11 @@ def test_log_task_assigned(metrics_callback): ) mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) metrics_callback.log_task_assigned(event) @@ -157,8 +174,11 @@ def test_log_task_updated(metrics_callback): ) mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) metrics_callback.log_task_updated(event) @@ -194,10 +214,7 @@ def test_log_task_completed(metrics_callback): parent_task_id="parent_1", processing_time_seconds=2.5, timestamp=datetime.now(), - token_usage={ - "input_tokens": 100, - "output_tokens": 50 - }, + token_usage={"input_tokens": 100, "output_tokens": 50}, ) metrics_callback.log_task_completed(event) @@ -234,13 +251,16 @@ def test_log_task_failed(metrics_callback): def test_log_message_error(metrics_callback): """Test log_message function with error level.""" - event = LogEvent(level="error", - message="Test error message", - metadata={"key": "value"}) + event = LogEvent( + level="error", message="Test error message", metadata={"key": "value"} + ) mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) metrics_callback.log_message(event) @@ -269,8 +289,11 @@ def test_log_all_tasks_completed(metrics_callback): event.total_tasks = 5 mock_span = Mock() - metrics_callback.tracer.start_as_current_span = Mock(return_value=Mock( - __enter__=Mock(return_value=mock_span), __exit__=Mock())) + metrics_callback.tracer.start_as_current_span = Mock( + return_value=Mock( + __enter__=Mock(return_value=mock_span), __exit__=Mock() + ) + ) metrics_callback.log_all_tasks_completed(event) @@ -282,10 +305,14 @@ def test_log_all_tasks_completed(metrics_callback): def test_batch_span_processor_configuration_prevents_oom(mock_env_vars): """Test BatchSpanProcessor config with limits to prevent OOM.""" - with patch("app.utils.telemetry.workforce_metrics.OTLPSpanExporter" - ) as mock_exporter_class, patch( - "app.utils.telemetry.workforce_metrics.BatchSpanProcessor" - ) as mock_processor_class: + with ( + patch( + "app.utils.telemetry.workforce_metrics.OTLPSpanExporter" + ) as mock_exporter_class, + patch( + "app.utils.telemetry.workforce_metrics.BatchSpanProcessor" + ) as mock_processor_class, + ): # Initialize tracer provider wm_module.initialize_tracer_provider() @@ -305,17 +332,22 @@ def test_batch_span_processor_configuration_prevents_oom(mock_env_vars): def test_missing_langfuse_env_vars_disables_tracing(): """Test that missing Langfuse env vars disables tracing.""" - with patch.dict("os.environ", {}, clear=True), patch( + with ( + patch.dict("os.environ", {}, clear=True), + patch( "app.utils.telemetry.workforce_metrics.OTLPSpanExporter" - ) as mock_exporter_class, patch( + ) as mock_exporter_class, + patch( "app.utils.telemetry.workforce_metrics.BatchSpanProcessor" - ) as mock_processor_class: + ) as mock_processor_class, + ): # Initialize tracer provider without credentials wm_module.initialize_tracer_provider() # Create callback without Langfuse credentials - callback = WorkforceMetricsCallback(project_id="test_project", - task_id="test_task") + callback = WorkforceMetricsCallback( + project_id="test_project", task_id="test_task" + ) # Verify tracing is disabled assert callback.enabled is False @@ -325,26 +357,29 @@ def test_missing_langfuse_env_vars_disables_tracing(): mock_processor_class.assert_not_called() # Verify log methods do nothing when disabled - event = WorkerCreatedEvent(worker_id="worker_1", - worker_type="test_worker", - role="test_role") + event = WorkerCreatedEvent( + worker_id="worker_1", worker_type="test_worker", role="test_role" + ) callback.log_worker_created(event) # Should not raise errors def test_multiple_callbacks_share_tracer_provider(mock_env_vars): """Test that multiple callbacks share the same TracerProvider.""" - with patch("app.utils.telemetry.workforce_metrics.BatchSpanProcessor" - ) as mock_processor_class: + with patch( + "app.utils.telemetry.workforce_metrics.BatchSpanProcessor" + ) as mock_processor_class: # Initialize tracer provider once wm_module.initialize_tracer_provider() # Create first callback - callback1 = WorkforceMetricsCallback(project_id="project1", - task_id="task1") + callback1 = WorkforceMetricsCallback( + project_id="project1", task_id="task1" + ) # Create second callback - callback2 = WorkforceMetricsCallback(project_id="project2", - task_id="task2") + callback2 = WorkforceMetricsCallback( + project_id="project2", task_id="task2" + ) # Verify BatchSpanProcessor was only called once (singleton) assert mock_processor_class.call_count == 1 diff --git a/backend/tests/unit/utils/test_single_agent_worker.py b/backend/tests/unit/utils/test_single_agent_worker.py index 1fba763a..1c64ab6b 100644 --- a/backend/tests/unit/utils/test_single_agent_worker.py +++ b/backend/tests/unit/utils/test_single_agent_worker.py @@ -13,27 +13,27 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from unittest.mock import AsyncMock, MagicMock, patch -import pytest +import pytest from camel.agents.chat_agent import AsyncStreamingChatAgentResponse from camel.societies.workforce.utils import TaskResult from camel.tasks import Task from camel.tasks.task import TaskState -from app.utils.single_agent_worker import SingleAgentWorker from app.agent.listen_chat_agent import ListenChatAgent +from app.utils.single_agent_worker import SingleAgentWorker @pytest.mark.unit class TestSingleAgentWorker: """Test cases for SingleAgentWorker class.""" - + def test_single_agent_worker_initialization(self): """Test SingleAgentWorker initialization.""" mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "worker_123" - + worker = SingleAgentWorker( description="Test worker description", worker=mock_worker, @@ -41,9 +41,9 @@ class TestSingleAgentWorker: pool_initial_size=2, pool_max_size=5, auto_scale_pool=True, - use_structured_output_handler=True + use_structured_output_handler=True, ) - + assert worker.worker is mock_worker assert worker.use_agent_pool is True assert worker.use_structured_output_handler is True @@ -57,56 +57,64 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "worker_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=True + use_structured_output_handler=True, ) - + # Mock the structured handler mock_structured_handler = MagicMock() worker.structured_handler = mock_structured_handler - + # Create test task task = Task(content="Test task content", id="test_task_123") dependencies = [] - + # Mock worker agent retrieval and return mock_worker_agent = AsyncMock() mock_worker_agent.role_name = "pooled_worker" mock_worker_agent.agent_id = "pooled_worker_123" - + # Mock response mock_response = MagicMock() mock_response.msg.content = "Task completed successfully" mock_response.info = {"usage": {"total_tokens": 100}} - + mock_worker_agent.astep.return_value = mock_response - + # Mock structured output parsing mock_task_result = TaskResult( - content="Task completed successfully", - failed=False + content="Task completed successfully", failed=False ) - mock_structured_handler.parse_structured_response.return_value = mock_task_result - mock_structured_handler.generate_structured_prompt.return_value = "Enhanced prompt" - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + mock_structured_handler.parse_structured_response.return_value = ( + mock_task_result + ) + mock_structured_handler.generate_structured_prompt.return_value = ( + "Enhanced prompt" + ) + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, dependencies) - + assert result == TaskState.DONE assert task.result == "Task completed successfully" assert "worker_attempts" in task.additional_info assert len(task.additional_info["worker_attempts"]) == 1 - + attempt = task.additional_info["worker_attempts"][0] assert attempt["agent_id"] == "pooled_worker_123" assert attempt["total_tokens"] == 100 - + mock_return_agent.assert_called_once_with(mock_worker_agent) @pytest.mark.asyncio @@ -115,48 +123,52 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "worker_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=False # Use native structured output + use_structured_output_handler=False, # Use native structured output ) - + # Create test task task = Task(content="Test task content", id="test_task_123") dependencies = [] - + # Mock worker agent mock_worker_agent = AsyncMock() mock_worker_agent.role_name = "pooled_worker" mock_worker_agent.agent_id = "pooled_worker_123" - + # Mock response with parsed result mock_response = MagicMock() mock_response.msg.content = "Task completed successfully" mock_response.msg.parsed = TaskResult( - content="Task completed successfully", - failed=False + content="Task completed successfully", failed=False ) mock_response.info = {"usage": {"total_tokens": 75}} - + mock_worker_agent.astep.return_value = mock_response - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, dependencies) - + assert result == TaskState.DONE assert task.result == "Task completed successfully" - + # Verify native structured output was used mock_worker_agent.astep.assert_called_once() call_args = mock_worker_agent.astep.call_args assert "response_format" in call_args.kwargs assert call_args.kwargs["response_format"] == TaskResult - + mock_return_agent.assert_called_once_with(mock_worker_agent) @pytest.mark.skip(reason="Complex streaming response mock - needs fixing") @@ -166,52 +178,65 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=True + use_structured_output_handler=True, ) - + # Mock structured handler mock_structured_handler = MagicMock() worker.structured_handler = mock_structured_handler - + task = Task(content="Test task content", id="test_task_123") dependencies = [] - + # Mock worker agent mock_worker_agent = AsyncMock() mock_worker_agent.role_name = "streaming_worker" mock_worker_agent.agent_id = "streaming_worker_123" - + # Create mock streaming response - mock_streaming_response = MagicMock(spec=AsyncStreamingChatAgentResponse) - + mock_streaming_response = MagicMock( + spec=AsyncStreamingChatAgentResponse + ) + # Mock the async iteration - create async generator async def async_chunks(): chunk1 = MagicMock() chunk1.msg.content = "Partial response" yield chunk1 chunk2 = MagicMock() - chunk2.msg.content = "Complete response" + chunk2.msg.content = "Complete response" yield chunk2 - + mock_streaming_response.__aiter__ = lambda self: async_chunks() - + mock_worker_agent.astep.return_value = mock_streaming_response - + # Mock structured parsing - mock_task_result = TaskResult(content="Complete response", failed=False) - mock_structured_handler.parse_structured_response.return_value = mock_task_result - mock_structured_handler.generate_structured_prompt.return_value = "Enhanced prompt" - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + mock_task_result = TaskResult( + content="Complete response", failed=False + ) + mock_structured_handler.parse_structured_response.return_value = ( + mock_task_result + ) + mock_structured_handler.generate_structured_prompt.return_value = ( + "Enhanced prompt" + ) + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, dependencies) - + assert result == TaskState.DONE assert task.result == "Complete response" mock_return_agent.assert_called_once_with(mock_worker_agent) @@ -222,25 +247,29 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( - description="Test worker", - worker=mock_worker + description="Test worker", worker=mock_worker ) - + task = Task(content="Test task content", id="test_task_123") dependencies = [] - + # Mock worker agent that raises exception mock_worker_agent = AsyncMock() mock_worker_agent.astep.side_effect = Exception("Processing error") - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, dependencies) - + assert result == TaskState.FAILED assert "Exception: Processing error" in task.result mock_return_agent.assert_called_once_with(mock_worker_agent) @@ -251,41 +280,49 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=True + use_structured_output_handler=True, ) - + # Mock structured handler mock_structured_handler = MagicMock() worker.structured_handler = mock_structured_handler - + task = Task(content="Test task content", id="test_task_123") dependencies = [] - + # Mock worker agent mock_worker_agent = AsyncMock() mock_response = MagicMock() mock_response.msg.content = "Task failed" mock_response.info = {"usage": {"total_tokens": 25}} mock_worker_agent.astep.return_value = mock_response - + # Mock failed task result mock_task_result = TaskResult( - content="Task failed due to error", - failed=True + content="Task failed due to error", failed=True ) - mock_structured_handler.parse_structured_response.return_value = mock_task_result - mock_structured_handler.generate_structured_prompt.return_value = "Enhanced prompt" - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + mock_structured_handler.parse_structured_response.return_value = ( + mock_task_result + ) + mock_structured_handler.generate_structured_prompt.return_value = ( + "Enhanced prompt" + ) + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, dependencies) - + assert result == TaskState.FAILED assert task.result == "Task failed due to error" mock_return_agent.assert_called_once_with(mock_worker_agent) @@ -296,39 +333,45 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=False + use_structured_output_handler=False, ) - + # Create main task and dependencies main_task = Task(content="Main task", id="main_123") dep_task1 = Task(content="Dependency 1", id="dep_1") dep_task2 = Task(content="Dependency 2", id="dep_2") dependencies = [dep_task1, dep_task2] - + # Mock worker agent mock_worker_agent = AsyncMock() mock_response = MagicMock() mock_response.msg.content = "Task completed with dependencies" mock_response.msg.parsed = TaskResult( - content="Task completed with dependencies", - failed=False + content="Task completed with dependencies", failed=False ) mock_response.info = {"usage": {"total_tokens": 120}} mock_worker_agent.astep.return_value = mock_response - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="Dependencies: dep_1, dep_2") as mock_get_deps: - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, + "_get_dep_tasks_info", + return_value="Dependencies: dep_1, dep_2", + ) as mock_get_deps, + ): result = await worker._process_task(main_task, dependencies) - + assert result == TaskState.DONE assert main_task.result == "Task completed with dependencies" - + # Verify dependencies were processed mock_get_deps.assert_called_once_with(dependencies) mock_return_agent.assert_called_once_with(mock_worker_agent) @@ -339,43 +382,47 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=False + use_structured_output_handler=False, ) - + # Create parent and child task parent_task = Task(content="Parent task", id="parent_123") child_task = Task(content="Child task", id="child_123") child_task.parent = parent_task - + # Mock worker agent mock_worker_agent = AsyncMock() mock_response = MagicMock() mock_response.msg.content = "Child task completed" mock_response.msg.parsed = TaskResult( - content="Child task completed", - failed=False + content="Child task completed", failed=False ) mock_response.info = {"usage": {"total_tokens": 80}} mock_worker_agent.astep.return_value = mock_response - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(child_task, []) - + assert result == TaskState.DONE assert child_task.result == "Child task completed" - + # Verify the prompt included parent task context call_args = mock_worker_agent.astep.call_args prompt = call_args[0][0] # First positional argument assert "Parent task" in prompt - + mock_return_agent.assert_called_once_with(mock_worker_agent) @pytest.mark.asyncio @@ -384,98 +431,108 @@ class TestSingleAgentWorker: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "test_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( description="Test worker", worker=mock_worker, - use_structured_output_handler=False + use_structured_output_handler=False, ) - + task = Task(content="Test task content", id="test_task_123") - + # Mock worker agent mock_worker_agent = AsyncMock() mock_response = MagicMock() mock_response.msg.content = "Task completed" mock_response.msg.parsed = TaskResult( - content="Task completed", - failed=False + content="Task completed", failed=False ) mock_response.info = {"usage": {"total_tokens": 50}} mock_worker_agent.astep.return_value = mock_response - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"), \ - patch('app.utils.single_agent_worker.is_task_result_insufficient', return_value=True): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + patch( + "app.utils.single_agent_worker.is_task_result_insufficient", + return_value=True, + ), + ): result = await worker._process_task(task, []) - + assert result == TaskState.FAILED mock_return_agent.assert_called_once_with(mock_worker_agent) def test_worker_inherits_from_base_class(self): """Test that SingleAgentWorker inherits from BaseSingleAgentWorker.""" - from camel.societies.workforce.single_agent_worker import SingleAgentWorker as BaseSingleAgentWorker - + from camel.societies.workforce.single_agent_worker import ( + SingleAgentWorker as BaseSingleAgentWorker, + ) + mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.agent_id = "test_agent_123" worker = SingleAgentWorker(description="Test", worker=mock_worker) - + assert isinstance(worker, BaseSingleAgentWorker) @pytest.mark.integration class TestSingleAgentWorkerIntegration: """Integration tests for SingleAgentWorker.""" - + @pytest.mark.asyncio async def test_worker_with_multiple_tasks(self): """Test worker processing multiple tasks in sequence.""" mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.role_name = "integration_worker" mock_worker.agent_id = "test_agent_123" - + worker = SingleAgentWorker( description="Integration test worker", worker=mock_worker, - use_structured_output_handler=False + use_structured_output_handler=False, ) - + # Create multiple tasks - tasks = [ - Task(content=f"Task {i}", id=f"task_{i}") - for i in range(3) - ] - + tasks = [Task(content=f"Task {i}", id=f"task_{i}") for i in range(3)] + # Mock worker agent for all tasks mock_worker_agent = AsyncMock() - + def mock_astep(prompt, **kwargs): mock_response = MagicMock() mock_response.msg.content = f"Completed: {prompt[:20]}..." mock_response.msg.parsed = TaskResult( - content=f"Completed: {prompt[:20]}...", - failed=False + content=f"Completed: {prompt[:20]}...", failed=False ) mock_response.info = {"usage": {"total_tokens": 60}} return mock_response - + mock_worker_agent.astep.side_effect = mock_astep - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent'), \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent"), + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): # Process all tasks results = [] for task in tasks: result = await worker._process_task(task, []) results.append(result) - + # All tasks should succeed assert all(result == TaskState.DONE for result in results) - + # Each task should have results for task in tasks: assert task.result is not None @@ -486,7 +543,7 @@ class TestSingleAgentWorkerIntegration: @pytest.mark.model_backend class TestSingleAgentWorkerWithLLM: """Tests that require LLM backend (marked for selective running).""" - + @pytest.mark.asyncio async def test_worker_with_real_agent(self): """Test SingleAgentWorker with real ListenChatAgent.""" @@ -505,26 +562,35 @@ class TestSingleAgentWorkerWithLLM: @pytest.mark.unit class TestSingleAgentWorkerErrorCases: """Test error cases and edge conditions for SingleAgentWorker.""" - + @pytest.mark.asyncio async def test_process_task_with_none_response(self): """Test _process_task when agent returns None response.""" mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.agent_id = "test_agent_123" - worker = SingleAgentWorker(description="Test", worker=mock_worker, use_structured_output_handler=False) - + worker = SingleAgentWorker( + description="Test", + worker=mock_worker, + use_structured_output_handler=False, + ) + task = Task(content="Test task", id="test_123") - + # Mock worker agent returning None mock_worker_agent = AsyncMock() mock_worker_agent.astep.return_value = None - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, []) - + # Should handle None response gracefully assert result == TaskState.FAILED mock_return_agent.assert_called_once_with(mock_worker_agent) @@ -534,24 +600,33 @@ class TestSingleAgentWorkerErrorCases: """Test _process_task with malformed response structure.""" mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.agent_id = "test_agent_123" - worker = SingleAgentWorker(description="Test", worker=mock_worker, use_structured_output_handler=False) - + worker = SingleAgentWorker( + description="Test", + worker=mock_worker, + use_structured_output_handler=False, + ) + task = Task(content="Test task", id="test_123") - + # Mock worker agent with malformed response mock_worker_agent = AsyncMock() mock_response = MagicMock() mock_response.msg = None # Missing msg attribute mock_response.info = {} mock_worker_agent.astep.return_value = mock_response - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): # Should handle malformed response and likely raise exception result = await worker._process_task(task, []) - + # Depending on implementation, this might fail or handle gracefully assert result in [TaskState.FAILED, TaskState.DONE] mock_return_agent.assert_called_once_with(mock_worker_agent) @@ -562,24 +637,35 @@ class TestSingleAgentWorkerErrorCases: mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.agent_id = "test_agent_123" mock_worker.role_name = "test_worker" - worker = SingleAgentWorker(description="Test", worker=mock_worker, use_structured_output_handler=False) - + worker = SingleAgentWorker( + description="Test", + worker=mock_worker, + use_structured_output_handler=False, + ) + task = Task(content="Test task", id="test_123") - + # Mock worker agent with missing usage info mock_worker_agent = AsyncMock() mock_response = MagicMock() mock_response.msg.content = "Task completed" - mock_response.msg.parsed = TaskResult(content="Task completed", failed=False) + mock_response.msg.parsed = TaskResult( + content="Task completed", failed=False + ) mock_response.info = {} # Missing usage information mock_worker_agent.astep.return_value = mock_response - - with patch.object(worker, '_get_worker_agent', return_value=mock_worker_agent), \ - patch.object(worker, '_return_worker_agent') as mock_return_agent, \ - patch.object(worker, '_get_dep_tasks_info', return_value="No dependencies"): - + + with ( + patch.object( + worker, "_get_worker_agent", return_value=mock_worker_agent + ), + patch.object(worker, "_return_worker_agent") as mock_return_agent, + patch.object( + worker, "_get_dep_tasks_info", return_value="No dependencies" + ), + ): result = await worker._process_task(task, []) - + assert result == TaskState.DONE assert task.additional_info["token_usage"]["total_tokens"] == 0 mock_return_agent.assert_called_once_with(mock_worker_agent) diff --git a/backend/tests/unit/utils/test_terminal_toolkit.py b/backend/tests/unit/utils/test_terminal_toolkit.py index c12740bc..1c04a27d 100644 --- a/backend/tests/unit/utils/test_terminal_toolkit.py +++ b/backend/tests/unit/utils/test_terminal_toolkit.py @@ -15,8 +15,10 @@ import asyncio import threading import time + import pytest -from app.service.task import task_locks, TaskLock + +from app.service.task import TaskLock, task_locks from app.utils.toolkit.terminal_toolkit import TerminalToolkit @@ -29,9 +31,11 @@ class TestTerminalToolkit: test_api_task_id = "test_api_task_123" if test_api_task_id not in task_locks: - task_locks[test_api_task_id] = TaskLock(id=test_api_task_id, queue=asyncio.Queue(), human_input={}) + task_locks[test_api_task_id] = TaskLock( + id=test_api_task_id, queue=asyncio.Queue(), human_input={} + ) toolkit = TerminalToolkit("test_api_task_123") - + # This should NOT raise RuntimeError: no running event loop # This simulates the exact scenario from the error traceback try: @@ -40,7 +44,9 @@ class TestTerminalToolkit: except RuntimeError as e: if "no running event loop" in str(e): - pytest.fail("RuntimeError: no running event loop should not be raised - the fix is not working!") + pytest.fail( + "RuntimeError: no running event loop should not be raised - the fix is not working!" + ) else: raise # Re-raise if it's a different RuntimeError @@ -49,9 +55,11 @@ class TestTerminalToolkit: test_api_task_id = "test_api_task_123" if test_api_task_id not in task_locks: - task_locks[test_api_task_id] = TaskLock(id=test_api_task_id, queue=asyncio.Queue(), human_input={}) + task_locks[test_api_task_id] = TaskLock( + id=test_api_task_id, queue=asyncio.Queue(), human_input={} + ) toolkit = TerminalToolkit("test_api_task_123") - + # Make multiple calls - none should raise RuntimeError try: for i in range(5): @@ -59,7 +67,9 @@ class TestTerminalToolkit: time.sleep(0.2) # Give threads time to complete except RuntimeError as e: if "no running event loop" in str(e): - pytest.fail("RuntimeError: no running event loop should not be raised!") + pytest.fail( + "RuntimeError: no running event loop should not be raised!" + ) else: raise @@ -68,7 +78,9 @@ class TestTerminalToolkit: test_api_task_id = "test_api_task_123" if test_api_task_id not in task_locks: - task_locks[test_api_task_id] = TaskLock(id=test_api_task_id, queue=asyncio.Queue(), human_input={}) + task_locks[test_api_task_id] = TaskLock( + id=test_api_task_id, queue=asyncio.Queue(), human_input={} + ) toolkit = TerminalToolkit("test_api_task_123") # Create multiple threads that call _write_to_log @@ -76,17 +88,17 @@ class TestTerminalToolkit: for i in range(5): thread = threading.Thread( target=toolkit._write_to_log, - args=(f"/tmp/test_{i}.log", f"Thread {i} output") + args=(f"/tmp/test_{i}.log", f"Thread {i} output"), ) threads.append(thread) thread.start() - + # Wait for all threads to complete for thread in threads: thread.join() - + time.sleep(0.2) # Give async operations time to complete - + # Should not have raised any RuntimeError def test_async_context_still_works(self): @@ -94,21 +106,22 @@ class TestTerminalToolkit: test_api_task_id = "test_api_task_123" if test_api_task_id not in task_locks: - task_locks[test_api_task_id] = TaskLock(id=test_api_task_id, queue=asyncio.Queue(), human_input={}) + task_locks[test_api_task_id] = TaskLock( + id=test_api_task_id, queue=asyncio.Queue(), human_input={} + ) toolkit = TerminalToolkit("test_api_task_123") async def test_async_context(): toolkit._write_to_log("/tmp/async_test.log", "Async context test") await asyncio.sleep(0.1) - + # Should work in async context without RuntimeError try: asyncio.run(test_async_context()) except RuntimeError as e: if "no running event loop" in str(e): - pytest.fail("RuntimeError: no running event loop should not be raised in async context!") + pytest.fail( + "RuntimeError: no running event loop should not be raised in async context!" + ) else: raise - - - diff --git a/backend/tests/unit/utils/test_workforce.py b/backend/tests/unit/utils/test_workforce.py index 81097328..69112527 100644 --- a/backend/tests/unit/utils/test_workforce.py +++ b/backend/tests/unit/utils/test_workforce.py @@ -13,34 +13,30 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from unittest.mock import AsyncMock, MagicMock, patch -import pytest +import pytest +from camel.societies.workforce.utils import TaskAssignment, TaskAssignResult from camel.societies.workforce.workforce import WorkforceState -from camel.societies.workforce.utils import TaskAssignResult, TaskAssignment from camel.tasks import Task from camel.tasks.task import TaskState -from camel.agents import ChatAgent -from app.utils.workforce import Workforce from app.agent.listen_chat_agent import ListenChatAgent -from app.service.task import ActionAssignTaskData, ActionTaskStateData, ActionEndData from app.exception.exception import UserException +from app.service.task import ActionAssignTaskData, ActionTaskStateData +from app.utils.workforce import Workforce @pytest.mark.unit class TestWorkforce: """Test cases for Workforce class.""" - + def test_workforce_initialization(self): """Test Workforce initialization with default settings.""" api_task_id = "test_api_task_123" description = "Test workforce" - - workforce = Workforce( - api_task_id=api_task_id, - description=description - ) - + + workforce = Workforce(api_task_id=api_task_id, description=description) + assert workforce.api_task_id == api_task_id assert workforce.description == description @@ -48,25 +44,29 @@ class TestWorkforce: """Test eigent_make_sub_tasks successfully decomposes task.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create test task task = Task(content="Create a web application", id="main_task") - + # Mock subtasks subtask1 = Task(content="Setup project structure", id="subtask_1") subtask2 = Task(content="Implement authentication", id="subtask_2") mock_subtasks = [subtask1, subtask2] - - with patch.object(workforce, 'reset'), \ - patch.object(workforce, 'set_channel'), \ - patch.object(workforce, '_decompose_task', return_value=mock_subtasks), \ - patch('app.utils.workforce.validate_task_content', return_value=True): - + + with ( + patch.object(workforce, "reset"), + patch.object(workforce, "set_channel"), + patch.object( + workforce, "_decompose_task", return_value=mock_subtasks + ), + patch( + "app.utils.workforce.validate_task_content", return_value=True + ), + ): result = workforce.eigent_make_sub_tasks(task) - + assert result == mock_subtasks assert workforce._task is task assert workforce._state == WorkforceState.RUNNING @@ -77,25 +77,31 @@ class TestWorkforce: """Test eigent_make_sub_tasks with streaming decomposition result.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + task = Task(content="Complex project task", id="main_task") - + # Mock streaming generator def mock_streaming_decomposition(): yield [Task(content="Phase 1", id="phase_1")] yield [Task(content="Phase 2", id="phase_2")] yield [Task(content="Phase 3", id="phase_3")] - - with patch.object(workforce, 'reset'), \ - patch.object(workforce, 'set_channel'), \ - patch.object(workforce, '_decompose_task', return_value=mock_streaming_decomposition()), \ - patch('app.utils.workforce.validate_task_content', return_value=True): - + + with ( + patch.object(workforce, "reset"), + patch.object(workforce, "set_channel"), + patch.object( + workforce, + "_decompose_task", + return_value=mock_streaming_decomposition(), + ), + patch( + "app.utils.workforce.validate_task_content", return_value=True + ), + ): result = workforce.eigent_make_sub_tasks(task) - + # Should have flattened all streaming results assert len(result) == 3 assert all(isinstance(subtask, Task) for subtask in result) @@ -107,17 +113,18 @@ class TestWorkforce: """Test eigent_make_sub_tasks with invalid task content.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create task with invalid content task = Task(content="", id="invalid_task") # Empty content - - with patch('app.utils.workforce.validate_task_content', return_value=False): + + with patch( + "app.utils.workforce.validate_task_content", return_value=False + ): with pytest.raises(UserException): workforce.eigent_make_sub_tasks(task) - + # Task should be marked as failed assert task.state == TaskState.FAILED assert "Invalid or empty content" in task.result @@ -127,26 +134,30 @@ class TestWorkforce: """Test eigent_start successfully starts workforce.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Mock subtasks subtasks = [ Task(content="Subtask 1", id="sub_1"), - Task(content="Subtask 2", id="sub_2") + Task(content="Subtask 2", id="sub_2"), ] - - with patch.object(workforce, 'start', new_callable=AsyncMock) as mock_start, \ - patch.object(workforce, 'save_snapshot') as mock_save_snapshot: - + + with ( + patch.object( + workforce, "start", new_callable=AsyncMock + ) as mock_start, + patch.object(workforce, "save_snapshot") as mock_save_snapshot, + ): await workforce.eigent_start(subtasks) - + # Should add subtasks to pending tasks assert len(workforce._pending_tasks) >= len(subtasks) - + # Should save snapshot and start - mock_save_snapshot.assert_called_once_with("Initial task decomposition") + mock_save_snapshot.assert_called_once_with( + "Initial task decomposition" + ) mock_start.assert_called_once() @pytest.mark.asyncio @@ -154,18 +165,23 @@ class TestWorkforce: """Test eigent_start handles exceptions properly.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + subtasks = [Task(content="Subtask 1", id="sub_1")] - - with patch.object(workforce, 'start', new_callable=AsyncMock, side_effect=Exception("Workforce start failed")) as mock_start, \ - patch.object(workforce, 'save_snapshot'): - + + with ( + patch.object( + workforce, + "start", + new_callable=AsyncMock, + side_effect=Exception("Workforce start failed"), + ) as mock_start, + patch.object(workforce, "save_snapshot"), + ): with pytest.raises(Exception, match="Workforce start failed"): await workforce.eigent_start(subtasks) - + # State should be set to STOPPED on exception assert workforce._state == WorkforceState.STOPPED @@ -174,32 +190,50 @@ class TestWorkforce: """Test _find_assignee sends proper task assignment notifications.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create test tasks main_task = Task(content="Main task", id="main") subtask1 = Task(content="Subtask 1", id="sub_1") subtask2 = Task(content="Subtask 2", id="sub_2") workforce._task = main_task - + tasks = [main_task, subtask1, subtask2] - + # Mock assignment result assignments = [ - TaskAssignment(task_id="main", assignee_id="coordinator", dependencies=[]), - TaskAssignment(task_id="sub_1", assignee_id="worker_1", dependencies=[]), - TaskAssignment(task_id="sub_2", assignee_id="worker_2", dependencies=["sub_1"]) + TaskAssignment( + task_id="main", assignee_id="coordinator", dependencies=[] + ), + TaskAssignment( + task_id="sub_1", assignee_id="worker_1", dependencies=[] + ), + TaskAssignment( + task_id="sub_2", assignee_id="worker_2", dependencies=["sub_1"] + ), ] mock_assign_result = TaskAssignResult(assignments=assignments) - - with patch('app.utils.workforce.get_task_lock', return_value=mock_task_lock), \ - patch('app.utils.workforce.get_camel_task', side_effect=lambda task_id, task_list: next((t for t in task_list if t.id == task_id), None)), \ - patch.object(workforce.__class__.__bases__[0], '_find_assignee', return_value=mock_assign_result): - + + with ( + patch( + "app.utils.workforce.get_task_lock", + return_value=mock_task_lock, + ), + patch( + "app.utils.workforce.get_camel_task", + side_effect=lambda task_id, task_list: next( + (t for t in task_list if t.id == task_id), None + ), + ), + patch.object( + workforce.__class__.__bases__[0], + "_find_assignee", + return_value=mock_assign_result, + ), + ): result = await workforce._find_assignee(tasks) - + assert result is mock_assign_result # Should have queued assignment notifications for subtasks (not main task) assert mock_task_lock.put_queue.call_count >= 1 @@ -209,22 +243,29 @@ class TestWorkforce: """Test _post_task sends running state notification.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create test tasks main_task = Task(content="Main task", id="main") subtask = Task(content="Subtask", id="sub_1") workforce._task = main_task - + assignee_id = "worker_1" - - with patch('app.utils.workforce.get_task_lock', return_value=mock_task_lock), \ - patch.object(workforce.__class__.__bases__[0], '_post_task', return_value=None) as mock_super_post: - + + with ( + patch( + "app.utils.workforce.get_task_lock", + return_value=mock_task_lock, + ), + patch.object( + workforce.__class__.__bases__[0], + "_post_task", + return_value=None, + ) as mock_super_post, + ): await workforce._post_task(subtask, assignee_id) - + # Should queue running state notification for subtask mock_task_lock.put_queue.assert_called_once() call_args = mock_task_lock.put_queue.call_args[0][0] @@ -232,7 +273,7 @@ class TestWorkforce: assert call_args.data["assignee_id"] == assignee_id assert call_args.data["task_id"] == "sub_1" assert call_args.data["state"] == "running" - + # Should call parent method mock_super_post.assert_called_once_with(subtask, assignee_id) @@ -240,41 +281,44 @@ class TestWorkforce: """Test add_single_agent_worker successfully adds worker.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create mock worker with required attributes mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.agent_id = "test_worker_123" description = "Test worker description" - - with patch.object(workforce, '_validate_agent_compatibility'), \ - patch.object(workforce, '_attach_pause_event_to_agent'), \ - patch.object(workforce, '_start_child_node_when_paused'): - - result = workforce.add_single_agent_worker(description, mock_worker, pool_max_size=5) - + + with ( + patch.object(workforce, "_validate_agent_compatibility"), + patch.object(workforce, "_attach_pause_event_to_agent"), + patch.object(workforce, "_start_child_node_when_paused"), + ): + result = workforce.add_single_agent_worker( + description, mock_worker, pool_max_size=5 + ) + assert result is workforce assert len(workforce._children) == 1 - + # Check that the added worker is a SingleAgentWorker added_worker = workforce._children[0] - assert hasattr(added_worker, 'worker') + assert hasattr(added_worker, "worker") assert added_worker.worker is mock_worker def test_add_single_agent_worker_while_running(self): """Test add_single_agent_worker raises error when workforce is running.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) workforce._state = WorkforceState.RUNNING - + mock_worker = MagicMock(spec=ListenChatAgent) - - with pytest.raises(RuntimeError, match="Cannot add workers while workforce is running"): + + with pytest.raises( + RuntimeError, match="Cannot add workers while workforce is running" + ): workforce.add_single_agent_worker("Test worker", mock_worker) @pytest.mark.asyncio @@ -282,21 +326,28 @@ class TestWorkforce: """Test _handle_completed_task sends completion notification.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create completed task task = Task(content="Completed task", id="completed_123") task.state = TaskState.DONE task.result = "Task completed successfully" task.failure_count = 0 - - with patch('app.utils.workforce.get_task_lock', return_value=mock_task_lock), \ - patch.object(workforce.__class__.__bases__[0], '_handle_completed_task', return_value=None) as mock_super_handle: - + + with ( + patch( + "app.utils.workforce.get_task_lock", + return_value=mock_task_lock, + ), + patch.object( + workforce.__class__.__bases__[0], + "_handle_completed_task", + return_value=None, + ) as mock_super_handle, + ): await workforce._handle_completed_task(task) - + # Should queue task state notification mock_task_lock.put_queue.assert_called_once() call_args = mock_task_lock.put_queue.call_args[0][0] @@ -304,7 +355,7 @@ class TestWorkforce: assert call_args.data["task_id"] == "completed_123" assert call_args.data["state"] == TaskState.DONE assert call_args.data["result"] == "Task completed successfully" - + # Should call parent method mock_super_handle.assert_called_once_with(task) @@ -313,22 +364,29 @@ class TestWorkforce: """Test _handle_failed_task sends failure notification.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - + # Create failed task task = Task(content="Failed task", id="failed_123") task.state = TaskState.FAILED task.failure_count = 2 - - with patch('app.utils.workforce.get_task_lock', return_value=mock_task_lock), \ - patch.object(workforce.__class__.__bases__[0], '_handle_failed_task', return_value=True) as mock_super_handle: - + + with ( + patch( + "app.utils.workforce.get_task_lock", + return_value=mock_task_lock, + ), + patch.object( + workforce.__class__.__bases__[0], + "_handle_failed_task", + return_value=True, + ) as mock_super_handle, + ): result = await workforce._handle_failed_task(task) - + assert result is True - + # Should queue task state notification mock_task_lock.put_queue.assert_called_once() call_args = mock_task_lock.put_queue.call_args[0][0] @@ -336,7 +394,7 @@ class TestWorkforce: assert call_args.data["task_id"] == "failed_123" assert call_args.data["state"] == TaskState.FAILED assert call_args.data["failure_count"] == 2 - + # Should call parent method mock_super_handle.assert_called_once_with(task) @@ -345,18 +403,23 @@ class TestWorkforce: """Test stop method sends end notification.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - - with patch('app.utils.workforce.get_task_lock', return_value=mock_task_lock), \ - patch.object(workforce.__class__.__bases__[0], 'stop') as mock_super_stop: - + + with ( + patch( + "app.utils.workforce.get_task_lock", + return_value=mock_task_lock, + ), + patch.object( + workforce.__class__.__bases__[0], "stop" + ) as mock_super_stop, + ): workforce.stop() - + # Should call parent stop method mock_super_stop.assert_called_once() - + # Should queue end notification assert mock_task_lock.add_background_task.call_count == 1 @@ -365,13 +428,12 @@ class TestWorkforce: """Test cleanup method deletes task lock.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - - with patch('app.service.task.delete_task_lock') as mock_delete: + + with patch("app.service.task.delete_task_lock") as mock_delete: await workforce.cleanup() - + mock_delete.assert_called_once_with(api_task_id) @pytest.mark.asyncio @@ -379,12 +441,13 @@ class TestWorkforce: """Test cleanup handles exceptions gracefully.""" api_task_id = "test_api_task_123" workforce = Workforce( - api_task_id=api_task_id, - description="Test workforce" + api_task_id=api_task_id, description="Test workforce" ) - - with patch('app.service.task.delete_task_lock', side_effect=Exception("Delete failed")): + with patch( + "app.service.task.delete_task_lock", + side_effect=Exception("Delete failed"), + ): # Should not raise exception await workforce.cleanup() @@ -392,62 +455,70 @@ class TestWorkforce: @pytest.mark.integration class TestWorkforceIntegration: """Integration tests for Workforce class.""" - + def setup_method(self): """Clean up before each test.""" from app.service.task import task_locks + task_locks.clear() @pytest.mark.asyncio async def test_full_workforce_lifecycle(self): """Test complete workforce lifecycle from creation to cleanup.""" api_task_id = "integration_test_123" - + # Create task lock from app.service.task import create_task_lock - task_lock = create_task_lock(api_task_id) - + + create_task_lock(api_task_id) + # Create workforce workforce = Workforce( - api_task_id=api_task_id, - description="Integration test workforce" + api_task_id=api_task_id, description="Integration test workforce" ) - + # Create main task main_task = Task(content="Integration test task", id="main_task") - + # Mock subtasks subtasks = [ Task(content="Setup", id="setup_task"), Task(content="Implementation", id="impl_task"), - Task(content="Testing", id="test_task") + Task(content="Testing", id="test_task"), ] - - with patch.object(workforce, '_decompose_task', return_value=subtasks), \ - patch('app.utils.workforce.validate_task_content', return_value=True), \ - patch.object(workforce, 'start', new_callable=AsyncMock): - + + with ( + patch.object(workforce, "_decompose_task", return_value=subtasks), + patch( + "app.utils.workforce.validate_task_content", return_value=True + ), + patch.object(workforce, "start", new_callable=AsyncMock), + ): # Make subtasks result_subtasks = workforce.eigent_make_sub_tasks(main_task) assert len(result_subtasks) == 3 - + # Start workforce await workforce.eigent_start(result_subtasks) - + # Add worker mock_worker = MagicMock(spec=ListenChatAgent) mock_worker.agent_id = "integration_worker_123" - with patch.object(workforce, '_validate_agent_compatibility'), \ - patch.object(workforce, '_attach_pause_event_to_agent'), \ - patch.object(workforce, '_start_child_node_when_paused'): - workforce.add_single_agent_worker("Integration worker", mock_worker) - + with ( + patch.object(workforce, "_validate_agent_compatibility"), + patch.object(workforce, "_attach_pause_event_to_agent"), + patch.object(workforce, "_start_child_node_when_paused"), + ): + workforce.add_single_agent_worker( + "Integration worker", mock_worker + ) + assert len(workforce._children) == 1 - + # Stop workforce - with patch.object(workforce.__class__.__bases__[0], 'stop'): + with patch.object(workforce.__class__.__bases__[0], "stop"): workforce.stop() - + # Cleanup await workforce.cleanup() @@ -455,15 +526,15 @@ class TestWorkforceIntegration: async def test_workforce_with_multiple_workers(self): """Test workforce with multiple workers.""" api_task_id = "multi_worker_test_123" - + from app.service.task import create_task_lock + create_task_lock(api_task_id) - + workforce = Workforce( - api_task_id=api_task_id, - description="Multi-worker test workforce" + api_task_id=api_task_id, description="Multi-worker test workforce" ) - + # Add multiple workers workers = [] for i in range(3): @@ -471,16 +542,17 @@ class TestWorkforceIntegration: mock_worker.role_name = f"worker_{i}" mock_worker.agent_id = f"worker_{i}_123" workers.append(mock_worker) - - with patch.object(workforce, '_validate_agent_compatibility'), \ - patch.object(workforce, '_attach_pause_event_to_agent'), \ - patch.object(workforce, '_start_child_node_when_paused'): - + + with ( + patch.object(workforce, "_validate_agent_compatibility"), + patch.object(workforce, "_attach_pause_event_to_agent"), + patch.object(workforce, "_start_child_node_when_paused"), + ): for i, worker in enumerate(workers): workforce.add_single_agent_worker(f"Worker {i}", worker) - + assert len(workforce._children) == 3 - + # Cleanup await workforce.cleanup() @@ -488,32 +560,40 @@ class TestWorkforceIntegration: async def test_workforce_task_state_tracking(self): """Test workforce properly tracks task state changes.""" api_task_id = "task_tracking_test_123" - + from app.service.task import create_task_lock - task_lock = create_task_lock(api_task_id) - + + create_task_lock(api_task_id) + workforce = Workforce( - api_task_id=api_task_id, - description="Task tracking test workforce" + api_task_id=api_task_id, description="Task tracking test workforce" ) - + # Test completed task handling completed_task = Task(content="Completed task", id="completed") completed_task.state = TaskState.DONE completed_task.result = "Success" - - with patch.object(workforce.__class__.__bases__[0], '_handle_completed_task', return_value=None): + + with patch.object( + workforce.__class__.__bases__[0], + "_handle_completed_task", + return_value=None, + ): await workforce._handle_completed_task(completed_task) - + # Test failed task handling failed_task = Task(content="Failed task", id="failed") failed_task.state = TaskState.FAILED failed_task.failure_count = 1 - - with patch.object(workforce.__class__.__bases__[0], '_handle_failed_task', return_value=True): + + with patch.object( + workforce.__class__.__bases__[0], + "_handle_failed_task", + return_value=True, + ): result = await workforce._handle_failed_task(failed_task) assert result is True - + # Cleanup await workforce.cleanup() @@ -521,7 +601,7 @@ class TestWorkforceIntegration: @pytest.mark.model_backend class TestWorkforceWithLLM: """Tests that require LLM backend (marked for selective running).""" - + @pytest.mark.asyncio async def test_workforce_with_real_agents(self): """Test workforce with real agent implementations.""" @@ -540,15 +620,14 @@ class TestWorkforceWithLLM: @pytest.mark.unit class TestWorkforceErrorCases: """Test error cases and edge conditions for Workforce.""" - + def test_eigent_make_sub_tasks_with_none_task(self): """Test eigent_make_sub_tasks with None task.""" api_task_id = "error_test_123" workforce = Workforce( - api_task_id=api_task_id, - description="Error test workforce" + api_task_id=api_task_id, description="Error test workforce" ) - + with pytest.raises((AttributeError, TypeError)): workforce.eigent_make_sub_tasks(None) @@ -556,16 +635,17 @@ class TestWorkforceErrorCases: """Test eigent_make_sub_tasks with malformed task object.""" api_task_id = "error_test_123" workforce = Workforce( - api_task_id=api_task_id, - description="Error test workforce" + api_task_id=api_task_id, description="Error test workforce" ) - + # Create object that looks like task but isn't fake_task = MagicMock() fake_task.content = "Fake task content" fake_task.id = "fake_task" - - with patch('app.utils.workforce.validate_task_content', return_value=False): + + with patch( + "app.utils.workforce.validate_task_content", return_value=False + ): with pytest.raises(UserException): workforce.eigent_make_sub_tasks(fake_task) @@ -574,16 +654,16 @@ class TestWorkforceErrorCases: """Test eigent_start with empty subtasks list.""" api_task_id = "empty_test_123" workforce = Workforce( - api_task_id=api_task_id, - description="Empty test workforce" + api_task_id=api_task_id, description="Empty test workforce" ) - - with patch.object(workforce, 'start', new_callable=AsyncMock), \ - patch.object(workforce, 'save_snapshot'): - + + with ( + patch.object(workforce, "start", new_callable=AsyncMock), + patch.object(workforce, "save_snapshot"), + ): # Should handle empty subtasks gracefully await workforce.eigent_start([]) - + # Should still call start method workforce.start.assert_called_once() @@ -592,34 +672,47 @@ class TestWorkforceErrorCases: api_task_id = "invalid_worker_test_123" workforce = Workforce( api_task_id=api_task_id, - description="Invalid worker test workforce" + description="Invalid worker test workforce", ) - + # Try to add invalid worker invalid_worker = "not_an_agent" - - with patch.object(workforce, '_validate_agent_compatibility', side_effect=ValueError("Invalid agent")): + + with patch.object( + workforce, + "_validate_agent_compatibility", + side_effect=ValueError("Invalid agent"), + ): with pytest.raises(ValueError, match="Invalid agent"): - workforce.add_single_agent_worker("Invalid worker", invalid_worker) + workforce.add_single_agent_worker( + "Invalid worker", invalid_worker + ) @pytest.mark.asyncio async def test_find_assignee_with_get_task_lock_failure(self): """Test _find_assignee when get_task_lock fails after parent method succeeds.""" api_task_id = "lock_fail_test_123" workforce = Workforce( - api_task_id=api_task_id, - description="Lock fail test workforce" + api_task_id=api_task_id, description="Lock fail test workforce" ) - + tasks = [Task(content="Test task", id="test")] - - with patch.object(workforce.__class__.__bases__[0], '_find_assignee', return_value=TaskAssignResult(assignments=[])) as mock_super_find, \ - patch('app.utils.workforce.get_task_lock', side_effect=Exception("Task lock not found")): - + + with ( + patch.object( + workforce.__class__.__bases__[0], + "_find_assignee", + return_value=TaskAssignResult(assignments=[]), + ) as mock_super_find, + patch( + "app.utils.workforce.get_task_lock", + side_effect=Exception("Task lock not found"), + ), + ): # Should handle task lock failure and raise the exception after parent method succeeds with pytest.raises(Exception, match="Task lock not found"): await workforce._find_assignee(tasks) - + # Parent method should have been called first mock_super_find.assert_called_once_with(tasks) @@ -629,23 +722,27 @@ class TestWorkforceErrorCases: api_task_id = "nonexistent_lock_test_123" workforce = Workforce( api_task_id=api_task_id, - description="Nonexistent lock test workforce" + description="Nonexistent lock test workforce", ) - - with patch('app.service.task.delete_task_lock', side_effect=Exception("Task lock not found")): + + with patch( + "app.service.task.delete_task_lock", + side_effect=Exception("Task lock not found"), + ): # Should handle missing task lock gracefully await workforce.cleanup() def test_workforce_inheritance(self): """Test that Workforce properly inherits from BaseWorkforce.""" - from camel.societies.workforce.workforce import Workforce as BaseWorkforce - + from camel.societies.workforce.workforce import ( + Workforce as BaseWorkforce, + ) + api_task_id = "inheritance_test_123" workforce = Workforce( - api_task_id=api_task_id, - description="Inheritance test workforce" + api_task_id=api_task_id, description="Inheritance test workforce" ) - + assert isinstance(workforce, BaseWorkforce) - assert hasattr(workforce, 'api_task_id') + assert hasattr(workforce, "api_task_id") assert workforce.api_task_id == api_task_id diff --git a/build/installer.nsh b/build/installer.nsh index 1f50e21d..010e0f5c 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -1,4 +1,4 @@ !macro customInstallMode ; 跳过“选择安装类型”页面,直接进入下一步 Abort -!macroend \ No newline at end of file +!macroend diff --git a/config/notarize.cjs b/config/notarize.cjs index 673976b7..ef488816 100644 --- a/config/notarize.cjs +++ b/config/notarize.cjs @@ -8,14 +8,14 @@ exports.default = async function notarizing(context) { const appOutDir = context.appOutDir; const appName = context.packager.appInfo.productName; console.log("appOutDir", appOutDir); - + // Validate required environment variables if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) { console.warn("Missing Apple environment variables for notarization"); console.warn("Skipping notarization. Required: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID"); return; } - + return notarize({ tool: "notarytool", teamId: process.env.APPLE_TEAM_ID, diff --git a/docs/core/concepts.md b/docs/core/concepts.md index fb2b3120..666f17c2 100644 --- a/docs/core/concepts.md +++ b/docs/core/concepts.md @@ -1,7 +1,7 @@ --- -title: 'Concepts' -description: 'Understand the core terms and features that make Eigent unique.' -icon: 'key' +title: Concepts +description: Understand the core terms and features that make Eigent unique. +icon: key --- ## Workers diff --git a/docs/core/models/byok.md b/docs/core/models/byok.md index ae7b98fd..a4079b57 100644 --- a/docs/core/models/byok.md +++ b/docs/core/models/byok.md @@ -1,6 +1,6 @@ --- -title: 'Bring Your Own Key (BYOK)' -description: 'Configure your own API keys to use various LLM providers with Eigent.' +title: Bring Your Own Key (BYOK) +description: Configure your own API keys to use various LLM providers with Eigent. --- ## What is BYOK? @@ -16,15 +16,16 @@ description: 'Configure your own API keys to use various LLM providers with Eige ### Step 1: Get Your API Key 1. Visit the [OpenAI API Keys page](https://platform.openai.com/api-keys) -2. Click **"Create new secret key"** -3. Copy the generated key (you won't be able to see it again) +1. Click **"Create new secret key"** +1. Copy the generated key (you won't be able to see it again) ### Step 2: Configure in Eigent -1. Launch Eigent and go to **Settings** \> **Models** -2. Find the **OpenAI** card in the Custom Model section +1. Launch Eigent and go to **Settings** > **Models** - Screenshot 2026 01 20 At 18 13 45 -3. Fill in the following fields: +1. Fill in the following fields: | Field | Value | Example | | -------------- | ------------------------- | --------------------------- | @@ -41,7 +42,7 @@ description: 'Configure your own API keys to use various LLM providers with Eige | **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 +1. Click **Set as Default** to use this provider for your agents ## Configuration Fields diff --git a/docs/core/models/gemini.md b/docs/core/models/gemini.md index 41d05390..ef241789 100644 --- a/docs/core/models/gemini.md +++ b/docs/core/models/gemini.md @@ -1,6 +1,6 @@ --- -title: 'Gemini' -description: 'This guide walks you through setting up your Google Gemini API key within Eigent to enable the Gemini model for your AI workforce.' +title: Gemini +description: This guide walks you through setting up your Google Gemini API key within Eigent to enable the Gemini model for your AI workforce. --- ### Prerequisites diff --git a/docs/core/models/kimi.md b/docs/core/models/kimi.md index 8d2a4b43..dfe2c8b7 100644 --- a/docs/core/models/kimi.md +++ b/docs/core/models/kimi.md @@ -1,6 +1,6 @@ --- -title: 'Kimi' -description: 'This guide walks you through setting up your Kimi (Moonshot AI) API key within Eigent to enable the Kimi model for your AI workforce.' +title: Kimi +description: This guide walks you through setting up your Kimi (Moonshot AI) API key within Eigent to enable the Kimi model for your AI workforce. --- ### Prerequisites diff --git a/docs/core/models/local-model.md b/docs/core/models/local-model.md index 32346c9c..6978b518 100644 --- a/docs/core/models/local-model.md +++ b/docs/core/models/local-model.md @@ -1,6 +1,6 @@ --- -title: 'Models (Local Model)' -description: 'Configure and deploy your preferred LLM models with Eigent.' +title: Models (Local Model) +description: Configure and deploy your preferred LLM models with Eigent. --- ## **Self-Host Model** diff --git a/docs/core/models/minimax.md b/docs/core/models/minimax.md index c90ef715..c60a044a 100644 --- a/docs/core/models/minimax.md +++ b/docs/core/models/minimax.md @@ -1,6 +1,6 @@ --- -title: 'MiniMax' -description: 'This guide walks you through setting up your MiniMax API key within Eigent to enable the MiniMax model for your AI workforce.' +title: MiniMax +description: This guide walks you through setting up your MiniMax API key within Eigent to enable the MiniMax model for your AI workforce. --- ### Prerequisites diff --git a/docs/core/workforce.md b/docs/core/workforce.md index e60c2250..2b12b42e 100644 --- a/docs/core/workforce.md +++ b/docs/core/workforce.md @@ -19,6 +19,7 @@ With Workforce, agents plan, solve, and verify work together—like a project te - Built-in coordination, planning, and failure recovery - Ideal for hackathons, evaluations, code review, brainstorming, and more - Configure roles, toolsets, and communication patterns for *any* scenario + ## System Design diff --git a/docs/get_started/installation.md b/docs/get_started/installation.md index dfcf14ec..6d1f5cbf 100644 --- a/docs/get_started/installation.md +++ b/docs/get_started/installation.md @@ -10,12 +10,14 @@ icon: wrench - Download for macOS - Download for Windows - - **macOS Prerequisite** - Please ensure you are running macOS 11 (Big Sur) or a newer version to install Eigent. - +``` + + **macOS Prerequisite** + Please ensure you are running macOS 11 (Big Sur) or a newer version to install Eigent. + +``` - + - **On macOS:** Open the downloaded `.dmg` file and drag the Eigent icon into your Applications folder. - **On Windows:** Run the downloaded `.exe` installer and follow the on-screen instructions. diff --git a/docs/get_started/quick_start.md b/docs/get_started/quick_start.md index 8fef8089..733621f7 100644 --- a/docs/get_started/quick_start.md +++ b/docs/get_started/quick_start.md @@ -62,9 +62,9 @@ Cloud version users: outputs are also saved in your cloud workspace according to Eigent comes with four ready-to-work agents. Each is equipped with a specific set of tools and shines at specific tasks—click to explore: 1. **Developer Agent** – writes, debugs and executes code -2. **Browser Agent** – fetches and gathers info from the web -3. **Multimodal Agent** – ideals with images, videos and more -4. **Document Agent** – reads, writes and manages files (Markdown, PDF, Word, etc.) +1. **Browser Agent** – fetches and gathers info from the web +1. **Multimodal Agent** – ideals with images, videos and more +1. **Document Agent** – reads, writes and manages files (Markdown, PDF, Word, etc.) ![Pre-build Agents](/docs/images/quickstart_prebuiltagents.gif) @@ -124,7 +124,7 @@ Click on an agent icon to open its **Workspace**: