diff --git a/CHANGELOG.md b/CHANGELOG.md index e271c57..12426e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adjust research prompt to make sure related files are related to the base task, not just the research subtask. - Track tasks by ID and allow them to be deleted. - Make one_shot_completed tool available to research agent. +- Make sure file modification tools are not available when research only flag is used. ## [0.6.0] - 2024-12-17 diff --git a/ra_aid/__main__.py b/ra_aid/__main__.py index 2cb8355..e01eb6c 100644 --- a/ra_aid/__main__.py +++ b/ra_aid/__main__.py @@ -30,17 +30,29 @@ import time from anthropic import APIError, APITimeoutError, RateLimitError, InternalServerError from ra_aid.llm import initialize_llm -# Common tools used across multiple agents -COMMON_TOOLS = [ +# Read-only tools that don't modify system state +READ_ONLY_TOOLS = [ emit_related_files, emit_key_facts, delete_key_facts, emit_key_snippets, delete_key_snippets, read_file_tool, - write_file_tool, fuzzy_find_project_files, - ripgrep_search, + ripgrep_search +] + +# Tools that can modify files or system state +MODIFICATION_TOOLS = [ + write_file_tool, + file_str_replace, + run_shell_command, + run_programming_task +] + +# Common tools used across multiple agents +COMMON_TOOLS = READ_ONLY_TOOLS + [ + write_file_tool, file_str_replace ] @@ -134,56 +146,55 @@ implementation_memory = MemorySaver() def get_research_tools(research_only: bool = False, expert_enabled: bool = True) -> list: """Get the list of research tools based on mode and whether expert is enabled.""" - tools = RESEARCH_TOOLS.copy() # Start with research-specific tools including list_directory_tree + # Start with read-only tools + tools = READ_ONLY_TOOLS.copy() - # Add common tools - tools.extend(COMMON_TOOLS.copy()) + # Add research tools except run_shell_command + research_tools = [t for t in RESEARCH_TOOLS if t != run_shell_command] + tools.extend(research_tools) + # Add modification tools if not research_only + if not research_only: + tools.extend(MODIFICATION_TOOLS) + tools.append(request_complex_implementation) + + # Add expert tools if enabled if expert_enabled: tools.extend(EXPERT_TOOLS) - if not research_only: - tools.append(request_complex_implementation) - return tools def get_planning_tools(expert_enabled: bool = True) -> list: - tools = [ - list_directory_tree, + """Get the list of planning tools based on whether expert is enabled.""" + # Start with common tools + tools = COMMON_TOOLS.copy() + + # Add planning-specific tools + planning_tools = [ emit_plan, - emit_task, - swap_task_order, - emit_related_files, - emit_key_facts, - delete_key_facts, - emit_key_snippets, - delete_key_snippets, - read_file_tool, - fuzzy_find_project_files, - ripgrep_search + emit_task, + swap_task_order ] + tools.extend(planning_tools) + + # Add expert tools if enabled if expert_enabled: - tools.append(ask_expert) - tools.append(emit_expert_context) + tools.extend(EXPERT_TOOLS) + return tools def get_implementation_tools(expert_enabled: bool = True) -> list: - tools = [ - list_directory_tree, - run_shell_command, - run_programming_task, - emit_related_files, - emit_key_facts, - delete_key_facts, - emit_key_snippets, - delete_key_snippets, - read_file_tool, - fuzzy_find_project_files, - ripgrep_search - ] + """Get the list of implementation tools based on whether expert is enabled.""" + # Start with common tools + tools = COMMON_TOOLS.copy() + + # Add modification tools since it's not research-only + tools.extend(MODIFICATION_TOOLS) + + # Add expert tools if enabled if expert_enabled: - tools.append(ask_expert) - tools.append(emit_expert_context) + tools.extend(EXPERT_TOOLS) + return tools def is_informational_query() -> bool: diff --git a/ra_aid/config.py b/ra_aid/config.py new file mode 100644 index 0000000..e6d4fd3 --- /dev/null +++ b/ra_aid/config.py @@ -0,0 +1,93 @@ +"""Configuration and environment validation utilities.""" + +import os +import sys +from dataclasses import dataclass +from typing import Tuple, List + +from ra_aid import print_error + +@dataclass +class ProviderConfig: + """Configuration for a provider.""" + key_name: str + base_required: bool = False + +PROVIDER_CONFIGS = { + "anthropic": ProviderConfig("ANTHROPIC_API_KEY", base_required=True), + "openai": ProviderConfig("OPENAI_API_KEY", base_required=True), + "openrouter": ProviderConfig("OPENROUTER_API_KEY", base_required=True), + "openai-compatible": ProviderConfig("OPENAI_API_KEY", base_required=True), +} + +def validate_environment(args) -> Tuple[bool, List[str]]: + """Validate required environment variables and dependencies. + + Args: + args: The parsed command line arguments containing: + - provider: The main LLM provider + - expert_provider: The expert LLM provider + + Returns: + Tuple containing: + - bool: Whether expert mode is enabled + - List[str]: List of missing expert configuration items + + Raises: + SystemExit: If required base environment variables are missing + """ + missing = [] + provider = args.provider + expert_provider = args.expert_provider + + # Check API keys based on provider configs + if provider in PROVIDER_CONFIGS: + config = PROVIDER_CONFIGS[provider] + if config.base_required and not os.environ.get(config.key_name): + missing.append(f'{config.key_name} environment variable is not set') + + # Special case for openai-compatible needing base URL + if provider == "openai-compatible" and not os.environ.get('OPENAI_API_BASE'): + missing.append('OPENAI_API_BASE environment variable is not set') + + expert_missing = [] + if expert_provider in PROVIDER_CONFIGS: + config = PROVIDER_CONFIGS[expert_provider] + expert_key = f'EXPERT_{config.key_name}' + expert_key_missing = not os.environ.get(expert_key) + + # Try fallback to base key if providers match + fallback_available = expert_provider == provider and os.environ.get(config.key_name) + if expert_key_missing and fallback_available: + os.environ[expert_key] = os.environ[config.key_name] + expert_key_missing = False + + if expert_key_missing: + expert_missing.append(f'{expert_key} environment variable is not set') + + # Special case for openai-compatible expert needing base URL + if expert_provider == "openai-compatible": + expert_base = 'EXPERT_OPENAI_API_BASE' + base_missing = not os.environ.get(expert_base) + base_fallback = expert_provider == provider and os.environ.get('OPENAI_API_BASE') + + if base_missing and base_fallback: + os.environ[expert_base] = os.environ['OPENAI_API_BASE'] + base_missing = False + + if base_missing: + expert_missing.append(f'{expert_base} environment variable is not set') + + # If main keys missing, we must exit immediately + if missing: + print_error("Missing required dependencies:") + for item in missing: + print_error(f"- {item}") + sys.exit(1) + + # If expert keys missing, we disable expert tools instead of exiting + expert_enabled = True + if expert_missing: + expert_enabled = False + + return expert_enabled, expert_missing \ No newline at end of file