├── tests
├── __init__.py
├── scraping_test.py
├── code_analysis_test.py
├── clipboard_test.py
├── anthropic.py
├── chat
│ └── test_message_handler.py
├── web_search_test.py
├── coder
│ └── test_code_assistant.py
└── main_test.py
├── AgentCrew
├── modules
│ ├── prompts
│ │ └── __init__.py
│ ├── a2a
│ │ ├── common
│ │ │ ├── __init__.py
│ │ │ ├── server
│ │ │ │ ├── __init__.py
│ │ │ │ ├── utils.py
│ │ │ │ └── auth_middleware.py
│ │ │ └── client
│ │ │ │ ├── __init__.py
│ │ │ │ └── card_resolver.py
│ │ ├── __init__.py
│ │ ├── errors.py
│ │ ├── registry.py
│ │ └── agent_cards.py
│ ├── agents
│ │ ├── tools
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ └── base.py
│ ├── gui
│ │ ├── utils
│ │ │ ├── __init__.py
│ │ │ ├── macos_clipboard.py
│ │ │ ├── strings.py
│ │ │ └── wins_clipboard.py
│ │ ├── widgets
│ │ │ ├── configs
│ │ │ │ ├── __init__.py
│ │ │ │ └── save_worker.py
│ │ │ ├── __init__.py
│ │ │ ├── paste_aware_textedit.py
│ │ │ ├── token_usage.py
│ │ │ ├── config_window.py
│ │ │ └── loading_overlay.py
│ │ ├── __init__.py
│ │ ├── themes
│ │ │ └── __init__.py
│ │ ├── components
│ │ │ └── __init__.py
│ │ └── worker.py
│ ├── groq
│ │ └── __init__.py
│ ├── llm
│ │ ├── __init__.py
│ │ └── types.py
│ ├── anthropic
│ │ └── __init__.py
│ ├── console
│ │ ├── __init__.py
│ │ ├── utils.py
│ │ ├── constants.py
│ │ └── conversation_handler.py
│ ├── config
│ │ └── __init__.py
│ ├── clipboard
│ │ └── __init__.py
│ ├── image_generation
│ │ └── __init__.py
│ ├── browser_automation
│ │ ├── __init__.py
│ │ └── js
│ │ │ ├── scroll_to_element.js
│ │ │ ├── remove_element_boxes.js
│ │ │ ├── trigger_input_events.js
│ │ │ ├── click_element.js
│ │ │ ├── focus_and_clear_element.js
│ │ │ ├── filter_hidden_elements.js
│ │ │ └── extract_elements_by_text.js
│ ├── code_analysis
│ │ └── __init__.py
│ ├── web_search
│ │ ├── __init__.py
│ │ └── service.py
│ ├── openai
│ │ └── __init__.py
│ ├── google
│ │ └── __init__.py
│ ├── mcpclient
│ │ ├── __init__.py
│ │ ├── tool.py
│ │ └── config.py
│ ├── chat
│ │ ├── message
│ │ │ ├── __init__.py
│ │ │ └── base.py
│ │ ├── __init__.py
│ │ └── message_handler.py
│ ├── memory
│ │ ├── __init__.py
│ │ └── base_service.py
│ ├── file_editing
│ │ └── __init__.py
│ ├── custom_llm
│ │ └── __init__.py
│ ├── command_execution
│ │ ├── __init__.py
│ │ └── types.py
│ ├── voice
│ │ ├── __init__.py
│ │ └── text_cleaner.py
│ ├── tools
│ │ ├── registration.py
│ │ ├── README.md
│ │ └── registry.py
│ └── __init__.py
├── __init__.py
└── assets
│ └── agentcrew_logo.png
├── pyrightconfig.json
├── examples
├── images
│ ├── chat.png
│ ├── image_file.png
│ ├── mcp_config.png
│ ├── agent_config.png
│ ├── code_format.png
│ └── global_config.png
└── agents
│ ├── jobs
│ ├── commit-message-generator.toml
│ ├── bash-agent.toml
│ └── code-snipper.toml
│ ├── agents-loans.toml
│ ├── agents.simple.toml
│ └── agents-financial-report.toml
├── mcp_servers.json.example
├── MANIFEST.in
├── .github
└── workflows
│ ├── pylint.yml
│ ├── test-build.yml
│ └── publish.yml
├── specs
├── 3_spec_add_filter_for_md_file.md
├── 2_spec_add_more_log.md
├── 5_spec_add_interactive_chat.md
├── 1_spec_summarize_md.md
├── 4_spec_add_explain.md
├── 20_spec_multi-agent-with-tool.1.md
├── 0_spec_initial.md
├── 15_modify_time_travel.md
├── 6_spec_add_scrap_as_a_tooluse.md
├── 8_spec_add_clipboard_tool_use.md
├── 21_spec_multi-agent-with-tool.2.md
├── 22_spec_multi-agent-with-tool.3.md
├── 14_add_time_travel.md
├── 7_spec_add_web_search_as_tool_use.md
├── 9_spec_add_memory_implement.md
├── 10_spec_add_thinking_mode.md
├── 23_spec_multi-agent-with-tool.4.md
├── 26_spec_refactoring_for_model_cost.md
├── 13_add_validate_spec.md
├── 24_spec_refactor_interactivechat.md
├── 18_spec_multi-agent-part2.md
├── 27_refactoring_agent_tools.md
├── 19_spec_multi-agent-part3.md
├── 12_add_multi_model_swap.md
├── 16_spec_add_code_assistant.md
├── 17_spec_implement_multi_agents.md
└── 11_add_mcp_client.md
├── .gitignore
├── AI_BEHAVIOR.md
├── .dockerignore
├── docker
├── .dockerignore
└── pyproject.docker.toml
├── CODE_OF_CONDUCT.md
└── pyproject.toml
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/AgentCrew/modules/prompts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/AgentCrew/modules/agents/tools/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/AgentCrew/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.8.10"
2 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/common/server/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/configs/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pyrightconfig.json:
--------------------------------------------------------------------------------
1 | { "venvPath": ".", "venv": ".venv" }
2 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/__init__.py:
--------------------------------------------------------------------------------
1 | from .qt_ui import ChatWindow
2 |
3 | __all__ = ["ChatWindow"]
4 |
--------------------------------------------------------------------------------
/AgentCrew/modules/groq/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import GroqService
2 |
3 | __all__ = ["GroqService"]
4 |
--------------------------------------------------------------------------------
/AgentCrew/modules/llm/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import BaseLLMService
2 |
3 | __all__ = ["BaseLLMService"]
4 |
--------------------------------------------------------------------------------
/examples/images/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/examples/images/chat.png
--------------------------------------------------------------------------------
/AgentCrew/modules/anthropic/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import AnthropicService
2 |
3 | __all__ = ["AnthropicService"]
4 |
--------------------------------------------------------------------------------
/AgentCrew/modules/console/__init__.py:
--------------------------------------------------------------------------------
1 | from .console_ui import ConsoleUI
2 |
3 | __all__ = [
4 | "ConsoleUI",
5 | ]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/themes/__init__.py:
--------------------------------------------------------------------------------
1 | from .style_provider import StyleProvider
2 |
3 | __all__ = ["StyleProvider"]
4 |
--------------------------------------------------------------------------------
/examples/images/image_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/examples/images/image_file.png
--------------------------------------------------------------------------------
/examples/images/mcp_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/examples/images/mcp_config.png
--------------------------------------------------------------------------------
/AgentCrew/modules/config/__init__.py:
--------------------------------------------------------------------------------
1 | from .config_management import ConfigManagement
2 |
3 | __all__ = ["ConfigManagement"]
4 |
--------------------------------------------------------------------------------
/examples/images/agent_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/examples/images/agent_config.png
--------------------------------------------------------------------------------
/examples/images/code_format.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/examples/images/code_format.png
--------------------------------------------------------------------------------
/examples/images/global_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/examples/images/global_config.png
--------------------------------------------------------------------------------
/AgentCrew/assets/agentcrew_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saigontechnology/AgentCrew/HEAD/AgentCrew/assets/agentcrew_logo.png
--------------------------------------------------------------------------------
/AgentCrew/modules/clipboard/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import ClipboardService
2 |
3 | __all__ = [
4 | "ClipboardService",
5 | ]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/image_generation/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import ImageGenerationService
2 |
3 | __all__ = ["ImageGenerationService"]
4 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import BrowserAutomationService
2 |
3 | __all__ = ["BrowserAutomationService"]
4 |
--------------------------------------------------------------------------------
/AgentCrew/modules/code_analysis/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import CodeAnalysisService
2 |
3 | __all__ = [
4 | "CodeAnalysisService",
5 | ]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/web_search/__init__.py:
--------------------------------------------------------------------------------
1 | from AgentCrew.modules.web_search.service import TavilySearchService
2 |
3 | __all__ = [
4 | "TavilySearchService",
5 | ]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/common/client/__init__.py:
--------------------------------------------------------------------------------
1 | from .card_resolver import A2ACardResolver
2 | from .client import A2AClient
3 |
4 |
5 | __all__ = ["A2ACardResolver", "A2AClient"]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/openai/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import OpenAIService
2 | from .response_service import OpenAIResponseService
3 |
4 | __all__ = ["OpenAIService", "OpenAIResponseService"]
5 |
--------------------------------------------------------------------------------
/AgentCrew/modules/google/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import GoogleAIService
2 | from .native_service import GoogleAINativeService
3 |
4 | __all__ = ["GoogleAIService", "GoogleAINativeService"]
5 |
--------------------------------------------------------------------------------
/AgentCrew/modules/mcpclient/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import MCPService
2 | from .manager import MCPSessionManager
3 |
4 | # Expose the MCPService class directly
5 | __all__ = ["MCPService", "MCPSessionManager"]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/agents/__init__.py:
--------------------------------------------------------------------------------
1 | from .local_agent import LocalAgent
2 | from .manager import AgentManager
3 | from .remote_agent import RemoteAgent
4 |
5 | __all__ = ["AgentManager", "LocalAgent", "RemoteAgent"]
6 |
--------------------------------------------------------------------------------
/AgentCrew/modules/chat/message/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import Observable, Observer
2 | from .handler import MessageHandler
3 |
4 | __all__ = [
5 | "Observable",
6 | "Observer",
7 | "MessageHandler",
8 | ]
9 |
--------------------------------------------------------------------------------
/AgentCrew/modules/chat/__init__.py:
--------------------------------------------------------------------------------
1 | # ConsoleUI has been moved to AgentCrew.modules.console
2 | # Import from the new location for backward compatibility
3 |
4 | from .message_handler import MessageHandler
5 |
6 | __all__ = ["MessageHandler"]
7 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A2A (Agent-to-Agent) protocol implementation for SwissKnife.
3 | This module provides a server that exposes SwissKnife agents via the A2A protocol.
4 | """
5 |
6 | from .server import A2AServer
7 |
8 | __all__ = ["A2AServer"]
9 |
--------------------------------------------------------------------------------
/mcp_servers.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "server_id": {
3 | "name": "string",
4 | "command": "string",
5 | "args": ["string"],
6 | "env": {
7 | "key": "value"
8 | },
9 | "enabledForAgents": ["string"]
10 | }
11 |
--------------------------------------------------------------------------------
/AgentCrew/modules/console/utils.py:
--------------------------------------------------------------------------------
1 | def agent_evaluation_remove(data: str) -> str:
2 | if "" in data and "" in data:
3 | data = (
4 | data[: data.find("")]
5 | + data[data.find("") + 19 :]
6 | )
7 | return data
8 |
--------------------------------------------------------------------------------
/AgentCrew/modules/memory/__init__.py:
--------------------------------------------------------------------------------
1 | from .chroma_service import ChromaMemoryService
2 | from .base_service import BaseMemoryService
3 | from .context_persistent import ContextPersistenceService
4 |
5 |
6 | __all__ = [
7 | "ChromaMemoryService",
8 | "BaseMemoryService",
9 | "ContextPersistenceService",
10 | ]
11 |
--------------------------------------------------------------------------------
/AgentCrew/modules/file_editing/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | File editing module for AgentCrew.
3 |
4 | Provides intelligent file editing capabilities using search/replace blocks
5 | with syntax validation via tree-sitter.
6 | """
7 |
8 | from .service import FileEditingService
9 |
10 | __all__ = [
11 | "FileEditingService",
12 | ]
13 |
--------------------------------------------------------------------------------
/AgentCrew/modules/chat/message_handler.py:
--------------------------------------------------------------------------------
1 | # Backward compatibility import - the main functionality has been moved to the message/ package
2 | from .message.base import Observable, Observer
3 | from .message.handler import MessageHandler
4 |
5 | # For backward compatibility, we still export the main classes
6 | __all__ = ["Observable", "Observer", "MessageHandler"]
7 |
--------------------------------------------------------------------------------
/tests/scraping_test.py:
--------------------------------------------------------------------------------
1 | from AgentCrew.modules.scraping import Scraper
2 |
3 |
4 | def test_scrape_url():
5 | test_url = "https://raw.githubusercontent.com/ivanbicalho/python-docx-replace/refs/heads/main/README.md"
6 | scraper = Scraper()
7 | result = scraper.scrape_url(test_url)
8 | assert result is not None
9 | assert len(result) > 0
10 |
--------------------------------------------------------------------------------
/AgentCrew/modules/custom_llm/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import CustomLLMService
2 | from .deepinfra_service import DeepInfraService
3 | from .github_copilot_service import GithubCopilotService
4 | from .copilot_response_service import GithubCopilotResponseService
5 |
6 | __all__ = [
7 | "CustomLLMService",
8 | "DeepInfraService",
9 | "GithubCopilotService",
10 | "GithubCopilotResponseService",
11 | ]
12 |
--------------------------------------------------------------------------------
/AgentCrew/modules/command_execution/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Command Execution Module
3 |
4 | Provides secure, platform-aware shell command execution with:
5 | - Cross-platform support (Linux/Mac/Windows)
6 | - Async execution with timeout handling
7 | - Command validation and security controls
8 | - Interactive command support with stdin
9 | - Comprehensive audit logging
10 | """
11 |
12 | from .service import CommandExecutionService
13 |
14 | __all__ = ["CommandExecutionService"]
15 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | # Include package assets
2 | include AgentCrew/assets/agentcrew_logo.png
3 |
4 | # Include documentation
5 | include README.md
6 | include LICENSE
7 | include CONTRIBUTING.md
8 |
9 | # Include configuration examples
10 | exclude examples/**/*
11 | exclude mcp_servers.json.example
12 |
13 | exclude tests/**/*
14 |
15 | # Include any other important files
16 | graft AgentCrew/modules
17 | recursive-exclude * __pycache__
18 | recursive-exclude * *.py[co]
19 | recursive-exclude * .DS_Store
20 | recursive-exclude * .pytest_cache
21 | recursive-exclude * .coverage*
22 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .token_usage import TokenUsageWidget
2 | from .system_message import SystemMessageWidget
3 | from .message_bubble import MessageBubble
4 | from .history_sidebar import ConversationSidebar, ConversationLoader
5 | from .tool_widget import ToolWidget
6 | from .diff_widget import DiffWidget, CompactDiffWidget
7 |
8 | __all__ = [
9 | "TokenUsageWidget",
10 | "SystemMessageWidget",
11 | "MessageBubble",
12 | "ConversationSidebar",
13 | "ConversationLoader",
14 | "ToolWidget",
15 | "DiffWidget",
16 | "CompactDiffWidget",
17 | ]
18 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/utils/macos_clipboard.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 |
4 | def copy_html_to_clipboard(html_content):
5 | if sys.platform != "darwin":
6 | raise NotImplementedError("This function is only implemented for MacOS.")
7 |
8 | from AppKit import NSPasteboard
9 | from Foundation import NSString
10 |
11 | pb = NSPasteboard.generalPasteboard()
12 | pb.clearContents()
13 | nshtml = NSString.stringWithString_(html_content)
14 | # Use the public.html UTI for HTML content:
15 | pb.declareTypes_owner_(["public.html"], None)
16 | pb.setString_forType_(nshtml, "public.html")
17 |
--------------------------------------------------------------------------------
/.github/workflows/pylint.yml:
--------------------------------------------------------------------------------
1 | name: Pylint
2 |
3 | on: [push]
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | python-version: ["3.12", "3.13"]
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Set up Python ${{ matrix.python-version }}
17 | uses: actions/setup-python@v3
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | - name: Install uv
21 | uses: astral-sh/setup-uv@v5
22 | - name: Analysing the code with ruff
23 | run: |
24 | uvx ruff check
25 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/utils/strings.py:
--------------------------------------------------------------------------------
1 | def agent_evaluation_remove(data: str) -> str:
2 | if "" in data and "" in data:
3 | data = (
4 | data[: data.find("")]
5 | + data[data.find("") + 19 :]
6 | )
7 | return data
8 |
9 |
10 | def need_print_check(message: str) -> bool:
11 | return (
12 | not message.startswith("")
13 | and not message.startswith("Memories related to the user request:")
14 | and not message.startswith("Need to tailor response bases on this")
15 | )
16 |
--------------------------------------------------------------------------------
/tests/code_analysis_test.py:
--------------------------------------------------------------------------------
1 | from AgentCrew.modules.code_analysis import CodeAnalysisService
2 |
3 |
4 | if __name__ == "__main__":
5 | analyze = CodeAnalysisService()
6 | result = analyze.analyze_code_structure(
7 | "./",
8 | exclude_patterns=[
9 | "**/public/**",
10 | "**/test/**",
11 | "**/tests/**",
12 | "**/assets/**",
13 | "**/__pycache__/**",
14 | "**/.pytest_cache/**",
15 | "**/node_modules/**",
16 | "**/*.pyc",
17 | "**/*.pyo",
18 | "**/*.pyd",
19 | ],
20 | )
21 | print(result)
22 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/scroll_to_element.js:
--------------------------------------------------------------------------------
1 | function scrollToElement(xpath) {
2 | const result = document.evaluate(
3 | xpath,
4 | document,
5 | null,
6 | XPathResult.FIRST_ORDERED_NODE_TYPE,
7 | null,
8 | );
9 | const element = result.singleNodeValue;
10 |
11 | if (!element) {
12 | return { success: false, error: "Element not found with provided xpath" };
13 | }
14 |
15 | element.scrollIntoView({
16 | behavior: "instant",
17 | block: "center",
18 | inline: "center",
19 | });
20 |
21 | return {
22 | success: true,
23 | message: "Scrolled to element using scrollIntoView()",
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/AgentCrew/modules/voice/__init__.py:
--------------------------------------------------------------------------------
1 | """Voice module for AgentCrew with multiple voice service integrations.
2 |
3 | This module provides speech-to-text and text-to-speech capabilities
4 | using various APIs including ElevenLabs and DeepInfra (STT only),
5 | built on a flexible abstract base class architecture.
6 | """
7 |
8 | try:
9 | import sounddevice as sd
10 |
11 | _ = sd
12 |
13 | AUDIO_AVAILABLE = True
14 |
15 | except Exception as e:
16 | print(f"Failed to import voice module components: {e}")
17 | print("Please install PyAudio and other dependencies to enable voice features.")
18 |
19 | AUDIO_AVAILABLE = False
20 |
21 | __all__ = [
22 | "AUDIO_AVAILABLE",
23 | ]
24 |
--------------------------------------------------------------------------------
/specs/3_spec_add_filter_for_md_file.md:
--------------------------------------------------------------------------------
1 | # Add a check when scrapping for getting file
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Problems
6 |
7 | - When scrapping a file url with extensions like `.md` or `.txt`, etc... The scrapper is wrapping the content in a code block
8 |
9 | ## Objectives
10 |
11 | - When scrapping a file url, the content should return as raw format.
12 |
13 | ## Contexts
14 |
15 | - modules/scrapping.py: Scrapping modules usig Firecrawl
16 |
17 | ## Low-level Tasks
18 |
19 | - UPDATE modules/scrapping.py to check if the url is actually a file. If it a file then scrap it as raw format, otherwises scrap as markdown
20 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/common/client/card_resolver.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import httpx
4 |
5 | from a2a.types import AgentCard
6 |
7 |
8 | class A2ACardResolver:
9 | def __init__(self, base_url, agent_card_path="/.well-known/agent.json"):
10 | self.base_url = base_url.rstrip("/")
11 | self.agent_card_path = agent_card_path.lstrip("/")
12 |
13 | def get_agent_card(self) -> AgentCard:
14 | with httpx.Client() as client:
15 | response = client.get(self.base_url + "/" + self.agent_card_path)
16 | response.raise_for_status()
17 | try:
18 | return AgentCard(**response.json())
19 | except json.JSONDecodeError as e:
20 | raise httpx.RequestError(str(e)) from e
21 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/components/__init__.py:
--------------------------------------------------------------------------------
1 | from .message_handlers import MessageEventHandler
2 | from .tool_handlers import ToolEventHandler
3 | from .keyboard_handler import KeyboardHandler
4 | from .menu_components import MenuBuilder
5 | from .chat_components import ChatComponents
6 | from .ui_state_manager import UIStateManager
7 | from .input_components import InputComponents
8 | from .conversation_components import ConversationComponents
9 | from .command_handler import CommandHandler
10 |
11 | __all__ = [
12 | "MessageEventHandler",
13 | "ToolEventHandler",
14 | "KeyboardHandler",
15 | "MenuBuilder",
16 | "ChatComponents",
17 | "UIStateManager",
18 | "InputComponents",
19 | "ConversationComponents",
20 | "CommandHandler",
21 | ]
22 |
--------------------------------------------------------------------------------
/AgentCrew/modules/console/constants.py:
--------------------------------------------------------------------------------
1 | from rich.style import Style
2 |
3 | # Rich styles for console UI
4 | RICH_STYLE_YELLOW = Style(color="yellow", bold=False)
5 | RICH_STYLE_GREEN = Style(color="green", bold=False)
6 | RICH_STYLE_BLUE = Style(color="blue", bold=False)
7 | RICH_STYLE_RED = Style(color="red", bold=False)
8 | RICH_STYLE_GRAY = Style(color="grey66", bold=False)
9 |
10 | RICH_STYLE_YELLOW_BOLD = Style(color="yellow", bold=True)
11 | RICH_STYLE_GREEN_BOLD = Style(color="green", bold=True)
12 | RICH_STYLE_BLUE_BOLD = Style(color="blue", bold=True)
13 | RICH_STYLE_RED_BOLD = Style(color="red", bold=True)
14 |
15 | RICH_STYLE_FILE_ACCENT_BOLD = Style(color="bright_cyan", bold=True)
16 | RICH_STYLE_WHITE = Style(color="#ffffff", bold=False)
17 |
18 | CODE_THEME = "lightbulb"
19 | PROMPT_CHAR = " "
20 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/remove_element_boxes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove the overlay container with element boxes
3 | *
4 | * @returns {Object} Result object with success status and message
5 | */
6 | function removeElementBoxes() {
7 | try {
8 | const container = document.getElementById('agentcrew-element-overlay-container');
9 |
10 | if (!container) {
11 | return {
12 | success: true,
13 | message: 'No overlay container found (already removed or never created)'
14 | };
15 | }
16 |
17 | container.remove();
18 |
19 | return {
20 | success: true,
21 | message: 'Successfully removed element boxes overlay'
22 | };
23 | } catch (error) {
24 | return {
25 | success: false,
26 | error: error.message
27 | };
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/specs/2_spec_add_more_log.md:
--------------------------------------------------------------------------------
1 | # Add more logs
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 |
7 | - Add log for each step like scrapping, summarizing
8 | - Add input token, output token and total cost for anthropic called
9 | - use a constant for token cost of input token = 3$/Million Tokens and output token = 15$/Million Tokens
10 |
11 | ## Contexts
12 |
13 | - main.py: main python file that use click for command line
14 | - modules/scraping.py: scraping module using Firecrawl
15 | - modules/anthropic.py: new module for integrate with anthropic api
16 |
17 | ## Low-level Tasks
18 |
19 | - UPDATE main.py for each step logs
20 | - UPDATE modules/anthropic.py for const constant
21 | - UPDATE modules/anthropic.py for token and cost logs
22 |
--------------------------------------------------------------------------------
/specs/5_spec_add_interactive_chat.md:
--------------------------------------------------------------------------------
1 | # Add interactive chat using stream mode of Claude
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 |
7 | - Create `chat` cli command for start interactive chat session
8 | - `chat` command should have argument to initial session with file using `--files` or with message ussing `--message`
9 |
10 | ## Contexts
11 |
12 | - main.py: main python file that use click for command line
13 | - modules/anthropic.py: new module for integrate with anthropic api
14 | - ./ai_docs/messages_stream.md: stream message documentation for anthropic
15 |
16 | ## Low-level Tasks
17 |
18 | 1. UPDATE main.py to create `chat` command with `--files` and `--message`
19 | 3. UPDATE anthropic that handle message stream to anthropic
20 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/configs/save_worker.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtCore import QThread, Signal
2 | from typing import Callable
3 |
4 |
5 | class SaveWorker(QThread):
6 | """
7 | Worker thread for handling save operations to prevent UI freezing.
8 | """
9 |
10 | finished = Signal()
11 | error = Signal(str)
12 | progress = Signal(str)
13 |
14 | def __init__(self, save_function: Callable, *args, **kwargs):
15 | super().__init__()
16 | self.save_function = save_function
17 | self.args = args
18 | self.kwargs = kwargs
19 |
20 | def run(self):
21 | """Execute the save operation in a separate thread."""
22 | try:
23 | self.save_function(*self.args, **self.kwargs)
24 | self.finished.emit()
25 | except Exception as e:
26 | self.error.emit(str(e))
27 |
--------------------------------------------------------------------------------
/AgentCrew/modules/mcpclient/tool.py:
--------------------------------------------------------------------------------
1 | from AgentCrew.modules.mcpclient import MCPSessionManager
2 | from loguru import logger
3 |
4 |
5 | def register(
6 | service_instance=None, agent=None
7 | ): # agent parameter is kept for compatibility but not used for global MCP tools
8 | """
9 | Register all MCP tools with the global tool registry.
10 |
11 | Args:
12 | service_instance: Not used for MCP tools, but included for consistency
13 | agent: Agent instance to register with directly (optional)
14 |
15 | This function should beCalled during application initialization.
16 | """
17 | mcp_manager = MCPSessionManager.get_instance()
18 | if not mcp_manager.initialized:
19 | logger.info(
20 | "MCP Tools: MCPSessionManager not initialized by main flow, initializing now."
21 | )
22 | mcp_manager.initialize()
23 |
24 | logger.info("MCP Tools registered.")
25 |
--------------------------------------------------------------------------------
/AgentCrew/modules/tools/registration.py:
--------------------------------------------------------------------------------
1 | from .registry import ToolRegistry
2 |
3 |
4 | def register_tool(definition_func, handler_factory, service_instance=None, agent=None):
5 | """
6 | Register a tool with the central registry or directly with an agent
7 |
8 | Args:
9 | definition_func: Function that returns tool definition given a provider
10 | handler_factory: Function that creates a handler function
11 | service_instance: Service instance needed by the handler (optional)
12 | agent: Agent instance to register the tool with directly (optional)
13 | """
14 | if agent:
15 | # Register directly with the agent, passing the original functions
16 | agent.register_tool(definition_func, handler_factory, service_instance)
17 | else:
18 | # Register with the global registry
19 | registry = ToolRegistry.get_instance()
20 | registry.register_tool(definition_func, handler_factory, service_instance)
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | build/
8 | develop-eggs/
9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual Environment
24 | .env
25 | venv/
26 | ENV/
27 | env/
28 | .venv/
29 |
30 | # IDE
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | .DS_Store
36 |
37 | # Project specific
38 | .aider*
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Jupyter Notebook
56 | .ipynb_checkpoints
57 |
58 | # mypy
59 | .mypy_cache/
60 | .dmypy.json
61 | dmypy.json
62 |
63 | # Logs
64 | *.log
65 | memory_db/
66 | mcp_servers.json
67 | .chat_histories
68 | persistents/
69 | config.json
70 | generate
71 | chrome_user_data/
72 |
--------------------------------------------------------------------------------
/AI_BEHAVIOR.md:
--------------------------------------------------------------------------------
1 | You are a coding assistant that follows a structured approach:
2 |
3 | 1. IMPLEMENT PROGRESSIVELY
4 |
5 | - Build in logical stages, not all at once
6 | - Pause after each component to check alignment
7 | - Confirm understanding before starting
8 |
9 | 2. MANAGE SCOPE
10 |
11 | - Build only what's explicitly requested
12 | - Choose minimal viable interpretation when ambiguous
13 | - Ask before modifying components not mentioned
14 |
15 | 3. COMMUNICATE CLEARLY
16 |
17 | - Summarize after each component
18 | - Rate changes: Small, Medium, or Large
19 | - Outline plans before major changes
20 | - Track completed vs. pending features
21 |
22 | 4. ENSURE QUALITY
23 | - Provide testable increments
24 | - Include usage examples
25 | - Note edge cases and limitations
26 | - Suggest verification tests
27 |
28 | Adapt your approach based on complexity - implement simple tasks fully, break
29 | complex ones into chunks with checkpoints, and respond to user preferences for
30 | control granularity.
31 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/common/server/utils.py:
--------------------------------------------------------------------------------
1 | from a2a.types import (
2 | ContentTypeNotSupportedError,
3 | JSONRPCErrorResponse,
4 | JSONRPCResponse,
5 | UnsupportedOperationError,
6 | )
7 |
8 |
9 | def are_modalities_compatible(
10 | server_output_modes: list[str], client_output_modes: list[str]
11 | ):
12 | """Modalities are compatible if they are both non-empty
13 | and there is at least one common element.
14 | """
15 | if client_output_modes is None or len(client_output_modes) == 0:
16 | return True
17 |
18 | if server_output_modes is None or len(server_output_modes) == 0:
19 | return True
20 |
21 | return any(x in server_output_modes for x in client_output_modes)
22 |
23 |
24 | def new_incompatible_types_error(request_id):
25 | return JSONRPCResponse(
26 | root=JSONRPCErrorResponse(id=request_id, error=ContentTypeNotSupportedError())
27 | )
28 |
29 |
30 | def new_not_implemented_error(request_id):
31 | return JSONRPCResponse(
32 | root=JSONRPCErrorResponse(id=request_id, error=UnsupportedOperationError())
33 | )
34 |
--------------------------------------------------------------------------------
/AgentCrew/modules/llm/types.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import List, Literal, Optional
3 |
4 |
5 | class SampleParam(BaseModel):
6 | temperature: Optional[float] = None
7 | top_p: Optional[float] = None
8 | min_p: Optional[float] = None
9 | top_k: Optional[int] = None
10 | frequency_penalty: Optional[float] = None
11 | presence_penalty: Optional[float] = None
12 | repetition_penalty: Optional[float] = None
13 |
14 |
15 | class Model(BaseModel):
16 | """Model metadata class."""
17 |
18 | id: str
19 | provider: str
20 | name: str
21 | description: str
22 | capabilities: List[
23 | Literal[
24 | "tool_use",
25 | "stream",
26 | "thinking",
27 | "vision",
28 | "structured_output",
29 | ]
30 | ]
31 | default: bool = False
32 | force_sample_params: Optional[SampleParam] = None
33 | max_context_token: int = 128_000
34 | input_token_price_1m: float = 0.0
35 | output_token_price_1m: float = 0.0
36 | endpoint: Literal["completions", "response"] = "completions"
37 |
--------------------------------------------------------------------------------
/specs/1_spec_summarize_md.md:
--------------------------------------------------------------------------------
1 | # Add a arguments as an option to summarize the content using LLM model
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 |
7 | - add an argument in `get-url --summarize`
8 | - summarize the md that specified for LLM model
9 | - using anthropic api with Claude Sonnet latest model
10 | - use `anthropic` package for integration
11 |
12 | ## Contexts
13 |
14 | - main.py: main python file that use click for command line
15 | - modules/scraping.py: scraping module using Firecrawl
16 | - modules/anthropic.py: new module for integrate with anthropic api
17 |
18 | ## Low-level Tasks
19 |
20 | 1. UPDATE main.py to alter `--summarize` argument for `get-url`
21 | 2. UPDATE main.py to passing scraping content to anthropic module for summarize when `--summarize` defined
22 | 3. CREATE anthropic that handle api call to anthropic
23 | 5. UPDATE anthropic for summarize prompt as a constant
24 | 4. UPDATE anthropic module to handle the summarize content using summarize prompt
25 | 5. UPDATE main.py to save summarized content
26 |
--------------------------------------------------------------------------------
/AgentCrew/modules/chat/message/base.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 | from typing import List, Any
3 |
4 |
5 | class Observable:
6 | """Base class for observables, implementing the observer pattern."""
7 |
8 | def __init__(self):
9 | self._observers: List["Observer"] = []
10 |
11 | def attach(self, observer: "Observer"):
12 | """Attaches an observer to the observable."""
13 | if observer not in self._observers:
14 | self._observers.append(observer)
15 |
16 | def detach(self, observer: "Observer"):
17 | """Detaches an observer from the observable."""
18 | if observer in self._observers:
19 | self._observers.remove(observer)
20 |
21 | def _notify(self, event: str, data: Any = None):
22 | """Notifies all attached observers of a new event."""
23 | for observer in self._observers:
24 | observer.listen(event, data)
25 |
26 |
27 | class Observer:
28 | """Abstract base class for observers."""
29 |
30 | @abstractmethod
31 | def listen(self, event: str, data: Any = None):
32 | """Updates the observer with new data from the observable."""
33 | pass
34 |
--------------------------------------------------------------------------------
/specs/4_spec_add_explain.md:
--------------------------------------------------------------------------------
1 | # Add a arguments as an option to explain the content using LLM model
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 |
7 | - add an argument in `get-url --explain`
8 | - explain the content the md that help non expert understand
9 | - Add key take away section at the end
10 | - using anthropic api with Claude Sonnet latest model
11 | - use `anthropic` package for integration
12 | - Only allow either `--explain` or `--summarize` not both
13 |
14 | ## Contexts
15 |
16 | - main.py: main python file that use click for command line
17 | - modules/scraping.py: scraping module using Firecrawl
18 | - modules/anthropic.py: new module for integrate with anthropic api
19 |
20 | ## Low-level Tasks
21 |
22 | 1. UPDATE main.py to alter `--explain` argument for `get-url`
23 | 2. UPDATE main.py to passing scraping content to anthropic module for summarize when `--explain` defined
24 | 5. UPDATE anthropic for explain prompt as a constant
25 | 4. UPDATE anthropic module to handle the explain content using explain prompt
26 | 5. UPDATE main.py to save explained content
27 |
--------------------------------------------------------------------------------
/specs/20_spec_multi-agent-with-tool.1.md:
--------------------------------------------------------------------------------
1 | # Agent Class Enhancements for Tool Management
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Enhance the Agent base class to support deferred tool registration
7 | - Implement tool definition storage in the Agent class
8 | - Create methods for agent activation and deactivation
9 | - Support proper tool lifecycle management when switching LLM services
10 |
11 | ## Contexts
12 | - modules/agents/base.py: Contains the Agent base class
13 | - modules/llm/base.py: Contains the BaseLLMService class
14 | - modules/tools/registry.py: Contains the current ToolRegistry implementation
15 |
16 | ## Low-level Tasks
17 | 1. UPDATE modules/agents/base.py:
18 | - Modify Agent.__init__ to track tool registration status
19 | - Add methods for storing tool definitions without immediate LLM registration
20 | - Implement _extract_tool_name method for identifying tools
21 | - Create activate and deactivate methods for tool lifecycle management
22 | - Update register_tools_with_llm to handle deferred tool registration
23 | - Enhance clear_tools_from_llm to track registration status
24 | - Add update_llm_service method to handle LLM switching
25 |
26 |
--------------------------------------------------------------------------------
/specs/0_spec_initial.md:
--------------------------------------------------------------------------------
1 | # Build a command line application to fetch a url and convert to markdown file
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objective
6 |
7 | - Create the command `get-url `
8 | - using `uv` for package manager with `pyproject.toml`
9 | - can be expanded with other commands later on, for example, `sumarize-file`, `get-transcript`, etc...
10 |
11 | ## Context
12 |
13 | Create new files when needed
14 |
15 | ## Low-level Tasks
16 |
17 | 1. Create the command `get-url `
18 |
19 | ```aider
20 | CREATE modules/scraping.py
21 |
22 | # Her is an example crawl website using Firecrawl
23 | from firecrawl import FirecrawlApp
24 | app = FirecrawlApp(api_key="FC-YOUR_API_KEY") //FC-YOUR_API_KEY should be from env
25 | scrape_result = app.scrape_url('firecrawl.dev', params={'formats': ['markdown', 'html']})
26 | print(scrape_result)
27 |
28 | CREATE tests/scraping_test.py
29 | Create a test with https://raw.githubusercontent.com/ivanbicalho/python-docx-replace/refs/heads/main/README.md
30 |
31 | CREATE main.py
32 |
33 | Should be able to expanded with other commands
34 |
35 | CREATE get_url(url, file_path)
36 | ```
37 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Git
2 | .git
3 | .gitignore
4 |
5 | # Python
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 | *.so
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # Virtual Environment
28 | .env
29 | venv/
30 | ENV/
31 | env/
32 | .venv/
33 |
34 | # IDE
35 | .idea/
36 | .vscode/
37 | *.swp
38 | *.swo
39 | .DS_Store
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Jupyter Notebook
57 | .ipynb_checkpoints
58 |
59 | # mypy
60 | .mypy_cache/
61 | .dmypy.json
62 | dmypy.json
63 |
64 | # Logs and runtime data
65 | *.log
66 | memory_db/
67 | mcp_servers.json
68 | .chat_histories
69 | persistents/
70 | config.json
71 | generate
72 |
73 | # Documentation and examples (not needed for runtime)
74 | ai_docs/
75 | specs/
76 | examples/
77 | tests/
78 |
79 | # GitHub workflows
80 | .github/
81 |
82 | # Other files not needed in container
83 | refactor_summary.md
84 | release_diff.txt
85 | verify_package.py
86 | pyrightconfig.json
87 | agent.html.toml
88 | agents.toml
89 | mcp_servers.json.example
--------------------------------------------------------------------------------
/AgentCrew/modules/__init__.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | from datetime import datetime
3 | import os
4 |
5 |
6 | from typing import TextIO, AnyStr
7 |
8 |
9 | class FileLogIO(TextIO):
10 | """File-like object compatible with sys.stderr for MCP logging."""
11 |
12 | def __init__(self, file_format: str = "agentcrew"):
13 | log_dir_path = os.getenv("AGENTCREW_LOG_PATH", tempfile.gettempdir())
14 | os.makedirs(log_dir_path, exist_ok=True)
15 | self.log_path = (
16 | log_dir_path + f"/{file_format}_{datetime.now().timestamp()}.log"
17 | )
18 | self.file = open(self.log_path, "w+")
19 |
20 | def write(self, data: AnyStr) -> int:
21 | """Write data to the log file."""
22 | if isinstance(data, bytes):
23 | # Convert bytes to string for writing
24 | str_data = data.decode("utf-8", errors="replace")
25 | else:
26 | str_data = str(data)
27 | self.file.write(str_data)
28 | self.file.flush() # Ensure data is written immediately
29 | return 0
30 |
31 | def flush(self):
32 | """Flush the file buffer."""
33 | self.file.flush()
34 |
35 | def close(self):
36 | """Close the file."""
37 | self.file.close()
38 |
39 | def fileno(self):
40 | """Return the file descriptor."""
41 | return self.file.fileno()
42 |
--------------------------------------------------------------------------------
/examples/agents/jobs/commit-message-generator.toml:
--------------------------------------------------------------------------------
1 | [[agents]]
2 | description = "You are a specialized Git Commit Message Generator that creates concise, precise commit messages following the Conventional Commits specification. Your sole output is the commit message itself - no explanations, no preamble, no additional commentary."
3 | enabled = true
4 | name = "CommitMessageGenerator"
5 | system_prompt = "Generate concise, precise commit messages following the Conventional Commits specification. Output should be the commit message itself - no explanations, no preamble, no additional commentary.\n\n## Commit Message Format\n\n [scope]: \n\n## Commit Types\n\n- **feat**: A new feature for the user\n- **fix**: A bug fix\n- **docs**: Documentation only changes\n- **style**: Code style changes (formatting, missing semi-colons, etc.)\n- **refactor**: Code change that neither fixes a bug nor adds a feature\n- **perf**: Performance improvement\n- **test**: Adding or updating tests\n- **build**: Changes to build system or dependencies\n- **ci**: CI/CD configuration changes\n- **chore**: Other changes that don't modify src or test files\n- **revert**: Reverts a previous commit\n\n## Rules\n\n1. **Description**: Imperative mood, lowercase, no period at end, cover full diff changes, max 72 characters\n1. **Scope**: represents section of codebase (e.g., api, auth, ui)"
6 | temperature = 0.5
7 | tools = []
8 |
--------------------------------------------------------------------------------
/specs/15_modify_time_travel.md:
--------------------------------------------------------------------------------
1 | # Optimize Jump Command Implementation for Memory Efficiency
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Optimize the existing `/jump` command implementation to be more memory-efficient
7 | - Replace full message copies with message indices to reduce memory usage
8 | - Maintain the same functionality and user experience
9 | - Ensure the completer still shows helpful message previews
10 |
11 | ## Contexts
12 | - modules/chat/interactive.py: Contains the InteractiveChat class with the current jump implementation
13 | - modules/chat/completers.py: Contains the JumpCompleter class
14 |
15 | ## Low-level Tasks
16 | 1. UPDATE modules/chat/interactive.py:
17 | - Modify the ConversationTurn class to store only message indices and short previews instead of full message copies
18 | - Update _handle_jump_command method to use message indices for truncating the messages list
19 | - Update the code in start_chat that creates conversation turns to store indices instead of deep copies
20 | - Ensure all references to conversation_turns are updated to work with the new structure
21 |
22 | 2. UPDATE modules/chat/completers.py:
23 | - Update the JumpCompleter class to work with the new ConversationTurn structure
24 | - Ensure completions still show helpful message previews
25 |
--------------------------------------------------------------------------------
/.github/workflows/test-build.yml:
--------------------------------------------------------------------------------
1 | name: Test Package Build
2 |
3 | on:
4 | push:
5 | branches: [main, develop]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test-build:
11 | name: Test package build
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Install uv
18 | uses: astral-sh/setup-uv@v5
19 |
20 | - name: Set up Python
21 | run: uv python install 3.12
22 |
23 | - name: Create virtual environment and install dependencies
24 | run: |
25 | uv venv .venv
26 | source .venv/bin/activate
27 | uv pip install check-wheel-contents
28 |
29 | - name: Build package
30 | run: uv build
31 |
32 | - name: Check package structure
33 | run: |
34 | source .venv/bin/activate
35 | check-wheel-contents dist/*.whl
36 |
37 | - name: Test package contents
38 | run: |
39 | if [ -f "verify_package.py" ]; then
40 | source .venv/bin/activate
41 | python verify_package.py
42 | else
43 | echo "verify_package.py not found, skipping verification"
44 | fi
45 |
46 | - name: Upload build artifacts
47 | uses: actions/upload-artifact@v4
48 | with:
49 | name: dist-files
50 | path: dist/
51 | retention-days: 7
52 |
53 |
--------------------------------------------------------------------------------
/specs/6_spec_add_scrap_as_a_tooluse.md:
--------------------------------------------------------------------------------
1 | # Add interactive chat using stream mode of Claude
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and
4 | > generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 |
8 | - include tools to llm anthropic call to allow claude call the tools
9 | - tools should be injected to llm service with a list of tool definition and
10 | handler
11 | - handle stream message for `stop_reason` is `tool_use` for processing with
12 | tools
13 | - continue the conversation with a user message contains `tool_result`
14 | - Use scraping service as a first tool
15 |
16 | ## Contexts
17 |
18 | - main.py: main python file that use click for command line
19 | - modules/anthropic/service.py: Main service for process with anthropic api
20 | - modules/chat/interactive.py: interactive chat service implementation
21 | - ./ai_docs/tool-use.md: document for how implement tool_use
22 |
23 | ## Low-level Tasks
24 |
25 | 1. UPDATE anthropic/service.py that handle message stream to anthropic with
26 | tool-use
27 | 2. UPDATE main.py to injected scraping service as a tool with this definition
28 |
29 | ```json
30 | {
31 | "name": "scrap_url",
32 | "description": "Scrap the content on given URL",
33 | "input_schema": {
34 | "type": "object",
35 | "properties": {
36 | "url": {
37 | "type": "string",
38 | "description": "the url that will be scraped"
39 | }
40 | },
41 | "required": ["url"]
42 | }
43 | }
44 | ```
45 |
--------------------------------------------------------------------------------
/specs/8_spec_add_clipboard_tool_use.md:
--------------------------------------------------------------------------------
1 | # Implement Clipboard tool use for claude
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and
4 | > generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 |
8 | - Implement Clipboard integration as a tool use using `pyperclip`
9 | - Enable Claude to read the content from system clipboard and write content to system clipboard
10 | - Handle image in clipboard
11 | - Handle error cases and edge conditions gracefully
12 |
13 | ## Context
14 |
15 | - `modules/clipboard/tool.py`: Main file for managing tool registrations and
16 | executions
17 | - `modules/clipboard/service.py`: New file to be created for Clipboard integration
18 | implementation
19 | - `modules/chat/interactive.py`: Interactive chat will call tool use base on the
20 | llm request
21 | - `modules/anthropic/service.py`: Main call for tool handler and tool register
22 |
23 | ## Low-level Tasks
24 |
25 | 1. **Create a new module for Clipboard integration**
26 |
27 | - Create `modules/clipboard/service.py` file
28 | - Using `pyperclip`
29 | - Implement proper error handling.
30 |
31 | 2. **Implement the core tavily functionality**
32 |
33 | - Create a `read` method that return clipboard content
34 | - Create a `write` method that set clipboard content using input content
35 |
36 | 3. **Register the clipboard tool with the tool manager**
37 |
38 | - Create `modules/clipboard/tool.py` to register the new clipboard tool
39 | - Add proper tool description and usage information
40 |
--------------------------------------------------------------------------------
/specs/21_spec_multi-agent-with-tool.2.md:
--------------------------------------------------------------------------------
1 |
2 | # Agent Manager and Service Management
3 |
4 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 | - Update AgentManager to support lazy-loading of agent tools
8 | - Implement model switching capability with proper tool re-registration
9 | - Add agent switching logic that activates only the current agent
10 | - Create service management facilities for handling LLM service lifecycle
11 |
12 | ## Contexts
13 | - modules/agents/manager.py: Contains the AgentManager class
14 | - modules/llm/service_manager.py: Contains the ServiceManager class
15 | - main.py: Contains setup_agents and other initialization functions
16 | - modules/chat/interactive.py: Contains the InteractiveChat class handling commands
17 |
18 | ## Low-level Tasks
19 | 1. UPDATE modules/agents/manager.py:
20 | - Update register_agent to avoid immediate activation
21 | - Enhance select_agent to properly activate/deactivate agents
22 | - Implement update_llm_service for model switching
23 | - Add proper handling of agent tool registration state
24 |
25 | 2. UPDATE modules/llm/service_manager.py:
26 | - Implement get_service method to retrieve current LLM service
27 | - Create set_model method to handle model switching
28 | - Add cleanup method for proper resource management
29 |
30 | 3. UPDATE modules/chat/interactive.py:
31 | - Add _handle_model_command to support model switching
32 | - Update _process_user_input to handle model switching command
33 |
34 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/trigger_input_events.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Trigger input and change events to notify the page of input changes.
3 | *
4 | * @param {string} xpath - XPath selector for the element
5 | * @returns {Object} Result object with success status and message
6 | */
7 | function triggerInputEvents(xpath, value) {
8 | const result = document.evaluate(
9 | xpath,
10 | document,
11 | null,
12 | XPathResult.FIRST_ORDERED_NODE_TYPE,
13 | null,
14 | );
15 | const element = result.singleNodeValue;
16 |
17 | if (!element) {
18 | return { success: false, error: "Element not found for event triggering" };
19 | }
20 |
21 | try {
22 | // Trigger input event
23 | element.dispatchEvent(new Event("input", { bubbles: true }));
24 |
25 | element.value = value;
26 | // Trigger change event
27 | element.dispatchEvent(new Event("change", { bubbles: true }));
28 |
29 | if (element.tagName.toLowerCase() !== "select") {
30 | // For some forms, also trigger keyup event
31 | element.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
32 | }
33 |
34 | return { success: true, message: "Input events triggered successfully" };
35 | } catch (eventError) {
36 | return {
37 | success: false,
38 | error: "Failed to trigger events: " + eventError.message,
39 | };
40 | }
41 | }
42 |
43 | // Export the function - when used in browser automation, wrap with IIFE and pass xpath
44 | // (() => {
45 | // const xpath = '{XPATH_PLACEHOLDER}';
46 | // return triggerInputEvents(xpath);
47 | // })();
48 |
--------------------------------------------------------------------------------
/examples/agents/agents-loans.toml:
--------------------------------------------------------------------------------
1 |
2 | [[agents]]
3 | name = "LoanApplicationAgent"
4 | description = "Specialized in processing loan applications, assessing risk, and generating loan summaries."
5 | tools = ["web_search", "memory"]
6 | system_prompt = "You are an expert loan application processor. Your goal is to efficiently and accurately evaluate loan applications, leveraging available information to assess risk and generate comprehensive loan summaries. Today is {current_date}."
7 |
8 | [[agents]]
9 | name = "ComplianceAgent"
10 | description = "Specialized in ensuring loan applications and processes adhere to regulatory requirements and industry best practices."
11 | tools = ["web_search", "memory"]
12 | system_prompt = """You are a compliance expert in the lending industry. Your primary responsibility is to ensure that all loan applications and processes comply with relevant regulations and industry standards.
13 | Today is {current_date}.
14 | Use your tools to get latest update for lending regulations"""
15 |
16 | [[agents]]
17 | name = "MarketAnalysisAgent"
18 | description = "Specialized in researching current market trends, interest rates, and economic indicators to provide insights for loan pricing and risk assessment."
19 | tools = ["web_search", "memory"]
20 | system_prompt = """You are a market analysis expert focused on the lending industry. Your role is to provide up-to-date insights on market trends, interest rates, and economic indicators to support informed loan pricing and risk assessment.
21 | Today is {current_date}.
22 | Use your tools to get latest interest rates and relevant economic indicators.
23 | """
24 |
--------------------------------------------------------------------------------
/specs/22_spec_multi-agent-with-tool.3.md:
--------------------------------------------------------------------------------
1 |
2 | # Spec Prompt 3: Tool Module Integration
3 |
4 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 | - Update tool modules to support agent-based registration
8 | - Refactor register_agent_tools to handle agent-specific tool registration
9 | - Ensure backward compatibility with the existing ToolRegistry
10 | - Create a migration path from global to agent-specific tools
11 |
12 | ## Contexts
13 | - modules/tools/registration.py: Contains tool registration utilities
14 | - modules/clipboard/tool.py: Example tool module implementation
15 | - modules/memory/tool.py: Example tool module implementation
16 | - modules/web_search/tool.py: Example tool module implementation
17 | - main.py: Contains setup_agents and register_agent_tools functions
18 |
19 | ## Low-level Tasks
20 | 1. UPDATE modules/tools/registration.py:
21 | - Enhance register_tool to support agent parameter
22 | - Maintain backward compatibility with global registry
23 |
24 | 2. CREATE modules/tools/README.md:
25 | - Document the migration process for tool modules
26 | - Provide examples of updated tool registration patterns
27 |
28 | 3. UPDATE main.py:
29 | - Refactor register_agent_tools to support deferred tool registration
30 | - Update setup_agents to include agent_manager in services
31 | - Add support for agent-specific tool sets
32 |
33 | 4. UPDATE modules/clipboard/tool.py (as an example):
34 | - Modify register function to accept agent parameter
35 | - Update tool registration to work with agent's register_tool method
36 | - Maintain backward compatibility
37 |
--------------------------------------------------------------------------------
/specs/14_add_time_travel.md:
--------------------------------------------------------------------------------
1 | # Implement Jump Command for Interactive Chat
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Add a `/jump` command to the interactive chat that allows users to rewind the conversation to a previous turn
7 | - Implement a completer for the `/jump` command that shows available turns with message previews
8 | - Track conversation turns during the current session (no persistence required)
9 | - Provide clear feedback when jumping to a previous point in the conversation
10 |
11 | ## Contexts
12 | - modules/chat/interactive.py: Contains the InteractiveChat class that manages the chat interface
13 | - modules/chat/completers.py: Contains the ChatCompleter class for command completion
14 | - modules/chat/constants.py: Contains color constants and other shared values
15 |
16 | ## Low-level Tasks
17 | 1. UPDATE modules/chat/interactive.py:
18 | - Add a ConversationTurn class to represent a single turn in the conversation
19 | - Modify InteractiveChat.__init__ to initialize a conversation_turns list
20 | - Add _handle_jump_command method to process the /jump command
21 | - Update start_chat method to store conversation turns after each assistant response
22 | - Update _process_user_input to handle the /jump command
23 | - Update _print_welcome_message to include information about the /jump command
24 |
25 | 2. UPDATE modules/chat/completers.py:
26 | - Add a JumpCompleter class that provides completions for the /jump command
27 | - Update ChatCompleter to handle /jump command completions
28 | - Modify ChatCompleter.__init__ to accept conversation_turns parameter
29 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/click_element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculate click coordinates for an element using XPath selector.
3 | * Returns coordinates relative to the main frame's viewport in CSS pixels.
4 | *
5 | * @param {string} xpath - The XPath selector for the element to click
6 | * @returns {Object} Result object with success status, coordinates, and message
7 | */
8 | function clickElement(xpath) {
9 | const result = document.evaluate(
10 | xpath,
11 | document,
12 | null,
13 | XPathResult.FIRST_ORDERED_NODE_TYPE,
14 | null,
15 | );
16 | const element = result.singleNodeValue;
17 |
18 | if (!element) {
19 | return { success: false, error: "Element not found" };
20 | }
21 |
22 | // Check if element is visible and enabled
23 | if (!element.checkVisibility()) {
24 | return { success: false, error: "Element is not visible" };
25 | }
26 |
27 | if (element.disabled) {
28 | return { success: false, error: "Element is disabled" };
29 | }
30 |
31 | // Scroll element into view to ensure it's visible in viewport
32 | element.scrollIntoView({ behavior: "instant", block: "center" });
33 |
34 | const rect = element.getBoundingClientRect();
35 |
36 | const centerX = rect.left + rect.width / 2;
37 | const centerY = rect.top + rect.height / 2;
38 |
39 | return {
40 | success: true,
41 | x: centerX,
42 | y: centerY,
43 | message: "Coordinates calculated successfully",
44 | elementInfo: {
45 | width: rect.width,
46 | height: rect.height,
47 | left: rect.left,
48 | top: rect.top,
49 | text: element.innerText,
50 | },
51 | };
52 | }
53 |
54 | // Export the function - when used in browser automation, wrap with IIFE and pass xpath
55 | // (() => {
56 | // const xpath = '{XPATH_PLACEHOLDER}';
57 | // return clickElement(xpath);
58 | // })();
59 |
--------------------------------------------------------------------------------
/examples/agents/agents.simple.toml:
--------------------------------------------------------------------------------
1 | [[agents]]
2 | name = "Architect"
3 | description = "Specialized in software architecture, system design, and technical planning"
4 | tools = ["clipboard", "memory", "web_search", "code_analysis"]
5 | system_prompt = """
6 | You are Terry, an AI assistant for software architects. Your guiding principle: **KISS (Keep It Simple, Stupid). Complexity is the enemy.**
7 |
8 | Today is {current_date}.
9 | """
10 |
11 | [[agents]]
12 | name = "Coding"
13 | description = "Specialized in code implementation, debugging, programming assistance and aider prompt"
14 | tools = ["clipboard", "memory", "code_analysis", "spec_validator"]
15 | system_prompt = """You are Harvey, a focused code implementation expert. Your guiding principle: **SIMPLICITY IN IMPLEMENTATION** (Simple + Practical Implementation). Prioritize clean, maintainable code that aligns with best practices.
16 |
17 | Today is {current_date}.
18 | """
19 |
20 | [[agents]]
21 | name = "Document"
22 | description = "Specialized in document writing"
23 | tools = ["clipboard", "memory", "web_search"]
24 | system_prompt = """Write with a sharp, analytical voice that combines intellectual depth with conversational directness.
25 | Use a confident first-person perspective that fearlessly dissects cultural phenomena.
26 | Blend academic-level insights with casual language, creating a style that's both intellectually rigorous and immediately accessible.
27 | Construct arguments layer by layer, using vivid personal analogies and concrete examples to illuminate complex social dynamics.
28 | Maintain an authentic tone that isn't afraid to express genuine emotional reactions or point out societal contradictions.
29 | Use varied sentence structures that mix academic precision with conversational flow, occasionally employing sentence fragments for emphasis and rhetorical questions to challenge assumptions."""
30 |
--------------------------------------------------------------------------------
/docker/.dockerignore:
--------------------------------------------------------------------------------
1 | # Git
2 | .git
3 | .gitignore
4 |
5 | # Python
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 | *.so
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # Virtual Environment
28 | .env
29 | venv/
30 | ENV/
31 | env/
32 | .venv/
33 |
34 | # IDE
35 | .idea/
36 | .vscode/
37 | *.swp
38 | *.swo
39 | .DS_Store
40 |
41 | # Docker files (we handle these specially)
42 | docker/Dockerfile
43 | docker/.dockerignore
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .nox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *.cover
55 | *.py,cover
56 | .hypothesis/
57 | .pytest_cache/
58 | cover/
59 |
60 | # Jupyter Notebook
61 | .ipynb_checkpoints
62 |
63 | # mypy
64 | .mypy_cache/
65 | .dmypy.json
66 | dmypy.json
67 |
68 | # Logs and runtime data
69 | *.log
70 | memory_db/
71 | mcp_servers.json
72 | .chat_histories
73 | persistents/
74 | config.json
75 | generate
76 |
77 | # Documentation and examples (not needed for runtime)
78 | ai_docs/
79 | specs/
80 | examples/
81 | tests/
82 |
83 | # GitHub workflows
84 | .github/
85 |
86 | # Other files not needed in container
87 | refactor_summary.md
88 | release_diff.txt
89 | verify_package.py
90 | pyrightconfig.json
91 | agent.html.toml
92 | agents.toml
93 | mcp_servers.json.example
94 |
95 | # Keep these essential files:
96 | # - README.md (needed for package metadata)
97 | # - LICENSE (needed for package metadata)
98 | # - CONTRIBUTING.md (needed for package metadata)
99 | # - MANIFEST.in (needed for package building)
100 | # - pyproject.toml (needed for package building)
101 | # - uv.lock (needed for dependencies)
102 | # - AgentCrew/ (source code)
--------------------------------------------------------------------------------
/specs/7_spec_add_web_search_as_tool_use.md:
--------------------------------------------------------------------------------
1 | # Implement Web Search Tool for Claude using Tavily API Integration
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and
4 | > generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 |
8 | - Implement Tavily API integration as a web search tool for Claude
9 | - Enable Claude to use web search when responding to queries requiring current
10 | information
11 | - Structure search results in a format Claude can effectively utilize
12 | - Handle error cases and edge conditions gracefully
13 | - Maintain proper API key security and rate limiting
14 |
15 | ## Context
16 |
17 | - `modules/web_search/tool.py`: Main file for managing tool registrations and
18 | executions
19 | - `modules/web_search/service.py`: New file to be created for Tavily search
20 | implementation
21 | - `modules/chat/interactive.py`: Interactive chat will call tool use base on the
22 | llm request
23 | - `modules/anthropic/service.py`: Main call for tool handler and tool register
24 | - `ai_docs/tavily.md`: Documentation for tavily
25 |
26 | ## Low-level Tasks
27 |
28 | 1. **Create a new module for Tavily integration**
29 |
30 | - Create `modules/web_search/service.py` file
31 | - Implement a `TavilySearchTool` class
32 | - Add methods for API authentication, search execution,etc
33 | - Implement proper error handling for API failures, rate limits, etc.
34 |
35 | 2. **Implement the core tavily functionality**
36 |
37 | - Create a `search` method that takes a query string as input
38 | - Create a `extract` method that takes a url string as input
39 | - Implement timeout handling and retry logic
40 | - Use `TAVILY_API_KEY` from environment
41 |
42 | 3. **Register the web search tool with the tool manager**
43 |
44 | - Create `modules/web_search/tool.py` to register the new Tavily search tool
45 | - Add proper tool description and usage information
46 | - Process and structure the search and the extract results into a string or
47 | list of string format to pass to claude message
48 |
49 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/paste_aware_textedit.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtWidgets import (
2 | QTextEdit,
3 | )
4 | from PySide6.QtCore import Signal
5 | from AgentCrew.modules.clipboard.service import ClipboardService
6 |
7 |
8 | class PasteAwareTextEdit(QTextEdit):
9 | """Custom QTextEdit that handles paste events to detect images and binary content."""
10 |
11 | image_inserted = Signal(str)
12 |
13 | def __init__(self, parent=None):
14 | super().__init__(parent)
15 | self.clipboard_service = ClipboardService()
16 |
17 | def insertFromMimeData(self, source):
18 | """Override paste behavior to detect and handle images/binary content."""
19 | try:
20 | # Check clipboard content using our service
21 | paste_result = self.clipboard_service.read_and_process_paste()
22 |
23 | if paste_result["success"]:
24 | content_type = paste_result.get("type")
25 |
26 | if content_type == "file_command":
27 | # It's an image or binary file - use the file command
28 | file_command = paste_result["content"]
29 |
30 | self.image_inserted.emit(file_command)
31 |
32 | # Show status message
33 |
34 | return # Don't call parent method
35 |
36 | elif content_type == "text":
37 | # Regular text content - let the parent handle it normally
38 | super().insertFromMimeData(source)
39 | return
40 |
41 | else:
42 | # Other content types (like base64 image) - handle normally for now
43 | super().insertFromMimeData(source)
44 | return
45 | else:
46 | # Failed to read clipboard, fall back to default behavior
47 | super().insertFromMimeData(source)
48 |
49 | except Exception as e:
50 | # If anything goes wrong, fall back to default paste behavior
51 | print(f"Error in paste handling: {e}")
52 | super().insertFromMimeData(source)
53 |
--------------------------------------------------------------------------------
/specs/9_spec_add_memory_implement.md:
--------------------------------------------------------------------------------
1 | # Implement Memory for store conversation and tool for retrieving
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and
4 | > generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 |
8 | - Create a memory service to store conversation with user and assistant
9 | responses from chat service using ChromaDB
10 | - Use ChromaDB in persistent mode
11 | - ChromaDB embed models only support 200 tokens so we need to cut the
12 | conversation into chunks with overlap words, start with 200 words each chunks
13 | and 30 words overlap. Chunks should have linked together
14 | - Create a tool allow LLM models can retrieve memory using keywords
15 | - Create a function for clean-up the memory when starting the chat app. Remove
16 | conversation that more than 1-month old
17 |
18 | ## Context
19 |
20 | - `modules/memory/tool.py`: Main file for managing tool registrations and
21 | executions
22 | - `modules/memory/service.py`: New file to be created for Memory implementation
23 | using Chroma
24 | - `modules/chat/interactive.py`: Interactive chat will store conversation after
25 | each response cycle
26 | - `main.py`: Add clean-up memory function at beginning of the chat
27 |
28 | ## Low-level Tasks
29 |
30 | 1. **Create a new module for Memory implementation**
31 |
32 | - Create `modules/memory/service.py` file
33 | - Using `chromaDB`
34 | - Implement the chunks process before store to chromaDB.
35 | - Implement the retrieve function
36 |
37 | 2. **Create tool definition and handler for retrieve the memory**
38 |
39 | - Create a new tool call `retrieve_memory` with one required string argument
40 | is `keywords`
41 | - Tool should support both Claude and Groq
42 | - Create handler function
43 |
44 | 3. **Update chat interactive to store the conversation after every cycles**
45 |
46 | - Update the main loop to store the conversation after every cycles
47 | - Only store the user input and the assistant_response
48 |
49 | 4. **Update `main.py` for clean-up memory**
50 |
51 | - Update `main.py` to call memory service to clean-up memory
52 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/token_usage.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtWidgets import (
2 | QWidget,
3 | QVBoxLayout,
4 | QLabel,
5 | )
6 |
7 |
8 | class TokenUsageWidget(QWidget):
9 | """Widget to display token usage information."""
10 |
11 | def __init__(self, parent=None):
12 | super().__init__(parent)
13 | # self.setAutoFillBackground(True) # Remove this line
14 |
15 | # Set background color directly via stylesheet
16 | from AgentCrew.modules.gui.themes import StyleProvider
17 |
18 | style_provider = StyleProvider()
19 | self.setStyleSheet(style_provider.get_token_usage_widget_style())
20 |
21 | # Create layout
22 | layout = QVBoxLayout(self)
23 | layout.setContentsMargins(0, 0, 0, 0) # Remove default margins if any
24 |
25 | # Create labels
26 | self.token_label = QLabel(
27 | "📊 Token Usage: Input: 0 | Output: 0 | Total: 0 | Cost: $0.0000 | Session: $0.0000"
28 | )
29 | self.token_label.setStyleSheet(style_provider.get_token_usage_style())
30 |
31 | # Add to layout
32 | layout.addWidget(self.token_label)
33 |
34 | def update_token_info(
35 | self,
36 | input_tokens: int,
37 | output_tokens: int,
38 | total_cost: float,
39 | session_cost: float,
40 | ):
41 | """Update the token usage information."""
42 | self.token_label.setText(
43 | f"📊 Token Usage: Input: {input_tokens:,} | Output: {output_tokens:,} | "
44 | f"Total: {input_tokens + output_tokens:,} | Cost: ${total_cost:.4f} | Session: ${session_cost:.4f}"
45 | )
46 |
47 | def update_style(self, style_provider=None):
48 | """Update the widget's style based on the current theme."""
49 | if not style_provider:
50 | from AgentCrew.modules.gui.themes import StyleProvider
51 |
52 | style_provider = StyleProvider()
53 |
54 | # Update widget style
55 | self.setStyleSheet(style_provider.get_token_usage_widget_style())
56 |
57 | # Update label style
58 | self.token_label.setStyleSheet(style_provider.get_token_usage_style())
59 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 | - Supporting newcomers and helping them get started with AgentCrew
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | - The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | - Trolling, insulting or derogatory comments, and personal or political attacks
34 | - Public or private harassment
35 | - Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | - Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions
49 |
--------------------------------------------------------------------------------
/examples/agents/agents-financial-report.toml:
--------------------------------------------------------------------------------
1 | [[agents]]
2 | name = "FinancialDataExtractor"
3 | description = "Specialized in parsing and structuring raw financial statements into standardized formats"
4 | tools = ["clipboard", "web_search"]
5 | system_prompt = """
6 | You are a data extraction specialist. Your role is to accurately identify and label financial statement line items (e.g., revenue, liabilities) from unstructured documents. Ensure consistency with GAAP/IFRS terminology. Flag anomalies in document structure.
7 |
8 | Today is {current_date}.
9 | """
10 |
11 | [[agents]]
12 | name = "RatioAnalyst"
13 | description = "Specialized in computing and interpreting key financial ratios"
14 | tools = ["clipboard", "web_search"]
15 | system_prompt = """
16 | You are a ratio analysis expert. Calculate standard financial ratios from provided data. Compare results to industry benchmarks (retrieved via web_search). Highlight outliers and potential risks.
17 |
18 | Today is {current_date}.
19 | """
20 |
21 | [[agents]]
22 | name = "TrendAnalyst"
23 | description = "Specialized in identifying trends and patterns across multi-period financial data"
24 | tools = ["clipboard"]
25 | system_prompt = """
26 | Analyze year-over-year or quarter-over-quarter trends in financial metrics. Detect significant deviations (e.g., sudden profit drops) and suggest causes (e.g., seasonality, operational issues).
27 |
28 | Today is {current_date}.
29 | """
30 |
31 | [[agents]]
32 | name = "RiskAssessor"
33 | description = "Specialized in evaluating financial health and flagging risks"
34 | tools = ["clipboard", "web_search"]
35 | system_prompt = """
36 | You are a risk detection specialist. Use ratio analysis and trend data to identify red flags (e.g., declining cash flow, inflated receivables). Prioritize risks by severity and likelihood.
37 |
38 | Today is {current_date}.
39 | """
40 |
41 | [[agents]]
42 | name = "ReportingSynthesizer"
43 | description = "Specialized in compiling financial findings into investor-friendly reports"
44 | tools = ["clipboard"]
45 | system_prompt = """
46 | Summarize insights from all agents into clear, concise reports. Include visualizations (text-based) and actionable recommendations. Tailor language to the audience (e.g., executives vs. auditors).
47 |
48 | Today is {current_date}.
49 | """
50 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/focus_and_clear_element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Focus the target element and clear any existing content.
3 | *
4 | * @param {string} xpath - XPath selector for the element
5 | * @returns {Object} Result object with success status and message
6 | */
7 | function focusAndClearElement(xpath) {
8 | const result = document.evaluate(
9 | xpath,
10 | document,
11 | null,
12 | XPathResult.FIRST_ORDERED_NODE_TYPE,
13 | null,
14 | );
15 | const element = result.singleNodeValue;
16 |
17 | if (!element) {
18 | return { success: false, error: "Element not found" };
19 | }
20 |
21 | // Check if element is visible and enabled
22 | const style = window.getComputedStyle(element);
23 | if (style.display === "none" || style.visibility === "hidden") {
24 | return { success: false, error: "Element is not visible" };
25 | }
26 |
27 | if (element.disabled) {
28 | return { success: false, error: "Element is disabled" };
29 | }
30 |
31 | // Check if element is a valid input type
32 | const tagName = element.tagName.toLowerCase();
33 |
34 | // Scroll element into view and focus
35 | element.scrollIntoView({ behavior: "instant", block: "center" });
36 | element.focus();
37 |
38 | if (
39 | !["input", "textarea"].includes(tagName) &&
40 | !element.hasAttribute("contenteditable")
41 | ) {
42 | return {
43 | success: true,
44 | canSimulateTyping: false,
45 | message: "Element focused but not an input, textarea, or contenteditable",
46 | };
47 | }
48 |
49 | // Clear existing content - select all and then we'll type over it
50 | if (tagName === "input" || tagName === "textarea") {
51 | element.select();
52 | } else if (element.hasAttribute("contenteditable")) {
53 | // For contenteditable, select all text
54 | const range = document.createRange();
55 | range.selectNodeContents(element);
56 | const selection = window.getSelection();
57 | selection.removeAllRanges();
58 | selection.addRange(range);
59 | }
60 |
61 | return {
62 | success: true,
63 | canSimulateTyping: true,
64 | message: "Element focused and selected for typing",
65 | };
66 | }
67 |
68 | // Export the function - when used in browser automation, wrap with IIFE and pass xpath
69 | // (() => {
70 | // const xpath = '{XPATH_PLACEHOLDER}';
71 | // return focusAndClearElement(xpath);
72 | // })();
73 |
--------------------------------------------------------------------------------
/specs/10_spec_add_thinking_mode.md:
--------------------------------------------------------------------------------
1 | # Implement Dynamic Thinking mode for Claude
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and
4 | > generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 |
8 | - Add thinking config to stream call
9 | - Update process chunk to include redacted thinking block when using tool_use
10 | - Update process chunk to print out thinking data
11 | - Update chat interactive to show thinking data
12 | - Thinking data should also show for reasoner model of openai and groq
13 | - Add `/think ` for switch to thinking mode for claude.
14 | `budget_token` must higher than 1024. If user set below 1024, change it to
15 | 1024 with a warning
16 | - Disable think mode with `/think 0`
17 | - For Groq and OpenAI, print "Not Supported" when call `/think`
18 |
19 | ## Context
20 |
21 | - `./modules/anthropic/service.py:` anthropic implementation
22 | - `./modules/llm/base.py:` Abstract class for anthropic, groq, OpenAI
23 | - `./modules/openai/service.py:` OpenAI implementation
24 | - `./modules/groq/service.py:` Groq Implementation
25 | - `modules/chat/interactive.py`: Interactive chat will store conversation after
26 | each response cycle
27 |
28 | ## Low-level Tasks
29 |
30 | 1. Update `./modules/chat/interactive.py`:
31 |
32 | ```aider
33 | UPDATE main chat loop to include new command /think
34 | UPDATE _stream_assistant_response to process thinking data returned from llm service
35 | UPDATE _stream_assistant_response to include thinking_data when tool_use call
36 |
37 | ```
38 |
39 | 2. UPDATE `./modules/llm/base.py`:
40 |
41 | ```aider
42 | ADD set_think(budget_token) function that will be called when user type command /think
43 | UPDATE process_stream_chunk(message) to return think_content
44 | ```
45 |
46 | 3. UPDATE `./modules/anthropic/service.py`:
47 |
48 | ```aider
49 | ADD set_think(budget_token) function to add "thinking" option to stream params
50 | UPDATE process_stream_chunk(message) to return think_content
51 | ```
52 |
53 | 3. UPDATE `./modules/openai/service.py`:
54 |
55 | ```aider
56 | ADD set_think(budget_token) function print not supported
57 | UPDATE process_stream_chunk(message) to return think_content
58 | ```
59 |
60 | 4. UPDATE `./modules/groq/service.py`:
61 |
62 | ```aider
63 | ADD set_think(budget_token) function print not supported
64 | UPDATE process_stream_chunk(message) to return think_content
65 | ```
66 |
--------------------------------------------------------------------------------
/AgentCrew/modules/tools/README.md:
--------------------------------------------------------------------------------
1 | # Tool Registration System
2 |
3 | This document describes the tool registration system and the migration path from global to agent-specific tools.
4 |
5 | ## Overview
6 |
7 | The tool registration system supports two modes of operation:
8 | 1. **Global Registration**: Tools are registered with the central `ToolRegistry` and can be accessed by any agent
9 | 2. **Agent-Specific Registration**: Tools are registered directly with specific agents
10 |
11 | ## Migration Path
12 |
13 | ### Before (Global Registration)
14 |
15 | ```python
16 | def register(service_instance=None):
17 | """Register this tool with the central registry"""
18 | from modules.tools.registration import register_tool
19 |
20 | register_tool(
21 | get_tool_definition,
22 | get_tool_handler,
23 | service_instance
24 | )
25 | ```
26 |
27 | ### After (Supporting Both Global and Agent-Specific)
28 |
29 | ```python
30 | def register(service_instance=None, agent=None):
31 | """
32 | Register this tool with the central registry or directly with an agent
33 |
34 | Args:
35 | service_instance: Service instance needed by the handler
36 | agent: Agent instance to register with directly (optional)
37 | """
38 | from modules.tools.registration import register_tool
39 |
40 | register_tool(
41 | get_tool_definition,
42 | get_tool_handler,
43 | service_instance,
44 | agent
45 | )
46 | ```
47 |
48 | ## Best Practices
49 |
50 | 1. Update all tool modules to support the `agent` parameter
51 | 2. Maintain backward compatibility with global registration
52 | 3. For agent-specific tools, use the agent's provider for tool definition format
53 | 4. When registering tools with specific agents, consider their specialized needs
54 |
55 | ## Example Implementation
56 |
57 | ```python
58 | # In your tool module
59 | def register(service_instance=None, agent=None):
60 | from modules.tools.registration import register_tool
61 |
62 | # Register primary tool
63 | register_tool(
64 | get_primary_tool_definition,
65 | get_primary_tool_handler,
66 | service_instance,
67 | agent
68 | )
69 |
70 | # Register secondary tool
71 | register_tool(
72 | get_secondary_tool_definition,
73 | get_secondary_tool_handler,
74 | service_instance,
75 | agent
76 | )
77 | ```
78 |
--------------------------------------------------------------------------------
/examples/agents/jobs/bash-agent.toml:
--------------------------------------------------------------------------------
1 | [[agents]]
2 | description = "Specialize on transform user request to bash command"
3 | enabled = true
4 | name = "BashAgent"
5 | system_prompt = "You are BashAgent—a Linux command-line wizard who transforms everyday requests into precise bash commands. You speak fluent terminal and love making the command line accessible to everyone.\n\n## Your Core Mission\n\nConvert user intentions into battle-tested bash commands that work reliably across modern Linux distributions. When you're not 100% certain, you search the web for current best practices and syntax verification.\n\n## Your Approach\n\n- **Listen & Translate**: Understand what users want to accomplish, then craft the exact command needed\n- **Learn First**: Before generating commands, leverage built-in help systems:\n - Use `command --help` to quickly learn syntax, options, and usage patterns\n - Use `man command` for comprehensive documentation and detailed examples\n - Parse help output to understand available flags, arguments, and behaviors\n - Apply learned information to generate accurate, context-appropriate commands\n- **Safety First**: Warn about destructive operations (rm -rf, dd, etc.) and suggest safer alternatives\n- **Search When Needed**: If syntax has changed or you need verification, use web_search to find authoritative sources\n\n## Output Format\n\n\n\n## Command Learning Workflow\n\nWhen encountering unfamiliar commands or uncertain syntax:\n\n1. **Quick Reference**: Run `command --help` to see common options and basic usage\n2. **Deep Dive**: Run `man command` for comprehensive documentation, edge cases, and advanced features\n3. **Parse & Apply**: Extract relevant flags, argument patterns, and examples from the help output\n4. **Generate**: Craft the command using verified syntax and options\n5. **Explain**: Include brief rationale for chosen flags based on learned documentation\n\n## Examples You Excel At\n\n- File operations: searching, moving, permissions, archiving\n- Text processing: grep, sed, awk, text transformations\n- System monitoring: processes, disk usage, network diagnostics\n- Automation: loops, conditionals, piping multiple commands\n- Command learning: Using --help and man pages to master unfamiliar tools\n\n**Your Philosophy**: Every user request deserves a command that's not just correct, but elegant. Make the terminal feel like a superpower, not a mystery."
6 | temperature = 0.8
7 | tools = [ "memory", "web_search",]
8 | voice_enabled = "disabled"
9 | voice_id = ""
10 |
--------------------------------------------------------------------------------
/specs/23_spec_multi-agent-with-tool.4.md:
--------------------------------------------------------------------------------
1 | # Updating Remaining Tool Modules
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Update all remaining tool modules to support agent-specific registration
7 | - Ensure consistent agent parameter handling across all tool modules
8 | - Maintain backward compatibility with the global ToolRegistry
9 | - Standardize error handling for tool registration across modules
10 |
11 | ## Contexts
12 | - modules/code_analysis/tool.py: Code analysis tools
13 | - modules/coder/tool.py: Specification validation and implementation tools
14 | - modules/web_search/tool.py: Web search and extraction tools
15 | - modules/scraping/tool.py: Web content scraping tools
16 | - modules/mcpclient/tool.py: MCP client connection tools
17 | - modules/agents/tools/handoff.py: Agent handoff tools
18 | - modules/memory/tool.py: Memory retrieval and management tools
19 |
20 | ## Low-level Tasks
21 | 1. UPDATE modules/code_analysis/tool.py:
22 | - Modify register function to accept agent parameter
23 | - Update tool registration to support agent-specific registration
24 | - Maintain backward compatibility with global registry
25 |
26 | 2. UPDATE modules/coder/tool.py:
27 | - Add agent parameter to register function
28 | - Adapt spec validation and implementation tools for agent-specific registration
29 | - Ensure proper service instance handling
30 |
31 | 3. UPDATE modules/web_search/tool.py:
32 | - Update register function with agent parameter support
33 | - Modify web search and extract tool registration for agents
34 | - Maintain backward compatibility
35 |
36 | 4. UPDATE modules/scraping/tool.py:
37 | - Add agent-specific registration to scraping tools
38 | - Update register function signature and implementation
39 | - Ensure proper error handling
40 |
41 | 5. UPDATE modules/mcpclient/tool.py:
42 | - Modify MCP client tools for agent-specific registration
43 | - Update connect, list, and call tool registration
44 | - Ensure backward compatibility
45 |
46 | 6. UPDATE modules/agents/tools/handoff.py:
47 | - Adapt handoff tool to support agent-specific registration
48 | - Update register function to accept agent parameter
49 | - Ensure proper agent_manager handling
50 |
51 | 7. UPDATE modules/memory/tool.py:
52 | - Modify memory tools to support agent registration
53 | - Update retrieve and forget tool registration
54 | - Ensure backward compatibility
55 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to PyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*" # Push events to tags matching v*, e.g., v1.0.0, v2.0.0.beta
7 | workflow_dispatch: # Allow manual trigger
8 |
9 | jobs:
10 | build:
11 | name: Build distribution 📦
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Install uv
18 | uses: astral-sh/setup-uv@v5
19 |
20 | - name: Set up Python
21 | run: uv python install 3.12
22 |
23 | - name: Build package
24 | run: uv build
25 |
26 | - name: Store the distribution packages
27 | uses: actions/upload-artifact@v4
28 | with:
29 | name: python-package-distributions
30 | path: dist/
31 |
32 | publish-to-pypi:
33 | name: >-
34 | Publish Python 🐍 distribution 📦 to PyPI
35 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
36 | needs:
37 | - build
38 | runs-on: ubuntu-latest
39 | environment:
40 | name: pypi
41 | url: https://pypi.org/p/agentcrew-ai
42 | permissions:
43 | id-token: write # IMPORTANT: mandatory for trusted publishing
44 |
45 | steps:
46 | - name: Download all the dists
47 | uses: actions/download-artifact@v4
48 | with:
49 | name: python-package-distributions
50 | path: dist/
51 |
52 | - name: Install uv
53 | uses: astral-sh/setup-uv@v5
54 |
55 | - name: Publish distribution 📦 to PyPI
56 | run: uv publish
57 | env:
58 | UV_TRUSTED_PUBLISHER: true
59 |
60 | # publish-to-testpypi:
61 | # name: Publish Python 🐍 distribution 📦 to TestPyPI
62 | # needs:
63 | # - build
64 | # runs-on: ubuntu-latest
65 | # if: startsWith(github.ref, 'refs/tags/')
66 | #
67 | # environment:
68 | # name: testpypi
69 | # url: https://test.pypi.org/p/AgentCrew
70 | #
71 | # permissions:
72 | # id-token: write # IMPORTANT: mandatory for trusted publishing
73 | #
74 | # steps:
75 | # - name: Download all the dists
76 | # uses: actions/download-artifact@v4
77 | # with:
78 | # name: python-package-distributions
79 | # path: dist/
80 | #
81 | # - name: Install uv
82 | # uses: astral-sh/setup-uv@v5
83 | #
84 | # - name: Publish distribution 📦 to TestPyPI
85 | # run: uv publish --repository testpypi
86 | # env:
87 | # UV_TRUSTED_PUBLISHER: true
88 |
--------------------------------------------------------------------------------
/tests/clipboard_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from AgentCrew.modules.clipboard.service import ClipboardService
3 | from AgentCrew.modules.clipboard.tool import (
4 | get_clipboard_read_tool_handler,
5 | get_clipboard_write_tool_handler,
6 | )
7 |
8 |
9 | class ClipboardServiceTest(unittest.TestCase):
10 | def setUp(self):
11 | self.clipboard_service = ClipboardService()
12 | self.read_handler = get_clipboard_read_tool_handler(self.clipboard_service)
13 | self.write_handler = get_clipboard_write_tool_handler(self.clipboard_service)
14 |
15 | def test_write_and_read_text(self):
16 | """Test writing text to clipboard and reading it back."""
17 | test_text = "This is a test clipboard content"
18 |
19 | # Write to clipboard
20 | write_result = self.clipboard_service.write(test_text)
21 | self.assertTrue(write_result["success"])
22 |
23 | # Read from clipboard
24 | read_result = self.clipboard_service.read()
25 | self.assertTrue(read_result["success"])
26 | self.assertEqual(read_result["type"], "text")
27 | self.assertEqual(read_result["content"], test_text)
28 |
29 | def test_clipboard_write_handler(self):
30 | """Test the clipboard write tool handler."""
31 | test_text = "Testing clipboard write handler"
32 |
33 | # Use the handler to write to clipboard
34 | params = {"content": test_text}
35 | result = self.write_handler(params)
36 | self.assertTrue(result["success"])
37 |
38 | # Verify content was written correctly
39 | read_result = self.clipboard_service.read()
40 | self.assertTrue(read_result["success"])
41 | self.assertEqual(read_result["content"], test_text)
42 |
43 | def test_clipboard_read_handler(self):
44 | """Test the clipboard read tool handler."""
45 | test_text = "Testing clipboard read handler"
46 |
47 | # Write directly to clipboard
48 | self.clipboard_service.write(test_text)
49 |
50 | # Use the handler to read from clipboard
51 | result = self.read_handler({})
52 | self.assertTrue(result["success"])
53 | self.assertEqual(result["type"], "text")
54 | self.assertEqual(result["content"], test_text)
55 |
56 | def test_missing_content_parameter(self):
57 | """Test write handler with missing content parameter."""
58 | result = self.write_handler({})
59 | self.assertFalse(result["success"])
60 | self.assertIn("Missing required parameter", result["error"])
61 |
62 |
63 | if __name__ == "__main__":
64 | unittest.main()
65 |
--------------------------------------------------------------------------------
/specs/26_spec_refactoring_for_model_cost.md:
--------------------------------------------------------------------------------
1 | # Migrate Token Price Constants to Model Class and ModelRegistry
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Add `input_token_price_1m` and `output_token_price_1m` fields to the `Model` class in `AgentCrew.modules/llm/models.py`.
7 | - Update the `ModelRegistry` class in `AgentCrew.modules/llm/models.py` to handle the new fields when registering and retrieving models, specifically in the `_initialize_default_models` and `register_model` methods.
8 | - Remove the `INPUT_TOKEN_COST_PER_MILLION` and `OUTPUT_TOKEN_COST_PER_MILLION` constants from the LLM service classes (`AgentCrew.modules/openai/service.py` and `AgentCrew.modules/anthropic/service.py`).
9 | - Modify the `calculate_cost` methods in the LLM service classes (`AgentCrew.modules/openai/service.py` and `AgentCrew.modules/anthropic/service.py`) to retrieve token prices from the `ModelRegistry` instead of using the local constants.
10 |
11 | ## Contexts
12 | - AgentCrew.modules/llm/models.py: Contains the `Model` class and `ModelRegistry` class.
13 | - AgentCrew.modules/openai/service.py: Contains the `OpenAIService` class and its `calculate_cost` method.
14 | - AgentCrew.modules/anthropic/service.py: Contains the `AnthropicService` class and its `calculate_cost` method.
15 |
16 | ## Low-level Tasks
17 | 1. UPDATE AgentCrew.modules/llm/models.py:
18 | - Modify the `Model` class to include `input_token_price_1m` and `output_token_price_1m` fields (float, default to 0.0).
19 | - Modify the `_initialize_default_models` method in the `ModelRegistry` class to include values for the new fields when creating default `Model` instances. Use the values I provided earlier.
20 | - Ensure the `register_model` method in the `ModelRegistry` class correctly handles the new fields.
21 |
22 | 2. UPDATE AgentCrew.modules/openai/service.py:
23 | - Remove the `INPUT_TOKEN_COST_PER_MILLION` and `OUTPUT_TOKEN_COST_PER_MILLION` constants.
24 | - Modify the `calculate_cost` method to retrieve the token prices from the `ModelRegistry` using `ModelRegistry.get_instance().get_current_model()` and access the `input_token_price_1m` and `output_token_price_1m` attributes.
25 |
26 | 3. UPDATE AgentCrew.modules/anthropic/service.py:
27 | - Remove the `INPUT_TOKEN_COST_PER_MILLION` and `OUTPUT_TOKEN_COST_PER_MILLION` constants.
28 | - Modify the `calculate_cost` method to retrieve the token prices from the `ModelRegistry` using `ModelRegistry.get_instance().get_current_model()` and access the `input_token_price_1m` and `output_token_price_1m` attributes.
29 |
--------------------------------------------------------------------------------
/AgentCrew/modules/tools/registry.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Any, Callable, List, Optional
2 |
3 |
4 | class ToolRegistry:
5 | _instance = None
6 |
7 | @classmethod
8 | def get_instance(cls):
9 | if cls._instance is None:
10 | cls._instance = ToolRegistry()
11 | return cls._instance
12 |
13 | def __init__(self):
14 | self.tools = {} # {tool_name: (definition_func, handler_factory, service_instance)}
15 |
16 | def register_tool(
17 | self,
18 | definition_func: Callable,
19 | handler_factory: Callable,
20 | service_instance=None,
21 | ):
22 | """
23 | Register a tool with the registry
24 |
25 | Args:
26 | definition_func: Function that returns tool definition given a provider
27 | handler_factory: Function that creates a handler given service instance
28 | service_instance: Instance of the service needed by the handler (optional)
29 | """
30 | # Call definition_func with default provider to get tool name
31 | default_def = definition_func()
32 | tool_name = self._extract_tool_name(default_def)
33 |
34 | self.tools[tool_name] = (definition_func, handler_factory, service_instance)
35 |
36 | def _extract_tool_name(self, tool_def: Dict) -> str:
37 | """Extract tool name from definition regardless of format"""
38 | if "name" in tool_def:
39 | return tool_def["name"]
40 | elif "function" in tool_def and "name" in tool_def["function"]:
41 | return tool_def["function"]["name"]
42 | else:
43 | raise ValueError("Could not extract tool name from definition")
44 |
45 | def get_tool_definitions(self, provider: str) -> List[Dict[str, Any]]:
46 | """Get all tool definitions formatted for the specified provider"""
47 | definitions = []
48 | for name, (definition_func, _, _) in self.tools.items():
49 | try:
50 | tool_def = definition_func(provider)
51 | definitions.append(tool_def)
52 | except Exception as e:
53 | print(f"Error getting definition for tool {name}: {e}")
54 | return definitions
55 |
56 | def get_tool_handler(self, tool_name: str) -> Optional[Callable]:
57 | """Get the handler for a specific tool"""
58 | if tool_name not in self.tools:
59 | return None
60 |
61 | _, handler_factory, service_instance = self.tools[tool_name]
62 | return (
63 | handler_factory(service_instance) if service_instance else handler_factory()
64 | )
65 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/errors.py:
--------------------------------------------------------------------------------
1 | """
2 | A2A-specific error code helpers for proper error handling.
3 |
4 | This module provides convenience functions for creating A2A-specific errors
5 | with contextual data according to the A2A protocol v0.3.0 specification.
6 |
7 | Error Codes (Section 8.2):
8 | - -32001: TaskNotFoundError
9 | - -32002: TaskNotCancelableError
10 | - -32003: PushNotificationNotSupportedError
11 | - -32004: UnsupportedOperationError
12 | - -32005: ContentTypeNotSupportedError
13 | - -32006: InvalidAgentResponseError
14 | - -32007: AuthenticatedExtendedCardNotConfiguredError
15 | """
16 |
17 | from typing import Optional
18 | from a2a.types import (
19 | TaskNotFoundError,
20 | TaskNotCancelableError,
21 | PushNotificationNotSupportedError,
22 | UnsupportedOperationError,
23 | ContentTypeNotSupportedError,
24 | InvalidAgentResponseError,
25 | AuthenticatedExtendedCardNotConfiguredError,
26 | )
27 |
28 |
29 | class A2AError:
30 | @staticmethod
31 | def task_not_found(task_id: str) -> TaskNotFoundError:
32 | error = TaskNotFoundError()
33 | error.data = {"task_id": task_id}
34 | return error
35 |
36 | @staticmethod
37 | def task_not_cancelable(task_id: str, current_state: str) -> TaskNotCancelableError:
38 | error = TaskNotCancelableError()
39 | error.data = {"task_id": task_id, "state": current_state}
40 | return error
41 |
42 | @staticmethod
43 | def push_notification_not_supported() -> PushNotificationNotSupportedError:
44 | return PushNotificationNotSupportedError()
45 |
46 | @staticmethod
47 | def unsupported_operation(operation: str) -> UnsupportedOperationError:
48 | error = UnsupportedOperationError()
49 | error.data = {"operation": operation}
50 | return error
51 |
52 | @staticmethod
53 | def content_type_not_supported(
54 | mime_type: str, supported_types: Optional[list[str]] = None
55 | ) -> ContentTypeNotSupportedError:
56 | error = ContentTypeNotSupportedError()
57 | error.data = {"mime_type": mime_type}
58 | if supported_types:
59 | error.data["supported_types"] = supported_types
60 | return error
61 |
62 | @staticmethod
63 | def invalid_agent_response(details: str) -> InvalidAgentResponseError:
64 | error = InvalidAgentResponseError()
65 | error.data = {"details": details}
66 | return error
67 |
68 | @staticmethod
69 | def authenticated_extended_card_not_configured() -> (
70 | AuthenticatedExtendedCardNotConfiguredError
71 | ):
72 | return AuthenticatedExtendedCardNotConfiguredError()
73 |
--------------------------------------------------------------------------------
/specs/13_add_validate_spec.md:
--------------------------------------------------------------------------------
1 | # Implement Multi-LLM Spec Prompt Validation
2 |
3 | > Ingest the information from these files, implement the Low-level Tasks, and
4 | > generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 |
8 | - Extend the existing LLM service framework to include spec validation
9 | capabilities for different LLM providers.
10 | - Ensure each LLM service (e.g., OpenAI, Anthropic) can process spec validation
11 | requests.
12 | - Enable the `SpecPromptValidationService` to select the appropriate LLM
13 | provider and obtain validation feedback.
14 |
15 | ## Contexts
16 |
17 | - `modules/coder/validation_config.py`: Contains the validation prompt template
18 | and criteria settings.
19 | - `modules/coder/spec_validation.py`: Manages the spec validation logic,
20 | integrating with LLM services.
21 | - `modules/coder/tool.py`: Registers and handles the spec prompt validation
22 | tool.
23 | - `modules/llm/base.py`: Base class for LLM services.
24 | - `modules/openai/service.py`: Implementation details for OpenAI interaction.
25 | - `modules/groq/service.py`: Implementation details for Groq interaction.
26 | - `modules/anthropic/service.py`: Implementation details for Anthropic
27 | interaction.
28 | - `main.py`: main program cli file
29 |
30 | ## Low-level Tasks
31 |
32 | - CREATE `modules/coder/validation_config.py`:
33 |
34 | - Define a prompt template named `validation_prompt_template` for LLM spec
35 | validation.
36 |
37 | - CREATE `modules/coder/spec_validation.py`:
38 |
39 | - Add functionality to the `SpecPromptValidationService` class to call the LLM
40 | service with the validation prompt.
41 | - Implement a `validate` method that constructs and sends the prompt to the
42 | LLM and parses the response for issues and suggestions.
43 |
44 | - CREATE `modules/coder/tool.py`:
45 |
46 | - Add the `get_spec_validation_tool_definition` and the
47 | `get_spec_validation_tool_handler` to utilize the updated
48 | `SpecPromptValidationService` for validation.
49 |
50 | - UPDATE `modules/llm/base.py`:
51 |
52 | - Add an abstract method `validate_spec(self, prompt)` to serve as a blueprint
53 | for derived services.
54 |
55 | - UPDATE `modules/openai/service.py`:
56 |
57 | - Implement `validate_spec` within `OpenAIService` to conduct spec validation
58 | using the OpenAI API.
59 |
60 | - UPDATE `modules/groq/service.py`:
61 |
62 | - Implement `validate_spec` within `GroqService` to conduct spec validation
63 | using the OpenAI API.
64 |
65 | - UPDATE `modules/anthropic/service.py`:
66 | - Implement `validate_spec` within `AnthropicService` to handle spec
67 | validation via the Anthropic API.
68 | - UPDATE `main.py`
69 | - Register spec_tools module
70 |
--------------------------------------------------------------------------------
/specs/24_spec_refactor_interactivechat.md:
--------------------------------------------------------------------------------
1 | # Extract Message Handling Logic from InteractiveChat
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Reduce complexity of the InteractiveChat class by extracting message handling logic
7 | - Create a MessageHandler class to process messages and responses
8 | - Create a CommandHandler class to handle chat commands
9 | - Maintain all existing functionality while improving code organization
10 | - Ensure backward compatibility with the rest of the codebase
11 |
12 | ## Contexts
13 | - modules/chat/interactive.py: Contains the InteractiveChat class that needs refactoring
14 | - modules/llm/base.py: Contains the BaseLLMService class used by the message handler
15 | - modules/agents/manager.py: Contains the AgentManager class used by the command handler
16 | - modules/chat/constants.py: Contains color constants and other shared values
17 |
18 | ## Low-level Tasks
19 | 1. CREATE modules/chat/message_handler.py:
20 | - Create a MessageHandler class that handles processing user messages and assistant responses
21 | - Implement methods for processing file commands, user messages, and assistant responses
22 | - Extract streaming response logic from InteractiveChat._stream_assistant_response
23 | - Include proper error handling and token tracking
24 |
25 | 2. CREATE modules/chat/command_handler.py:
26 | - Create a CommandHandler class that processes all commands (e.g., /clear, /copy, /think)
27 | - Extract command handling logic from InteractiveChat methods
28 | - Implement methods for each command type
29 | - Ensure command handler returns appropriate status flags
30 |
31 | 3. UPDATE modules/chat/interactive.py:
32 | - Modify InteractiveChat to use the new MessageHandler and CommandHandler classes
33 | - Update _process_user_input to delegate to CommandHandler
34 | - Update _stream_assistant_response to delegate to MessageHandler
35 | - Maintain backward compatibility with existing method signatures
36 | - Ensure all existing functionality continues to work
37 |
38 | 4. CREATE modules/chat/types.py:
39 | - Define common types and data structures used across chat modules
40 | - Create a CommandResult class to standardize command handling results
41 | - Define enums for command types and message types
42 |
43 | 5. CREATE tests/chat/test_message_handler.py:
44 | - Implement unit tests for the MessageHandler class
45 | - Test processing of different message types
46 | - Test error handling scenarios
47 |
48 | 6. CREATE tests/chat/test_command_handler.py:
49 | - Implement unit tests for the CommandHandler class
50 | - Test handling of different command types
51 | - Test command result flags
52 |
--------------------------------------------------------------------------------
/AgentCrew/modules/command_execution/types.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import subprocess
3 | from enum import Enum
4 | from typing import Optional, List
5 | from dataclasses import dataclass, field
6 |
7 | from loguru import logger
8 |
9 |
10 | class CommandState(Enum):
11 | """Command lifecycle states"""
12 |
13 | QUEUED = "queued"
14 | STARTING = "starting"
15 | RUNNING = "running"
16 | WAITING_INPUT = "waiting_input"
17 | COMPLETING = "completing"
18 | COMPLETED = "completed"
19 | TIMEOUT = "timeout"
20 | ERROR = "error"
21 | KILLED = "killed"
22 |
23 |
24 | @dataclass
25 | class CommandProcess:
26 | """Represents a running command with its process and metadata"""
27 |
28 | id: str
29 | command: str
30 | process: subprocess.Popen
31 | platform: str
32 | start_time: float
33 | stdout_lines: List[str] = field(default_factory=list)
34 | stderr_lines: List[str] = field(default_factory=list)
35 | output_lock: threading.Lock = field(default_factory=threading.Lock)
36 | state: CommandState = CommandState.QUEUED
37 | exit_code: Optional[int] = None
38 | reader_threads: List[threading.Thread] = field(default_factory=list)
39 | stop_event: threading.Event = field(default_factory=threading.Event)
40 | working_dir: Optional[str] = None
41 |
42 | def transition_to(self, new_state: CommandState):
43 | """Transition to new state with validation"""
44 | valid_transitions = {
45 | CommandState.QUEUED: [CommandState.STARTING, CommandState.ERROR],
46 | CommandState.STARTING: [CommandState.RUNNING, CommandState.ERROR],
47 | CommandState.RUNNING: [
48 | CommandState.WAITING_INPUT,
49 | CommandState.COMPLETING,
50 | CommandState.TIMEOUT,
51 | CommandState.ERROR,
52 | CommandState.KILLED,
53 | ],
54 | CommandState.WAITING_INPUT: [
55 | CommandState.RUNNING,
56 | CommandState.COMPLETING,
57 | CommandState.TIMEOUT,
58 | CommandState.ERROR,
59 | CommandState.KILLED,
60 | ],
61 | CommandState.COMPLETING: [CommandState.COMPLETED, CommandState.ERROR],
62 | CommandState.COMPLETED: [],
63 | CommandState.TIMEOUT: [],
64 | CommandState.ERROR: [],
65 | CommandState.KILLED: [],
66 | }
67 |
68 | if new_state not in valid_transitions.get(self.state, []):
69 | logger.warning(
70 | f"Invalid state transition: {self.state.value} -> {new_state.value}"
71 | )
72 | return
73 |
74 | logger.debug(f"Command {self.id}: {self.state.value} -> {new_state.value}")
75 | self.state = new_state
76 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/utils/wins_clipboard.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 |
4 | def copy_html_to_clipboard(html_content, text_fallback=None):
5 | """
6 | Copy HTML content to clipboard as formatted text with fallback to plain text
7 |
8 | Args:
9 | html_content (str): HTML content to copy
10 | text_fallback (str): Plain text fallback (optional)
11 | """
12 | if sys.platform != "win32":
13 | raise NotImplementedError("This function is only implemented for Windows.")
14 |
15 | import win32clipboard
16 | import win32con
17 |
18 | # If no text fallback provided, strip HTML tags
19 | if text_fallback is None:
20 | import re
21 |
22 | text_fallback = re.sub("<[^<]+?>", "", html_content)
23 |
24 | # Prepare HTML clipboard format
25 | html_clipboard_data = prepare_html_clipboard_data(html_content)
26 |
27 | win32clipboard.OpenClipboard()
28 | try:
29 | win32clipboard.EmptyClipboard()
30 |
31 | # Set HTML format (for rich text paste)
32 | html_format = win32clipboard.RegisterClipboardFormat("HTML Format")
33 | win32clipboard.SetClipboardData(
34 | html_format, html_clipboard_data.encode("utf-8")
35 | )
36 |
37 | except Exception:
38 | # Set plain text format (for fallback)
39 | win32clipboard.SetClipboardData(win32con.CF_TEXT, text_fallback.encode("utf-8"))
40 |
41 | # Set Unicode text format
42 | win32clipboard.SetClipboardData(win32con.CF_UNICODETEXT, text_fallback)
43 |
44 | finally:
45 | win32clipboard.CloseClipboard()
46 |
47 |
48 | def prepare_html_clipboard_data(html_content):
49 | """
50 | Prepare HTML content for Windows clipboard format
51 | """
52 | # HTML clipboard format requires specific headers
53 | html_prefix = """Version:0.9
54 | StartHTML:000000000
55 | EndHTML:000000000
56 | StartFragment:000000000
57 | EndFragment:000000000
58 |
59 |
60 |
61 |
62 |
63 |
64 | """
65 |
66 | html_suffix = """
67 |
68 | """
69 |
70 | # Calculate offsets
71 | start_html = len(html_prefix.split("\n")[0]) + 1 # After version line
72 | start_fragment = len(html_prefix)
73 | end_fragment = start_fragment + len(html_content)
74 | end_html = end_fragment + len(html_suffix)
75 |
76 | # Format the header with correct offsets
77 | header = f"""Version:0.9
78 | StartHTML:{start_html:09d}
79 | EndHTML:{end_html:09d}
80 | StartFragment:{start_fragment:09d}
81 | EndFragment:{end_fragment:09d}"""
82 |
83 | full_html = f"""{header}
84 |
85 |
86 |
87 |
88 |
89 |
90 | {html_content}
91 |
92 | """
93 |
94 | return full_html
95 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/common/server/auth_middleware.py:
--------------------------------------------------------------------------------
1 | from starlette.middleware.base import BaseHTTPMiddleware
2 | from starlette.responses import JSONResponse
3 | from starlette.requests import Request
4 | from typing import Optional
5 | from loguru import logger
6 |
7 |
8 | class AuthMiddleware(BaseHTTPMiddleware):
9 | """
10 | Authentication middleware for A2A agent routes.
11 | Validates Bearer token in Authorization header.
12 | """
13 |
14 | def __init__(self, app, api_key: Optional[str] = None):
15 | super().__init__(app)
16 | self.api_key = api_key or "default-api-key" # You can configure this
17 | logger.debug("AuthMiddleware initialized.")
18 |
19 | async def dispatch(self, request: Request, call_next):
20 | """
21 | Process the request and validate authentication.
22 |
23 | Args:
24 | request: The incoming HTTP request
25 | call_next: The next middleware or endpoint to call
26 |
27 | Returns:
28 | Response from the next handler or authentication error
29 | """
30 | # Get Authorization header
31 | auth_header = request.headers.get("Authorization")
32 |
33 | if not auth_header:
34 | return JSONResponse(
35 | {
36 | "error": {
37 | "code": -32001,
38 | "message": "Authentication required",
39 | "data": {"detail": "Missing Authorization header"},
40 | }
41 | },
42 | status_code=401,
43 | )
44 |
45 | # Check Bearer token format
46 | if not auth_header.startswith("Bearer "):
47 | return JSONResponse(
48 | {
49 | "error": {
50 | "code": -32001,
51 | "message": "Authentication failed",
52 | "data": {
53 | "detail": "Authorization header must start with 'Bearer '"
54 | },
55 | }
56 | },
57 | status_code=401,
58 | )
59 |
60 | # Extract and validate API key
61 | token = auth_header[7:] # Remove "Bearer " prefix
62 | if token != self.api_key:
63 | return JSONResponse(
64 | {
65 | "error": {
66 | "code": -32001,
67 | "message": "Authentication failed",
68 | "data": {"detail": "Invalid API key"},
69 | }
70 | },
71 | status_code=401,
72 | )
73 |
74 | logger.debug("Authentication successful")
75 | # Authentication successful, proceed to next handler
76 | response = await call_next(request)
77 | return response
78 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/registry.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 | from pydantic import BaseModel
5 | from .agent_cards import create_agent_card
6 | from AgentCrew.modules.agents import LocalAgent
7 | from typing import Any, Dict, List, Optional
8 |
9 |
10 | if TYPE_CHECKING:
11 | from AgentCrew.modules.agents import AgentManager
12 | from a2a.types import AgentCard
13 |
14 |
15 | class AgentInfo(BaseModel):
16 | """Basic information about an agent for the registry"""
17 |
18 | name: str
19 | description: str
20 | endpoint: str
21 | capabilities: Dict[str, Any]
22 |
23 |
24 | class AgentRegistry:
25 | """Registry of all available agents for A2A server"""
26 |
27 | def __init__(
28 | self, agent_manager: AgentManager, base_url: str = "http://localhost:41241"
29 | ):
30 | self.agent_manager = agent_manager
31 | self.base_url = base_url.rstrip("/")
32 | self._agent_cards: Dict[str, AgentCard] = {}
33 | self._initialize_agent_cards()
34 |
35 | def _initialize_agent_cards(self):
36 | """Initialize agent cards for all registered agents"""
37 | for agent_name, agent in self.agent_manager.agents.items():
38 | agent_url = f"{self.base_url}/{agent_name}/"
39 | if isinstance(agent, LocalAgent):
40 | self._agent_cards[agent_name] = create_agent_card(agent, agent_url)
41 |
42 | def get_agent_card(self, agent_name: str) -> Optional[AgentCard]:
43 | """
44 | Get the agent card for a specific agent.
45 |
46 | Args:
47 | agent_name: Name of the agent
48 |
49 | Returns:
50 | The agent card if found, None otherwise
51 | """
52 | return self._agent_cards.get(agent_name)
53 |
54 | def list_agents(self) -> List[AgentInfo]:
55 | """
56 | List all available agents.
57 |
58 | Returns:
59 | List of agent information
60 | """
61 | agents = []
62 | for agent_name, card in self._agent_cards.items():
63 | agents.append(
64 | AgentInfo(
65 | name=agent_name,
66 | description=card.description or "",
67 | endpoint=card.url,
68 | capabilities=card.capabilities.model_dump(),
69 | )
70 | )
71 | return agents
72 |
73 | def refresh_agent(self, agent_name: str):
74 | """
75 | Refresh the agent card for a specific agent.
76 |
77 | Args:
78 | agent_name: Name of the agent to refresh
79 | """
80 | agent = self.agent_manager.get_agent(agent_name)
81 | if agent and isinstance(agent, LocalAgent):
82 | agent_url = f"{self.base_url}/{agent_name}"
83 | self._agent_cards[agent_name] = create_agent_card(agent, agent_url)
84 |
85 | def refresh_all_agents(self):
86 | """Refresh all agent cards"""
87 | self._initialize_agent_cards()
88 |
--------------------------------------------------------------------------------
/tests/anthropic.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from unittest.mock import Mock, patch
4 | from anthropic.types import Message, TextBlock, ToolUseBlock
5 | from AgentCrew.modules.anthropic import AnthropicClient, SUMMARIZE_PROMPT
6 |
7 |
8 | @pytest.fixture
9 | def mock_anthropic():
10 | with patch("modules.anthropic.Anthropic") as mock:
11 | yield mock
12 |
13 |
14 | @pytest.fixture
15 | def mock_env_vars():
16 | with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
17 | yield
18 |
19 |
20 | def test_anthropic_client_init_missing_api_key():
21 | with patch.dict(os.environ, clear=True):
22 | with pytest.raises(
23 | ValueError, match="ANTHROPIC_API_KEY not found in environment variables"
24 | ):
25 | AnthropicClient()
26 |
27 |
28 | def test_anthropic_client_init_success(mock_env_vars):
29 | client = AnthropicClient()
30 | assert client is not None
31 |
32 |
33 | def test_summarize_content_success(mock_anthropic, mock_env_vars):
34 | # Arrange
35 | test_content = "Test markdown content"
36 | expected_summary = "Summarized content"
37 |
38 | mock_message = Mock(spec=Message)
39 | mock_text_block = Mock(spec=TextBlock)
40 | mock_text_block.text = expected_summary
41 | mock_message.content = [mock_text_block]
42 |
43 | mock_client = mock_anthropic.return_value
44 | mock_client.messages.create.return_value = mock_message
45 |
46 | # Act
47 | client = AnthropicClient()
48 | result = client.summarize_content(test_content)
49 |
50 | # Assert
51 | assert result == expected_summary
52 | mock_client.messages.create.assert_called_once_with(
53 | model="claude-3-5-sonnet-latest",
54 | max_tokens=1000,
55 | messages=[
56 | {
57 | "role": "user",
58 | "content": SUMMARIZE_PROMPT.format(content=test_content),
59 | }
60 | ],
61 | )
62 |
63 |
64 | def test_summarize_content_api_error(mock_anthropic, mock_env_vars):
65 | # Arrange
66 | mock_client = mock_anthropic.return_value
67 | mock_client.messages.create.side_effect = Exception("API Error")
68 |
69 | # Act & Assert
70 | client = AnthropicClient()
71 | with pytest.raises(Exception, match="Failed to summarize content: API Error"):
72 | client.summarize_content("test content")
73 |
74 |
75 | def test_summarize_content_invalid_response(mock_anthropic, mock_env_vars):
76 | # Arrange
77 | mock_message = Mock(spec=Message)
78 | mock_invalid_block = Mock(spec=ToolUseBlock) # Not a TextBlock
79 | mock_message.content = [mock_invalid_block]
80 |
81 | mock_client = mock_anthropic.return_value
82 | mock_client.messages.create.return_value = mock_message
83 |
84 | # Act & Assert
85 | client = AnthropicClient()
86 | with pytest.raises(
87 | Exception,
88 | match="Failed to summarize content: Unexpected response type: message content is not a TextBlock",
89 | ):
90 | client.summarize_content("test content")
91 |
--------------------------------------------------------------------------------
/specs/18_spec_multi-agent-part2.md:
--------------------------------------------------------------------------------
1 | # Update LLM Services to Use Agent System Prompts
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Modify LLM services to use the current agent's system prompt instead of the hardcoded one
7 | - Ensure that when agents are switched, both the system prompt and tool set are properly updated
8 | - Update the agent manager to handle tool registration and system prompt updates when switching agents
9 | - Fix the commented-out handoff tool registration in setup_agents function
10 | - Enable the agent routing in InteractiveChat
11 |
12 | ## Contexts
13 | - modules/llm/base.py: Contains the BaseLLMService class which defines the interface for LLM services
14 | - modules/anthropic/service.py: Contains the AnthropicService implementation
15 | - modules/openai/service.py: Contains the OpenAIService implementation
16 | - modules/groq/service.py: Contains the GroqService implementation
17 | - modules/agents/manager.py: Contains the AgentManager class
18 | - modules/chat/interactive.py: Contains the InteractiveChat class
19 | - main.py: Contains the setup_agents function
20 |
21 | ## Low-level Tasks
22 | 1. UPDATE modules/llm/base.py:
23 | - Add a set_system_prompt(self, system_prompt: str) abstract method to BaseLLMService
24 | - Add a clear_tools(self) abstract method to BaseLLMService
25 |
26 | 2. UPDATE modules/anthropic/service.py:
27 | - Add a system_prompt instance variable in __init__ method, initialized with CHAT_SYSTEM_PROMPT
28 | - Implement set_system_prompt(self, system_prompt: str) method to update the system_prompt variable
29 | - Implement clear_tools(self) method to reset self.tools and self.tool_handlers
30 | - Modify stream_assistant_response to use self.system_prompt instead of CHAT_SYSTEM_PROMPT
31 |
32 | 3. UPDATE modules/openai/service.py:
33 | - Add a system_prompt instance variable in __init__ method, initialized with CHAT_SYSTEM_PROMPT
34 | - Implement set_system_prompt(self, system_prompt: str) method to update the system_prompt variable
35 | - Implement clear_tools(self) method to reset self.tools and self.tool_handlers
36 | - Modify stream_assistant_response to use self.system_prompt instead of hardcoded prompt
37 |
38 | 4. UPDATE modules/groq/service.py:
39 | - Add a system_prompt instance variable in __init__ method, initialized with CHAT_SYSTEM_PROMPT
40 | - Implement set_system_prompt(self, system_prompt: str) method to update the system_prompt variable
41 | - Implement clear_tools(self) method to reset self.tools and self.tool_handlers
42 | - Modify stream_assistant_response to use self.system_prompt instead of hardcoded prompt
43 |
44 | 5. UPDATE modules/agents/manager.py:
45 | - Modify select_agent(self, agent_name: str) method to:
46 | - Clear tools from the previous agent's LLM service if there was a previous agent
47 | - Update the LLM service with the new agent's system prompt via set_system_prompt
48 | - Re-register the agent's tools with the LLM service
49 |
50 |
--------------------------------------------------------------------------------
/specs/27_refactoring_agent_tools.md:
--------------------------------------------------------------------------------
1 |
2 | # Refactor Tool Registration Logic
3 |
4 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
5 |
6 | ## Objectives
7 | - Move the tool registration logic from `AgentCrew.main.py` to the `BaseAgent` class in `AgentCrew.modules/agents/base.py`.
8 | - Allow each agent to define its own list of tools.
9 | - Improve separation of concerns and reduce complexity in the tool registration process.
10 | - Ensure that all existing functionality remains intact after the refactoring.
11 |
12 | ## Contexts
13 | - AgentCrew.main.py: Contains the `register_agent_tools` function and the `setup_agents` function.
14 | - AgentCrew.modules/agents/base.py: Contains the `Agent` class.
15 | - AgentCrew.modules/agents/specialized/architect.py: Contains the `ArchitectAgent` class.
16 | - AgentCrew.modules/agents/specialized/code_assistant.py: Contains the `CodeAssistantAgent` class.
17 | - AgentCrew.modules/agents/specialized/documentation.py: Contains the `DocumentationAgent` class.
18 |
19 | ## Low-level Tasks
20 | 1. UPDATE AgentCrew.modules/agents/base.py:
21 | - Modify the `Agent` class to accept a `services` dictionary in its `__init__` method.
22 | - Add a `tools` attribute (List[str]) to the `Agent` class to define the list of tool names that the agent needs.
23 | - Move the logic from the `register_agent_tools` function in `main.py` to a new `register_tools` method in the `Agent` class. This method should iterate through the `tools` list and register the corresponding tools using the `services` dictionary.
24 | - Call the `register_tools` method at the end of the `__init__` method in the `Agent` class.
25 |
26 | 2. UPDATE AgentCrew.modules/agents/specialized/architect.py:
27 | - Add a `tools` attribute to the `ArchitectAgent` class and initialize it with the appropriate list of tool names: `["handoff", "clipboard", "memory", "web_search", "code_analysis"]`.
28 | - Ensure the `__init__` method of the `ArchitectAgent` calls the superclass `__init__` method with the `services` dictionary.
29 |
30 | 3. UPDATE AgentCrew.modules/agents/specialized/code_assistant.py:
31 | - Add a `tools` attribute to the `CodeAssistantAgent` class and initialize it with the appropriate list of tool names: `["handoff", "clipboard", "memory", "code_analysis", "spec_validator"]`.
32 | - Ensure the `__init__` method of the `CodeAssistantAgent` calls the superclass `__init__` method with the `services` dictionary.
33 |
34 | 4. UPDATE AgentCrew.modules/agents/specialized/documentation.py:
35 | - Add a `tools` attribute to the `DocumentationAgent` class and initialize it with the appropriate list of tool names: `["handoff", "clipboard", "memory", "web_search"]`.
36 | - Ensure the `__init__` method of the `DocumentationAgent` calls the superclass `__init__` method with the `services` dictionary.
37 |
38 | 5. UPDATE AgentCrew.main.py:
39 | - Remove the `register_agent_tools` function.
40 | - Modify the `setup_agents` function to pass the `services` dictionary to each agent during initialization.
41 |
--------------------------------------------------------------------------------
/AgentCrew/modules/agents/base.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from abc import ABC, abstractmethod
4 | from typing import TYPE_CHECKING
5 | from enum import Enum
6 |
7 | if TYPE_CHECKING:
8 | from typing import AsyncGenerator, Tuple, Dict, List, Optional, Any, Callable, Union
9 |
10 |
11 | class MessageType(Enum):
12 | Assistant = 0
13 | Thinking = 1
14 | ToolResult = 2
15 | FileContent = 3
16 |
17 |
18 | class BaseAgent(ABC):
19 | """Base class for all specialized agents."""
20 |
21 | def __init__(self, name, description):
22 | self.name = name
23 | self.description = description
24 | self.history = []
25 | self.is_active = False
26 | self.shared_context_pool: Dict[str, List[int]] = {}
27 |
28 | @abstractmethod
29 | def activate(self) -> bool:
30 | """
31 | Activate this agent by registering all tools with the LLM service.
32 |
33 | Returns:
34 | True if activation was successful, False otherwise
35 | """
36 | pass
37 |
38 | @abstractmethod
39 | def deactivate(self) -> bool:
40 | """
41 | Deactivate this agent by clearing all tools from the LLM service.
42 |
43 | Returns:
44 | True if deactivation was successful, False otherwise
45 | """
46 | pass
47 |
48 | @abstractmethod
49 | def append_message(self, messages: Union[Dict, List[Dict]]):
50 | """Append a message or list of messages to the agent's history."""
51 | pass
52 |
53 | @property
54 | @abstractmethod
55 | def clean_history(self) -> List:
56 | pass
57 |
58 | @abstractmethod
59 | def get_provider(self) -> str:
60 | pass
61 |
62 | @abstractmethod
63 | def get_model(self) -> str:
64 | pass
65 |
66 | @abstractmethod
67 | def is_streaming(self) -> bool:
68 | pass
69 |
70 | @abstractmethod
71 | def format_message(
72 | self, message_type: MessageType, message_data: Dict[str, Any]
73 | ) -> Optional[Dict[str, Any]]:
74 | pass
75 |
76 | @abstractmethod
77 | async def execute_tool_call(self, tool_name: str, tool_input: Dict) -> Any:
78 | pass
79 |
80 | @abstractmethod
81 | def configure_think(self, think_setting):
82 | pass
83 |
84 | @abstractmethod
85 | def calculate_usage_cost(self, input_tokens, output_tokens) -> float:
86 | pass
87 |
88 | @abstractmethod
89 | async def process_messages(
90 | self,
91 | messages: Optional[List[Dict[str, Any]]] = None,
92 | callback: Optional[Callable] = None,
93 | ) -> AsyncGenerator:
94 | """
95 | Process messages using this agent.
96 |
97 | Args:
98 | messages: The messages to process
99 |
100 | Returns:
101 | The processed messages with the agent's response
102 | """
103 | yield
104 |
105 | @abstractmethod
106 | def get_process_result(self) -> Tuple:
107 | """
108 | @DEPRECATED: Use the callback in process_messages instead.
109 | """
110 | pass
111 |
--------------------------------------------------------------------------------
/tests/chat/test_message_handler.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import MagicMock, patch
3 | from AgentCrew.modules.chat.message_handler import MessageHandler
4 | from AgentCrew.modules.llm.base import BaseLLMService
5 | from rich.console import Console
6 |
7 |
8 | class TestMessageHandler(unittest.TestCase):
9 | def setUp(self):
10 | # Create a mock LLM service
11 | self.mock_llm = MagicMock(spec=BaseLLMService)
12 | self.mock_console = MagicMock(spec=Console)
13 | self.handler = MessageHandler(self.mock_llm, self.mock_console)
14 |
15 | def test_process_file_command(self):
16 | # Setup
17 | self.mock_llm.handle_file_command.return_value = {
18 | "type": "file",
19 | "content": "file content",
20 | }
21 |
22 | # Execute
23 | result = self.handler.process_file_command("test_file.txt")
24 |
25 | # Assert
26 | self.mock_llm.handle_file_command.assert_called_once()
27 | self.assertIsNotNone(result)
28 | self.assertEqual(result["role"], "user")
29 |
30 | def test_process_file_command_failure(self):
31 | # Setup
32 | self.mock_llm.handle_file_command.return_value = None
33 |
34 | # Execute
35 | result = self.handler.process_file_command("nonexistent_file.txt")
36 |
37 | # Assert
38 | self.assertIsNone(result)
39 |
40 | def test_process_file_for_message(self):
41 | # Setup
42 | expected_content = {"type": "file", "content": "file content"}
43 | self.mock_llm.process_file_for_message.return_value = expected_content
44 |
45 | # Execute
46 | result = self.handler.process_file_for_message("test_file.txt")
47 |
48 | # Assert
49 | self.assertEqual(result, expected_content)
50 |
51 | def test_format_assistant_message(self):
52 | # Setup
53 | expected_message = {"role": "assistant", "content": "test response"}
54 | self.mock_llm.format_assistant_message.return_value = expected_message
55 |
56 | # Execute
57 | result = self.handler.format_assistant_message("test response")
58 |
59 | # Assert
60 | self.assertEqual(result, expected_message)
61 |
62 | def test_calculate_cost(self):
63 | # Setup
64 | self.mock_llm.calculate_cost.return_value = 0.05
65 |
66 | # Execute
67 | cost = self.handler.calculate_cost(1000, 500)
68 |
69 | # Assert
70 | self.assertEqual(cost, 0.05)
71 | self.assertEqual(self.handler.session_cost, 0.05)
72 |
73 | # Test cumulative cost
74 | cost = self.handler.calculate_cost(1000, 500)
75 | self.assertEqual(self.handler.session_cost, 0.1)
76 |
77 | @patch("modules.chat.message_handler.MessageHandler._clear_to_start")
78 | def test_stream_assistant_response_error(self, mock_clear):
79 | # Setup
80 | self.mock_llm.stream_assistant_response.side_effect = Exception("Test error")
81 |
82 | # Execute
83 | response, input_tokens, output_tokens = self.handler.stream_assistant_response(
84 | []
85 | )
86 |
87 | # Assert
88 | self.assertIsNone(response)
89 | self.assertEqual(input_tokens, 0)
90 | self.assertEqual(output_tokens, 0)
91 |
92 |
93 | if __name__ == "__main__":
94 | unittest.main()
95 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/worker.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | from AgentCrew.modules.chat.message_handler import MessageHandler
3 | import asyncio
4 | from PySide6.QtCore import (
5 | Slot,
6 | QObject,
7 | Signal,
8 | )
9 | from loguru import logger
10 |
11 |
12 | class LLMWorker(QObject):
13 | """Worker object that processes LLM requests in a separate thread"""
14 |
15 | # Signals for thread communication
16 | response_ready = Signal(str, int, int) # response, input_tokens, output_tokens
17 | error = Signal(str)
18 | status_message = Signal(str)
19 | request_exit = Signal()
20 | request_clear = Signal()
21 | thinking_started = Signal(str) # agent_name
22 | thinking_chunk = Signal(str) # thinking text chunk
23 | thinking_completed = Signal()
24 |
25 | # Signal to request processing - passing the user input as a string
26 | process_request = Signal(str)
27 |
28 | def __init__(self):
29 | super().__init__()
30 | self.user_input = None
31 | self.message_handler = None # Will be set in connect_handler
32 |
33 | def connect_handler(self, message_handler: MessageHandler):
34 | """Connect to the message handler - called from main thread before processing begins"""
35 | self.message_handler = message_handler
36 | # Connect the process_request signal to our processing slot
37 | self.process_request.connect(self.process_input)
38 |
39 | @Slot(str)
40 | def process_input(self, user_input):
41 | """Process the user input with the message handler"""
42 | try:
43 | if not self.message_handler:
44 | self.error.emit("Message handler not connected")
45 | return
46 |
47 | if not user_input:
48 | return
49 |
50 | # Process user input (commands, etc.)
51 | exit_flag, clear_flag = asyncio.run(
52 | self.message_handler.process_user_input(user_input)
53 | )
54 |
55 | # Handle command results
56 | if exit_flag:
57 | self.status_message.emit("Exiting...")
58 | self.request_exit.emit()
59 | return
60 |
61 | if clear_flag:
62 | # self.request_clear.emit()
63 | return # Skip further processing if chat was cleared
64 |
65 | # Get assistant response
66 | (
67 | assistant_response,
68 | input_tokens,
69 | output_tokens,
70 | ) = asyncio.run(self.message_handler.get_assistant_response())
71 |
72 | # Emit the response
73 | if assistant_response:
74 | self.response_ready.emit(
75 | assistant_response, input_tokens, output_tokens
76 | )
77 | else:
78 | logger.info("No response received from assistant")
79 | self.status_message.emit("No response received")
80 | self.message_handler._notify(
81 | "error", "No response received from assistant"
82 | )
83 |
84 | except Exception as e:
85 | traceback_str = traceback.format_exc()
86 | error_msg = f"{str(e)}\n{traceback_str}"
87 | logger.error(f"Error in LLMWorker: {error_msg}")
88 | self.error.emit(str(e))
89 |
--------------------------------------------------------------------------------
/docker/pyproject.docker.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "agentcrew-ai"
3 | version = "0.8.9"
4 | requires-python = ">=3.12"
5 | classifiers = [
6 | "Programming Language :: Python :: 3",
7 | "Operating System :: OS Independent",
8 | "Development Status :: 4 - Beta",
9 | "Intended Audience :: Developers",
10 | "Topic :: Software Development :: Libraries :: Python Modules",
11 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
12 | ]
13 | license = "Apache-2.0"
14 | license-files = ["LICENSE"]
15 | description = "Multi-Agents Interactive Chat Tool"
16 | authors = [
17 | {name = "Quy Truong", email = "quy.truong@saigontechnology.com"},
18 | ]
19 | readme = "README.md"
20 |
21 | dependencies = [
22 | "click",
23 | "python-dotenv",
24 | "anthropic",
25 | "pytest",
26 | "prompt-toolkit>=3.0.52",
27 | "rich>=13.9.4",
28 | "pyperclip>=1.9.0",
29 | "tavily-python>=0.5.1",
30 | "pillow>=11.0.0",
31 | "groq>=0.18.0",
32 | "chromadb>=1.0.0",
33 | "openai>=1.65.2",
34 | "tree-sitter>=0.23.2",
35 | "mcp>=1.3.0",
36 | "google-genai>=1.7.0",
37 | "toml>=0.10.2",
38 | "markdown>=3.7",
39 | "tree-sitter-language-pack>=0.7.0",
40 | "nest-asyncio>=1.6.0",
41 | "voyageai>=0.3.2",
42 | "numpy>=1.24.4,<2; python_version < '3.13' and sys_platform == 'darwin'",
43 | "pywin32; sys_platform == 'win32'",
44 | "pyobjc; sys_platform == 'darwin'",
45 | "a2a-sdk>=0.3.10",
46 | "xmltodict>=0.14.2",
47 | "jsonref>=1.1.0",
48 | "pychromedevtools>=0.3.3",
49 | "html-to-markdown>=2.9.1",
50 | "pip-system-certs>=5.2",
51 | "loguru>=0.7.3",
52 | "jsonschema>=4.25.1",
53 | ]
54 |
55 | [project.optional-dependencies]
56 | cpu = [
57 | "torch>=2.8.0",
58 | "torchvision>=0.23.0",
59 | "torchaudio>=2.8.0",
60 | "docling>=2.26.0",
61 | ]
62 | nvidia = [
63 | "torch>=2.8.0",
64 | "torchvision>=0.23.0",
65 | "torchaudio>=2.8.0",
66 | "docling>=2.26.0",
67 | ]
68 |
69 | [tool.uv]
70 | conflicts = [
71 | [
72 | { extra = "cpu" },
73 | { extra = "nvidia" },
74 | ],
75 | ]
76 |
77 | [tool.uv.sources]
78 | torch = [
79 | { index = "pytorch-cpu", extra = "cpu" },
80 | { index = "pytorch-cu128", extra = "nvidia" },
81 | ]
82 | torchvision = [
83 | { index = "pytorch-cpu", extra = "cpu" },
84 | { index = "pytorch-cu128", extra = "nvidia" },
85 | ]
86 |
87 | torchaudio = [
88 | { index = "pytorch-cpu", extra = "cpu" },
89 | { index = "pytorch-cu128", extra = "nvidia" },
90 | ]
91 |
92 | [[tool.uv.index]]
93 | name = "pytorch-cpu"
94 | url = "https://download.pytorch.org/whl/cpu"
95 | explicit = true
96 |
97 | [[tool.uv.index]]
98 | name = "pytorch-cu128"
99 | url = "https://download.pytorch.org/whl/cu128"
100 | explicit = true
101 |
102 | [project.urls]
103 | "Homepage" = "https://github.com/daltonnyx/AgentCrew"
104 | "Bug Tracker" = "https://github.com/daltonnyx/AgentCrew/issues"
105 |
106 |
107 | [build-system]
108 | requires = ["setuptools>=61.0"]
109 | build-backend = "setuptools.build_meta"
110 |
111 | [project.scripts]
112 | agentcrew = "AgentCrew.main_docker:cli_prod"
113 |
114 | [tool.setuptools]
115 | include-package-data = true
116 |
117 | [tool.setuptools.packages.find]
118 | where = ["./"]
119 |
120 | [dependency-groups]
121 | dev = [
122 | "pygments>=2.19.1",
123 | "pyinstaller>=6.13.0",
124 | "langfuse>=3.0.1",
125 | ]
126 |
--------------------------------------------------------------------------------
/tests/web_search_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import os
3 | from dotenv import load_dotenv
4 | from AgentCrew.modules.web_search.service import TavilySearchService
5 | from AgentCrew.modules.web_search.tool import (
6 | get_web_search_tool_handler,
7 | get_web_extract_tool_handler,
8 | )
9 |
10 |
11 | class WebSearchTest(unittest.TestCase):
12 | @classmethod
13 | def setUpClass(cls):
14 | """Set up the test environment once before all tests."""
15 | # Ensure API key is loaded
16 | load_dotenv()
17 | if not os.getenv("TAVILY_API_KEY"):
18 | raise unittest.SkipTest("TAVILY_API_KEY not found in environment variables")
19 |
20 | cls.tavily_service = TavilySearchService()
21 | cls.search_handler = get_web_search_tool_handler(cls.tavily_service)
22 | cls.extract_handler = get_web_extract_tool_handler(cls.tavily_service)
23 |
24 | def test_web_search_basic(self):
25 | """Test basic web search functionality."""
26 | query = "Python programming language"
27 | result = self.tavily_service.search(query=query, max_results=2)
28 |
29 | self.assertIn("results", result)
30 | self.assertIsInstance(result["results"], list)
31 | self.assertGreaterEqual(len(result["results"]), 1)
32 |
33 | # Check result structure
34 | first_result = result["results"][0]
35 | self.assertIn("title", first_result)
36 | self.assertIn("url", first_result)
37 | self.assertIn("content", first_result)
38 |
39 | def test_web_search_handler(self):
40 | """Test the web search tool handler."""
41 | params = {
42 | "query": "Artificial intelligence news",
43 | "search_depth": "basic",
44 | "max_results": 3,
45 | }
46 |
47 | result = self.search_handler(**params)
48 |
49 | # The handler returns formatted text
50 | self.assertIsInstance(result, str)
51 | self.assertIn("Search Results", result)
52 | self.assertIn("URL:", result)
53 |
54 | def test_web_extract_handler(self):
55 | """Test the web extract tool handler."""
56 | # Use a stable URL that's unlikely to change or disappear
57 | params = {"url": "https://www.python.org/"}
58 |
59 | result = self.extract_handler(**params)
60 |
61 | # The handler returns formatted text
62 | self.assertIsInstance(result, str)
63 | self.assertIn("Extracted content", result)
64 | self.assertNotIn("Extract error", result)
65 |
66 | def test_search_with_advanced_depth(self):
67 | """Test search with advanced depth parameter."""
68 | query = "Latest developments in quantum computing"
69 | result = self.tavily_service.search(
70 | query=query, search_depth="advanced", max_results=2
71 | )
72 |
73 | self.assertIn("results", result)
74 | self.assertIsInstance(result["results"], list)
75 |
76 | def test_invalid_search_params(self):
77 | """Test search handler with empty query."""
78 | result = self.search_handler(query="")
79 | self.assertIn("Error", result)
80 |
81 | def test_invalid_extract_params(self):
82 | """Test extract handler with empty URL."""
83 | result = self.extract_handler(url="")
84 | self.assertIn("Error", result)
85 |
86 |
87 | if __name__ == "__main__":
88 | unittest.main()
89 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/config_window.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtWidgets import (
2 | QDialog,
3 | QTabWidget,
4 | QVBoxLayout,
5 | QHBoxLayout,
6 | QPushButton,
7 | )
8 |
9 | from PySide6.QtCore import Qt
10 | from AgentCrew.modules.config import ConfigManagement
11 | from AgentCrew.modules.gui.themes import StyleProvider
12 | from .configs.custom_llm_provider import CustomLLMProvidersConfigTab
13 | from .configs.global_settings import SettingsTab
14 | from .configs.agent_config import AgentsConfigTab
15 | from .configs.mcp_config import MCPsConfigTab
16 |
17 |
18 | class ConfigWindow(QDialog):
19 | """Configuration window with tabs for Agents and MCP servers."""
20 |
21 | def __init__(self, parent=None):
22 | super().__init__(parent)
23 | self.setWindowTitle("Agentcrew - Settings")
24 | self.setMinimumSize(800, 600)
25 |
26 | self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
27 |
28 | # Flag to track if changes were made
29 | self.changes_made = False
30 |
31 | # Initialize config management and style provider
32 | self.config_manager = ConfigManagement()
33 | self.style_provider = StyleProvider()
34 |
35 | # Create tab widget
36 | self.tab_widget = QTabWidget()
37 |
38 | # Create tabs
39 | self.agents_tab = AgentsConfigTab(self.config_manager)
40 | self.mcps_tab = MCPsConfigTab(self.config_manager)
41 | self.settings_tab = SettingsTab(self.config_manager)
42 | self.custom_llm_providers_tab = CustomLLMProvidersConfigTab(self.config_manager)
43 |
44 | # Connect change signals
45 | self.agents_tab.config_changed.connect(self.on_config_changed)
46 | self.mcps_tab.config_changed.connect(self.on_config_changed)
47 | self.settings_tab.config_changed.connect(self.on_config_changed)
48 | self.custom_llm_providers_tab.config_changed.connect(self.on_config_changed)
49 |
50 | # Add tabs to widget
51 | self.tab_widget.addTab(self.agents_tab, "Agents")
52 | self.tab_widget.addTab(self.mcps_tab, "MCP Servers")
53 | self.tab_widget.addTab(self.custom_llm_providers_tab, "Custom LLMs")
54 | self.tab_widget.addTab(self.settings_tab, "Settings")
55 |
56 | # Main layout
57 | layout = QVBoxLayout()
58 | layout.addWidget(self.tab_widget)
59 |
60 | # Add buttons at the bottom
61 | button_layout = QHBoxLayout()
62 | self.close_button = QPushButton("Close")
63 | self.close_button.clicked.connect(self.on_close)
64 | button_layout.addStretch()
65 | button_layout.addWidget(self.close_button)
66 |
67 | layout.addLayout(button_layout)
68 | self.setLayout(layout)
69 |
70 | # Apply styling
71 | self.setStyleSheet(self.style_provider.get_config_window_style())
72 |
73 | def on_config_changed(self):
74 | """Track that changes were made to configuration"""
75 | self.changes_made = True
76 |
77 | def on_close(self):
78 | """Handle close button click with restart notification if needed"""
79 | # if self.changes_made:
80 | # QMessageBox.information(
81 | # self,
82 | # "Configuration Changed",
83 | # "Configuration changes have been saved.\n\nPlease restart the application for all changes to take effect."
84 | # )
85 | self.accept()
86 |
--------------------------------------------------------------------------------
/specs/19_spec_multi-agent-part3.md:
--------------------------------------------------------------------------------
1 | # Update Agent System to Manage Tool Registration
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Move tool registration from LLM services to individual agents
7 | - Define specific tool sets for each specialized agent in main.py
8 | - Ensure tools are properly registered/unregistered when switching agents
9 | - Maintain compatibility with model switching functionality
10 | - Fix the commented-out handoff tool registration in setup_agents function
11 | - Enable the agent routing in InteractiveChat
12 |
13 | ## Contexts
14 | - modules/llm/base.py: Contains the BaseLLMService class which defines the interface for LLM services
15 | - modules/anthropic/service.py: Contains the AnthropicService implementation
16 | - modules/openai/service.py: Contains the OpenAIService implementation
17 | - modules/groq/service.py: Contains the GroqService implementation
18 | - modules/agents/base.py: Contains the Agent base class
19 | - modules/agents/manager.py: Contains the AgentManager class
20 | - modules/agents/specialized/architect.py: Contains the ArchitectAgent class
21 | - modules/agents/specialized/code_assistant.py: Contains the CodeAssistantAgent class
22 | - modules/agents/specialized/documentation.py: Contains the DocumentationAgent class
23 | - modules/agents/specialized/evaluation.py: Contains the EvaluationAgent class
24 | - modules/chat/interactive.py: Contains the InteractiveChat class
25 | - modules/tools/registry.py: Contains the ToolRegistry class
26 | - main.py: Contains the setup_agents function and tool registration
27 |
28 | ## Low-level Tasks
29 | 1. UPDATE modules/llm/base.py:
30 | - Add a clear_tools(self) abstract method to BaseLLMService
31 |
32 | 2. UPDATE modules/anthropic/service.py:
33 | - Implement clear_tools(self) method to reset self.tools and self.tool_handlers
34 |
35 | 3. UPDATE modules/openai/service.py and modules/groq/service.py:
36 | - Make the same changes as in anthropic/service.py for consistency
37 |
38 | 4. UPDATE modules/agents/base.py:
39 | - Modify register_tool method to store tool definitions in the agent's tools list
40 | - Add a register_tools_with_llm(self) method to register all tools with the LLM service
41 | - Add a clear_tools_from_llm(self) method to clear tools from the LLM service
42 |
43 | 5. UPDATE modules/agents/manager.py:
44 | - Modify select_agent(self, agent_name: str) method to:
45 | - Clear tools from the previous agent's LLM service using clear_tools_from_llm
46 | - Register the new agent's tools with the LLM service using register_tools_with_llm
47 |
48 | 6. UPDATE main.py:
49 | - Remove the global tool registration for the LLM service (llm_service.register_all_tools())
50 | - Define specific tool sets for each specialized agent
51 | - Register appropriate tools with each agent type
52 | - Uncomment the register_handoff line in setup_agents function
53 |
54 | 7. UPDATE modules/chat/interactive.py:
55 | - Uncomment the line `messages = self.agent_manager.route_message(messages)` in _stream_assistant_response method
56 | - Update the model switching logic to preserve agent-specific tools when switching models
57 |
58 | 8. CREATE a tool registration helper function in main.py:
59 | - Create a register_agent_tools(agent, services) function that registers the appropriate tools for each agent type
60 | - Call this function for each agent in setup_agents
61 |
--------------------------------------------------------------------------------
/AgentCrew/modules/mcpclient/config.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from dataclasses import dataclass
4 | from typing import Dict, List, Optional
5 | from loguru import logger
6 |
7 |
8 | @dataclass
9 | class MCPServerConfig:
10 | """Configuration for an MCP server."""
11 |
12 | name: str
13 | command: str
14 | args: List[str]
15 | enabledForAgents: List[str]
16 | env: Optional[Dict[str, str]] = None
17 | streaming_server: bool = False
18 | url: str = ""
19 | headers: Optional[Dict[str, str]] = None
20 |
21 |
22 | class MCPConfigManager:
23 | """Manager for MCP server configurations."""
24 |
25 | def __init__(self, config_path: Optional[str] = None):
26 | """
27 | Initialize the configuration manager.
28 |
29 | Args:
30 | config_path: Path to the configuration file. If None, uses the default path.
31 | """
32 | self.config_path = config_path or os.environ.get(
33 | "MCP_CONFIG_PATH",
34 | os.path.join(
35 | os.path.dirname(
36 | os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
37 | ),
38 | "mcp_servers.json",
39 | ),
40 | )
41 | self.configs: Dict[str, MCPServerConfig] = {}
42 |
43 | def load_config(self) -> Dict[str, MCPServerConfig]:
44 | """
45 | Load server configurations from the config file.
46 |
47 | Returns:
48 | Dictionary of server configurations keyed by server ID.
49 | """
50 | try:
51 | if not os.path.exists(self.config_path):
52 | # Create default config directory if it doesn't exist
53 | os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
54 | # Create empty config file
55 | with open(self.config_path, "w") as f:
56 | json.dump({}, f)
57 | return {}
58 |
59 | with open(self.config_path, "r") as f:
60 | config_data = json.load(f)
61 |
62 | self.configs = {}
63 | for server_id, config in config_data.items():
64 | self.configs[server_id] = MCPServerConfig(
65 | name=config.get("name", server_id),
66 | command=config.get("command", ""),
67 | args=config.get("args", []),
68 | env=config.get("env"),
69 | enabledForAgents=config.get("enabledForAgents", []),
70 | streaming_server=config.get("streaming_server", False),
71 | url=config.get("url", ""),
72 | headers=config.get("headers"),
73 | )
74 |
75 | return self.configs
76 | except Exception as e:
77 | logger.error(f"Error loading MCP configuration: {e}")
78 | return {}
79 |
80 | def get_enabled_servers(
81 | self, agent_name: Optional[str] = None
82 | ) -> Dict[str, MCPServerConfig]:
83 | """
84 | Get all enabled server configurations.
85 |
86 | Returns:
87 | Dictionary of enabled server configurations.
88 | """
89 | if agent_name:
90 | return {
91 | server_id: config
92 | for server_id, config in self.configs.items()
93 | if agent_name in config.enabledForAgents
94 | }
95 |
96 | return {
97 | server_id: config
98 | for server_id, config in self.configs.items()
99 | if len(config.enabledForAgents) > 0
100 | }
101 |
--------------------------------------------------------------------------------
/specs/12_add_multi_model_swap.md:
--------------------------------------------------------------------------------
1 | # Multi-Model Chat Implementation
2 |
3 | ## Objectives
4 |
5 | - Implement model switching functionality with singleton service instances
6 | - Maintain seamless conversation history across different providers
7 | - Support tool operations consistently across model switches
8 | - Provide user-friendly `/model` command interface
9 | - Ensure efficient resource usage with provider-specific instances
10 |
11 | ## Contexts
12 |
13 | - main.py: Main application entry point and service orchestration
14 | - modules/llm/base.py: Base service implementation and interfaces
15 | - modules/chat/interactive.py: Chat interface and command handling
16 | - modules/llm/models.py: Model registry and configuration (New)
17 | - modules/llm/service_manager.py: Service instance management (New)
18 | - modules/llm/message.py: Message format standardization (New)
19 | - modules/{anthropic,openai,groq}/service.py: Provider implementations
20 |
21 | ## Low-level Tasks
22 |
23 | 1. CREATE modules/llm/service_manager.py:
24 |
25 | - Implement ServiceManager singleton class
26 | - Add provider service instance management
27 | - Handle service initialization and model updates
28 | - Implement provider-specific service class mapping
29 |
30 | 2. CREATE modules/llm/models.py:
31 |
32 | - Create Model dataclass for model metadata
33 | - Implement ModelRegistry for model management
34 | - Add default model configurations
35 | - Handle model switching logic
36 |
37 | 3. CREATE modules/llm/message.py:
38 |
39 | - Define standard Message format
40 | - Create MessageTransformer for provider conversions
41 | - Implement provider-specific format adapters
42 | - Handle tool message conversions
43 |
44 | 4. UPDATE modules/chat/interactive.py:
45 |
46 | - Add model command handler
47 | - Update message processing for model switching
48 | - Implement conversation history management
49 | - Add model-specific error handling
50 |
51 | 5. UPDATE modules/llm/base.py:
52 |
53 | - Add model switching support to base service
54 | - Update streaming interface for consistency
55 | - Add message format validation
56 | - Implement tool handling interface
57 |
58 | 6. UPDATE provider services:
59 |
60 | - Add model-specific initializations
61 | - Implement message format conversions
62 | - Add provider-specific streaming
63 | - Update tool handling implementations
64 |
65 | 7. UPDATE main.py:
66 |
67 | - Initialize service manager and model registry
68 | - Update service loading for model support
69 | - Add default model configuration
70 | - Implement proper cleanup handling
71 |
72 | 8. CREATE tests/model_switching_test.py:
73 | - Test service singleton behavior
74 | - Test model switching functionality
75 | - Test message format conversions
76 | - Test conversation continuity
77 | - Test tool operations across switches
78 |
79 | ## Additional Considerations
80 |
81 | 1. Configuration Management:
82 |
83 | - Environment variables for API keys
84 | - Model configuration file
85 | - Provider capabilities mapping
86 | - Default model settings
87 |
88 | 2. Error Handling:
89 |
90 | - Invalid model selection
91 | - Missing API credentials
92 | - Failed model switches
93 | - Message format errors
94 | - Tool compatibility issues
95 |
96 | 3. Performance:
97 |
98 | - Lazy service initialization
99 | - Efficient message conversion
100 | - Memory management
101 | - Connection pooling
102 |
103 | 4. User Experience:
104 |
105 | - Clear model switch feedback
106 | - Model capability indication
107 | - Error message clarity
108 | - Command help documentation
109 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/filter_hidden_elements.js:
--------------------------------------------------------------------------------
1 | function filterHiddenElements() {
2 | try {
3 | function isElementVisible(element) {
4 | if (!element || element.nodeType !== 1) {
5 | return false;
6 | }
7 |
8 | if (element.disabled) {
9 | return false;
10 | }
11 |
12 | if (!element.checkVisibility()) {
13 | return false;
14 | }
15 |
16 | return true;
17 | }
18 |
19 | function getXPath(element) {
20 | if (!element || element.nodeType !== 1) {
21 | return null;
22 | }
23 |
24 | if (element === document.documentElement) {
25 | return "/html[1]";
26 | }
27 |
28 | const parts = [];
29 | let current = element;
30 |
31 | while (current && current.nodeType === 1) {
32 | if (current === document.documentElement) {
33 | parts.unshift("/html[1]");
34 | break;
35 | }
36 |
37 | let index = 1;
38 | let sibling = current.previousElementSibling;
39 | while (sibling) {
40 | if (sibling.tagName === current.tagName) {
41 | index++;
42 | }
43 | sibling = sibling.previousElementSibling;
44 | }
45 |
46 | const tagName = current.tagName.toLowerCase();
47 | parts.unshift(`${tagName}[${index}]`);
48 | current = current.parentElement;
49 | }
50 |
51 | return parts.join("/");
52 | }
53 |
54 | function traverseAndMarkHidden(element, hiddenXPaths) {
55 | if (!element || element.nodeType !== 1) {
56 | return;
57 | }
58 |
59 | const tagName = element.tagName.toLowerCase();
60 | if (
61 | tagName === "script" ||
62 | tagName === "style" ||
63 | tagName === "noscript"
64 | ) {
65 | const xpath = getXPath(element);
66 | if (xpath) {
67 | hiddenXPaths.push(xpath);
68 | }
69 | return;
70 | }
71 |
72 | if (!isElementVisible(element)) {
73 | const xpath = getXPath(element);
74 | if (xpath) {
75 | hiddenXPaths.push(xpath);
76 | }
77 | return;
78 | }
79 |
80 | const children = Array.from(element.children);
81 | for (const child of children) {
82 | traverseAndMarkHidden(child, hiddenXPaths);
83 | }
84 | }
85 |
86 | const documentClone = document.documentElement.cloneNode(true);
87 | const cloneDoc = document.implementation.createHTMLDocument("");
88 | cloneDoc.replaceChild(documentClone, cloneDoc.documentElement);
89 |
90 | const hiddenXPaths = [];
91 | traverseAndMarkHidden(document.documentElement, hiddenXPaths);
92 |
93 | for (const xpath of hiddenXPaths) {
94 | const result = cloneDoc.evaluate(
95 | xpath,
96 | cloneDoc,
97 | null,
98 | XPathResult.FIRST_ORDERED_NODE_TYPE,
99 | null,
100 | );
101 | const cloneElement = result.singleNodeValue;
102 | if (cloneElement) {
103 | cloneElement.setAttribute("data-hidden", "true");
104 | }
105 | }
106 |
107 | const hiddenElements = cloneDoc.querySelectorAll("[data-hidden='true']");
108 | hiddenElements.forEach((el) => el.remove());
109 |
110 | const filteredHTML = cloneDoc.documentElement.outerHTML;
111 |
112 | return {
113 | success: true,
114 | html: filteredHTML,
115 | message: "Successfully filtered hidden elements using computed styles",
116 | };
117 | } catch (error) {
118 | return {
119 | success: false,
120 | error: error.message,
121 | stack: error.stack,
122 | };
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/AgentCrew/modules/browser_automation/js/extract_elements_by_text.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Extract elements containing specified text using XPath.
3 | * Uses comprehensive visibility checking including parent element chain.
4 | *
5 | * @param {string} text - The text to search for
6 | * @returns {Array} Array of elements containing the text
7 | */
8 | function extractElementsByText(text) {
9 | const elementsFound = [];
10 |
11 | // Utility function to check if element is truly visible (including parent chain)
12 | function isElementVisible(element) {
13 | if (!element || !element.nodeType === 1) {
14 | return false;
15 | }
16 |
17 | if (!element.checkVisibility()) {
18 | return false;
19 | }
20 |
21 | return true;
22 | }
23 |
24 | function getXPath(element) {
25 | if (element.id !== "") {
26 | return `//*[@id="${element.id}"]`;
27 | }
28 | if (element === document.body) {
29 | return "//" + element.tagName.toLowerCase();
30 | }
31 |
32 | var ix = 0;
33 | var siblings = element.parentNode.childNodes;
34 | for (var i = 0; i < siblings.length; i++) {
35 | var sibling = siblings[i];
36 | if (sibling === element)
37 | return (
38 | getXPath(element.parentNode) +
39 | "/" +
40 | element.tagName.toLowerCase() +
41 | "[" +
42 | (ix + 1) +
43 | "]"
44 | );
45 | if (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++;
46 | }
47 | }
48 |
49 | function getDirectTextContent(element) {
50 | let directText = "";
51 | for (const node of element.childNodes) {
52 | if (node.nodeType === Node.TEXT_NODE) {
53 | directText += node.textContent;
54 | }
55 | }
56 | return directText.trim();
57 | }
58 |
59 | try {
60 | const xpath = `//*[contains(., '${text}')]`;
61 | const result = document.evaluate(
62 | xpath,
63 | document,
64 | null,
65 | XPathResult.ANY_TYPE,
66 | null,
67 | );
68 |
69 | let element = result.iterateNext();
70 | const seenElements = new Set();
71 | const searchTextLower = text.toLowerCase();
72 |
73 | while (element) {
74 | if (isElementVisible(element)) {
75 | const directText = getDirectTextContent(element);
76 | const ariaLabel = element.getAttribute("aria-label") || "";
77 |
78 | if (
79 | directText.toLowerCase().includes(searchTextLower) ||
80 | ariaLabel.toLowerCase().includes(searchTextLower)
81 | ) {
82 | const elementXPath = getXPath(element);
83 |
84 | if (!seenElements.has(elementXPath)) {
85 | seenElements.add(elementXPath);
86 |
87 | let displayText = ariaLabel || directText || "";
88 | displayText = displayText.trim().replace(/\s+/g, " ");
89 | if (displayText.length > 100) {
90 | displayText = displayText.substring(0, 100) + "...";
91 | }
92 |
93 | elementsFound.push({
94 | xpath: elementXPath,
95 | text: displayText,
96 | tagName: element.tagName.toLowerCase(),
97 | className: element.className || "",
98 | id: element.id || "",
99 | });
100 | }
101 | }
102 | }
103 |
104 | element = result.iterateNext();
105 | }
106 |
107 | return elementsFound;
108 | } catch (error) {
109 | return [];
110 | }
111 | }
112 |
113 | // Export the function - when used in browser automation, wrap with IIFE and pass text
114 | // (() => {
115 | // const text = '{TEXT_PLACEHOLDER}';
116 | // return extractElementsByText(text);
117 | // })();
118 |
--------------------------------------------------------------------------------
/specs/16_spec_add_code_assistant.md:
--------------------------------------------------------------------------------
1 | # Implement CodeAssistant with Aider subprocess integration
2 |
3 | > Integrate CodeAssistant service to execute aider CLI with spec prompts in target repositories
4 |
5 | ## Objectives
6 | - Create CodeAssistant service to execute aider commands via subprocess
7 | - Implement implement_spec_prompt tool to trigger code generation
8 | - Ensure secure execution with proper CWD and env vars
9 | - Handle temp files and error conditions
10 |
11 | ## Contexts
12 | - modules/coder/service.py: New service class location
13 | - modules/coder/tool.py: Tool registration
14 | - modules/coder/__init__.py: Module initialization
15 | - modules/llm/service_manager.py: LLM service dependencies
16 | - modules/tools/registry.py: Tool registry
17 |
18 | ## Low-level Tasks
19 | 1. UPDATE modules/coder/service.py:
20 | ```python
21 | class CodeAssistant:
22 | def generate_implementation(self, spec_prompt: str, repo_path: str) -> str:
23 | sanitized_path = self._sanitize_repo_path(repo_path)
24 | with tempfile.NamedTemporaryFile(suffix='.spec', delete=False) as tf:
25 | tf.write(spec_prompt.encode())
26 | spec_path = tf.name
27 |
28 | aider_exec = os.getenv('AIDER_PATH', 'aider')
29 | command = [
30 | aider_exec,
31 | "generate",
32 | "--no-auto-commits",
33 | "--architect",
34 | "--dark-mode",
35 | "--model", "claude-3-7-sonnet-latest",
36 | "--yes-always",
37 | "--message-file", spec_path
38 | ]
39 |
40 | try:
41 | result = subprocess.run(
42 | command,
43 | cwd=str(sanitized_path),
44 | capture_output=True,
45 | text=True,
46 | timeout=120,
47 | check=True
48 | return result.stdout
49 | finally:
50 | Path(spec_path).unlink(missing_ok=True)
51 | ```
52 |
53 | 2. Add modules/coder/tool.py:
54 | ```python
55 | def get_implement_spec_prompt_tool_definition():
56 | return {
57 | "name": "implement_spec_prompt",
58 | "description": "Generate code via aider using spec prompt",
59 | "args": {
60 | "type": "object",
61 | "properties": {
62 | "spec_prompt": {"type": "string"},
63 | "repo_path": {"type": "string"}
64 | },
65 | "required": ["spec_prompt", "repo_path"]
66 | }
67 | }
68 |
69 | def handle_implement_spec_prompt(params):
70 | ca = CodeAssistant()
71 | return ca.generate_implementation(
72 | # ... (full implementation)
73 | ```
74 |
75 | 3. Add security checks in modules/coder/service.py:
76 | ```python
77 | def _sanitize_repo_path(self, repo_path: str) -> Path:
78 | base_dir = Path(__file__).resolve().parent.parent # Project root
79 | provided_path = Path(repo_path).resolve())
80 |
81 | if not provided_path.is_relative_to(base_dir):
82 | raise SecurityException("Path traversal detected")
83 |
84 | if not provided_path.is_dir():
85 | raise NotADirectoryError
86 | ```
87 |
88 | 4. Add unit test in tests/coder/test_code_assistant.py:
89 | ```python
90 | def test_aider_env_var_overide(monkeypatch):
91 | monkeypatch.setenv('AIDER_PATH', '/usr/local/bin/aider_custom')
92 | ca = CodeAssistant()
93 | with patch('subprocess.run') as mock_run:
94 | ca.generate_implementation("test spec", "/tmp")
95 | mock_run.assert_called_with(
96 | ["/usr/local/bin/aider_custom", ...], # Verify custom path used
97 | ...
98 | ```
99 |
100 | ## Critical Implementation Notes
101 | - Use `os.environ.get('AIDER_PATH', 'aider')` for executable path
102 | - Add `timeout=120` to `subprocess.run`
103 | - Ensure `cwd` is always the sanitized repo path
104 | - Add `missing_ok=True` when deleting temp files
105 |
--------------------------------------------------------------------------------
/AgentCrew/modules/console/conversation_handler.py:
--------------------------------------------------------------------------------
1 | """
2 | Conversation handling for console UI.
3 | Manages conversation loading, listing, and display functionality.
4 | """
5 |
6 | from __future__ import annotations
7 | from typing import List, Dict, Any
8 | from rich.text import Text
9 |
10 | from .constants import RICH_STYLE_YELLOW, RICH_STYLE_RED
11 |
12 | from typing import TYPE_CHECKING
13 |
14 | if TYPE_CHECKING:
15 | from .console_ui import ConsoleUI
16 |
17 |
18 | class ConversationHandler:
19 | """Handles conversation-related operations for the console UI."""
20 |
21 | def __init__(self, console_ui: ConsoleUI):
22 | """Initialize the conversation handler."""
23 | self.console = console_ui.console
24 | self.display_handlers = console_ui.display_handlers
25 | self._cached_conversations = []
26 |
27 | def handle_load_conversation(self, load_arg: str, message_handler):
28 | """
29 | Handle loading a conversation by number or ID.
30 |
31 | Args:
32 | load_arg: Either a conversation number (from the list) or a conversation ID
33 | message_handler: The message handler instance
34 | """
35 | # First check if we have a list of conversations cached
36 | if not self._cached_conversations:
37 | # If not, get the list first
38 | self._cached_conversations = message_handler.list_conversations()
39 |
40 | try:
41 | self.display_handlers.display_divider()
42 | # Check if the argument is a number (index in the list)
43 | if load_arg.isdigit():
44 | index = int(load_arg) - 1 # Convert to 0-based index
45 | if 0 <= index < len(self._cached_conversations):
46 | convo_id = self._cached_conversations[index].get("id")
47 | if convo_id:
48 | self.console.print(
49 | Text(
50 | f"Loading conversation #{load_arg}...",
51 | style=RICH_STYLE_YELLOW,
52 | )
53 | )
54 | messages = message_handler.load_conversation(convo_id)
55 | if messages:
56 | self.display_handlers.display_loaded_conversation(
57 | messages, message_handler.agent.name
58 | )
59 | return
60 | self.console.print(
61 | Text(
62 | "Invalid conversation number. Use '/list' to see available conversations.",
63 | style=RICH_STYLE_RED,
64 | )
65 | )
66 | else:
67 | # Assume it's a conversation ID
68 | self.console.print(
69 | Text(
70 | f"Loading conversation with ID: {load_arg}...",
71 | style=RICH_STYLE_YELLOW,
72 | )
73 | )
74 | messages = message_handler.load_conversation(load_arg)
75 | if messages:
76 | self.display_handlers.display_loaded_conversation(
77 | messages, message_handler.agent.name
78 | )
79 |
80 | self.console.print(
81 | Text("End of conversation history\n", style=RICH_STYLE_YELLOW)
82 | )
83 | except Exception as e:
84 | self.console.print(
85 | Text(f"Error loading conversation: {str(e)}", style=RICH_STYLE_RED)
86 | )
87 |
88 | def update_cached_conversations(self, conversations: List[Dict[str, Any]]):
89 | """Update the cached conversations list."""
90 | self._cached_conversations = conversations
91 |
92 | def get_cached_conversations(self):
93 | """Get the cached conversations list."""
94 | return self._cached_conversations
95 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "agentcrew-ai"
3 | version = "0.8.10"
4 | requires-python = ">=3.12"
5 | classifiers = [
6 | "Programming Language :: Python :: 3",
7 | "Operating System :: OS Independent",
8 | "Development Status :: 4 - Beta",
9 | "Intended Audience :: Developers",
10 | "Topic :: Software Development :: Libraries :: Python Modules",
11 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
12 | ]
13 | license = "Apache-2.0"
14 | license-files = ["LICENSE"]
15 | description = "Multi-Agents Interactive Chat Tool"
16 | authors = [
17 | {name = "Quy Truong", email = "quy.truong@saigontechnology.com"},
18 | ]
19 | readme = "README.md"
20 |
21 | dependencies = [
22 | "click",
23 | "python-dotenv",
24 | "anthropic",
25 | "pytest",
26 | "prompt-toolkit>=3.0.52",
27 | "rich>=13.9.4",
28 | "pyperclip>=1.9.0",
29 | "tavily-python>=0.5.1",
30 | "pillow>=11.0.0",
31 | "groq>=0.18.0",
32 | "chromadb>=1.0.0",
33 | "openai>=1.65.2",
34 | "tree-sitter>=0.23.2",
35 | "mcp>=1.3.0",
36 | "google-genai>=1.7.0",
37 | "pyside6>=6.8.3",
38 | "markdown>=3.7",
39 | "tree-sitter-language-pack>=0.7.0",
40 | "nest-asyncio>=1.6.0",
41 | "voyageai>=0.3.2",
42 | "numpy>=1.24.4,<2; python_version < '3.13' and sys_platform == 'darwin'",
43 | "pywin32; sys_platform == 'win32'",
44 | "pyobjc; sys_platform == 'darwin'",
45 | "a2a-sdk>=0.3.10",
46 | "qtawesome>=1.4.0",
47 | "xmltodict>=0.14.2",
48 | "elevenlabs>=2.12.1",
49 | "sounddevice>=0.5.2",
50 | "soundfile>=0.13.1",
51 | "jsonref>=1.1.0",
52 | "pychromedevtools>=0.3.3",
53 | "html-to-markdown>=2.9.1",
54 | "pip-system-certs>=5.2",
55 | "loguru>=0.7.3",
56 | "jsonschema>=4.25.1",
57 | "tomli-w>=1.2.0",
58 | ]
59 |
60 | [project.optional-dependencies]
61 | cpu = [
62 | "torch>=2.8.0",
63 | "torchvision>=0.23.0",
64 | "torchaudio>=2.8.0",
65 | "docling>=2.26.0",
66 | ]
67 | nvidia = [
68 | "torch>=2.8.0",
69 | "torchvision>=0.23.0",
70 | "torchaudio>=2.8.0",
71 | "docling>=2.26.0",
72 | ]
73 |
74 | [tool.uv]
75 | conflicts = [
76 | [
77 | { extra = "cpu" },
78 | { extra = "nvidia" },
79 | ],
80 | ]
81 |
82 | [tool.uv.sources]
83 | torch = [
84 | { index = "pytorch-default", extra= "cpu", marker = "sys_platform == 'darwin'" },
85 | { index = "pytorch-cpu", extra = "cpu", marker = "sys_platform != 'darwin'"},
86 | { index = "pytorch-cu128", extra = "nvidia" },
87 | ]
88 | torchvision = [
89 | { index = "pytorch-default", extra= "cpu", marker = "sys_platform == 'darwin'" },
90 | { index = "pytorch-cpu", extra = "cpu", marker = "sys_platform != 'darwin'"},
91 | { index = "pytorch-cu128", extra = "nvidia" },
92 | ]
93 |
94 | torchaudio = [
95 | { index = "pytorch-default", extra= "cpu", marker = "sys_platform == 'darwin'" },
96 | { index = "pytorch-cpu", extra = "cpu", marker = "sys_platform != 'darwin'"},
97 | { index = "pytorch-cu128", extra = "nvidia" },
98 | ]
99 |
100 |
101 | [[tool.uv.index]]
102 | name = "pytorch-default"
103 | url = "https://pypi.org/simple/"
104 | explicit = true
105 |
106 | [[tool.uv.index]]
107 | name = "pytorch-cpu"
108 | url = "https://download.pytorch.org/whl/cpu"
109 | explicit = true
110 |
111 | [[tool.uv.index]]
112 | name = "pytorch-cu128"
113 | url = "https://download.pytorch.org/whl/cu128"
114 | explicit = true
115 |
116 |
117 | [project.urls]
118 | "Homepage" = "https://github.com/daltonnyx/AgentCrew"
119 | "Bug Tracker" = "https://github.com/daltonnyx/AgentCrew/issues"
120 |
121 |
122 | [build-system]
123 | requires = ["setuptools>=61.0"]
124 | build-backend = "setuptools.build_meta"
125 |
126 | [project.scripts]
127 | agentcrew = "AgentCrew.main:cli_prod"
128 |
129 | [tool.setuptools]
130 | include-package-data = true
131 |
132 | [tool.setuptools.packages.find]
133 | where = ["./"]
134 |
135 | [dependency-groups]
136 | dev = [
137 | "pygments>=2.19.1",
138 | "pyinstaller>=6.13.0",
139 | "langfuse>=3.0.1",
140 | ]
141 |
--------------------------------------------------------------------------------
/AgentCrew/modules/voice/text_cleaner.py:
--------------------------------------------------------------------------------
1 | import re
2 | from typing import List
3 | from .base import BaseTextCleaner
4 |
5 |
6 | class TextCleaner(BaseTextCleaner):
7 | """Clean text for natural speech synthesis."""
8 |
9 | def __init__(self):
10 | """Initialize text cleaner with patterns."""
11 | # Patterns to remove completely
12 | self.remove_patterns = [
13 | r"```[\s\S]*?```", # Code blocks
14 | r"`[^`]+`", # Inline code
15 | r"\*\*([^*]+)\*\*", # Bold (keep content)
16 | r"\*([^*]+)\*", # Italic (keep content)
17 | r"#{1,6}\s*", # Headers
18 | r"!\[.*?\]\(.*?\)", # Images
19 | r"\[([^\]]+)\]\([^)]+\)", # Links (keep text)
20 | r"^\s*[-*+]\s+", # Bullet points
21 | r"^\s*\d+\.\s+", # Numbered lists
22 | r"^\s*>\s+", # Blockquotes
23 | r"---+", # Horizontal rules
24 | r"\|.*\|", # Tables
25 | ]
26 |
27 | # Replacements for better speech
28 | self.replacements = [
29 | # (r"\.{3,}", ", "), # Ellipsis
30 | # (r"\n{2,}", ". "), # Multiple newlines
31 | (r"\s+", " "), # Multiple spaces
32 | (r"&", " and "), # Ampersand
33 | (r"%", " percent"), # Percent
34 | (r"\$", " dollars "), # Dollar sign
35 | (r"€", " euros "), # Euro sign
36 | (r"£", " pounds "), # Pound sign
37 | (r"@", " at "), # At symbol
38 | (r"#", " number "), # Hash
39 | (r"/", " slash "), # Forward slash
40 | (r"\\", " backslash "), # Backslash
41 | ]
42 |
43 | # Common abbreviations
44 | self.abbreviations = {
45 | "e.g.": "for example",
46 | "i.e.": "that is",
47 | "etc.": "et cetera",
48 | "vs.": "versus",
49 | "Dr.": "Doctor",
50 | "Mr.": "Mister",
51 | "Mrs.": "Missus",
52 | "Ms.": "Miss",
53 | "Prof.": "Professor",
54 | "Sr.": "Senior",
55 | "Jr.": "Junior",
56 | }
57 |
58 | def clean_for_speech(self, text: str) -> str:
59 | """
60 | Clean text for natural speech synthesis.
61 |
62 | Args:
63 | text: Raw text to clean
64 |
65 | Returns:
66 | Cleaned text suitable for TTS
67 | """
68 | if not text:
69 | return ""
70 |
71 | # Remove code blocks and markdown formatting
72 | for pattern in self.remove_patterns:
73 | if pattern in [
74 | r"\*\*([^*]+)\*\*",
75 | r"\*([^*]+)\*",
76 | r"\[([^\]]+)\]\([^)]+\)",
77 | ]:
78 | # Keep the content for these patterns
79 | text = re.sub(pattern, r"\1", text, flags=re.MULTILINE)
80 | else:
81 | text = re.sub(pattern, "", text, flags=re.MULTILINE)
82 |
83 | # Apply replacements
84 | for pattern, replacement in self.replacements:
85 | text = re.sub(pattern, replacement, text)
86 |
87 | # Replace abbreviations
88 | for abbr, full in self.abbreviations.items():
89 | text = text.replace(abbr, full)
90 |
91 | # Clean up
92 | text = text.strip()
93 |
94 | # Remove empty parentheses and brackets
95 | text = re.sub(r"\(\s*\)", "", text)
96 | text = re.sub(r"\[\s*\]", "", text)
97 |
98 | # Ensure proper sentence ending
99 | if text and text[-1] not in ".!?:":
100 | text += "."
101 |
102 | return text
103 |
104 | def split_into_sentences(self, text: str) -> List[str]:
105 | """
106 | Split text into sentences for streaming.
107 |
108 | Args:
109 | text: Text to split
110 |
111 | Returns:
112 | List of sentences
113 | """
114 | # Simple sentence splitting
115 | sentences = re.split(r"(?<=[.!?])\s+", text)
116 | return [s.strip() for s in sentences if s.strip()]
117 |
--------------------------------------------------------------------------------
/AgentCrew/modules/gui/widgets/loading_overlay.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout
2 | from PySide6.QtCore import Qt, QTimer, Signal
3 | from PySide6.QtGui import QPainter, QColor
4 |
5 |
6 | class LoadingOverlay(QWidget):
7 | """
8 | A semi-transparent overlay widget with a loading spinner and message.
9 | Can be shown over any parent widget to indicate loading state.
10 | """
11 |
12 | finished = Signal()
13 |
14 | def __init__(self, parent=None, message="Loading..."):
15 | super().__init__(parent)
16 | self.message = message
17 | self.angle = 0
18 |
19 | self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
20 | self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
21 |
22 | layout = QVBoxLayout(self)
23 | layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
24 |
25 | self.label = QLabel(self.message)
26 | self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
27 | self.label.setStyleSheet("""
28 | QLabel {
29 | color: white;
30 | background-color: rgba(0, 0, 0, 200);
31 | padding: 20px 30px;
32 | border-radius: 10px;
33 | font-size: 14px;
34 | font-weight: bold;
35 | }
36 | """)
37 | layout.addWidget(self.label)
38 |
39 | self.timer = QTimer(self)
40 | self.timer.timeout.connect(self.rotate)
41 |
42 | self.hide()
43 |
44 | def showEvent(self, event):
45 | """Start animation when shown."""
46 | super().showEvent(event)
47 | self.timer.start(50) # Update every 50ms
48 |
49 | def hideEvent(self, event):
50 | """Stop animation when hidden."""
51 | super().hideEvent(event)
52 | self.timer.stop()
53 | self.finished.emit()
54 |
55 | def rotate(self):
56 | """Rotate the spinner."""
57 | self.angle = (self.angle + 10) % 360
58 | self.update()
59 |
60 | def paintEvent(self, event):
61 | """Paint the semi-transparent background and spinner."""
62 | painter = QPainter(self)
63 | painter.setRenderHint(QPainter.RenderHint.Antialiasing)
64 |
65 | painter.fillRect(self.rect(), QColor(0, 0, 0, 100))
66 |
67 | center_x = self.width() // 2
68 | center_y = self.height() // 2 - 40 # Above the label
69 | radius = 20
70 |
71 | painter.setPen(Qt.PenStyle.NoPen)
72 |
73 | for i in range(8):
74 | angle_offset = i * 45
75 | alpha = int(255 * (i + 1) / 8)
76 | color = QColor(255, 255, 255, alpha)
77 | painter.setBrush(color)
78 |
79 | angle_rad = (self.angle + angle_offset) * 3.14159 / 180
80 | x = center_x + radius * 0.7 * (i / 8) * painter.fontMetrics().height() / 20
81 | y = center_y + radius * 0.7 * (i / 8) * painter.fontMetrics().height() / 20
82 |
83 | import math
84 |
85 | x = center_x + int(radius * math.cos(angle_rad))
86 | y = center_y + int(radius * math.sin(angle_rad))
87 |
88 | dot_size = 6
89 | painter.drawEllipse(
90 | x - dot_size // 2, y - dot_size // 2, dot_size, dot_size
91 | )
92 |
93 | def set_message(self, message: str):
94 | """Update the loading message."""
95 | self.message = message
96 | self.label.setText(message)
97 |
98 | def show_loading(self):
99 | """Show the loading overlay."""
100 | if self.parent():
101 | # Resize to cover parent
102 | self.resize(self.parent().size()) # type: ignore
103 | self.raise_()
104 | self.show()
105 |
106 | def hide_loading(self):
107 | """Hide the loading overlay."""
108 | self.hide()
109 |
110 | def resizeEvent(self, event):
111 | """Keep overlay covering parent when resized."""
112 | super().resizeEvent(event)
113 | if self.parent() and self.isVisible():
114 | self.resize(self.parent().size()) # type: ignore
115 |
--------------------------------------------------------------------------------
/specs/17_spec_implement_multi_agents.md:
--------------------------------------------------------------------------------
1 | # Implement Multi-Agent Architecture with Agent Manager
2 |
3 | > Ingest the information from this file, implement the Low-level Tasks, and generate the code that will satisfy Objectives
4 |
5 | ## Objectives
6 | - Create a base Agent class that defines shared behavior across all specialized agents
7 | - Implement an AgentManager class to handle agent selection and task routing
8 | - Integrate the handoff tool for transferring tasks between specialized agents
9 | - Update InteractiveChat to work with the agent system instead of directly with LLM services
10 | - Modify main.py to initialize the multi-agent system
11 |
12 | ## Contexts
13 | - ./modules/llm/base.py: Contains the BaseLLMService class which defines the interface for LLM services
14 | - ./modules/chat/interactive.py: Contains the InteractiveChat class that manages the chat interface
15 | - ./main.py: Contains the main application entry point and service initialization
16 | - ./modules/tools/registry.py: Contains the ToolRegistry class for registering and managing tools
17 |
18 | ## Low-level Tasks
19 | 1. CREATE modules/agents/__init__.py:
20 | - Create an empty init file to mark the directory as a Python package
21 |
22 | 2. CREATE modules/agents/base.py:
23 | - Implement the base Agent class with the following methods:
24 | - __init__(self, name, description, llm_service)
25 | - register_tool(self, tool_definition, handler_function)
26 | - set_system_prompt(self, prompt)
27 | - get_system_prompt(self)
28 | - process_messages(self, messages)
29 |
30 | 3. CREATE modules/agents/manager.py:
31 | - Implement the AgentManager class with the following methods:
32 | - __init__(self)
33 | - register_agent(self, agent)
34 | - select_agent(self, agent_name)
35 | - get_agent(self, agent_name)
36 | - get_current_agent(self)
37 | - perform_handoff(self, target_agent_name, reason, context_summary=None)
38 | - route_message(self, messages)
39 |
40 | 4. CREATE modules/agents/tools/__init__.py:
41 | - Create an empty init file to mark the directory as a Python package
42 |
43 | 5. CREATE modules/agents/tools/handoff.py:
44 | - Implement the handoff tool with the following functions:
45 | - get_handoff_tool_definition()
46 | - get_handoff_tool_handler(agent_manager)
47 | - handle_handoff(params) (inside get_handoff_tool_handler)
48 | - register(agent_manager)
49 |
50 | 6. CREATE modules/agents/specialized/__init__.py:
51 | - Create an empty init file to mark the directory as a Python package
52 |
53 | 7. CREATE modules/agents/specialized/architect.py:
54 | - Implement the ArchitectAgent class with the following methods:
55 | - __init__(self, llm_service)
56 | - get_system_prompt(self) -> overriding the base method
57 |
58 | 8. CREATE modules/agents/specialized/code_assistant.py:
59 | - Implement the CodeAssistantAgent class with the following methods:
60 | - __init__(self, llm_service)
61 | - get_system_prompt(self) -> overriding the base method
62 |
63 | 9. CREATE modules/agents/specialized/documentation.py:
64 | - Implement the DocumentationAgent class with the following methods:
65 | - __init__(self, llm_service)
66 | - get_system_prompt(self) -> overriding the base method
67 |
68 | 10. CREATE modules/agents/specialized/evaluation.py:
69 | - Implement the EvaluationAgent class with the following methods:
70 | - __init__(self, llm_service)
71 | - get_system_prompt(self) -> overriding the base method
72 |
73 | 11. UPDATE modules/chat/interactive.py:
74 | - Modify InteractiveChat class:
75 | - Update __init__(self, agent_manager, memory_service=None)
76 | - Update _stream_assistant_response(self, messages, input_tokens=0, output_tokens=0)
77 | - Add _handle_agent_command(self, command)
78 | - Update _process_user_input to handle agent commands
79 | - Update _print_welcome_message to include agent command information
80 |
81 | 12. UPDATE main.py:
82 | - Add setup_agents(services) function to initialize the multi-agent system
83 | - Modify the chat function to use the agent manager instead of directly using LLM services
84 | - Update services_load to include agent manager initialization
85 | - Ensure all necessary services are passed to the appropriate agents
86 |
--------------------------------------------------------------------------------
/examples/agents/jobs/code-snipper.toml:
--------------------------------------------------------------------------------
1 | [[agents]]
2 | description = "expert code snippet specialist who helps developers find and create precise, production-ready code examples instantly"
3 | enabled = true
4 | name = "CodeSnipper"
5 | system_prompt = "You are **CodeSnipper**, an expert code snippet specialist who helps developers find and create precise, production-ready code examples instantly! 🚀\n\n## Your Mission\nTransform user queries into clean, copy-paste-ready code snippets with crystal-clear context. You're the developer's fastest path from \"How do I...?\" to working code.\n\n## Core Capabilities\n\n### 1. **Intelligent Code Search & Synthesis**\n- Search authoritative sources (official docs, Stack Overflow, GitHub) for proven solutions\n- Synthesize multiple approaches into the most elegant solution\n- Prioritize recent, actively-maintained examples\n\n### 2. **Precision Snippet Generation**\nWhen creating code snippets, deliver:\n\n**Essential Components:**\n- ✅ **Imports/Dependencies**: All required libraries, modules, packages at the top\n- ✅ **Core Code**: Clean, idiomatic implementation\n- ✅ **Contextual Comments**: ONLY when logic is non-obvious or requires explanation\n- ✅ **Language-specific best practices**: Follow conventions for the target language\n\n**Quality Standards:**\n- Use current syntax and APIs (avoid deprecated features)\n- Include proper error handling when relevant to the use case\n- Make code self-documenting through clear variable/function names\n- Keep it minimal—no unnecessary boilerplate\n\n### 3. **Comment Philosophy**\n> \"Code should explain the WHAT, comments explain the WHY (when needed)\"\n\n**Add comments when:**\n- Algorithm logic is complex or non-intuitive\n- Performance considerations exist\n- Edge cases are being handled\n- Language-specific quirks require explanation\n\n**Skip comments when:**\n- Code is self-explanatory\n- Variable/function names clearly indicate purpose\n- Standard library usage is straightforward\n\n## Response Format\n\nFor each request, structure your response as:\n\n// [Required imports/dependencies - if needed]\nimport/require/include statements\n\n// [Brief context comment - ONLY if non-obvious]\n[Your clean, production-ready code snippet]\n\n\n## Example Interaction\n\n**User**: \"split string with space in c++\"\n\n**You respond**:\n#include \n#include \n#include \n\nstd::vector splitString(const std::string& str) {\n std::vector tokens;\n std::istringstream stream(str);\n std::string token;\n \n while (stream >> token) {\n tokens.push_back(token);\n }\n \n return tokens;\n}\n\n## Operational Guidelines\n\n### Search Strategy\n1. **Prioritize official documentation** for language/framework-specific queries\n2. **Cross-reference Stack Overflow** for practical, battle-tested solutions\n3. **Check GitHub** for modern implementation patterns\n4. **Verify recency**: Prefer solutions updated within last 2-3 years\n\n### Language Detection\n- Auto-detect language from context when obvious\n- Ask for clarification if ambiguous (e.g., \"array manipulation\" could be JS, Python, Java, etc.)\n\n### Complexity Handling\n- **Simple queries**: Direct snippet only\n- **Complex queries**: May provide brief setup context or usage example\n- **Multi-step solutions**: Break into logical sections with clear headings\n\n### Version Awareness\nWhen relevant, target:\n- **Latest stable versions** of languages/frameworks\n- Note if snippet requires specific version (e.g., \"Requires Python 3.10+\")\n\n## What You DON'T Do\n- ❌ Write lengthy explanations when code is self-evident\n- ❌ Provide full application scaffolding (you're snippet-focused)\n- ❌ Include excessive error handling for simple examples\n- ❌ Add comments for every line (avoid noise)\n- ❌ Provide multiple full implementations (mention alternatives briefly instead)\n\n## Quality Checklist\nBefore responding, verify:\n- [ ] All required imports are included\n- [ ] Code follows language conventions\n- [ ] Snippet is minimal but complete\n- [ ] Comments add value (not just restate code)\n- [ ] Solution addresses the user's actual need\n\n---\n\n**Remember**: You're the espresso shot of coding help—concentrated, quick, and exactly what developers need to keep moving! ☕💻\n\nCurrent date: {current_date}"
6 | temperature = 1.0
7 | tools = [ "memory", "web_search",]
8 | voice_enabled = "disabled"
9 | voice_id = ""
10 |
--------------------------------------------------------------------------------
/specs/11_add_mcp_client.md:
--------------------------------------------------------------------------------
1 | # MCP Client Module Revision Specification
2 | >
3 | > Enhance the MCP client module to support dynamic tool registration from multiple servers
4 |
5 | ## Objectives
6 |
7 | - Implement JSON-based configuration for multiple MCP servers
8 | - Enable concurrent server connections and management
9 | - Support dynamic tool registration from each server
10 | - Maintain backward compatibility with existing tool registry
11 | - Provide robust error handling and resource management
12 |
13 | ## Context Files
14 |
15 | - modules/mcpclient/service.py: Current MCP service implementation
16 | - modules/mcpclient/tool.py: Tool registration and handlers
17 | - modules/tools/registry.py: Core tool registry functionality
18 | - tests/mcpclient_test.py: pytest file
19 |
20 | ## Requirements
21 |
22 | ### Configuration Management
23 |
24 | - JSON schema for server definitions
25 | - Support for multiple server configurations
26 | - Environment variable configuration
27 | - Server enable/disable functionality
28 |
29 | ### Server Connection
30 |
31 | - Asynchronous connection handling
32 | - Concurrent server initialization
33 | - Connection state management
34 | - Resource cleanup on shutdown
35 |
36 | ### Tool Registration
37 |
38 | - Dynamic tool discovery from servers
39 | - Namespace isolation between servers
40 | - Tool conflict resolution
41 | - Provider-agnostic tool definitions
42 |
43 | ## Low-level Tasks
44 |
45 | 1. CREATE modules/mcpclient/config.py:
46 |
47 | ```python
48 | # Required classes:
49 | - MCPServerConfig: Dataclass for server configuration
50 | - Properties: name, command, args, env, enabled
51 | - MCPConfigManager: Configuration file handler
52 | - Methods: load_config(), get_enabled_servers()
53 | ```
54 |
55 | 2. UPDATE modules/mcpclient/service.py:
56 |
57 | ```python
58 | # Required classes:
59 | - MCPService:
60 | - Properties:
61 | - sessions: Dict[str, ClientSession]
62 | - connected_servers: Dict[str, bool]
63 | - Methods:
64 | - connect_to_server(server_config: MCPServerConfig) -> bool
65 | - register_server_tools(server_name: str)
66 | - _format_tool_definition(tool: dict, server_name: str) -> dict
67 | - _create_tool_handler(server_name: str, tool_name: str)
68 | - cleanup()
69 | ```
70 |
71 | 3. CREATE modules/mcpclient/manager.py:
72 |
73 | ```python
74 | # Required classes:
75 | - MCPSessionManager:
76 | - Properties:
77 | - config_manager: MCPConfigManager
78 | - mcp_service: MCPService
79 | - Methods:
80 | - get_instance() -> MCPSessionManager
81 | - initialize_servers()
82 | - cleanup()
83 | ```
84 |
85 | 4. UPDATE modules/mcpclient/tool.py:
86 |
87 | ```python
88 | # Required functions:
89 | - get_mcp_connect_tool_definition() -> dict
90 | - handle_mcp_connect() -> dict
91 | - register()
92 | ```
93 |
94 | ## Implementation Details
95 |
96 | ### 1. Configuration Schema (config/mcp_servers.json)
97 |
98 | ```json
99 | {
100 | "server_id": {
101 | "name": "string",
102 | "command": "string",
103 | "args": ["string"],
104 | "env": {
105 | "key": "value"
106 | },
107 | "enabled": boolean
108 | }
109 | }
110 | ```
111 |
112 | ### 2. Tool Registration Format
113 |
114 | ```python
115 | {
116 | "name": "server_name.tool_name",
117 | "description": "Tool description",
118 | "parameters": {
119 | "type": "object",
120 | "properties": {},
121 | "required": []
122 | }
123 | }
124 | ```
125 |
126 | ### 3. Error Handling Requirements
127 |
128 | - Connection failures should not crash the system
129 | - Failed servers should be marked as disconnected
130 | - Tools from disconnected servers should be unregistered
131 | - Retry logic for failed connections
132 | - Proper resource cleanup on errors
133 |
134 | ### 4. Testing Requirements
135 |
136 | ```python
137 | # Required test cases:
138 | - Configuration loading and validation
139 | - Server connection success/failure
140 | - Tool registration and namespacing
141 | - Concurrent server initialization
142 | - Error handling and recovery
143 | - Resource cleanup
144 | ```
145 |
--------------------------------------------------------------------------------
/tests/coder/test_code_assistant.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import pytest
3 | from unittest.mock import patch, MagicMock
4 | from AgentCrew.modules.coding.service import AiderConfig, CodeAssistant
5 |
6 |
7 | class TestCodeAssistant:
8 | def test_sanitize_repo_path_valid(self, tmp_path):
9 | """Test that a valid path passes sanitization."""
10 | ca = CodeAssistant()
11 | result = ca._sanitize_repo_path(str(tmp_path))
12 | assert result == tmp_path.resolve()
13 |
14 | def test_sanitize_repo_path_not_dir(self, tmp_path):
15 | """Test that a file path raises NotADirectoryError."""
16 | test_file = tmp_path / "test.txt"
17 | test_file.write_text("test")
18 |
19 | ca = CodeAssistant()
20 | with pytest.raises(NotADirectoryError):
21 | ca._sanitize_repo_path(str(test_file))
22 |
23 | def test_sanitize_repo_path_not_exist(self):
24 | """Test that a non-existent path raises NotADirectoryError."""
25 | ca = CodeAssistant()
26 | with pytest.raises(NotADirectoryError):
27 | ca._sanitize_repo_path("/path/does/not/exist")
28 |
29 | def test_aider_env_var_override(self, monkeypatch, tmp_path):
30 | """Test that AIDER_PATH environment variable is respected."""
31 | monkeypatch.setenv("AIDER_PATH", "/usr/local/bin/aider_custom")
32 | ca = CodeAssistant()
33 |
34 | with patch("subprocess.run") as mock_run:
35 | mock_run.return_value = MagicMock(stdout="Test output")
36 | ca.generate_implementation(
37 | "test spec",
38 | str(tmp_path),
39 | aider_config=AiderConfig(
40 | "claude-3-7-sonnet-latest", "claude-3-5-sonnet-latest"
41 | ),
42 | )
43 |
44 | # Verify the custom path was used
45 | args, kwargs = mock_run.call_args
46 | assert args[0][0] == "/usr/local/bin/aider_custom"
47 | assert kwargs["timeout"] == 120
48 | assert kwargs["cwd"] == str(tmp_path.resolve())
49 |
50 | def test_generate_implementation_success(self, tmp_path):
51 | """Test successful code generation."""
52 | ca = CodeAssistant()
53 |
54 | with (
55 | patch("subprocess.run") as mock_run,
56 | patch("tempfile.NamedTemporaryFile") as mock_temp,
57 | ):
58 | # Mock the temporary file
59 | mock_temp_instance = MagicMock()
60 | mock_temp_instance.__enter__.return_value = mock_temp_instance
61 | mock_temp_instance.name = "/tmp/test_spec.spec"
62 | mock_temp.return_value = mock_temp_instance
63 |
64 | # Mock subprocess.run
65 | mock_run.return_value = MagicMock(stdout="Code generated successfully")
66 |
67 | result = ca.generate_implementation("test spec prompt", str(tmp_path))
68 |
69 | # Verify the result
70 | assert result == "Code generated successfully"
71 |
72 | # Verify subprocess.run was called with correct arguments
73 | args, kwargs = mock_run.call_args
74 | assert "--model" in args[0]
75 | assert "claude-3-7-sonnet-latest" in args[0]
76 | assert kwargs["timeout"] == 120
77 | assert kwargs["cwd"] == str(tmp_path.resolve())
78 |
79 | def test_generate_implementation_subprocess_error(self, tmp_path):
80 | """Test handling of subprocess error."""
81 | ca = CodeAssistant()
82 |
83 | with (
84 | patch("subprocess.run") as mock_run,
85 | patch("tempfile.NamedTemporaryFile") as mock_temp,
86 | patch("pathlib.Path.unlink") as mock_unlink,
87 | ):
88 | # Mock the temporary file
89 | mock_temp_instance = MagicMock()
90 | mock_temp_instance.__enter__.return_value = mock_temp_instance
91 | mock_temp_instance.name = "/tmp/test_spec.spec"
92 | mock_temp.return_value = mock_temp_instance
93 |
94 | # Mock subprocess.run to raise an error
95 | mock_run.side_effect = subprocess.SubprocessError("Command failed")
96 |
97 | result = ca.generate_implementation("test spec prompt", str(tmp_path))
98 |
99 | # Verify error handling
100 | assert "Error executing aider" in result
101 |
102 | # Verify temp file cleanup was attempted
103 | mock_unlink.assert_called_once()
104 |
--------------------------------------------------------------------------------
/AgentCrew/modules/web_search/service.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Dict, Any, List
3 | from dotenv import load_dotenv
4 | from tavily import TavilyClient
5 |
6 |
7 | class TavilySearchService:
8 | """Service for interacting with the Tavily Search API using the official SDK."""
9 |
10 | def __init__(self):
11 | """Initialize the Tavily search service with API key from environment."""
12 | load_dotenv()
13 | self.api_key = os.getenv("TAVILY_API_KEY")
14 | if not self.api_key:
15 | raise ValueError("TAVILY_API_KEY not found in environment variables")
16 |
17 | self.client = TavilyClient(api_key=self.api_key)
18 |
19 | def search(
20 | self,
21 | query: str,
22 | search_depth: str = "basic",
23 | topic: str = "general",
24 | include_domains: List[str] | None = None,
25 | exclude_domains: List[str] | None = None,
26 | max_results: int = 5,
27 | ) -> Dict[str, Any]:
28 | """
29 | Perform a web search using Tavily API.
30 |
31 | Args:
32 | query: The search query
33 | search_depth: 'basic' or 'advanced' search depth
34 | include_domains: List of domains to include in search
35 | exclude_domains: List of domains to exclude from search
36 | max_results: Maximum number of results to return
37 |
38 | Returns:
39 | Dict containing search results
40 | """
41 | try:
42 | params = {
43 | "query": query,
44 | "search_depth": search_depth,
45 | "max_results": max_results,
46 | "include_answer": search_depth,
47 | "topic": topic,
48 | }
49 |
50 | if include_domains:
51 | params["include_domains"] = include_domains
52 |
53 | if exclude_domains:
54 | params["exclude_domains"] = exclude_domains
55 |
56 | return self.client.search(**params)
57 | except Exception as e:
58 | print(f"❌ Search error: {str(e)}")
59 | return {"error": str(e)}
60 |
61 | def extract(self, url: str) -> Dict[str, Any]:
62 | """
63 | Extract content from a specific URL using Tavily API.
64 |
65 | Args:
66 | url: The URL to extract content from
67 |
68 | Returns:
69 | Dict containing the extracted content
70 | """
71 | try:
72 | return self.client.extract(url)
73 | except Exception as e:
74 | print(f"❌ Extract error: {str(e)}")
75 | return {"error": str(e)}
76 |
77 | def format_search_results(self, results: Dict[str, Any]) -> str:
78 | """Format search results into a readable string."""
79 | if "error" in results:
80 | return f"Search error: {results['error']}"
81 | formatted_text = ""
82 |
83 | if "answer" in results:
84 | formatted_text += f"**Query's Summary**: {results['answer']} \n\n"
85 |
86 | formatted_text += "**Search Results**: \n\n"
87 |
88 | if "results" in results:
89 | for i, result in enumerate(results["results"], 1):
90 | formatted_text += f"{i}. {result.get('title', 'No title')} (Matching score: {result.get('score', 'Unknown')}) \n"
91 | formatted_text += f" URL: {result.get('url', 'No URL')} \n"
92 | formatted_text += f" {result.get('content', 'No content')} \n\n"
93 | else:
94 | formatted_text += "No results found."
95 |
96 | return formatted_text
97 |
98 | def format_extract_results(self, results: Dict[str, Any]) -> str:
99 | """Format extract results into a readable string."""
100 |
101 | if "failed_results" in results and results["failed_results"]:
102 | result = results["failed_results"][0]
103 | return f"Extract failed: {result.get('error', 'Unknown error')}"
104 |
105 | if "results" in results and results["results"]:
106 | result = results["results"][0]
107 | url = result.get("url", "Unknown URL")
108 | content = result.get("raw_content", "No content available")
109 | # if self.llm:
110 | # content = self.llm.summarize_content(content)
111 | return f"Extracted content from {url}:\n\n{content}"
112 | else:
113 | return "No content could be extracted."
114 |
--------------------------------------------------------------------------------
/AgentCrew/modules/a2a/agent_cards.py:
--------------------------------------------------------------------------------
1 | """
2 | Functions for generating A2A agent cards from SwissKnife agents.
3 | """
4 |
5 | from typing import List
6 | from AgentCrew.modules.agents import LocalAgent
7 | from a2a.types import (
8 | AgentCard,
9 | AgentCapabilities,
10 | AgentSkill,
11 | SecurityScheme,
12 | APIKeySecurityScheme,
13 | AgentProvider,
14 | AgentInterface,
15 | TransportProtocol,
16 | )
17 | from AgentCrew import __version__
18 |
19 |
20 | def map_tool_to_skill(tool_name: str, tool_def) -> AgentSkill:
21 | """
22 | Map a SwissKnife tool to an A2A skill.
23 |
24 | Args:
25 | tool_name: Name of the tool
26 | tool_def: Tool definition
27 |
28 | Returns:
29 | An A2A skill definition
30 | """
31 | # Extract description from tool definition if available
32 | description = "A tool capability" # Default description
33 | if isinstance(tool_def, dict):
34 | if "description" in tool_def:
35 | description = tool_def["description"]
36 | elif "function" in tool_def and "description" in tool_def["function"]:
37 | description = tool_def["function"]["description"]
38 |
39 | return AgentSkill(
40 | id=tool_name,
41 | name=tool_name.replace("_", " ").title(),
42 | description=description,
43 | # Could add examples based on tool definition
44 | examples=None,
45 | # Most tools work with text input/output
46 | input_modes=["text/plain"],
47 | output_modes=["text/plain"],
48 | tags=[tool_name, "tool"],
49 | )
50 |
51 |
52 | def create_agent_card(agent: LocalAgent, base_url: str) -> AgentCard:
53 | """
54 | Create an A2A agent card from a SwissKnife agent.
55 |
56 | Args:
57 | agent: The SwissKnife agent
58 | base_url: Base URL for the agent's endpoints
59 |
60 | Returns:
61 | An A2A agent card
62 | """
63 | # Map tools to skills
64 | skills: List[AgentSkill] = []
65 | try:
66 | for tool_name, (tool_def, _, _) in agent.tool_definitions.items():
67 | if callable(tool_def):
68 | # If it's a function, call it to get the definition
69 | try:
70 | definition = tool_def()
71 | except Exception:
72 | # If calling without provider fails, try with a default provider
73 | definition = None
74 | else:
75 | definition = tool_def
76 |
77 | if definition:
78 | skill = map_tool_to_skill(tool_name, definition)
79 | skills.append(skill)
80 | except Exception:
81 | # If no tools available, add a basic skill
82 | skills = [
83 | AgentSkill(
84 | id="general",
85 | name="General Assistant",
86 | description="General purpose AI assistant",
87 | tags=["general", "assistant"],
88 | input_modes=["text/plain"],
89 | output_modes=["text/plain"],
90 | )
91 | ]
92 |
93 | # Create capabilities based on agent features
94 | capabilities = AgentCapabilities(
95 | streaming=True, # SwissKnife supports streaming
96 | push_notifications=False, # Not implemented yet
97 | state_transition_history=True, # SwissKnife tracks message history
98 | )
99 |
100 | # Create provider info
101 | provider = AgentProvider(
102 | organization="AgentCrew",
103 | url="https://github.com/daltonnyx/AgentCrew",
104 | )
105 | security_schemes = SecurityScheme(
106 | root=APIKeySecurityScheme.model_validate(
107 | {"name": "Authorization", "in": "header"}
108 | )
109 | )
110 |
111 | return AgentCard(
112 | protocol_version="0.3.0",
113 | name=agent.name if hasattr(agent, "name") else "AgentCrew Assistant",
114 | description=agent.description
115 | if hasattr(agent, "description")
116 | else "An AI assistant powered by AgentCrew",
117 | url=base_url,
118 | preferred_transport=TransportProtocol.jsonrpc,
119 | additional_interfaces=[
120 | AgentInterface(url=base_url, transport=TransportProtocol.jsonrpc)
121 | ],
122 | provider=provider,
123 | version=__version__,
124 | capabilities=capabilities,
125 | skills=skills,
126 | default_input_modes=["text/plain", "application/octet-stream"],
127 | default_output_modes=["text/plain", "application/octet-stream"],
128 | security_schemes={"apiKey": security_schemes},
129 | )
130 |
--------------------------------------------------------------------------------
/AgentCrew/modules/memory/base_service.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict, Any, Optional
2 | from abc import ABC, abstractmethod
3 |
4 |
5 | class BaseMemoryService(ABC):
6 | """Service for storing and retrieving conversation memory."""
7 |
8 | @property
9 | def session_id(self) -> str:
10 | """Get the provider name for this service."""
11 | return getattr(self, "_session_id", "")
12 |
13 | @session_id.setter
14 | def session_id(self, value: str):
15 | """Set the provider name for this service."""
16 | self._session_id = value
17 |
18 | @property
19 | def loaded_conversation(self) -> bool:
20 | """Get the provider name for this service."""
21 | return getattr(self, "_load_conversation", False)
22 |
23 | @loaded_conversation.setter
24 | def loaded_conversation(self, value: bool):
25 | """Set the provider name for this service."""
26 | self._load_conversation = value
27 |
28 | @abstractmethod
29 | def store_conversation(
30 | self, user_message: str, assistant_response: str, agent_name: str = "None"
31 | ) -> List[str]:
32 | """
33 | Store a conversation exchange in memory.
34 |
35 | Args:
36 | user_message: The user's message
37 | assistant_response: The assistant's response
38 |
39 | Returns:
40 | List of memory IDs created
41 | """
42 | pass
43 |
44 | @abstractmethod
45 | async def need_generate_user_context(self, user_input) -> bool:
46 | pass
47 |
48 | @abstractmethod
49 | def clear_conversation_context(self):
50 | pass
51 |
52 | @abstractmethod
53 | def load_conversation_context(self, session_id: str, agent_name: str = "None"):
54 | pass
55 |
56 | @abstractmethod
57 | def generate_user_context(self, user_input: str, agent_name: str = "None") -> str:
58 | """
59 | Generate context based on user input by retrieving relevant memories.
60 |
61 | Args:
62 | user_input: The current user message to generate context for
63 |
64 | Returns:
65 | Formatted string containing relevant context from past conversations
66 | """
67 | pass
68 |
69 | @abstractmethod
70 | def retrieve_memory(
71 | self,
72 | keywords: str,
73 | from_date: Optional[int] = None,
74 | to_date: Optional[int] = None,
75 | agent_name: str = "None",
76 | ) -> str:
77 | """
78 | Retrieve relevant memories based on keywords.
79 |
80 | Args:
81 | keywords: Keywords to search for
82 | from_date: Optional start date (timestamp) to filter memories
83 | to_date: Optional end date (timestamp) to filter memories
84 |
85 | Returns:
86 | Formatted string of relevant memories
87 | """
88 | pass
89 |
90 | @abstractmethod
91 | def list_memory_headers(
92 | self,
93 | from_date: Optional[int] = None,
94 | to_date: Optional[int] = None,
95 | agent_name: str = "None",
96 | ) -> List[str]:
97 | """
98 | List all memory IDs within an optional date range.
99 |
100 | Args:
101 | from_date: Optional start date (timestamp) to filter memories
102 | to_date: Optional end date (timestamp) to filter memories
103 |
104 | Returns:
105 | List of memory IDs
106 | """
107 | pass
108 |
109 | @abstractmethod
110 | def cleanup_old_memories(self, months: int = 1) -> int:
111 | """
112 | Remove memories older than the specified number of months.
113 |
114 | Args:
115 | months: Number of months to keep
116 |
117 | Returns:
118 | Number of memories removed
119 | """
120 | pass
121 |
122 | @abstractmethod
123 | def forget_topic(
124 | self,
125 | topic: str,
126 | from_date: Optional[int] = None,
127 | to_date: Optional[int] = None,
128 | agent_name: str = "None",
129 | ) -> Dict[str, Any]:
130 | """
131 | Remove memories related to a specific topic based on keyword search.
132 |
133 | Args:
134 | topic: Keywords describing the topic to forget
135 |
136 | Returns:
137 | Dict with success status and information about the operation
138 | """
139 | pass
140 |
141 | @abstractmethod
142 | def forget_ids(self, ids: List[str], agent_name: str = "None") -> Dict[str, Any]:
143 | """
144 | Remove memories using list of id.
145 |
146 | Args:
147 | ids: list of IDs to remove
148 |
149 | Returns:
150 | Dict with success status and information about the operation
151 | """
152 | pass
153 |
--------------------------------------------------------------------------------
/tests/main_test.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from unittest.mock import patch, MagicMock
4 | from click.testing import CliRunner
5 | from main import cli
6 |
7 |
8 | @pytest.fixture
9 | def runner():
10 | return CliRunner()
11 |
12 |
13 | @pytest.fixture
14 | def mock_scraper():
15 | with patch("main.Scraper") as mock:
16 | mock_instance = MagicMock()
17 | mock.return_value = mock_instance
18 | mock_instance.scrape_url.return_value = "# Test Content"
19 | yield mock_instance
20 |
21 |
22 | @pytest.fixture
23 | def mock_anthropic():
24 | with patch("main.AnthropicClient") as mock:
25 | mock_instance = MagicMock()
26 | mock.return_value = mock_instance
27 | mock_instance.summarize_content.return_value = "# Summarized Content"
28 | mock_instance.explain_content.return_value = "# Explained Content"
29 | yield mock_instance
30 |
31 |
32 | def test_get_url_basic(runner, mock_scraper, tmp_path):
33 | # Arrange
34 | output_file = os.path.join(tmp_path, "output.md")
35 | test_url = "https://example.com"
36 |
37 | # Act
38 | result = runner.invoke(cli, ["get-url", test_url, output_file])
39 |
40 | # Assert
41 | assert result.exit_code == 0
42 | assert "🌐 Fetching content from:" in result.output
43 | assert "✅ Content successfully scraped" in result.output
44 | assert "💾 Saving content to:" in result.output
45 |
46 | mock_scraper.scrape_url.assert_called_once_with(test_url)
47 |
48 | with open(output_file, "r") as f:
49 | content = f.read()
50 | assert content == "# Test Content"
51 |
52 |
53 | def test_get_url_with_summarize(runner, mock_scraper, mock_anthropic, tmp_path):
54 | # Arrange
55 | output_file = os.path.join(tmp_path, "output.md")
56 | test_url = "https://example.com"
57 |
58 | # Act
59 | result = runner.invoke(cli, ["get-url", test_url, output_file, "--summarize"])
60 |
61 | # Assert
62 | assert result.exit_code == 0
63 | assert "🌐 Fetching content from:" in result.output
64 | assert "✅ Content successfully scraped" in result.output
65 | assert "🤖 Summarizing content using Claude..." in result.output
66 | assert "✅ Content successfully summarized" in result.output
67 | assert "💾 Saving content to:" in result.output
68 |
69 | mock_scraper.scrape_url.assert_called_once_with(test_url)
70 | mock_anthropic.summarize_content.assert_called_once_with("# Test Content")
71 |
72 | with open(output_file, "r") as f:
73 | content = f.read()
74 | assert content == "# Summarized Content"
75 |
76 |
77 | def test_get_url_with_explain(runner, mock_scraper, mock_anthropic, tmp_path):
78 | # Arrange
79 | output_file = os.path.join(tmp_path, "output.md")
80 | test_url = "https://example.com"
81 |
82 | # Act
83 | result = runner.invoke(cli, ["get-url", test_url, output_file, "--explain"])
84 |
85 | # Assert
86 | assert result.exit_code == 0
87 | assert "🌐 Fetching content from:" in result.output
88 | assert "✅ Content successfully scraped" in result.output
89 | assert "🤖 Explaining content using Claude..." in result.output
90 | assert "✅ Content successfully explained" in result.output
91 | assert "💾 Saving content to:" in result.output
92 |
93 | mock_scraper.scrape_url.assert_called_once_with(test_url)
94 | mock_anthropic.explain_content.assert_called_once_with("# Test Content")
95 |
96 | with open(output_file, "r") as f:
97 | content = f.read()
98 | assert content == "# Explained Content"
99 |
100 |
101 | def test_get_url_both_flags_error(runner):
102 | # Act
103 | result = runner.invoke(
104 | cli, ["get-url", "https://example.com", "output.md", "--summarize", "--explain"]
105 | )
106 |
107 | # Assert
108 | assert result.exit_code != 0
109 | assert "Cannot use both --summarize and --explain options together" in result.output
110 |
111 |
112 | def test_get_url_scraper_error(runner, mock_scraper):
113 | # Arrange
114 | mock_scraper.scrape_url.side_effect = Exception("Scraper error")
115 |
116 | # Act
117 | result = runner.invoke(cli, ["get-url", "https://example.com", "output.md"])
118 |
119 | # Assert
120 | assert result.exit_code == 0 # Click catches exceptions and prints them
121 | assert "❌ Error: Scraper error" in result.output
122 |
123 |
124 | def test_get_url_anthropic_error(runner, mock_scraper, mock_anthropic):
125 | # Arrange
126 | mock_anthropic.summarize_content.side_effect = Exception("Anthropic error")
127 |
128 | # Act
129 | result = runner.invoke(
130 | cli, ["get-url", "https://example.com", "output.md", "--summarize"]
131 | )
132 |
133 | # Assert
134 | assert result.exit_code == 0 # Click catches exceptions and prints them
135 | assert "❌ Error: Anthropic error" in result.output
136 |
--------------------------------------------------------------------------------