├── 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 | --------------------------------------------------------------------------------