54 Commits
Author SHA1 Message Date
Martin a4a98fa601 Merge pull request #472 from kuishou68/fix/issue-471-remove-duplicate-soundfile
fix: remove duplicate soundfile entry in requirements.txt
2026-04-22 09:42:23 +02:00
Cocoon-Break 0eb42e4a42 fix: remove duplicate soundfile entry in requirements.txt (Closes #471) 2026-04-14 15:04:31 +08:00
Martin 4c0a72ff61 Merge pull request #468 from octo-patch/fix/issue-394-lm-studio-url-parsing
fix: correct LM Studio URL parsing and uvicorn port binding
2026-04-11 16:46:56 +02:00
Martin 021644d786 Merge pull request #467 from octo-patch/fix/issue-274-searxsearch-query-encoding
fix: URL-encode search query and detect empty results as failure
2026-04-11 16:46:42 +02:00
Martin 725ceeea20 Merge pull request #466 from octo-patch/fix/issue-332-ollama-configurable-port
fix: use configured port from server_address for local Ollama connections
2026-04-11 16:46:16 +02:00
Martin 69bbd31abf Merge pull request #465 from octo-patch/fix/issue-312-dynamic-remote-debugging-port
fix: use dynamic free port for remote debugging to prevent SessionNotCreatedException
2026-04-11 16:44:08 +02:00
Martin 2944ff6892 Merge pull request #463 from octo-patch/fix/issue-352-configurable-backend-url
fix: make REACT_APP_BACKEND_URL configurable for remote deployments
2026-04-11 16:41:57 +02:00
Martin 6fd329acc6 Merge pull request #462 from octo-patch/fix/searxsearch-init-and-api-port
fix: use BACKEND_PORT env var in uvicorn and honor base_url arg in searxSearch
2026-04-11 16:40:09 +02:00
octo-patch 05aa5c2104 fix: correct LM Studio URL parsing and uvicorn port binding (fixes #394)
- lm_studio_fn: use urlparse to extract port from server_address so that
  addresses with an http:// prefix (e.g. http://127.0.0.1:1234, as
  documented in README) no longer break the Docker URL construction.
  Previously split(":")[1] returned "//127.0.0.1" instead of the port
  number, producing a malformed URL.
  Also fix the non-Docker path to avoid prepending http:// when the address
  already includes a scheme.

- api.py: pass the computed port variable to uvicorn.run() instead of the
  hardcoded literal 7777, so that BACKEND_PORT environment variable is
  actually honoured when starting the server directly.
2026-04-11 10:37:17 +08:00
octo-patch a976afdbd7 fix: URL-encode search query and detect empty results as failure (fixes #274)
- Use urllib.parse.urlencode to properly encode the POST body, so queries
  containing special characters like '&', '+', '=' are sent correctly
- Extend execution_failure_check to also detect "No search results" as a
  failure, preventing the agent from silently looping when all SearXNG
  engines are rate-limited and return no results
2026-04-10 10:07:06 +08:00
octo-patch 29aafda41d fix: use configured port from server_address for local Ollama connections (fixes #332)
When is_local=True, the Ollama host URL previously hardcoded port 11434,
ignoring the port specified in provider_server_address in config.ini.
Users who need to run Ollama on a non-default port (e.g. because 11434
is already in use) had no way to configure this without editing source code.

Now the port is extracted from provider_server_address when present,
falling back to 11434 if only a hostname is given.
2026-04-09 10:23:19 +08:00
octo-patch 37c9652e6d fix: use dynamic free port for remote debugging to prevent SessionNotCreatedException (fixes #312)
The hardcoded --remote-debugging-port=9222 caused SessionNotCreatedException
when port 9222 was already in use by another Chrome instance or process.
Replace with a dynamically allocated free port via socket binding.
2026-04-08 10:36:48 +08:00
Octopus f8fc43f6d7 fix: make REACT_APP_BACKEND_URL configurable for remote deployments (fixes #352)
When AgenticSeek is deployed on a remote server and accessed from a
different machine, the frontend JavaScript runs in the user's browser
with localhost:7777 which resolves to the user's local machine, not
the server - causing the 'System offline' error even though the backend
is running.

Change docker-compose.yml to read REACT_APP_BACKEND_URL from .env
(defaulting to http://localhost:7777 for local use) and document the
variable in .env.example so users can point the frontend at their
server's public IP when deploying remotely.
2026-04-07 10:38:24 +08:00
Octopus 3fc2ac5235 fix: use BACKEND_PORT env var in uvicorn and honor base_url arg in searxSearch
- api.py: uvicorn.run was hardcoding port=7777 instead of using the
  port variable populated from BACKEND_PORT env var, making BACKEND_PORT
  ineffective when running python api.py directly
- searxSearch: add missing 'import sys' (caused NameError when run as
  standalone script) and use constructor base_url arg with env var
  fallback so searxSearch(base_url=...) actually takes effect
2026-04-06 10:43:16 +08:00
Martin 31205febfe Merge pull request #460 from octo-patch/fix/provider-registration-bugs
fix: register anthropic provider, fix togetherAI alias, fix dsk_deepseek exception handling
2026-04-05 16:39:32 +02:00
Martin 1fbd8b135b Merge branch 'main' into fix/provider-registration-bugs 2026-04-05 16:39:08 +02:00
Martin 7b8a2271a3 Merge pull request #459 from octo-patch/fix/issue-382-prevent-server-crash-on-query-error
fix: return HTTP 500 instead of crashing server on query errors
2026-04-05 11:59:00 +02:00
Martin 4e985d8df6 Merge pull request #457 from octo-patch/fix/issue-367-make-plan-infinite-loop
fix: add max retry limit to PlannerAgent.make_plan to prevent infinite loop
2026-04-05 11:58:09 +02:00
Martin 74d38100c4 Merge pull request #455 from octo-patch/fix/issue-359-planner-unbound-answer
fix: resolve UnboundLocalError in PlannerAgent.process when stop flag is set
2026-04-05 11:57:16 +02:00
Martin 9e4613df71 Merge pull request #450 from octo-patch/feature/upgrade-minimax-m27
feat: upgrade MiniMax default model to M2.7
2026-04-05 11:55:47 +02:00
Octopus 2b8bc83a97 fix: register anthropic provider, normalize togetherAI alias, fix dsk_deepseek exception handling
- Add 'anthropic' to available_providers and unsafe_providers so users can set
  provider_name = anthropic in config.ini and use ANTHROPIC_API_KEY
- Add provider name alias normalization so 'togetherAI' (as documented in README)
  is accepted as equivalent to 'together'
- Fix NameError in dsk_deepseek: add 'as e' to AuthenticationError, RateLimitError,
  and NetworkError except clauses where 'e' was referenced but not bound
- Remove unreachable 'return thought' dead code at end of lm_studio_fn
- Update README to list Anthropic as a supported API provider
2026-04-05 10:41:54 +08:00
Octopus 872d01f8e5 fix: return HTTP 500 instead of calling sys.exit(1) on query errors
Previously, any unhandled exception during query processing caused the
entire backend server to exit via sys.exit(1), making the frontend show
'System offline. Deploy backend first.' until the container was restarted.
Also, is_generating was never reset to False on exception, permanently
blocking all subsequent queries with 429 responses.

- Replace sys.exit(1) with a proper HTTP 500 error response
- Move is_generating = False to the finally block so it always resets

Fixes #382
2026-04-04 11:23:41 +08:00
Octopus d1ecb15703 fix: add max retry limit to PlannerAgent.make_plan to prevent infinite loop (fixes #367) 2026-04-03 10:51:23 +08:00
Octopus 7cde1549b2 fix: initialize answer variable and reset stop flag in PlannerAgent.process (fixes #359) 2026-04-02 16:52:06 +08:00
PR Bot 1432266694 feat: upgrade MiniMax default model to M2.7
- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to supported models list
- Update docstring to list M2.7 models before M2.5
- Update README/README_CHS model references
- Add unit tests for M2.7 and M2.7-highspeed models
- Keep all previous models as alternatives
2026-03-18 16:33:04 +08:00
Martin 1036c5fbc1 Merge pull request #449 from guoyangzhen/fix/optional-tts-dependency
fix: make kokoro TTS optional to resolve num2words Python 3.12+ incompatibility
2026-03-16 20:02:51 +01:00
Martin 77ed25306e Merge pull request #448 from guoyangzhen/fix/startup-script-integer-comparison
fix: handle empty dir_size_bytes and remove deprecated docker-compose version
2026-03-16 20:02:07 +01:00
Martin d47761a6a5 Merge pull request #447 from guoyangzhen/fix/frontend-react-scripts-not-found
fix(frontend): ensure react-scripts is available in Docker container
2026-03-16 20:01:00 +01:00
Martin e135683426 Merge pull request #444 from ximiximi423/feat/add-minimax-provider
feat: add MiniMax as new LLM provider
2026-03-16 19:57:55 +01:00
guoyangzhen 53a47be80c fix: make kokoro TTS optional 2026-03-14 17:26:21 +08:00
guoyangzhen 75ce967107 fix: make kokoro TTS optional 2026-03-14 17:26:19 +08:00
guoyangzhen 9a8ce59520 fix: handle empty dir_size_bytes and remove deprecated docker-compose version 2026-03-14 17:09:33 +08:00
guoyangzhen a97010739b fix: handle empty dir_size_bytes and remove deprecated docker-compose version 2026-03-14 17:09:31 +08:00
guoyangzhen 7066f62942 fix(frontend): ensure react-scripts is available in Docker container
Use npm ci + npm rebuild for deterministic installs. Add verification
step that catches missing react-scripts early.

Fixes #411
2026-03-14 16:41:21 +08:00
ximi fc268ed087 feat: add MiniMax as new LLM provider
Add support for MiniMax M2.5 series models via OpenAI-compatible API:
- Add minimax_fn() to Provider class with configurable base URL
- Support both international and China mainland endpoints
- Update documentation in README.md and README_CHS.md
- Add MINIMAX_API_KEY and MINIMAX_BASE_URL to .env.example
- Add unit tests for MiniMax provider

Made-with: Cursor
2026-03-06 17:10:52 +08:00
Martin 781275c881 Merge pull request #439 from Br1an67/feat/issue-108-web-form-handling
feat: add support for select, textarea, and file upload in web forms
2026-03-02 19:45:10 +01:00
Martin 2a69fbef4b Merge pull request #440 from Br1an67/feat/issue-52-add-tests
test: add unit tests for parsing functions, logger, and utility
2026-03-02 19:44:20 +01:00
Martin fa4cef0e74 Merge pull request #438 from Br1an67/fix/issue-356-auto-update-chromedriver
fix: auto-update ChromeDriver when version mismatches Chrome
2026-03-02 19:43:39 +01:00
Martin f63b2c436d Merge pull request #437 from Br1an67/fix/issue-409-json-parsing
fix: handle JSON parsing errors in planner agent
2026-03-02 19:42:42 +01:00
Martin cb6210293a Merge pull request #425 from Aquarius10th/fix-apt-get-error
Specify the base image version to fix dependency missing or version c…
2026-03-02 19:41:02 +01:00
Br1an67 165dcb8d97 test: add unit tests for parsing functions, logger, and utility
Add 34 tests across 3 test files:

- test_browser_agent_parsing.py (20 tests): expand existing tests with
  coverage for extract_links, extract_form, clean_links, parse_answer,
  select_link, jsonify_search_results, stringify_search_results, and
  select_unvisited
- test_logger.py (9 tests): Logger initialization, message writing, log
  level handling, deduplication, and folder creation
- test_utility.py (5 tests): get_color_map returns correct structure and
  color values

All tests avoid LLM requests per issue requirements.
2026-03-02 00:28:48 +08:00
Br1an67 1e827a903b feat: add support for select, textarea, and file upload in web forms
Extend form handling to support:
- <select> dropdowns: detect options, select by visible text or value
- <textarea> fields: fill with text content
- <input type="file">: upload files by absolute path
- All element types are also discovered inside shadow DOM

Changes:
- find_inputs.js: discover <select> (with options) and <textarea>
- browser.py: import Select, handle new element types in
  get_form_inputs() and fill_form_inputs()
2026-03-02 00:16:28 +08:00
Br1an67 16f7f7c917 fix: auto-update ChromeDriver when version mismatches Chrome
Add version compatibility check before using existing ChromeDriver.
When the installed ChromeDriver major version does not match Chrome,
chromedriver_autoinstaller downloads the correct version automatically
instead of failing with a version mismatch error.

Add helper functions get_chromedriver_version() and
is_chromedriver_compatible() with tests for version matching logic.
2026-03-02 00:10:23 +08:00
Br1an67 f350378170 fix: handle JSON parsing errors in planner agent
Add try-except for json.JSONDecodeError in parse_agent_tasks() to
gracefully handle malformed JSON from LLM responses (e.g. Gemini 2.5
Flash via OpenRouter). Returns empty list on parse failure, which
triggers the existing retry mechanism in make_plan().

Add test suite for parse_agent_tasks() covering valid JSON, malformed
JSON, truncated JSON, and invalid agent name cases.
2026-03-02 00:03:36 +08:00
Martin 9611bf4081 Merge pull request #436 from arikusi/add-turkish-readme
docs: add Turkish translation (README_TR.md)
2026-02-28 21:32:00 +01:00
arikusi aeb9145d6e docs: add Turkish translation (README_TR.md) 2026-02-27 01:39:42 +03:00
Frank Zhou 889545f356 Specify the base image version to fix dependency missing or version conflict issues when using apt-get.
The originally specified version 3.11-slim is only an alias representing the latest version in the 3.11.x-slim series (for example, on November 22nd, its exact version number was 3.11.14-slim). After the base image is updated, various unexpected errors may occur when executing apt-get.
2025-11-23 00:03:26 +08:00
Martin 782a933971 Merge pull request #424 from Fosowl/dev
fix: remove unavailable engine from searxng config.
2025-11-15 14:56:20 +01:00
Martin abc765861a Merge pull request #405 from Fosowl/dev
readme update
2025-09-14 20:15:49 +02:00
Martin 8a713e5ec0 Merge pull request #404 from Fosowl/dev
fix : update readme for correct ollama start
2025-09-14 18:23:32 +02:00
Martin 0decd84541 Merge pull request #403 from Fosowl/dev
Fix various ubuntu issues + CLI instructions
2025-09-13 23:50:03 +02:00
Martin be86fd07e1 Merge pull request #402 from Fosowl/dev
fix #400
2025-09-11 23:45:58 +02:00
Martin 308ba0e595 Merge pull request #379 from Fosowl/dev
readme update
2025-07-13 12:52:03 +02:00
Martin deaf142a0d Merge pull request #373 from Fosowl/dev
Fix backend url issue
2025-07-06 00:35:29 +02:00
28 changed files with 3459 additions and 1644 deletions
+7
View File
@@ -4,6 +4,9 @@ WORK_DIR="/Users/username/Documents/workspace_with_my_files"
OLLAMA_PORT="11434"
LM_STUDIO_PORT="1234"
BACKEND_PORT="7777"
# Set this to your server's public IP/hostname when accessing the UI from a remote machine.
# Example: REACT_APP_BACKEND_URL=http://192.168.1.100:7777
REACT_APP_BACKEND_URL=http://localhost:7777
CUSTOM_ADDITIONAL_LLM_PORT="11435"
OPENAI_API_KEY='xxxxx'
DEEPSEEK_API_KEY='xxxxx'
@@ -11,3 +14,7 @@ OPENROUTER_API_KEY='xxxxx'
TOGETHER_API_KEY='xxxxx'
GOOGLE_API_KEY='xxxxx'
ANTHROPIC_API_KEY='xxxxx'
MINIMAX_API_KEY='xxxxx'
# Optional: MiniMax API base URL (default: https://api.minimax.io/v1)
# For mainland China users: https://api.minimaxi.com/v1
# MINIMAX_BASE_URL='https://api.minimax.io/v1'
+1 -1
View File
@@ -1,5 +1,5 @@
FROM --platform=linux/amd64 python:3.11-slim
FROM --platform=linux/amd64 python:3.11.12
ENV DEBIAN_FRONTEND=noninteractive
# Install essential packages and Chrome dependencies
+2 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*A **100% local alternative to Manus AI**, this voice-enabled AI assistant autonomously browses the web, writes code, and plans tasks while keeping all data on your device. Tailored for local reasoning models, it runs entirely on your hardware, ensuring complete privacy and zero cloud dependency.*
@@ -240,6 +240,7 @@ provider_server_address = # Typically ignored or can be left blank when is_local
| Hugging Face | `huggingface` | No | Use models from Hugging Face Inference API. | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
| TogetherAI | `togetherAI` | No | Use various open-source models via TogetherAI API.| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) |
| OpenRouter | `openrouter` | No | Use OpenRouter Models| [https://openrouter.ai/](https://openrouter.ai/) |
| MiniMax | `minimax` | No | Use MiniMax models (e.g., MiniMax-M2.7, MiniMax-M2.5).| [platform.minimax.io](https://platform.minimax.io/user-center/basic-information) |
*Note:*
* We advise against using `gpt-4o` or other OpenAI models for complex web browsing and task planning as current prompt optimizations are geared towards models like Deepseek.
+2 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*一个**100%本地运行的 Manus AI 替代品**,支持语音的 AI 助手,可自主浏览网页、编写代码、规划任务,所有数据仅保存在你的设备上。专为本地推理模型设计,完全在你的硬件上运行,确保隐私无忧,无需云端依赖。*
@@ -230,6 +230,7 @@ provider_server_address = # 当 is_local = False 时,对于大多数 API 通
| Hugging Face | `huggingface` | 否 | 使用 Hugging Face Inference API 中的模型。 | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
| TogetherAI | `togetherAI` | 否 | 通过 TogetherAI API 使用各种开源模型。| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) |
| OpenRouter | `openrouter` | No | 通过 OpenRouter 使用各种开源模型| [https://openrouter.ai/](https://openrouter.ai/) |
| MiniMax | `minimax` | 否 | 使用 MiniMax 模型(如 MiniMax-M2.7、MiniMax-M2.5)。 | [platform.minimax.io](https://platform.minimax.io/user-center/basic-information) |
*注意:*
* 我们不建议将 `gpt-4o` 或其他 OpenAI 模型用于复杂的网页浏览和任务规划,因为当前的提示优化针对 Deepseek 等模型。
+1 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*一個**100%本地運行的 Manus AI 替代品**,支援語音的 AI 助手,可自主瀏覽網頁、編寫代碼、規劃任務,所有數據僅保存在你的設備上。專為本地推理模型設計,完全在你的硬件上運行,確保隱私無憂,無需雲端依賴。*
+1 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*Un asistente de IA con capacidad de voz que es una **alternativa 100% local a Manus AI**, navega autónomamente por la web, escribe código y planifica tareas manteniendo todos los datos en tu dispositivo. Diseñado para modelos de razonamiento local, funciona completamente en tu hardware, garantizando privacidad total y cero dependencia de la nube.*
+1 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*Un assistant IA avec reconnaissance vocale qui est une **alternative 100% locale à Manus AI**, navigue de manière autonome sur le web, écrit du code et planifie des tâches tout en gardant toutes les données sur votre appareil. Conçu pour des modèles de raisonnement locaux, il fonctionne entièrement sur votre matériel, garantissant une confidentialité totale et zéro dépendance au cloud.*
+1 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*音声対応のAIアシスタントで、**100%ローカルで動作するManus AIの代替品**です。自律的にウェブを閲覧し、コードを書き、タスクを計画し、すべてのデータをデバイス上に保持します。ローカル推論モデル向けに設計されており、完全にあなたのハードウェア上で動作し、プライバシーを保証し、クラウドへの依存をゼロにします。*
+1 -1
View File
@@ -4,7 +4,7 @@
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md)
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*Um assistente de IA com reconhecimento de voz que é uma **alternativa 100% local ao Manus AI**, navega autonomamente na web, escreve código e planeja tarefas enquanto mantém todos os dados no seu dispositivo. Projetado para modelos de raciocínio local, funciona inteiramente no seu hardware, garantindo total privacidade e zero dependência de nuvem.*
+692
View File
@@ -0,0 +1,692 @@
# AgenticSeek: Gizlilik Odaklı, Yerel Manus Alternatifi
<p align="center">
<img align="center" src="./media/agentic_seek_logo.png" width="300" height="300" alt="Agentic Seek Logo">
<p>
English | [中文](./README_CHS.md) | [繁體中文](./README_CHT.md) | [Français](./README_FR.md) | [日本語](./README_JP.md) | [Português (Brasil)](./README_PTBR.md) | [Español](./README_ES.md) | [Türkçe](./README_TR.md)
*Sesli komut destekli bu yapay zeka asistanı, **Manus AI'ya %100 yerel bir alternatiftir**. Web'de otonom olarak gezinir, kod yazar ve görevleri planlar; tüm verilerinizi cihazınızda tutar. Yerel akıl yürütme modelleri için tasarlanmış olup tamamen kendi donanımınızda çalışır, eksiksiz gizlilik ve sıfır bulut bağımlılığı sağlar.*
[![AgenticSeek'i Ziyaret Et](https://img.shields.io/static/v1?label=Website&message=AgenticSeek&color=blue&style=flat-square)](https://fosowl.github.io/agenticSeek.html) ![License](https://img.shields.io/badge/license-GPL--3.0-green) [![Discord](https://img.shields.io/badge/Discord-Join%20Us-7289DA?logo=discord&logoColor=white)](https://discord.gg/8hGDaME3TC) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fosowl.svg?style=social&label=Update%20%40Fosowl)](https://x.com/Martin993886460) [![GitHub stars](https://img.shields.io/github/stars/Fosowl/agenticSeek?style=social)](https://github.com/Fosowl/agenticSeek/stargazers)
### Neden AgenticSeek?
* 🔒 Tamamen Yerel ve Gizli - Her şey kendi makinenizde çalışır — bulut yok, veri paylaşımı yok. Dosyalarınız, konuşmalarınız ve aramalarınız gizli kalır.
* 🌐 Akıllı Web Tarama - AgenticSeek internette kendi başına gezinebilir — arama yapar, okur, bilgi çıkarır, web formlarını doldurur — tamamen eller serbest.
* 💻 Otonom Kodlama Asistanı - Koda mı ihtiyacınız var? Python, C, Go, Java ve daha fazlasında program yazabilir, hata ayıklayabilir ve çalıştırabilir — denetim olmadan.
* 🧠 Akıllı Ajan Seçimi - Siz sorarsınız, o otomatik olarak iş için en uygun ajanı belirler. Elinizin altında uzmanlardan oluşan bir ekip gibi.
* 📋 Karmaşık Görevleri Planlar ve Yürütür - Seyahat planlamasından karmaşık projelere kadar — büyük görevleri adımlara bölebilir ve birden fazla yapay zeka ajanı kullanarak işleri tamamlayabilir.
* 🎙️ Ses Desteği - Temiz, hızlı, fütüristik ses ve konuşmadan metne dönüştürme ile bilim kurgu filmlerindeki kişisel yapay zekanızla konuşuyormuş gibi etkileşim kurabilirsiniz. (Geliştirme aşamasında)
### **Demo**
> *AgenticSeek projesini arayabilir misin, hangi becerilerin gerektiğini öğrenip ardından CV_candidates.zip dosyasını açarak projeye en uygun adayları söyleyebilir misin?*
https://github.com/user-attachments/assets/b8ca60e9-7b3b-4533-840e-08f9ac426316
Uyarı: Bu demo ve görünen tüm dosyalar (ör: CV_candidates.zip) tamamen kurgusaldır. Bir şirket değiliz, aday değil açık kaynak katkıda bulunanlar arıyoruz.
> 🛠⚠️ **Aktif Olarak Geliştirilmektedir**
> 🙏 Bu proje bir yan proje olarak başladı ve sıfır yol haritası, sıfır finansmanla geliştirilmektedir. GitHub Trending'e girerek beklentilerin çok ötesine geçti. Katkılar, geri bildirimler ve sabır derinden takdir edilmektedir.
## Ön Gereksinimler
Başlamadan önce aşağıdaki yazılımların yüklü olduğundan emin olun:
* **Git:** Depoyu klonlamak için. [Git İndir](https://git-scm.com/downloads)
* **Python 3.10.x:** Python 3.10.x sürümünü kullanmanızı şiddetle tavsiye ederiz. Diğer sürümler bağımlılık hatalarına yol açabilir. [Python 3.10 İndir](https://www.python.org/downloads/release/python-3100/) (3.10.x sürümünü seçin).
* **Docker Engine ve Docker Compose:** SearxNG gibi paketlenmiş servisleri çalıştırmak için.
* Docker Desktop yükleyin (Docker Compose V2 dahildir): [Windows](https://docs.docker.com/desktop/install/windows-install/) | [Mac](https://docs.docker.com/desktop/install/mac-install/) | [Linux](https://docs.docker.com/desktop/install/linux-install/)
* Alternatif olarak, Linux üzerinde Docker Engine ve Docker Compose'u ayrı ayrı yükleyin: [Docker Engine](https://docs.docker.com/engine/install/) | [Docker Compose](https://docs.docker.com/compose/install/) (Compose V2 yüklediğinizden emin olun, ör: `sudo apt-get install docker-compose-plugin`).
### 1. **Depoyu klonlayın ve kurulumu yapın**
```sh
git clone https://github.com/Fosowl/agenticSeek.git
cd agenticSeek
mv .env.example .env
```
### 2. .env dosyasının içeriğini değiştirin
```sh
SEARXNG_BASE_URL="http://searxng:8080" # CLI modunda çalıştırıyorsanız http://127.0.0.1:8080 kullanın
REDIS_BASE_URL="redis://redis:6379/0"
WORK_DIR="/Users/mlg/Documents/workspace_for_ai"
OLLAMA_PORT="11434"
LM_STUDIO_PORT="1234"
CUSTOM_ADDITIONAL_LLM_PORT="11435"
OPENAI_API_KEY='optional'
DEEPSEEK_API_KEY='optional'
OPENROUTER_API_KEY='optional'
TOGETHER_API_KEY='optional'
GOOGLE_API_KEY='optional'
ANTHROPIC_API_KEY='optional'
```
`.env` dosyasını ihtiyaçlarınıza göre güncelleyin:
- **SEARXNG_BASE_URL**: CLI modunda çalışmıyorsanız değiştirmeyin.
- **REDIS_BASE_URL**: Değiştirmeyin.
- **WORK_DIR**: Yerel makinenizdeki çalışma dizininin yolu. AgenticSeek bu dosyaları okuyabilir ve onlarla etkileşim kurabilir.
- **OLLAMA_PORT**: Ollama servisi için port numarası.
- **LM_STUDIO_PORT**: LM Studio servisi için port numarası.
- **CUSTOM_ADDITIONAL_LLM_PORT**: Ek özel LLM servisi için port.
**API anahtarları, LLM'yi yerel olarak çalıştırmayı tercih eden kullanıcılar için tamamen isteğe bağlıdır. Bu projenin birincil amacı da budur. Yeterli donanımınız varsa boş bırakın.**
### 3. **Docker'ı Başlatın**
Docker'ın sisteminizde yüklü ve çalışır durumda olduğundan emin olun. Docker'ı aşağıdaki komutlarla başlatabilirsiniz:
- **Linux/macOS:**
Terminal açın ve çalıştırın:
```sh
sudo systemctl start docker
```
Veya yüklüyse Docker Desktop'ı uygulama menüsünden başlatın.
- **Windows:**
Başlat menüsünden Docker Desktop'ı başlatın.
Docker'ın çalıştığını doğrulamak için:
```sh
docker info
```
Docker kurulumunuz hakkında bilgi görüyorsanız, düzgün çalışıyor demektir.
Özet için aşağıdaki [Yerel Sağlayıcılar](#yerel-sağlayıcılar-listesi) tablosuna bakın.
Sonraki adım: [AgenticSeek'i yerel olarak çalıştırın](#servisleri-başlatın-ve-çalıştırın)
*Sorun yaşıyorsanız [Sorun Giderme](#sorun-giderme) bölümüne bakın.*
*Donanımınız LLM'leri yerel olarak çalıştıramıyorsa [API ile Çalıştırma Kurulumu](#api-ile-çalıştırma-kurulumu) bölümüne bakın.*
*Ayrıntılı `config.ini` açıklamaları için [Yapılandırma](#yapılandırma) bölümüne bakın.*
---
## LLM'yi Makinenizde Yerel Olarak Çalıştırma Kurulumu
**Donanım Gereksinimleri:**
LLM'leri yerel olarak çalıştırmak için yeterli donanıma ihtiyacınız olacaktır. En azından Magistral, Qwen veya Deepseek 14B çalıştırabilecek bir GPU gereklidir. Ayrıntılı model/performans önerileri için SSS bölümüne bakın.
**Yerel sağlayıcınızı ayarlayın**
Yerel sağlayıcınızı başlatın (örneğin ollama ile):
AgenticSeek'i ana makinede (CLI modu) çalıştırmayacaksanız, sağlayıcı dinleme adresini ayarlayın:
```sh
export OLLAMA_HOST=0.0.0.0:11434
```
Ardından sağlayıcınızı başlatın:
```sh
ollama serve
```
Desteklenen yerel sağlayıcıların listesi için aşağıya bakın.
**config.ini dosyasını güncelleyin**
config.ini dosyasında provider_name'i desteklenen bir sağlayıcıya ve provider_model'i sağlayıcınızın desteklediği bir LLM'e ayarlayın. *Magistral* veya *Deepseek* gibi akıl yürütme modellerini öneriyoruz.
Gerekli donanım için README sonundaki **SSS** bölümüne bakın.
```sh
[MAIN]
is_local = True # Yerel mi yoksa uzak sağlayıcı ile mi çalıştırıyorsunuz.
provider_name = ollama # veya lm-studio, openai, vb.
provider_model = deepseek-r1:14b # donanımınıza uygun bir model seçin
provider_server_address = 127.0.0.1:11434
agent_name = Jarvis # yapay zekanızın adı
recover_last_session = True # önceki oturumu kurtarıp kurtarmayacağı
save_session = True # mevcut oturumu hatırlayıp hatırlamayacağı
speak = False # metinden sese dönüştürme
listen = False # sesten metne dönüştürme, yalnızca CLI için, deneysel
jarvis_personality = False # daha "Jarvis" benzeri bir kişilik kullanıp kullanmayacağı (deneysel)
languages = en zh # Dil listesi, metinden sese varsayılan olarak listedeki ilk dili kullanır
[BROWSER]
headless_browser = True # CLI modunda değilseniz değiştirmeyin.
stealth_mode = True # Tarayıcı algılamasını azaltmak için gizli selenium kullanır
```
**Uyarı**:
- `config.ini` dosya biçimi yorum satırlarını desteklemez.
Örnek yapılandırmayı doğrudan kopyalayıp yapıştırmayın, çünkü yorum satırları hatalara neden olur. Bunun yerine `config.ini` dosyasını yorum satırları olmadan istediğiniz ayarlarla manuel olarak düzenleyin.
- LLM çalıştırmak için LM-studio kullanıyorsanız provider_name'i `openai` olarak *AYARLAMAYIN*. `lm-studio` olarak ayarlayın.
- Bazı sağlayıcılar (ör: lm-studio) IP'nin önünde `http://` gerektirir. Örneğin: `http://127.0.0.1:1234`
**Yerel Sağlayıcılar Listesi**
| Sağlayıcı | Yerel mi? | Açıklama |
|-----------|--------|-----------------------------------------------------------|
| ollama | Evet | Ollama kullanarak LLM'leri kolayca yerel olarak çalıştırın |
| lm-studio | Evet | LM Studio ile LLM'leri yerel olarak çalıştırın (`provider_name` değerini `lm-studio` olarak ayarlayın)|
| openai | Evet | OpenAI uyumlu API kullanın (ör: llama.cpp sunucusu) |
Sonraki adım: [Servisleri başlatın ve AgenticSeek'i çalıştırın](#servisleri-başlatın-ve-çalıştırın)
*Sorun yaşıyorsanız [Sorun Giderme](#sorun-giderme) bölümüne bakın.*
*Donanımınız LLM'leri yerel olarak çalıştıramıyorsa [API ile Çalıştırma Kurulumu](#api-ile-çalıştırma-kurulumu) bölümüne bakın.*
*Ayrıntılı `config.ini` açıklamaları için [Yapılandırma](#yapılandırma) bölümüne bakın.*
## API ile Çalıştırma Kurulumu
Bu kurulum harici, bulut tabanlı LLM sağlayıcılarını kullanır. Seçtiğiniz servisten bir API anahtarına ihtiyacınız olacaktır.
**1. Bir API Sağlayıcısı Seçin ve API Anahtarı Alın:**
Aşağıdaki [API Sağlayıcıları Listesi](#api-sağlayıcıları-listesi) bölümüne bakın. Kaydolmak ve API anahtarı almak için web sitelerini ziyaret edin.
**2. API Anahtarınızı Ortam Değişkeni Olarak Ayarlayın:**
* **Linux/macOS:**
Terminal açın ve `export` komutunu kullanın. Kalıcılık için bunu shell profil dosyanıza (ör: `~/.bashrc`, `~/.zshrc`) eklemeniz önerilir.
```sh
export PROVIDER_API_KEY="api_anahtarınız"
# PROVIDER_API_KEY'i ilgili değişken adıyla değiştirin, ör: OPENAI_API_KEY, GOOGLE_API_KEY
```
TogetherAI örneği:
```sh
export TOGETHER_API_KEY="xxxxxxxxxxxxxxxxxxxxxx"
```
* **Windows:**
* **Komut İstemi (Geçerli oturum için geçici):**
```cmd
set PROVIDER_API_KEY=api_anahtarınız
```
* **PowerShell (Geçerli oturum için geçici):**
```powershell
$env:PROVIDER_API_KEY="api_anahtarınız"
```
* **Kalıcı:** Windows arama çubuğunda "ortam değişkenleri" arayın, "Sistem ortam değişkenlerini düzenle" seçeneğine tıklayın, ardından "Ortam Değişkenleri..." düğmesine tıklayın. Uygun adla (ör: `OPENAI_API_KEY`) yeni bir Kullanıcı değişkeni ekleyin ve değer olarak anahtarınızı girin.
*(Daha fazla ayrıntı için SSS: [API anahtarlarını nasıl ayarlarım?](#api-anahtarlarını-nasıl-ayarlarım) bölümüne bakın.)*
**3. `config.ini` Dosyasını Güncelleyin:**
```ini
[MAIN]
is_local = False
provider_name = openai # Veya google, deepseek, togetherAI, huggingface
provider_model = gpt-3.5-turbo # Veya gemini-1.5-flash, deepseek-chat, mistralai/Mixtral-8x7B-Instruct-v0.1 vb.
provider_server_address = # is_local = False olduğunda çoğu API için genellikle yok sayılır veya boş bırakılabilir
# ... diğer ayarlar ...
```
*Uyarı:* config.ini değerlerinin sonunda boşluk olmadığından emin olun.
**API Sağlayıcıları Listesi**
| Sağlayıcı | `provider_name` | Yerel mi? | Açıklama | API Anahtarı Bağlantısı (Örnekler) |
|--------------|-----------------|--------|---------------------------------------------------|---------------------------------------------|
| OpenAI | `openai` | Hayır | OpenAI API'si üzerinden ChatGPT modellerini kullanın. | [platform.openai.com/signup](https://platform.openai.com/signup) |
| Google Gemini| `google` | Hayır | Google AI Studio üzerinden Google Gemini modellerini kullanın. | [aistudio.google.com/keys](https://aistudio.google.com/keys) |
| Deepseek | `deepseek` | Hayır | Deepseek API'si üzerinden Deepseek modellerini kullanın. | [platform.deepseek.com](https://platform.deepseek.com) |
| Hugging Face | `huggingface` | Hayır | Hugging Face Inference API'den modelleri kullanın. | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
| TogetherAI | `togetherAI` | Hayır | TogetherAI API'si üzerinden çeşitli açık kaynak modelleri kullanın.| [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) |
| OpenRouter | `openrouter` | Hayır | OpenRouter Modellerini kullanın| [https://openrouter.ai/](https://openrouter.ai/) |
*Not:*
* Karmaşık web tarama ve görev planlama için `gpt-4o` veya diğer OpenAI modellerini kullanmanızı önermiyoruz, çünkü mevcut prompt optimizasyonları Deepseek gibi modellere yöneliktir.
* Kodlama/bash görevleri Gemini ile sorun yaşayabilir, çünkü Deepseek için optimize edilmiş biçimlendirme talimatlarına kesinlikle uymayabilir.
* `is_local = False` olduğunda `config.ini` içindeki `provider_server_address` genellikle kullanılmaz, çünkü API uç noktaları genellikle ilgili sağlayıcı kütüphanesinde sabit kodlanmıştır.
Sonraki adım: [Servisleri başlatın ve AgenticSeek'i çalıştırın](#servisleri-başlatın-ve-çalıştırın)
*Sorun yaşıyorsanız **Bilinen Sorunlar** bölümüne bakın*
*Ayrıntılı yapılandırma dosyası açıklamaları için **Yapılandırma** bölümüne bakın.*
---
## Servisleri Başlatın ve Çalıştırın
Varsayılan olarak AgenticSeek tamamen Docker içinde çalıştırılır.
**Seçenek 1:** Docker'da çalıştırın, web arayüzünü kullanın:
Gerekli servisleri başlatın. Bu, docker-compose.yml'deki tüm servisleri başlatacaktır:
- searxng
- redis (searxng için gerekli)
- frontend
- backend (web arayüzü kullanırken `full` parametresi ile)
```sh
./start_services.sh full # MacOS
start start_services.cmd full # Windows
```
**Uyarı:** Bu adım tüm Docker imajlarını indirip yükleyecektir, bu işlem 30 dakikaya kadar sürebilir. Servisleri başlattıktan sonra, herhangi bir mesaj göndermeden önce backend servisinin tamamen çalışır duruma gelmesini bekleyin (loglarda **backend: "GET /health HTTP/1.1" 200 OK** mesajını görmelisiniz). İlk çalıştırmada backend servisinin başlaması 5 dakika sürebilir.
`http://localhost:3000/` adresine gidin ve web arayüzünü görmelisiniz.
*Servis başlatma sorun giderme:* Bu betikler başarısız olursa, Docker Engine'in çalıştığından ve Docker Compose'un (V2, `docker compose`) doğru şekilde yüklendiğinden emin olun. Hata mesajları için terminal çıktısını kontrol edin. Bkz. [SSS: AgenticSeek'i veya betiklerini çalıştırırken hata alıyorum.](#sss-sorun-giderme)
**Seçenek 2:** CLI modu:
CLI arayüzü ile çalıştırmak için paketleri ana makineye yüklemeniz gerekir:
```sh
./install.sh
./install.bat # windows
```
Ardından `config.ini` içindeki SEARXNG_BASE_URL değerini şu şekilde değiştirmelisiniz:
```sh
SEARXNG_BASE_URL="http://localhost:8080"
```
Gerekli servisleri başlatın. Bu, docker-compose.yml'deki bazı servisleri başlatacaktır:
- searxng
- redis (searxng için gerekli)
- frontend
```sh
./start_services.sh # MacOS
start start_services.cmd # Windows
```
Çalıştırın: `uv run python -m ensurepip` komutu ile uv'nin pip'i etkin olduğundan emin olun.
CLI'yi kullanın: `uv run cli.py`
---
## Kullanım
Servislerin `./start_services.sh full` ile çalıştığından emin olun ve web arayüzü için `localhost:3000` adresine gidin.
Yapılandırmada `listen = True` ayarlayarak konuşmadan metne dönüştürmeyi de kullanabilirsiniz. Yalnızca CLI modu için.
Çıkmak için `goodbye` yazın/söyleyin.
İşte bazı kullanım örnekleri:
> *Python'da bir yılan oyunu yap!*
> *Rennes, Fransa'daki en iyi kafeleri internette ara ve adres bilgileriyle birlikte üçünü rennes_cafes.txt dosyasına kaydet.*
> *Bir sayının faktöriyelini hesaplayan bir Go programı yaz, çalışma alanına factorial.go olarak kaydet*
> *summer_pictures klasöründeki tüm JPG dosyalarını ara, bugünün tarihiyle yeniden adlandır ve yeniden adlandırılan dosyaların listesini photos_list.txt dosyasına kaydet*
> *2024'ün popüler bilim kurgu filmlerini internette ara ve bu gece izlemek için üçünü seç. Listeyi movie_night.txt dosyasına kaydet.*
> *2025'in en son yapay zeka haberleri makalelerini internette ara, üçünü seç ve başlıklarını ve özetlerini kazımak için bir Python betiği yaz. Betiği news_scraper.py olarak ve özetleri /home/projects içinde ai_news.txt olarak kaydet*
> *Cuma, internette ücretsiz bir hisse senedi fiyat API'si ara, supersuper7434567@gmail.com ile kaydol ve ardından API'yi kullanarak Tesla'nın günlük fiyatlarını çekmek için bir Python betiği yaz, sonuçları stock_prices.csv dosyasına kaydet*
*Form doldurma yeteneklerinin hâlâ deneysel olduğunu ve başarısız olabileceğini unutmayın.*
Sorgunuzu yazdıktan sonra AgenticSeek göreve en uygun ajanı atayacaktır.
Bu erken bir prototip olduğundan, ajan yönlendirme sistemi sorgunuza göre her zaman doğru ajanı atamayabilir.
Bu nedenle, ne istediğiniz ve yapay zekanın nasıl ilerlemesi gerektiği konusunda çok açık olun. Örneğin, web araması yapmasını istiyorsanız şunu demeyin:
`Solo seyahat için iyi ülkeler biliyor musun?`
Bunun yerine şöyle sorun:
`Web araması yap ve solo seyahat için en iyi ülkeleri bul`
---
## **LLM'yi Kendi Sunucunuzda Çalıştırma Kurulumu**
Güçlü bir bilgisayarınız veya kullanabileceğiniz bir sunucunuz varsa, ancak dizüstü bilgisayarınızdan kullanmak istiyorsanız, özel LLM sunucumuzu kullanarak LLM'yi uzak bir sunucuda çalıştırma seçeneğiniz vardır.
Yapay zeka modelini çalıştıracak "sunucunuzda" IP adresini alın
```sh
ip a | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | cut -d/ -f1 # yerel IP
curl https://ipinfo.io/ip # genel IP
```
Not: Windows veya macOS için IP adresini bulmak üzere sırasıyla ipconfig veya ifconfig kullanın.
Depoyu klonlayın ve `server/` klasörüne girin.
```sh
git clone --depth 1 https://github.com/Fosowl/agenticSeek.git
cd agenticSeek/llm_server/
```
Sunucuya özel gereksinimleri yükleyin:
```sh
pip3 install -r requirements.txt
```
Sunucu betiğini çalıştırın.
```sh
python3 app.py --provider ollama --port 3333
```
LLM servisi olarak `ollama` veya `llamacpp` arasında seçim yapabilirsiniz.
Şimdi kişisel bilgisayarınızda:
`config.ini` dosyasında `provider_name` değerini `server` ve `provider_model` değerini `deepseek-r1:xxb` olarak ayarlayın.
`provider_server_address` değerini modeli çalıştıracak makinenin IP adresine ayarlayın.
```sh
[MAIN]
is_local = False
provider_name = server
provider_model = deepseek-r1:70b
provider_server_address = http://x.x.x.x:3333
```
Sonraki adım: [Servisleri başlatın ve AgenticSeek'i çalıştırın](#servisleri-başlatın-ve-çalıştırın)
---
## Konuşmadan Metne
Uyarı: Konuşmadan metne özelliği şu anda yalnızca CLI modunda çalışmaktadır.
Konuşmadan metne özelliğinin şu anda yalnızca İngilizce dilinde çalıştığını lütfen unutmayın.
Konuşmadan metne işlevi varsayılan olarak devre dışıdır. Etkinleştirmek için config.ini dosyasında listen seçeneğini True olarak ayarlayın:
```
listen = True
```
Etkinleştirildiğinde, konuşmadan metne özelliği girdinizi işlemeye başlamadan önce bir tetikleyici anahtar kelimeyi (ajanın adı) dinler. Ajanın adını *config.ini* dosyasındaki `agent_name` değerini güncelleyerek özelleştirebilirsiniz:
```
agent_name = Friday
```
En iyi tanıma performansı için ajan adı olarak "John" veya "Emma" gibi yaygın bir İngilizce isim kullanmanızı öneririz.
Transkript görünmeye başladığında, uyandırmak için ajanın adını yüksek sesle söyleyin (ör: "Friday").
Sorgunuzu net bir şekilde söyleyin.
Sisteme devam etmesi gerektiğini bildirmek için isteğinizi bir onay ifadesiyle bitirin. Onay ifadesi örnekleri:
```
"do it", "go ahead", "execute", "run", "start", "thanks", "would ya", "please", "okay?", "proceed", "continue", "go on", "do that", "go it", "do you understand?"
```
## Yapılandırma
Örnek yapılandırma:
```
[MAIN]
is_local = True
provider_name = ollama
provider_model = deepseek-r1:32b
provider_server_address = http://127.0.0.1:11434 # Ollama örneği; LM-Studio için http://127.0.0.1:1234 kullanın
agent_name = Friday
recover_last_session = False
save_session = False
speak = False
listen = False
jarvis_personality = False
languages = en zh # TTS ve potansiyel yönlendirme için dil listesi.
[BROWSER]
headless_browser = False
stealth_mode = False
```
**`config.ini` Ayarlarının Açıklaması**:
* **`[MAIN]` Bölümü:**
* `is_local`: Yerel LLM sağlayıcısı (Ollama, LM-Studio, yerel OpenAI uyumlu sunucu) veya kendi barındırdığınız sunucu seçeneğini kullanıyorsanız `True`. Bulut tabanlı API (OpenAI, Google, vb.) kullanıyorsanız `False`.
* `provider_name`: LLM sağlayıcısını belirtir.
* Yerel seçenekler: `ollama`, `lm-studio`, `openai` (yerel OpenAI uyumlu sunucular için), `server` (kendi barındırdığınız sunucu kurulumu için).
* API seçenekleri: `openai`, `google`, `deepseek`, `huggingface`, `togetherAI`.
* `provider_model`: Seçilen sağlayıcı için belirli model adı veya kimliği (ör: Ollama için `deepseekcoder:6.7b`, OpenAI API için `gpt-3.5-turbo`, TogetherAI için `mistralai/Mixtral-8x7B-Instruct-v0.1`).
* `provider_server_address`: LLM sağlayıcınızın adresi.
* Yerel sağlayıcılar için: ör: Ollama için `http://127.0.0.1:11434`, LM-Studio için `http://127.0.0.1:1234`.
* `server` sağlayıcı türü için: Kendi barındırdığınız LLM sunucunuzun adresi (ör: `http://sunucu_ip_adresiniz:3333`).
* Bulut API'leri (`is_local = False`) için: Genellikle yok sayılır veya boş bırakılabilir, çünkü API uç noktası genellikle istemci kütüphanesi tarafından işlenir.
* `agent_name`: Yapay zeka asistanının adı (ör: Friday). Etkinleştirilmişse konuşmadan metne için tetikleyici kelime olarak kullanılır.
* `recover_last_session`: Önceki oturumun durumunu kurtarmaya çalışmak için `True`, yeni başlamak için `False`.
* `save_session`: Mevcut oturumun durumunu olası kurtarma için kaydetmek için `True`, aksi takdirde `False`.
* `speak`: Metinden sese sesli çıktıyı etkinleştirmek için `True`, devre dışı bırakmak için `False`.
* `listen`: Konuşmadan metne sesli girdiyi etkinleştirmek için `True` (yalnızca CLI modu), devre dışı bırakmak için `False`.
* `work_dir`: **Kritik:** AgenticSeek'in dosya okuyacağı/yazacağı dizin. **Bu yolun sisteminizde geçerli ve erişilebilir olduğundan emin olun.**
* `jarvis_personality`: Daha "Jarvis-benzeri" bir sistem istemi kullanmak için `True` (deneysel), standart istem için `False`.
* `languages`: Virgülle ayrılmış dil listesi (ör: `en, zh, fr`). TTS ses seçimi için kullanılır (varsayılan olarak ilki) ve LLM yönlendiricisine yardımcı olabilir. Yönlendirici verimliliği için çok fazla veya çok benzer dil kullanmaktan kaçının.
* **`[BROWSER]` Bölümü:**
* `headless_browser`: Otomatik tarayıcıyı görünür pencere olmadan çalıştırmak için `True` (web arayüzü veya etkileşimsiz kullanım için önerilir). Tarayıcı penceresini göstermek için `False` (CLI modu veya hata ayıklama için kullanışlıdır).
* `stealth_mode`: Tarayıcı otomasyonunun tespit edilmesini zorlaştıran önlemleri etkinleştirmek için `True`. Anticaptcha gibi tarayıcı eklentilerinin manuel olarak yüklenmesini gerektirebilir.
Bu bölüm desteklenen LLM sağlayıcı türlerini özetler. `config.ini` dosyasında yapılandırın.
**Yerel Sağlayıcılar (Kendi Donanımınızda Çalışır):**
| `config.ini`'deki Sağlayıcı Adı | `is_local` | Açıklama | Kurulum Bölümü |
|-------------------------------|------------|-----------------------------------------------------------------------------|------------------------------------------------------------------|
| `ollama` | `True` | Ollama kullanarak yerel LLM'leri sunun. | [LLM'yi yerel olarak çalıştırma kurulumu](#llmyi-makinenizde-yerel-olarak-çalıştırma-kurulumu) |
| `lm-studio` | `True` | LM-Studio kullanarak yerel LLM'leri sunun. | [LLM'yi yerel olarak çalıştırma kurulumu](#llmyi-makinenizde-yerel-olarak-çalıştırma-kurulumu) |
| `openai` (yerel sunucu için) | `True` | OpenAI uyumlu API sunan yerel bir sunucuya bağlanın (ör: llama.cpp). | [LLM'yi yerel olarak çalıştırma kurulumu](#llmyi-makinenizde-yerel-olarak-çalıştırma-kurulumu) |
| `server` | `False` | Başka bir makinede çalışan AgenticSeek kendi barındırmalı LLM sunucusuna bağlanın. | [LLM'yi kendi sunucunuzda çalıştırma kurulumu](#llmyi-kendi-sunucunuzda-çalıştırma-kurulumu) |
**API Sağlayıcıları (Bulut Tabanlı):**
| `config.ini`'deki Sağlayıcı Adı | `is_local` | Açıklama | Kurulum Bölümü |
|-------------------------------|------------|--------------------------------------------------|-----------------------------------------------------|
| `openai` | `False` | OpenAI'ın resmi API'sini kullanın (ör: GPT-3.5, GPT-4). | [API ile çalıştırma kurulumu](#api-ile-çalıştırma-kurulumu) |
| `google` | `False` | Google Gemini modellerini API üzerinden kullanın. | [API ile çalıştırma kurulumu](#api-ile-çalıştırma-kurulumu) |
| `deepseek` | `False` | Deepseek'in resmi API'sini kullanın. | [API ile çalıştırma kurulumu](#api-ile-çalıştırma-kurulumu) |
| `huggingface` | `False` | Hugging Face Inference API'yi kullanın. | [API ile çalıştırma kurulumu](#api-ile-çalıştırma-kurulumu) |
| `togetherAI` | `False` | TogetherAI API'si üzerinden çeşitli açık modelleri kullanın. | [API ile çalıştırma kurulumu](#api-ile-çalıştırma-kurulumu) |
---
## Sorun Giderme
Sorunlarla karşılaşırsanız bu bölüm rehberlik sağlar.
# Bilinen Sorunlar
## ChromeDriver Sorunları
**Hata Örneği:** `SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version XXX`
### Temel Neden
ChromeDriver sürüm uyumsuzluğu şu durumlarda oluşur:
1. Yüklü ChromeDriver sürümünüz Chrome tarayıcı sürümünüzle eşleşmiyor
2. Docker ortamlarında `undetected_chromedriver`, bağlanan ikili dosyayı atlayarak kendi ChromeDriver sürümünü indirebilir
### Çözüm Adımları
#### 1. Chrome Sürümünüzü Kontrol Edin
Google Chrome'u açın → `Ayarlar > Chrome Hakkında` bölümünden sürümünüzü bulun (ör: "Sürüm 134.0.6998.88")
#### 2. Eşleşen ChromeDriver'ı İndirin
**Chrome 115 ve sonrası için:** [Chrome for Testing API](https://googlechromelabs.github.io/chrome-for-testing/) kullanın
- Chrome for Testing uygunluk panosunu ziyaret edin
- Chrome sürümünüzü veya en yakın eşleşmeyi bulun
- İşletim sisteminiz için ChromeDriver'ı indirin (Docker ortamları için Linux64)
**Eski Chrome sürümleri için:** [Eski ChromeDriver indirmeleri](https://chromedriver.chromium.org/downloads) kullanın
![Chrome for Testing'den ChromeDriver İndirin](./media/chromedriver_readme.png)
#### 3. ChromeDriver'ı Yükleyin (Bir Yöntem Seçin)
**Yöntem A: Proje Kök Dizini (Docker için Önerilir)**
```bash
# İndirilen chromedriver ikili dosyasını proje kök dizinine yerleştirin
cp path/to/downloaded/chromedriver ./chromedriver
chmod +x ./chromedriver # Linux/macOS'ta çalıştırılabilir yapın
```
**Yöntem B: Sistem PATH'i**
```bash
# Linux/macOS
sudo mv chromedriver /usr/local/bin/
sudo chmod +x /usr/local/bin/chromedriver
# Windows: chromedriver.exe dosyasını PATH'inizdeki bir klasöre yerleştirin
```
#### 4. Kurulumu Doğrulayın
```bash
# ChromeDriver sürümünü test edin
./chromedriver --version
# Veya PATH'teyse:
chromedriver --version
```
### Docker'a Özel Notlar
⚠️ **Docker Kullanıcıları İçin Önemli:**
- Docker volume mount yaklaşımı gizli modda (`undetected_chromedriver`) çalışmayabilir
- **Çözüm**: ChromeDriver'ı proje kök dizinine `./chromedriver` olarak yerleştirin
- Uygulama bu ikili dosyayı otomatik olarak algılayıp kullanacaktır
- Loglarda şunu görmelisiniz: `"Using ChromeDriver from project root: ./chromedriver"`
### Sorun Giderme İpuçları
1. **Hâlâ sürüm uyumsuzluğu mu var?**
- ChromeDriver'ın çalıştırılabilir olduğunu doğrulayın: `ls -la ./chromedriver`
- ChromeDriver sürümünü kontrol edin: `./chromedriver --version`
- Chrome tarayıcı sürümünüzle eşleştiğinden emin olun
2. **Docker container sorunları mı var?**
- Backend loglarını kontrol edin: `docker logs backend`
- Şu mesajı arayın: `"Using ChromeDriver from project root"`
- Bulunamazsa dosyanın var olduğunu ve çalıştırılabilir olduğunu doğrulayın
3. **Chrome for Testing sürümleri**
- Mümkün olduğunca tam sürüm eşleşmesi kullanın
- 134.0.6998.88 sürümü için ChromeDriver 134.0.6998.165 kullanın (en yakın mevcut sürüm)
- Ana sürüm numaraları eşleşmelidir (134 = 134)
### Sürüm Uyumluluk Matrisi
| Chrome Sürümü | ChromeDriver Sürümü | Durum |
|----------------|---------------------|---------|
| 134.0.6998.x | 134.0.6998.165 | ✅ Çalışır |
| 133.0.6943.x | 133.0.6943.141 | ✅ Çalışır |
| 132.0.6834.x | 132.0.6834.159 | ✅ Çalışır |
*En güncel uyumluluk için [Chrome for Testing panosunu](https://googlechromelabs.github.io/chrome-for-testing/) kontrol edin*
`Exception: Failed to initialize browser: Message: session not created: This version of ChromeDriver only supports Chrome version 113
Current browser version is 134.0.6998.89 with binary path`
Bu hata, tarayıcınız ve chromedriver sürümü arasında uyumsuzluk varsa oluşur.
En son sürümü indirmek için şu adrese gidin:
https://developer.chrome.com/docs/chromedriver/downloads
Chrome sürüm 115 veya daha yenisini kullanıyorsanız:
https://googlechromelabs.github.io/chrome-for-testing/
adresinden işletim sisteminize uygun chromedriver sürümünü indirin.
![alt text](./media/chromedriver_readme.png)
Bu bölüm eksikse lütfen bir issue açın.
## Bağlantı Adaptörü Sorunları
```
Exception: Provider lm-studio failed: HTTP request failed: No connection adapters were found for '127.0.0.1:1234/v1/chat/completions'` (Not: port değişebilir)
```
* **Neden:** `config.ini` dosyasında `lm-studio` (veya benzeri yerel OpenAI uyumlu sunucular) için `provider_server_address` değerinde `http://` öneki eksik veya yanlış porta yönlendiriliyor.
* **Çözüm:**
* Adresin `http://` içerdiğinden emin olun. LM-Studio varsayılan olarak genellikle `http://127.0.0.1:1234` kullanır.
* Doğru `config.ini`: `provider_server_address = http://127.0.0.1:1234` (veya gerçek LM-Studio sunucu portunuz).
## SearxNG Temel URL'si Belirtilmemiş
```
raise ValueError("SearxNG base URL must be provided either as an argument or via the SEARXNG_BASE_URL environment variable.")
ValueError: SearxNG base URL must be provided either as an argument or via the SEARXNG_BASE_URL environment variable.`
```
Bu hata, CLI modunu yanlış SearxNG temel URL'si ile çalıştırdığınızda oluşabilir.
SEARXNG_BASE_URL, Docker'da mı yoksa ana makinede mi çalıştırdığınıza göre değişmelidir:
**Ana makinede çalıştırma**: `SEARXNG_BASE_URL="http://localhost:8080"`
**Tamamen Docker'da çalıştırma (web arayüzü)**: `SEARXNG_BASE_URL="http://searxng:8080"`
## SSS
**S: Hangi donanıma ihtiyacım var?**
| Model Boyutu | GPU | Yorum |
|-----------|--------|-----------------------------------------------------------|
| 7B | 8GB VRAM | ⚠️ Önerilmez. Düşük performans, sık halüsinasyonlar ve planlayıcı ajanlar büyük olasılıkla başarısız olur. |
| 14B | 12 GB VRAM (ör: RTX 3060) | ✅ Basit görevler için kullanılabilir. Web tarama ve planlama görevlerinde zorlanabilir. |
| 32B | 24+ GB VRAM (ör: RTX 4090) | 🚀 Çoğu görevde başarılı, görev planlamasında hâlâ zorlanabilir |
| 70B+ | 48+ GB VRAM | 💪 Mükemmel. Gelişmiş kullanım senaryoları için önerilir. |
**S: Hata alıyorum, ne yapmalıyım?**
Yerel sunucunun çalıştığından (`ollama serve`), `config.ini` dosyanızın sağlayıcınızla eşleştiğinden ve bağımlılıkların yüklü olduğundan emin olun. Hiçbiri işe yaramazsa bir issue açmaktan çekinmeyin.
**S: Gerçekten %100 yerel çalışabilir mi?**
Evet, Ollama, lm-studio veya server sağlayıcıları ile tüm konuşmadan metne, LLM ve metinden sese modelleri yerel olarak çalışır. Yerel olmayan seçenekler (OpenAI veya diğer API'ler) isteğe bağlıdır.
**S: Manus varken neden AgenticSeek kullanmalıyım?**
Manus'un aksine, AgenticSeek harici sistemlerden bağımsızlığa öncelik verir, size daha fazla kontrol, gizlilik ve API maliyetinden kaçınma imkânı sunar.
**S: Projenin arkasında kim var?**
Proje benim tarafımdan, bakımcı olarak görev yapan iki arkadaşım ve GitHub'daki açık kaynak topluluğundan katkıda bulunanlarla birlikte oluşturuldu. Bir startup veya herhangi bir kuruluşla bağlantılı değiliz, sadece tutkulu bireylerden oluşan bir grubuz.
X'te kişisel hesabım (https://x.com/Martin993886460) dışındaki herhangi bir AgenticSeek hesabı taklittir.
## Katkıda Bulunma
AgenticSeek'i geliştirmek için geliştiriciler arıyoruz! Açık issue'lara veya tartışmalara göz atın.
[Katkıda bulunma rehberi](./docs/CONTRIBUTING.md)
## Sponsorlar:
AgenticSeek'in yeteneklerini uçuş arama, seyahat planlama veya en iyi alışveriş fırsatlarını yakalama gibi özelliklerle geliştirmek ister misiniz? Daha fazla Jarvis benzeri yetenek açmak için SerpApi ile özel bir araç oluşturmayı düşünün. SerpApi ile, tam kontrolü elinizde tutarken ajanınızı özel görevler için güçlendirebilirsiniz.
<a href="https://serpapi.com/"><img src="./media/banners/sponsor_banner_serpapi.png" height="350" alt="SerpApi Banner" ></a>
Özel araçları nasıl entegre edeceğinizi öğrenmek için [Contributing.md](./docs/CONTRIBUTING.md) dosyasına bakın!
### **Patron Sponsor**:
- [tatra-labs](https://github.com/tatra-labs)
## Bakımcılar:
> [Fosowl](https://github.com/Fosowl) | Paris Saati
> [antoineVIVIES](https://github.com/antoineVIVIES) | Taipei Saati
## Özel Teşekkürler:
> [tcsenpai](https://github.com/tcsenpai) ve [plitc](https://github.com/plitc) Backend dockerizasyonuna yardımları için
[![Star History Chart](https://api.star-history.com/svg?repos=Fosowl/agenticSeek&type=Date)](https://www.star-history.com/#Fosowl/agenticSeek&Date)
+5 -2
View File
@@ -278,8 +278,11 @@ async def process_query(request: QueryRequest):
return JSONResponse(status_code=200, content=query_resp.jsonify())
except Exception as e:
logger.error(f"An error occurred: {str(e)}")
sys.exit(1)
query_resp.answer = f"An error occurred: {str(e)}"
query_resp.reasoning = f"Error: {str(e)}"
return JSONResponse(status_code=500, content=query_resp.jsonify())
finally:
is_generating = False
logger.info("Processing finished")
if config.getboolean('MAIN', 'save_session'):
interaction.save_session()
@@ -296,4 +299,4 @@ if __name__ == "__main__":
port = int(envport)
else:
port = 7777
uvicorn.run(api, host="0.0.0.0", port=7777)
uvicorn.run(api, host="0.0.0.0", port=port)
+1 -2
View File
@@ -1,4 +1,3 @@
version: '3.8'
services:
redis:
@@ -65,7 +64,7 @@ services:
environment:
- NODE_ENV=development
- CHOKIDAR_USEPOLLING=true
- REACT_APP_BACKEND_URL=http://localhost:7777
- REACT_APP_BACKEND_URL=${REACT_APP_BACKEND_URL:-http://localhost:7777}
networks:
- agentic-seek-net
+7 -4
View File
@@ -2,15 +2,18 @@ FROM node:18
WORKDIR /app
# Install dependencies
# Copy package files
COPY agentic-seek-front/package.json agentic-seek-front/package-lock.json ./
RUN npm install
# Install dependencies with explicit bin linking
RUN npm ci && npm rebuild
# Copy application code
COPY agentic-seek-front/ .
# Expose port
# Verify react-scripts is available (catches install issues early)
RUN test -f node_modules/.bin/react-scripts || npm install react-scripts
EXPOSE 3000
# Run the application
CMD ["npm", "start"]
+3 -2
View File
@@ -1,4 +1,3 @@
kokoro==0.9.4
certifi==2025.4.26
fastapi>=0.115.12
flask>=3.1.0
@@ -19,7 +18,6 @@ transformers>=4.46.3
torch>=2.4.1
ollama>=0.4.7
scipy>=1.9.3
soundfile>=0.13.1
protobuf>=3.20.3
termcolor>=2.4.0
pypdf>=5.4.0
@@ -46,3 +44,6 @@ openai
sniffio
ordered_set
pypinyin
# Optional: TTS support (requires Python <3.12)
# pip install kokoro==0.9.4 soundfile ipython
+13 -1
View File
@@ -76,7 +76,12 @@ class PlannerAgent(Agent):
if blocks == None:
return []
for block in blocks:
try:
line_json = json.loads(block)
except json.JSONDecodeError as e:
self.logger.warning(f"Failed to parse JSON block: {e}")
pretty_print(f"JSON parsing error: {e}", color="warning")
return []
if 'plan' in line_json:
for task in line_json['plan']:
if task['agent'].lower() not in [ag_name.lower() for ag_name in self.agents.keys()]:
@@ -142,17 +147,23 @@ class PlannerAgent(Agent):
pretty_print(f"{task['agent']} -> {task['task']}", color="info")
pretty_print("▔▗ E N D ▖▔", color="status")
async def make_plan(self, prompt: str) -> str:
async def make_plan(self, prompt: str, max_retries: int = 4) -> str:
"""
Asks the LLM to make a plan.
Args:
prompt (str): The prompt to be sent to the LLM.
max_retries (int): Maximum number of retries before giving up.
Returns:
str: The plan made by the LLM.
"""
ok = False
answer = None
retries = 0
while not ok:
if retries >= max_retries:
pretty_print(f"Failed to make a plan after {max_retries} attempts. Giving up.", color="failure")
self.logger.warning(f"make_plan exceeded max retries ({max_retries}).")
return []
animate_thinking("Thinking...", color="status")
self.memory.push('user', prompt)
answer, reasoning = await self.llm_request()
@@ -163,6 +174,7 @@ class PlannerAgent(Agent):
self.show_plan(agents_tasks, answer)
prompt = f"Failed to parse the tasks. Please write down your task followed by a json plan within ```json. Do not ask for clarification.\n"
pretty_print("Failed to make plan. Retrying...", color="warning")
retries += 1
continue
self.show_plan(agents_tasks, answer)
ok = True
+76 -6
View File
@@ -2,7 +2,7 @@ from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.common.action_chains import ActionChains
@@ -15,11 +15,13 @@ import undetected_chromedriver as uc
import chromedriver_autoinstaller
import certifi
import ssl
import subprocess
import time
import random
import os
import shutil
import uuid
import socket
import tempfile
import markdownify
import sys
@@ -76,20 +78,51 @@ def get_random_user_agent() -> str:
]
return random.choice(user_agents)
def get_chromedriver_version(chromedriver_path: str) -> str:
"""Get the major version of a chromedriver binary. Returns empty string on failure."""
try:
result = subprocess.run(
[chromedriver_path, "--version"],
capture_output=True, text=True, timeout=10
)
# Output format: "ChromeDriver 125.0.6422.78 (...)"
return result.stdout.strip().split()[1].split('.')[0]
except Exception:
return ""
def is_chromedriver_compatible(chromedriver_path: str) -> bool:
"""Check if a chromedriver binary is compatible with the installed Chrome version."""
try:
chrome_version = chromedriver_autoinstaller.get_chrome_version()
if not chrome_version:
return True # Can't determine Chrome version, assume compatible
chrome_major = chrome_version.split('.')[0]
driver_major = get_chromedriver_version(chromedriver_path)
if not driver_major:
return True # Can't determine driver version, assume compatible
return chrome_major == driver_major
except Exception:
return True # On any error, assume compatible to avoid blocking
def install_chromedriver() -> str:
"""
Install the ChromeDriver if not already installed. Return the path.
Automatically updates the driver if the version does not match the installed Chrome.
"""
# First try to use chromedriver in the project root directory (as per README)
project_root_chromedriver = "./chromedriver"
if os.path.exists(project_root_chromedriver) and os.access(project_root_chromedriver, os.X_OK):
if is_chromedriver_compatible(project_root_chromedriver):
print(f"Using ChromeDriver from project root: {project_root_chromedriver}")
return project_root_chromedriver
print("ChromeDriver in project root is outdated, attempting auto-update...")
# Then try to use the system-installed chromedriver
chromedriver_path = shutil.which("chromedriver")
if chromedriver_path:
if is_chromedriver_compatible(chromedriver_path):
return chromedriver_path
print(f"System ChromeDriver at {chromedriver_path} is outdated, attempting auto-update...")
# In Docker environment, try the fixed path
if os.path.exists('/.dockerenv'):
@@ -98,9 +131,9 @@ def install_chromedriver() -> str:
print(f"Using Docker ChromeDriver at {docker_chromedriver_path}")
return docker_chromedriver_path
# Fallback to auto-installer only if no other option works
# Auto-install matching ChromeDriver version
try:
print("ChromeDriver not found, attempting to install automatically...")
print("Installing matching ChromeDriver version automatically...")
chromedriver_path = chromedriver_autoinstaller.install()
except Exception as e:
raise FileNotFoundError(
@@ -121,6 +154,12 @@ def bypass_ssl() -> str:
pretty_print("Bypassing SSL verification issues, we strongly advice you update your certifi SSL certificate.", color="warning")
ssl._create_default_https_context = ssl._create_unverified_context
def get_free_port() -> int:
"""Find and return a free TCP port on the local machine."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
return s.getsockname()[1]
def create_chrome_options(headless=False, stealth_mode=True, crx_path="./crx/nopecha.crx", lang="en") -> Options:
"""Create Chrome options - separated for reusability."""
chrome_options = Options()
@@ -147,7 +186,7 @@ def create_chrome_options(headless=False, stealth_mode=True, crx_path="./crx/nop
chrome_options.add_argument("--disable-extensions")
chrome_options.add_argument("--disable-background-timer-throttling")
chrome_options.add_argument("--timezone=Europe/Paris")
chrome_options.add_argument('--remote-debugging-port=9222')
chrome_options.add_argument(f'--remote-debugging-port={get_free_port()}')
chrome_options.add_argument('--disable-background-timer-throttling')
chrome_options.add_argument('--disable-backgrounding-occluded-windows')
chrome_options.add_argument('--disable-renderer-backgrounding')
@@ -498,7 +537,16 @@ class Browser:
if input_type in ["hidden", "submit", "button", "image"] or not element["displayed"]:
continue
input_name = element.get("text") or element.get("id") or input_type
if input_type == "checkbox" or input_type == "radio":
if input_type == "select":
options = element.get("options", [])
options_str = ", ".join(opt["text"] for opt in options if opt["text"])
selected = next((opt["text"] for opt in options if opt.get("selected")), "")
form_strings.append(f"[{input_name}](select: {selected}) options: [{options_str}]")
elif input_type == "textarea":
form_strings.append(f"[{input_name}]("")")
elif input_type == "file":
form_strings.append(f"[{input_name}](file: )")
elif input_type == "checkbox" or input_type == "radio":
try:
checked_status = "checked" if element.is_selected() else "unchecked"
except Exception as e:
@@ -662,7 +710,29 @@ class Browser:
self.logger.warning(f"Element '{name}' is not interactable (not displayed or disabled)")
continue
input_type = (element.get_attribute("type") or "text").lower()
if input_type in ["checkbox", "radio"]:
tag_name = (element.tag_name or "").lower()
if tag_name == "select":
try:
select = Select(element)
select.select_by_visible_text(value)
self.logger.info(f"Selected '{value}' for {name}")
except Exception:
try:
select.select_by_value(value)
self.logger.info(f"Selected value '{value}' for {name}")
except Exception as sel_e:
self.logger.warning(f"Could not select '{value}' for {name}: {sel_e}")
elif tag_name == "textarea":
element.clear()
element.send_keys(value)
self.logger.info(f"Filled textarea {name}")
elif input_type == "file":
if os.path.isabs(value) and os.path.exists(value):
element.send_keys(value)
self.logger.info(f"Uploaded file '{value}' for {name}")
else:
self.logger.warning(f"File not found: {value}")
elif input_type in ["checkbox", "radio"]:
is_checked = element.is_selected()
should_be_checked = value.lower() == "checked"
+59 -10
View File
@@ -17,6 +17,9 @@ from sources.utility import pretty_print, animate_thinking
class Provider:
def __init__(self, provider_name, model, server_address="127.0.0.1:5000", is_local=False):
self.provider_name = provider_name.lower()
# Normalize provider name aliases (e.g. README documents 'togetherAI' but canonical key is 'together')
_aliases = {"togetherai": "together"}
self.provider_name = _aliases.get(self.provider_name, self.provider_name)
self.model = model
self.is_local = is_local
self.server_ip = server_address
@@ -32,12 +35,14 @@ class Provider:
"together": self.together_fn,
"dsk_deepseek": self.dsk_deepseek,
"openrouter": self.openrouter_fn,
"anthropic": self.anthropic_fn,
"minimax": self.minimax_fn,
"test": self.test_fn
}
self.logger = Logger("provider.log")
self.api_key = None
self.internal_url, self.in_docker = self.get_internal_url()
self.unsafe_providers = ["openai", "deepseek", "dsk_deepseek", "together", "google", "openrouter"]
self.unsafe_providers = ["openai", "deepseek", "dsk_deepseek", "together", "google", "openrouter", "anthropic", "minimax"]
if self.provider_name not in self.available_providers:
raise ValueError(f"Unknown provider: {provider_name}")
if self.provider_name in self.unsafe_providers and self.is_local == False:
@@ -160,7 +165,11 @@ class Provider:
Use local or remote Ollama server to generate text.
"""
thought = ""
host = f"{self.internal_url}:11434" if self.is_local else f"http://{self.server_address}"
if self.is_local:
server_port = self.server_address.split(":")[-1] if ":" in str(self.server_address) else "11434"
host = f"{self.internal_url}:{server_port}"
else:
host = f"http://{self.server_address}"
client = OllamaClient(host=host)
try:
@@ -339,13 +348,21 @@ class Provider:
Use local lm-studio server to generate text.
"""
if self.in_docker:
# Extract port from server_address if present
# Extract port from server_address, handling both "host:port" and "http://host:port"
port = "1234" # default
if ":" in self.server_address:
port = self.server_address.split(":")[1]
addr = self.server_address
if "://" not in addr:
addr = f"http://{addr}"
parsed_addr = urlparse(addr)
if parsed_addr.port:
port = str(parsed_addr.port)
url = f"{self.internal_url}:{port}"
else:
url = f"http://{self.server_ip}"
# Normalize the address to ensure it has a scheme prefix
addr = self.server_ip
if "://" not in addr:
addr = f"http://{addr}"
url = addr
route_start = f"{url}/v1/chat/completions"
payload = {
"messages": history,
@@ -387,7 +404,6 @@ class Provider:
if "LM Studio" in str(e):
raise # Re-raise our custom exceptions
raise Exception(f"Unexpected error: {str(e)}") from e
return thought
def openrouter_fn(self, history, verbose=False):
"""
@@ -412,6 +428,39 @@ class Provider:
except Exception as e:
raise Exception(f"OpenRouter API error: {str(e)}") from e
def minimax_fn(self, history, verbose=False):
"""
Use MiniMax API to generate text via OpenAI-compatible interface.
Supported models:
- MiniMax-M2.7: Latest flagship model with enhanced reasoning and coding
- MiniMax-M2.7-highspeed: High-speed version of M2.7 for low-latency scenarios
- MiniMax-M2.5: Peak performance model (~60 tps), 204,800 context window
- MiniMax-M2.5-highspeed: Same performance, faster (~100 tps)
Note: temperature must be in range (0.0, 1.0], default is 1.0
"""
load_dotenv()
base_url = os.getenv("MINIMAX_BASE_URL", "https://api.minimax.io/v1")
client = OpenAI(api_key=self.api_key, base_url=base_url)
if self.is_local:
raise Exception("MiniMax is not available for local use. Change config.ini")
try:
response = client.chat.completions.create(
model=self.model,
messages=history,
temperature=1.0,
)
if response is None:
raise Exception("MiniMax response is empty.")
thought = response.choices[0].message.content
if verbose:
print(thought)
return thought
except Exception as e:
raise Exception(f"MiniMax API error: {str(e)}") from e
def dsk_deepseek(self, history, verbose=False):
"""
Use: xtekky/deepseek4free
@@ -436,13 +485,13 @@ class Provider:
if chunk['type'] == 'text':
thought += chunk['content']
return thought
except AuthenticationError:
except AuthenticationError as e:
raise AuthenticationError("Authentication failed. Please check your token.") from e
except RateLimitError:
except RateLimitError as e:
raise RateLimitError("Rate limit exceeded. Please wait before making more requests.") from e
except CloudflareError as e:
raise CloudflareError(f"Cloudflare protection encountered: {str(e)}") from e
except NetworkError:
except NetworkError as e:
raise NetworkError("Network error occurred. Check your internet connection.") from e
except APIError as e:
raise APIError(f"API error occurred: {str(e)}") from e
+2 -1
View File
@@ -11,7 +11,8 @@ try:
from IPython.display import display, Audio
import soundfile as sf
except ImportError:
print("Speech synthesis disabled. Please install the kokoro package.")
print("Speech synthesis disabled. To enable TTS, install: pip install kokoro==0.9.4 soundfile ipython")
print("Note: kokoro requires Python <3.12 due to num2words dependency.")
IMPORT_FOUND = False
if __name__ == "__main__":
+12 -3
View File
@@ -1,6 +1,8 @@
import requests
from bs4 import BeautifulSoup
import sys
import os
from urllib.parse import urlencode
if __name__ == "__main__": # if running as a script for individual testing
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
@@ -16,7 +18,7 @@ class searxSearch(Tools):
self.tag = "web_search"
self.name = "searxSearch"
self.description = "A tool for searching a SearxNG for web search"
self.base_url = os.getenv("SEARXNG_BASE_URL") # Requires a SearxNG base URL
self.base_url = base_url or os.getenv("SEARXNG_BASE_URL") # Requires a SearxNG base URL
self.user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
self.paywall_keywords = [
"Member-only", "access denied", "restricted content", "404", "this page is not working"
@@ -77,7 +79,14 @@ class searxSearch(Tools):
'Upgrade-Insecure-Requests': '1',
'User-Agent': self.user_agent
}
data = f"q={query}&categories=general&language=auto&time_range=&safesearch=0&theme=simple".encode('utf-8')
data = urlencode({
'q': query,
'categories': 'general',
'language': 'auto',
'time_range': '',
'safesearch': '0',
'theme': 'simple'
}).encode('utf-8')
try:
response = requests.post(search_url, headers=headers, data=data, verify=False)
response.raise_for_status()
@@ -101,7 +110,7 @@ class searxSearch(Tools):
"""
Checks if the execution failed based on the output.
"""
return "Error" in output
return "Error" in output or "No search results" in output
def interpreter_feedback(self, output: str) -> str:
"""
+31
View File
@@ -11,6 +11,37 @@ function findInputs(element, result = []) {
displayed: isElementDisplayed(input)
});
});
// Find all <select> elements (dropdowns / multi-choice)
const selects = element.querySelectorAll('select');
selects.forEach(select => {
const options = Array.from(select.options).map(opt => ({
value: opt.value,
text: opt.textContent.trim(),
selected: opt.selected
}));
result.push({
tagName: select.tagName,
text: select.name || '',
type: 'select',
class: select.className || '',
xpath: getXPath(select),
displayed: isElementDisplayed(select),
multiple: select.multiple,
options: options
});
});
// Find all <textarea> elements
const textareas = element.querySelectorAll('textarea');
textareas.forEach(textarea => {
result.push({
tagName: textarea.tagName,
text: textarea.name || '',
type: 'textarea',
class: textarea.className || '',
xpath: getXPath(textarea),
displayed: isElementDisplayed(textarea)
});
});
const allElements = element.querySelectorAll('*');
allElements.forEach(el => {
if (el.shadowRoot) {
+1 -1
View File
@@ -20,7 +20,7 @@ max_size_bytes=$((2 * 1024 * 1024 * 1024 * 10))
echo "Mounting $WORK_DIR ($dir_size_bytes bytes) to docker."
if [ "$dir_size_bytes" -gt "$max_size_bytes" ]; then
if [ -n "$dir_size_bytes" ] && [ "$dir_size_bytes" -gt "$max_size_bytes" ]; then
echo "Error: WORK_DIR ($WORK_DIR) contains more than 20GB of data ($(du -sh "$WORK_DIR" 2>/dev/null | awk '{print $1}'))."
exit 1
fi
+148 -10
View File
@@ -1,20 +1,37 @@
import unittest
import os
import sys
from unittest.mock import MagicMock
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # Add project root to Python path
# Mock heavy dependencies
for mod_name in [
'torch', 'transformers', 'kokoro', 'adaptive_classifier', 'text2emotion',
'ollama', 'openai', 'together', 'IPython', 'IPython.display',
'playsound3', 'soundfile', 'pyaudio', 'librosa',
'pypdf', 'langid', 'pypinyin', 'fake_useragent',
'chromedriver_autoinstaller', 'num2words', 'sentencepiece', 'sacremoses',
'scipy', 'numpy', 'selenium_stealth', 'undetected_chromedriver',
'markdownify',
]:
if mod_name not in sys.modules:
sys.modules[mod_name] = MagicMock()
os.environ.setdefault('WORK_DIR', '/tmp')
from sources.agents.browser_agent import BrowserAgent
class TestBrowserAgentParsing(unittest.TestCase):
def setUp(self):
# Initialize a basic BrowserAgent instance for testing
self.agent = BrowserAgent(
name="TestAgent",
prompt_path="../prompts/base/browser_agent.txt",
provider=None
)
self.agent = BrowserAgent.__new__(BrowserAgent)
self.agent.notes = []
self.agent.navigable_links = []
self.agent.search_history = []
self.agent.current_page = ""
self.agent.logger = MagicMock()
def test_extract_links(self):
# Test various link formats
test_text = """
Check this out: https://thriveonai.com/15-ai-startups-in-japan-to-take-note-of, and www.google.com!
Also try https://test.org/about?page=1, hey this one as well bro https://weatherstack.com/documentation/.
@@ -28,8 +45,17 @@ class TestBrowserAgentParsing(unittest.TestCase):
result = self.agent.extract_links(test_text)
self.assertEqual(result, expected)
def test_extract_links_no_links(self):
"""Test that text without links returns empty list."""
result = self.agent.extract_links("No links here at all.")
self.assertEqual(result, [])
def test_extract_links_single_link(self):
"""Test extraction of a single link."""
result = self.agent.extract_links("Visit https://example.com for details")
self.assertEqual(result, ["https://example.com"])
def test_extract_form(self):
# Test form extraction
test_text = """
Fill this: [username](john) and [password](secret123)
Not a form: [random]text
@@ -38,8 +64,18 @@ class TestBrowserAgentParsing(unittest.TestCase):
result = self.agent.extract_form(test_text)
self.assertEqual(result, expected)
def test_extract_form_empty(self):
"""Test form extraction with no form inputs."""
result = self.agent.extract_form("Just regular text here.")
self.assertEqual(result, [])
def test_extract_form_checkbox(self):
"""Test form extraction with checkbox values."""
text = "[agree](checked) and [newsletter](unchecked)"
result = self.agent.extract_form(text)
self.assertEqual(len(result), 2)
def test_clean_links(self):
# Test link cleaning
test_links = [
"https://example.com.",
"www.test.com,",
@@ -55,8 +91,13 @@ class TestBrowserAgentParsing(unittest.TestCase):
result = self.agent.clean_links(test_links)
self.assertEqual(result, expected)
def test_clean_links_with_slash(self):
"""Test that trailing slash is stripped since it's not alphanumeric."""
links = ["https://example.com/path/"]
result = self.agent.clean_links(links)
self.assertEqual(result, ["https://example.com/path"])
def test_parse_answer(self):
# Test parsing answer with notes and links
test_text = """
Here's some info
Note: This is important. We are doing test it's very cool.
@@ -66,5 +107,102 @@ class TestBrowserAgentParsing(unittest.TestCase):
self.agent.parse_answer(test_text)
self.assertEqual(self.agent.notes[0], "Note: This is important. We are doing test it's very cool.")
def test_parse_answer_extracts_links(self):
"""Test that parse_answer returns extracted links."""
text = "Navigate to https://example.com and https://test.org"
links = self.agent.parse_answer(text)
self.assertIn("https://example.com", links)
self.assertIn("https://test.org", links)
def test_parse_answer_no_notes(self):
"""Test parse_answer with no notes section."""
text = "Go to https://example.com"
self.agent.parse_answer(text)
# Notes should have an empty entry
self.assertEqual(len(self.agent.notes), 1)
def test_select_link_unvisited(self):
"""Test selecting first unvisited link."""
self.agent.search_history = ["https://visited.com"]
self.agent.current_page = "https://current.com"
links = ["https://visited.com", "https://current.com", "https://new.com"]
result = self.agent.select_link(links)
self.assertEqual(result, "https://new.com")
def test_select_link_all_visited(self):
"""Test that None is returned when all links are visited."""
self.agent.search_history = ["https://a.com", "https://b.com"]
self.agent.current_page = ""
links = ["https://a.com", "https://b.com"]
result = self.agent.select_link(links)
self.assertIsNone(result)
def test_select_link_empty(self):
"""Test with empty links list."""
result = self.agent.select_link([])
self.assertIsNone(result)
def test_jsonify_search_results(self):
"""Test parsing search result text into structured data."""
text = """Title: Result One
Snippet: First result snippet
Link: https://one.com
Title: Result Two
Snippet: Second result snippet
Link: https://two.com"""
results = self.agent.jsonify_search_results(text)
self.assertEqual(len(results), 2)
self.assertEqual(results[0]["title"], "Result One")
self.assertEqual(results[0]["link"], "https://one.com")
self.assertEqual(results[1]["snippet"], "Second result snippet")
def test_jsonify_search_results_empty(self):
"""Test with empty search results."""
results = self.agent.jsonify_search_results("")
self.assertEqual(results, [])
def test_jsonify_search_results_partial(self):
"""Test with partial result (only title and link)."""
text = """Title: Partial Result
Link: https://partial.com"""
results = self.agent.jsonify_search_results(text)
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["title"], "Partial Result")
self.assertNotIn("snippet", results[0])
def test_stringify_search_results(self):
"""Test converting structured results back to string."""
results = [
{"link": "https://one.com", "snippet": "First snippet"},
{"link": "https://two.com", "snippet": "Second snippet"}
]
output = self.agent.stringify_search_results(results)
self.assertIn("https://one.com", output)
self.assertIn("First snippet", output)
self.assertIn("https://two.com", output)
def test_select_unvisited(self):
"""Test filtering visited results."""
self.agent.search_history = ["https://visited.com"]
results = [
{"link": "https://visited.com", "title": "Old"},
{"link": "https://new.com", "title": "New"}
]
unvisited = self.agent.select_unvisited(results)
self.assertEqual(len(unvisited), 1)
self.assertEqual(unvisited[0]["link"], "https://new.com")
def test_select_unvisited_all_new(self):
"""Test when no results are visited."""
self.agent.search_history = []
results = [
{"link": "https://a.com", "title": "A"},
{"link": "https://b.com", "title": "B"}
]
unvisited = self.agent.select_unvisited(results)
self.assertEqual(len(unvisited), 2)
if __name__ == "__main__":
unittest.main()
+82
View File
@@ -0,0 +1,82 @@
import unittest
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from unittest.mock import patch, MagicMock
# Mock heavy dependencies
for mod_name in [
'torch', 'transformers', 'kokoro', 'adaptive_classifier', 'text2emotion',
'ollama', 'openai', 'together', 'IPython', 'IPython.display',
'playsound3', 'soundfile', 'pyaudio', 'librosa',
'pypdf', 'langid', 'pypinyin', 'fake_useragent',
'num2words', 'sentencepiece', 'sacremoses',
'scipy', 'numpy', 'selenium_stealth', 'undetected_chromedriver',
'markdownify', 'chromedriver_autoinstaller',
]:
if mod_name not in sys.modules:
sys.modules[mod_name] = MagicMock()
os.environ.setdefault('WORK_DIR', '/tmp')
from sources.browser import get_chromedriver_version, is_chromedriver_compatible
class TestChromedriverVersionCheck(unittest.TestCase):
"""Test suite for ChromeDriver version checking and auto-update logic."""
@patch('sources.browser.subprocess.run')
def test_get_chromedriver_version_success(self, mock_run):
"""Test extracting major version from chromedriver --version output."""
mock_run.return_value = MagicMock(
stdout="ChromeDriver 125.0.6422.78 (abc123)\n"
)
self.assertEqual(get_chromedriver_version("/usr/bin/chromedriver"), "125")
@patch('sources.browser.subprocess.run')
def test_get_chromedriver_version_failure(self, mock_run):
"""Test graceful failure when chromedriver --version fails."""
mock_run.side_effect = FileNotFoundError("not found")
self.assertEqual(get_chromedriver_version("/nonexistent"), "")
@patch('sources.browser.subprocess.run')
def test_get_chromedriver_version_timeout(self, mock_run):
"""Test graceful failure on timeout."""
import subprocess
mock_run.side_effect = subprocess.TimeoutExpired(cmd="chromedriver", timeout=10)
self.assertEqual(get_chromedriver_version("/usr/bin/chromedriver"), "")
@patch('sources.browser.chromedriver_autoinstaller.get_chrome_version')
@patch('sources.browser.get_chromedriver_version')
def test_compatible_versions(self, mock_driver_ver, mock_chrome_ver):
"""Test that matching major versions are compatible."""
mock_chrome_ver.return_value = "125.0.6422.78"
mock_driver_ver.return_value = "125"
self.assertTrue(is_chromedriver_compatible("/usr/bin/chromedriver"))
@patch('sources.browser.chromedriver_autoinstaller.get_chrome_version')
@patch('sources.browser.get_chromedriver_version')
def test_incompatible_versions(self, mock_driver_ver, mock_chrome_ver):
"""Test that mismatched major versions are incompatible."""
mock_chrome_ver.return_value = "126.0.6478.55"
mock_driver_ver.return_value = "125"
self.assertFalse(is_chromedriver_compatible("/usr/bin/chromedriver"))
@patch('sources.browser.chromedriver_autoinstaller.get_chrome_version')
def test_no_chrome_version_assumes_compatible(self, mock_chrome_ver):
"""Test that missing Chrome version defaults to compatible."""
mock_chrome_ver.return_value = None
self.assertTrue(is_chromedriver_compatible("/usr/bin/chromedriver"))
@patch('sources.browser.chromedriver_autoinstaller.get_chrome_version')
@patch('sources.browser.get_chromedriver_version')
def test_no_driver_version_assumes_compatible(self, mock_driver_ver, mock_chrome_ver):
"""Test that missing driver version defaults to compatible."""
mock_chrome_ver.return_value = "125.0.6422.78"
mock_driver_ver.return_value = ""
self.assertTrue(is_chromedriver_compatible("/usr/bin/chromedriver"))
if __name__ == '__main__':
unittest.main()
+92
View File
@@ -0,0 +1,92 @@
import unittest
import os
import sys
import shutil
import logging
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from sources.logger import Logger
class TestLogger(unittest.TestCase):
"""Test suite for the Logger class."""
def setUp(self):
self.logger = Logger("test_logger.log")
def tearDown(self):
if os.path.exists('.logs'):
for handler in self.logger.logger.handlers[:]:
handler.close()
self.logger.logger.removeHandler(handler)
log_path = os.path.join('.logs', 'test_logger.log')
if os.path.exists(log_path):
os.remove(log_path)
def test_initialization(self):
"""Test logger initializes correctly."""
self.assertTrue(self.logger.enabled)
self.assertIsNotNone(self.logger.logger)
self.assertTrue(os.path.exists('.logs'))
def test_log_creates_file(self):
"""Test that logging creates a log file."""
self.logger.info("test message")
self.assertTrue(os.path.exists(self.logger.log_path))
def test_log_writes_message(self):
"""Test that log messages are written to file."""
self.logger.info("hello world")
with open(self.logger.log_path, 'r') as f:
content = f.read()
self.assertIn("hello world", content)
def test_log_deduplication(self):
"""Test that consecutive identical messages are not duplicated."""
self.logger.info("duplicate message")
self.logger.info("duplicate message")
with open(self.logger.log_path, 'r') as f:
content = f.read()
self.assertEqual(content.count("duplicate message"), 1)
def test_log_different_messages(self):
"""Test that different messages are all written."""
self.logger.info("message one")
self.logger.info("message two")
with open(self.logger.log_path, 'r') as f:
content = f.read()
self.assertIn("message one", content)
self.assertIn("message two", content)
def test_error_level(self):
"""Test error level logging."""
self.logger.error("error occurred")
with open(self.logger.log_path, 'r') as f:
content = f.read()
self.assertIn("ERROR", content)
self.assertIn("error occurred", content)
def test_warning_level(self):
"""Test warning level logging."""
self.logger.warning("warning issued")
with open(self.logger.log_path, 'r') as f:
content = f.read()
self.assertIn("WARNING", content)
self.assertIn("warning issued", content)
def test_create_folder(self):
"""Test folder creation."""
test_path = ".test_log_folder"
result = self.logger.create_folder(test_path)
self.assertTrue(result)
self.assertTrue(os.path.exists(test_path))
os.rmdir(test_path)
def test_create_folder_already_exists(self):
"""Test folder creation when folder already exists."""
result = self.logger.create_folder('.logs')
self.assertTrue(result)
if __name__ == '__main__':
unittest.main()
+232
View File
@@ -0,0 +1,232 @@
import unittest
from unittest.mock import patch, MagicMock
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from sources.llm_provider import Provider
class TestMiniMaxProvider(unittest.TestCase):
"""Test cases for MiniMax provider integration."""
def test_minimax_provider_registered(self):
"""Test that minimax provider is registered in available_providers."""
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
self.assertIn("minimax", provider.available_providers)
def test_minimax_in_unsafe_providers(self):
"""Test that minimax is in unsafe_providers list."""
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
self.assertIn("minimax", provider.unsafe_providers)
def test_minimax_api_key_required(self):
"""Test that API key is fetched for minimax provider."""
with patch.object(Provider, 'get_api_key', return_value='test-minimax-key') as mock_get_key:
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
mock_get_key.assert_called_with("minimax")
self.assertEqual(provider.api_key, 'test-minimax-key')
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_local_not_supported(self, mock_openai_class):
"""Test that minimax provider raises error when is_local=True."""
provider = Provider("minimax", "MiniMax-M2.5", is_local=True)
provider.api_key = 'test-key'
history = [{"role": "user", "content": "Hello"}]
with self.assertRaises(Exception) as context:
provider.minimax_fn(history)
self.assertIn("not available for local use", str(context.exception))
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_uses_correct_base_url(self, mock_openai_class):
"""Test that minimax provider uses correct base URL."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Hello!"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
mock_openai_class.assert_called_with(
api_key='test-key',
base_url='https://api.minimax.io/v1'
)
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {
'MINIMAX_API_KEY': 'test-key',
'MINIMAX_BASE_URL': 'https://api.minimaxi.com/v1'
})
def test_minimax_custom_base_url(self, mock_openai_class):
"""Test that minimax provider uses custom base URL from env."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Hello!"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
mock_openai_class.assert_called_with(
api_key='test-key',
base_url='https://api.minimaxi.com/v1'
)
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_uses_temperature_one(self, mock_openai_class):
"""Test that minimax provider uses temperature=1.0."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Hello!"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['temperature'], 1.0)
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_returns_response_content(self, mock_openai_class):
"""Test that minimax provider returns response content."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Test response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
result = provider.minimax_fn(history)
self.assertEqual(result, "Test response")
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_handles_empty_response(self, mock_openai_class):
"""Test that minimax provider handles empty response."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_client.chat.completions.create.return_value = None
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
with self.assertRaises(Exception) as context:
provider.minimax_fn(history)
self.assertIn("response is empty", str(context.exception))
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_handles_api_error(self, mock_openai_class):
"""Test that minimax provider handles API errors."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_client.chat.completions.create.side_effect = Exception("API rate limit exceeded")
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
with self.assertRaises(Exception) as context:
provider.minimax_fn(history)
self.assertIn("MiniMax API error", str(context.exception))
class TestMiniMaxProviderModels(unittest.TestCase):
"""Test cases for MiniMax provider model configurations."""
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_m27_model(self, mock_openai_class):
"""Test MiniMax-M2.7 model."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.7", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['model'], "MiniMax-M2.7")
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_m27_highspeed_model(self, mock_openai_class):
"""Test MiniMax-M2.7-highspeed model."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.7-highspeed", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['model'], "MiniMax-M2.7-highspeed")
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_m25_model(self, mock_openai_class):
"""Test MiniMax-M2.5 model."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['model'], "MiniMax-M2.5")
@patch('sources.llm_provider.OpenAI')
@patch.dict(os.environ, {'MINIMAX_API_KEY': 'test-key'})
def test_minimax_m25_highspeed_model(self, mock_openai_class):
"""Test MiniMax-M2.5-highspeed model."""
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
mock_response = MagicMock()
mock_response.choices = [MagicMock(message=MagicMock(content="Response"))]
mock_client.chat.completions.create.return_value = mock_response
with patch.object(Provider, 'get_api_key', return_value='test-key'):
provider = Provider("minimax", "MiniMax-M2.5-highspeed", is_local=False)
history = [{"role": "user", "content": "Hello"}]
provider.minimax_fn(history)
call_kwargs = mock_client.chat.completions.create.call_args[1]
self.assertEqual(call_kwargs['model'], "MiniMax-M2.5-highspeed")
if __name__ == '__main__':
unittest.main()
+108
View File
@@ -0,0 +1,108 @@
import unittest
import json
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from unittest.mock import MagicMock, patch
# Mock heavy dependencies to allow import without installing them all
for mod_name in [
'torch', 'transformers', 'kokoro', 'adaptive_classifier', 'text2emotion',
'ollama', 'openai', 'together', 'IPython', 'IPython.display',
'playsound3', 'soundfile', 'pyaudio', 'librosa',
'pypdf', 'langid', 'pypinyin', 'fake_useragent',
'chromedriver_autoinstaller', 'num2words', 'sentencepiece', 'sacremoses',
'scipy', 'numpy', 'selenium_stealth', 'undetected_chromedriver',
'markdownify',
]:
if mod_name not in sys.modules:
sys.modules[mod_name] = MagicMock()
os.environ.setdefault('WORK_DIR', '/tmp')
from sources.agents.planner_agent import PlannerAgent
class TestPlannerAgentParsing(unittest.TestCase):
"""Test suite for PlannerAgent.parse_agent_tasks JSON parsing robustness."""
def setUp(self):
self.agent = PlannerAgent.__new__(PlannerAgent)
self.agent.tools = {"json": MagicMock()}
self.agent.tools["json"].tag = "json"
self.agent.logger = MagicMock()
self.agent.agents = {
"coder": MagicMock(),
"file": MagicMock(),
"web": MagicMock(),
"casual": MagicMock()
}
def test_parse_valid_json(self):
"""Test that valid JSON plan is parsed correctly."""
valid_json = '{"plan": [{"agent": "web", "id": "1", "task": "Search info", "need": []}]}'
self.agent.tools["json"].load_exec_block.return_value = ([valid_json], None)
with patch.object(self.agent, 'get_task_names', return_value=["Task 1: Search info"]):
result = self.agent.parse_agent_tasks("dummy text")
self.assertEqual(len(result), 1)
self.assertEqual(result[0][1]['agent'], 'web')
def test_parse_malformed_json_returns_empty(self):
"""Test that malformed JSON returns empty list instead of crashing."""
malformed_json = '{"plan": [{"agent": "web", "id": "1" "task": "missing comma"}]}'
self.agent.tools["json"].load_exec_block.return_value = ([malformed_json], None)
with patch.object(self.agent, 'get_task_names', return_value=[]):
result = self.agent.parse_agent_tasks("dummy text")
self.assertEqual(result, [])
self.agent.logger.warning.assert_called_once()
def test_parse_truncated_json_returns_empty(self):
"""Test that truncated JSON returns empty list instead of crashing."""
truncated_json = '{"plan": [{"agent": "web", "id": "1", "task": "Search'
self.agent.tools["json"].load_exec_block.return_value = ([truncated_json], None)
with patch.object(self.agent, 'get_task_names', return_value=[]):
result = self.agent.parse_agent_tasks("dummy text")
self.assertEqual(result, [])
self.agent.logger.warning.assert_called_once()
def test_parse_no_blocks_returns_empty(self):
"""Test that missing blocks returns empty list."""
self.agent.tools["json"].load_exec_block.return_value = (None, None)
with patch.object(self.agent, 'get_task_names', return_value=[]):
result = self.agent.parse_agent_tasks("no json here")
self.assertEqual(result, [])
def test_parse_invalid_agent_returns_empty(self):
"""Test that an unknown agent name returns empty list."""
valid_json = '{"plan": [{"agent": "unknown_agent", "id": "1", "task": "Do something", "need": []}]}'
self.agent.tools["json"].load_exec_block.return_value = ([valid_json], None)
with patch.object(self.agent, 'get_task_names', return_value=["Task 1"]):
result = self.agent.parse_agent_tasks("dummy text")
self.assertEqual(result, [])
def test_parse_multiple_tasks(self):
"""Test parsing a plan with multiple tasks."""
multi_task_json = '{"plan": [{"agent": "web", "id": "1", "task": "Search", "need": []}, {"agent": "coder", "id": "2", "task": "Code it", "need": ["1"]}]}'
self.agent.tools["json"].load_exec_block.return_value = ([multi_task_json], None)
with patch.object(self.agent, 'get_task_names', return_value=["Task 1: Search", "Task 2: Code it"]):
result = self.agent.parse_agent_tasks("dummy text")
self.assertEqual(len(result), 2)
self.assertEqual(result[0][1]['agent'], 'web')
self.assertEqual(result[1][1]['agent'], 'coder')
if __name__ == '__main__':
unittest.main()
+42
View File
@@ -0,0 +1,42 @@
import unittest
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from sources.utility import get_color_map
class TestUtility(unittest.TestCase):
"""Test suite for utility module functions."""
def test_get_color_map_returns_dict(self):
"""Test that get_color_map returns a dictionary."""
color_map = get_color_map()
self.assertIsInstance(color_map, dict)
def test_get_color_map_has_required_keys(self):
"""Test that color map contains all required color keys."""
color_map = get_color_map()
required_keys = ["success", "failure", "status", "code", "warning", "output", "info"]
for key in required_keys:
self.assertIn(key, color_map, f"Missing key: {key}")
def test_get_color_map_values_are_strings(self):
"""Test that all color values are strings."""
color_map = get_color_map()
for key, value in color_map.items():
self.assertIsInstance(value, str, f"Value for '{key}' should be a string")
def test_success_is_green(self):
"""Test that success maps to green."""
color_map = get_color_map()
self.assertEqual(color_map["success"], "green")
def test_failure_is_red(self):
"""Test that failure maps to red."""
color_map = get_color_map()
self.assertEqual(color_map["failure"], "red")
if __name__ == '__main__':
unittest.main()
Generated
+1829 -1587
View File
File diff suppressed because it is too large Load Diff