feat(Makefile): add commands for code checking and fixing using ruff (#63)
refactor(ra_aid/__init__.py): reorganize imports for better readability refactor(ra_aid/__main__.py): clean up imports and improve structure refactor(ra_aid/agent_utils.py): streamline imports and improve organization refactor(ra_aid/agents/ciayn_agent.py): enhance code structure and readability refactor(ra_aid/chat_models/deepseek_chat.py): tidy up imports for clarity refactor(ra_aid/config.py): maintain consistent formatting and organization refactor(ra_aid/console/__init__.py): improve import structure for clarity refactor(ra_aid/console/cowboy_messages.py): enhance code readability refactor(ra_aid/console/formatting.py): clean up formatting functions for consistency refactor(ra_aid/console/output.py): improve output handling for better clarity refactor(ra_aid/dependencies.py): enhance dependency checking structure refactor(ra_aid/env.py): streamline environment validation logic refactor(ra_aid/exceptions.py): improve exception handling structure refactor(ra_aid/file_listing.py): enhance file listing functionality refactor(ra_aid/llm.py): improve language model initialization logic refactor(ra_aid/logging_config.py): tidy up logging configuration refactor(ra_aid/models_tokens.py): maintain consistent formatting refactor(ra_aid/proc/interactive.py): enhance subprocess handling refactor(ra_aid/project_info.py): improve project information handling refactor(ra_aid/project_state.py): streamline project state management refactor(ra_aid/provider_strategy.py): enhance provider validation logic refactor(ra_aid/tests/test_env.py): improve test structure and readability refactor(ra_aid/text/__init__.py): maintain consistent import structure refactor(ra_aid/text/processing.py): enhance text processing functions refactor(ra_aid/tool_configs.py): improve tool configuration structure refactor(ra_aid/tools/__init__.py): tidy up tool imports for clarity refactor(ra_aid/tools/agent.py): enhance agent tool functionality refactor(ra_aid/tools/expert.py): improve expert tool handling refactor(ra_aid/tools/file_str_replace.py): streamline file string replacement logic refactor(ra_aid/tools/fuzzy_find.py): enhance fuzzy find functionality refactor(ra_aid/tools/handle_user_defined_test_cmd_execution.py): improve test command execution logic refactor(ra_aid/tools/human.py): enhance human interaction handling refactor(ra_aid/tools/list_directory.py): improve directory listing functionality refactor(ra_aid/tools/memory.py): streamline memory management functions refactor(ra_aid/tools/programmer.py): enhance programming task handling refactor(ra_aid/tools/read_file.py): improve file reading functionality refactor(ra_aid/tools/reflection.py): maintain consistent function structure refactor(ra_aid/tools/research.py): enhance research tool handling refactor(ra_aid/tools/ripgrep.py): improve ripgrep search functionality refactor(ra_aid/tools/ripgrep.py): enhance ripgrep search command handling refactor(ra_aid/tools/ripgrep.py): improve search result handling refactor(ra_aid/tools/ripgrep.py): streamline search parameters handling refactor(ra_aid/tools/ripgrep.py): enhance error handling for search operations refactor(ra_aid/tools/ripgrep.py): improve output formatting for search results refactor(ra_aid/tools/ripgrep.py): maintain consistent command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search result display refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor(ra_aid/tools/ripgrep.py): enhance search command execution logic refactor(ra_aid/tools/ripgrep.py): improve search command output formatting refactor(ra_aid/tools/ripgrep.py): streamline search command construction refactor(ra_aid/tools/ripgrep.py): enhance search command error handling refactor(ra_aid/tools/ripgrep.py): improve search command output handling refactor(ra_aid/tools/ripgrep.py): maintain consistent search command structure refactor( style(server.ts): update variable naming from lowercase 'port' to uppercase 'PORT' for consistency and clarity feat(server.ts): allow server to listen on a configurable port using process.env.PORT or default to PORT style(shell.py): reorder import statements for better organization and readability style(shell.py): remove unnecessary blank lines for cleaner code style(web_search_tavily.py): reorder import statements for better organization style(write_file.py): format code for consistency and readability style(extract_changelog.py): format code for consistency and readability style(generate_swebench_dataset.py): format code for consistency and readability style(test_ciayn_agent.py): format code for consistency and readability style(test_cowboy_messages.py): format code for consistency and readability style(test_interactive.py): format code for consistency and readability style(test_agent_utils.py): format code for consistency and readability style(test_default_provider.py): format code for consistency and readability style(test_env.py): format code for consistency and readability style(test_llm.py): format code for consistency and readability style(test_main.py): format code for consistency and readability style(test_programmer.py): format code for consistency and readability style(test_provider_integration.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_utils.py): format code for consistency and readability style(test_execution.py): format code for consistency and readability style(test_expert.py): format code for consistency and readability style(test_file_str_replace.py): format code for consistency and readability style(test_fuzzy_find.py): format code for consistency and readability style(test_handle_user_defined_test_cmd_execution.py): format code for consistency and readability style(test_list_directory.py): format code for consistency and readability style(test_user_defined_test_cmd_execution.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and readability style(test_tool_configs.py): format code for consistency and style(tests): standardize string quotes to double quotes for consistency across test files style(tests): format code for better readability by adjusting spacing and line breaks style(tests): remove unnecessary blank lines to improve code cleanliness style(tests): ensure consistent use of whitespace around operators and after commas style(tests): align comments and docstrings for better visual structure and clarity
This commit is contained in:
parent
b44f1c73eb
commit
b00fd47573
11
Makefile
11
Makefile
|
|
@ -9,3 +9,14 @@ setup-dev:
|
||||||
|
|
||||||
setup-hooks: setup-dev
|
setup-hooks: setup-dev
|
||||||
pre-commit install
|
pre-commit install
|
||||||
|
|
||||||
|
check:
|
||||||
|
ruff check
|
||||||
|
|
||||||
|
fix:
|
||||||
|
ruff check . --select I --fix # First sort imports
|
||||||
|
ruff format .
|
||||||
|
ruff check --fix
|
||||||
|
|
||||||
|
fix-basic:
|
||||||
|
ruff check --fix
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
from .console.formatting import print_stage_header, print_task_header, print_error, print_interrupt
|
from .agent_utils import run_agent_with_retry
|
||||||
|
from .console.formatting import (
|
||||||
|
print_error,
|
||||||
|
print_interrupt,
|
||||||
|
print_stage_header,
|
||||||
|
print_task_header,
|
||||||
|
)
|
||||||
from .console.output import print_agent_output
|
from .console.output import print_agent_output
|
||||||
from .text.processing import truncate_output
|
from .text.processing import truncate_output
|
||||||
from .agent_utils import run_agent_with_retry
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'print_stage_header',
|
"print_stage_header",
|
||||||
'print_task_header',
|
"print_task_header",
|
||||||
'print_agent_output',
|
"print_agent_output",
|
||||||
'truncate_output',
|
"truncate_output",
|
||||||
'print_error',
|
"print_error",
|
||||||
'print_interrupt',
|
"print_interrupt",
|
||||||
'run_agent_with_retry',
|
"run_agent_with_retry",
|
||||||
'__version__'
|
"__version__",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.console import Console
|
|
||||||
from langgraph.checkpoint.memory import MemorySaver
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
from ra_aid.config import DEFAULT_MAX_TEST_CMD_RETRIES, DEFAULT_RECURSION_LIMIT
|
from rich.console import Console
|
||||||
from ra_aid.env import validate_environment
|
from rich.panel import Panel
|
||||||
from ra_aid.project_info import get_project_info, format_project_info
|
|
||||||
from ra_aid.tools.memory import _global_memory
|
from ra_aid import print_error, print_stage_header
|
||||||
from ra_aid.tools.human import ask_human
|
|
||||||
from ra_aid import print_stage_header, print_error
|
|
||||||
from ra_aid.__version__ import __version__
|
from ra_aid.__version__ import __version__
|
||||||
from ra_aid.agent_utils import (
|
from ra_aid.agent_utils import (
|
||||||
AgentInterrupt,
|
AgentInterrupt,
|
||||||
run_agent_with_retry,
|
|
||||||
run_research_agent,
|
|
||||||
run_planning_agent,
|
|
||||||
create_agent,
|
create_agent,
|
||||||
|
run_agent_with_retry,
|
||||||
|
run_planning_agent,
|
||||||
|
run_research_agent,
|
||||||
)
|
)
|
||||||
from ra_aid.prompts import CHAT_PROMPT, WEB_RESEARCH_PROMPT_SECTION_CHAT
|
from ra_aid.config import DEFAULT_MAX_TEST_CMD_RETRIES, DEFAULT_RECURSION_LIMIT
|
||||||
from ra_aid.llm import initialize_llm
|
|
||||||
from ra_aid.logging_config import setup_logging, get_logger
|
|
||||||
from ra_aid.tool_configs import get_chat_tools
|
|
||||||
from ra_aid.dependencies import check_dependencies
|
from ra_aid.dependencies import check_dependencies
|
||||||
import os
|
from ra_aid.env import validate_environment
|
||||||
|
from ra_aid.llm import initialize_llm
|
||||||
|
from ra_aid.logging_config import get_logger, setup_logging
|
||||||
|
from ra_aid.project_info import format_project_info, get_project_info
|
||||||
|
from ra_aid.prompts import CHAT_PROMPT, WEB_RESEARCH_PROMPT_SECTION_CHAT
|
||||||
|
from ra_aid.tool_configs import get_chat_tools
|
||||||
|
from ra_aid.tools.human import ask_human
|
||||||
|
from ra_aid.tools.memory import _global_memory
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -151,12 +153,12 @@ Examples:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--test-cmd",
|
"--test-cmd",
|
||||||
type=str,
|
type=str,
|
||||||
help="Test command to run before completing tasks (e.g. 'pytest tests/')"
|
help="Test command to run before completing tasks (e.g. 'pytest tests/')",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--auto-test",
|
"--auto-test",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Automatically run tests before completing tasks"
|
help="Automatically run tests before completing tasks",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--max-test-cmd-retries",
|
"--max-test-cmd-retries",
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,65 @@
|
||||||
"""Utility functions for working with agents."""
|
"""Utility functions for working with agents."""
|
||||||
|
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional, Any, List, Dict, Sequence
|
from typing import Any, Dict, List, Optional, Sequence
|
||||||
from langchain_core.messages import BaseMessage, trim_messages
|
|
||||||
from litellm import get_model_info
|
|
||||||
import litellm
|
import litellm
|
||||||
|
from anthropic import APIError, APITimeoutError, InternalServerError, RateLimitError
|
||||||
import signal
|
|
||||||
|
|
||||||
from langgraph.checkpoint.memory import MemorySaver
|
|
||||||
from langgraph.prebuilt.chat_agent_executor import AgentState
|
|
||||||
from ra_aid.config import DEFAULT_MAX_TEST_CMD_RETRIES, DEFAULT_RECURSION_LIMIT
|
|
||||||
from ra_aid.models_tokens import DEFAULT_TOKEN_LIMIT, models_tokens
|
|
||||||
from ra_aid.agents.ciayn_agent import CiaynAgent
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from ra_aid.project_info import (
|
|
||||||
get_project_info,
|
|
||||||
format_project_info,
|
|
||||||
display_project_status,
|
|
||||||
)
|
|
||||||
|
|
||||||
from langgraph.prebuilt import create_react_agent
|
|
||||||
from ra_aid.console.formatting import print_stage_header, print_error
|
|
||||||
from langchain_core.language_models import BaseChatModel
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
from langchain_core.messages import BaseMessage, HumanMessage, trim_messages
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from ra_aid.console.output import print_agent_output
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
from ra_aid.logging_config import get_logger
|
from langgraph.prebuilt import create_react_agent
|
||||||
from ra_aid.exceptions import AgentInterrupt
|
from langgraph.prebuilt.chat_agent_executor import AgentState
|
||||||
from ra_aid.tool_configs import (
|
from litellm import get_model_info
|
||||||
get_implementation_tools,
|
|
||||||
get_research_tools,
|
|
||||||
get_planning_tools,
|
|
||||||
get_web_research_tools,
|
|
||||||
)
|
|
||||||
from ra_aid.prompts import (
|
|
||||||
IMPLEMENTATION_PROMPT,
|
|
||||||
EXPERT_PROMPT_SECTION_IMPLEMENTATION,
|
|
||||||
HUMAN_PROMPT_SECTION_IMPLEMENTATION,
|
|
||||||
EXPERT_PROMPT_SECTION_RESEARCH,
|
|
||||||
WEB_RESEARCH_PROMPT_SECTION_RESEARCH,
|
|
||||||
WEB_RESEARCH_PROMPT_SECTION_CHAT,
|
|
||||||
WEB_RESEARCH_PROMPT_SECTION_PLANNING,
|
|
||||||
RESEARCH_PROMPT,
|
|
||||||
RESEARCH_ONLY_PROMPT,
|
|
||||||
HUMAN_PROMPT_SECTION_RESEARCH,
|
|
||||||
PLANNING_PROMPT,
|
|
||||||
EXPERT_PROMPT_SECTION_PLANNING,
|
|
||||||
HUMAN_PROMPT_SECTION_PLANNING,
|
|
||||||
WEB_RESEARCH_PROMPT,
|
|
||||||
)
|
|
||||||
|
|
||||||
from langchain_core.messages import HumanMessage
|
|
||||||
from anthropic import APIError, APITimeoutError, RateLimitError, InternalServerError
|
|
||||||
from ra_aid.tools.human import ask_human
|
|
||||||
from ra_aid.tools.shell import run_shell_command
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from ra_aid.agents.ciayn_agent import CiaynAgent
|
||||||
|
from ra_aid.config import DEFAULT_MAX_TEST_CMD_RETRIES, DEFAULT_RECURSION_LIMIT
|
||||||
|
from ra_aid.console.formatting import print_error, print_stage_header
|
||||||
|
from ra_aid.console.output import print_agent_output
|
||||||
|
from ra_aid.exceptions import AgentInterrupt
|
||||||
|
from ra_aid.logging_config import get_logger
|
||||||
|
from ra_aid.models_tokens import DEFAULT_TOKEN_LIMIT, models_tokens
|
||||||
|
from ra_aid.project_info import (
|
||||||
|
display_project_status,
|
||||||
|
format_project_info,
|
||||||
|
get_project_info,
|
||||||
|
)
|
||||||
|
from ra_aid.prompts import (
|
||||||
|
EXPERT_PROMPT_SECTION_IMPLEMENTATION,
|
||||||
|
EXPERT_PROMPT_SECTION_PLANNING,
|
||||||
|
EXPERT_PROMPT_SECTION_RESEARCH,
|
||||||
|
HUMAN_PROMPT_SECTION_IMPLEMENTATION,
|
||||||
|
HUMAN_PROMPT_SECTION_PLANNING,
|
||||||
|
HUMAN_PROMPT_SECTION_RESEARCH,
|
||||||
|
IMPLEMENTATION_PROMPT,
|
||||||
|
PLANNING_PROMPT,
|
||||||
|
RESEARCH_ONLY_PROMPT,
|
||||||
|
RESEARCH_PROMPT,
|
||||||
|
WEB_RESEARCH_PROMPT,
|
||||||
|
WEB_RESEARCH_PROMPT_SECTION_CHAT,
|
||||||
|
WEB_RESEARCH_PROMPT_SECTION_PLANNING,
|
||||||
|
WEB_RESEARCH_PROMPT_SECTION_RESEARCH,
|
||||||
|
)
|
||||||
|
from ra_aid.tool_configs import (
|
||||||
|
get_implementation_tools,
|
||||||
|
get_planning_tools,
|
||||||
|
get_research_tools,
|
||||||
|
get_web_research_tools,
|
||||||
|
)
|
||||||
|
from ra_aid.tools.handle_user_defined_test_cmd_execution import execute_test_command
|
||||||
from ra_aid.tools.memory import (
|
from ra_aid.tools.memory import (
|
||||||
_global_memory,
|
_global_memory,
|
||||||
get_memory_value,
|
get_memory_value,
|
||||||
get_related_files,
|
get_related_files,
|
||||||
)
|
)
|
||||||
from ra_aid.tools.handle_user_defined_test_cmd_execution import execute_test_command
|
|
||||||
|
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
@ -723,7 +715,7 @@ def run_agent_with_retry(agent, prompt: str, config: dict) -> Optional[str]:
|
||||||
max_retries = 20
|
max_retries = 20
|
||||||
base_delay = 1
|
base_delay = 1
|
||||||
test_attempts = 0
|
test_attempts = 0
|
||||||
max_test_retries = config.get("max_test_cmd_retries", DEFAULT_MAX_TEST_CMD_RETRIES)
|
_max_test_retries = config.get("max_test_cmd_retries", DEFAULT_MAX_TEST_CMD_RETRIES)
|
||||||
auto_test = config.get("auto_test", False)
|
auto_test = config.get("auto_test", False)
|
||||||
original_prompt = prompt
|
original_prompt = prompt
|
||||||
|
|
||||||
|
|
@ -754,11 +746,10 @@ def run_agent_with_retry(agent, prompt: str, config: dict) -> Optional[str]:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Execute test command if configured
|
# Execute test command if configured
|
||||||
should_break, prompt, auto_test, test_attempts = execute_test_command(
|
should_break, prompt, auto_test, test_attempts = (
|
||||||
config,
|
execute_test_command(
|
||||||
original_prompt,
|
config, original_prompt, test_attempts, auto_test
|
||||||
test_attempts,
|
)
|
||||||
auto_test
|
|
||||||
)
|
)
|
||||||
if should_break:
|
if should_break:
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Any, Generator, List, Optional, Union
|
from typing import Any, Dict, Generator, List, Optional, Union
|
||||||
|
|
||||||
|
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
||||||
|
|
||||||
|
from ra_aid.exceptions import ToolExecutionError
|
||||||
|
from ra_aid.logging_config import get_logger
|
||||||
from ra_aid.models_tokens import DEFAULT_TOKEN_LIMIT
|
from ra_aid.models_tokens import DEFAULT_TOKEN_LIMIT
|
||||||
from ra_aid.tools.reflection import get_function_info
|
from ra_aid.tools.reflection import get_function_info
|
||||||
|
|
||||||
from langchain_core.messages import AIMessage, HumanMessage, BaseMessage, SystemMessage
|
|
||||||
from ra_aid.exceptions import ToolExecutionError
|
|
||||||
from ra_aid.logging_config import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChunkMessage:
|
class ChunkMessage:
|
||||||
content: str
|
content: str
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
|
||||||
def validate_function_call_pattern(s: str) -> bool:
|
def validate_function_call_pattern(s: str) -> bool:
|
||||||
"""Check if a string matches the expected function call pattern.
|
"""Check if a string matches the expected function call pattern.
|
||||||
|
|
||||||
|
|
@ -34,6 +36,7 @@ def validate_function_call_pattern(s: str) -> bool:
|
||||||
pattern = r"^\s*[\w_\-]+\s*\([^)(]*(?:\([^)(]*\)[^)(]*)*\)\s*$"
|
pattern = r"^\s*[\w_\-]+\s*\([^)(]*(?:\([^)(]*\)[^)(]*)*\)\s*$"
|
||||||
return not re.match(pattern, s, re.DOTALL)
|
return not re.match(pattern, s, re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
class CiaynAgent:
|
class CiaynAgent:
|
||||||
"""Code Is All You Need (CIAYN) agent that uses generated Python code for tool interaction.
|
"""Code Is All You Need (CIAYN) agent that uses generated Python code for tool interaction.
|
||||||
|
|
||||||
|
|
@ -65,8 +68,13 @@ class CiaynAgent:
|
||||||
- Memory management with configurable limits
|
- Memory management with configurable limits
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
def __init__(self, model, tools: list, max_history_messages: int = 50, max_tokens: Optional[int] = DEFAULT_TOKEN_LIMIT):
|
self,
|
||||||
|
model,
|
||||||
|
tools: list,
|
||||||
|
max_history_messages: int = 50,
|
||||||
|
max_tokens: Optional[int] = DEFAULT_TOKEN_LIMIT,
|
||||||
|
):
|
||||||
"""Initialize the agent with a model and list of tools.
|
"""Initialize the agent with a model and list of tools.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -93,7 +101,8 @@ class CiaynAgent:
|
||||||
functions_list = "\n\n".join(self.available_functions)
|
functions_list = "\n\n".join(self.available_functions)
|
||||||
|
|
||||||
# Build the complete prompt without f-strings for the static parts
|
# Build the complete prompt without f-strings for the static parts
|
||||||
base_prompt += """
|
base_prompt += (
|
||||||
|
"""
|
||||||
|
|
||||||
<agent instructions>
|
<agent instructions>
|
||||||
You are a ReAct agent. You run in a loop and use ONE of the available functions per iteration.
|
You are a ReAct agent. You run in a loop and use ONE of the available functions per iteration.
|
||||||
|
|
@ -111,7 +120,9 @@ You typically don't want to keep calling the same function over and over with th
|
||||||
|
|
||||||
You must ONLY use ONE of the following functions (these are the ONLY functions that exist):
|
You must ONLY use ONE of the following functions (these are the ONLY functions that exist):
|
||||||
|
|
||||||
<available functions>""" + functions_list + """
|
<available functions>"""
|
||||||
|
+ functions_list
|
||||||
|
+ """
|
||||||
</available functions>
|
</available functions>
|
||||||
|
|
||||||
You may use any of the above functions to complete your job. Use the best one for the current step you are on. Be efficient, avoid getting stuck in repetitive loops, and do not hesitate to call functions which delegate your work to make your life easier.
|
You may use any of the above functions to complete your job. Use the best one for the current step you are on. Be efficient, avoid getting stuck in repetitive loops, and do not hesitate to call functions which delegate your work to make your life easier.
|
||||||
|
|
@ -202,15 +213,13 @@ You have often been criticized for:
|
||||||
|
|
||||||
DO NOT CLAIM YOU ARE FINISHED UNTIL YOU ACTUALLY ARE!
|
DO NOT CLAIM YOU ARE FINISHED UNTIL YOU ACTUALLY ARE!
|
||||||
Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
|
)
|
||||||
|
|
||||||
return base_prompt
|
return base_prompt
|
||||||
|
|
||||||
def _execute_tool(self, code: str) -> str:
|
def _execute_tool(self, code: str) -> str:
|
||||||
"""Execute a tool call and return its result."""
|
"""Execute a tool call and return its result."""
|
||||||
globals_dict = {
|
globals_dict = {tool.func.__name__: tool.func for tool in self.tools}
|
||||||
tool.func.__name__: tool.func
|
|
||||||
for tool in self.tools
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
code = code.strip()
|
code = code.strip()
|
||||||
|
|
@ -229,20 +238,12 @@ Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
|
|
||||||
def _create_agent_chunk(self, content: str) -> Dict[str, Any]:
|
def _create_agent_chunk(self, content: str) -> Dict[str, Any]:
|
||||||
"""Create an agent chunk in the format expected by print_agent_output."""
|
"""Create an agent chunk in the format expected by print_agent_output."""
|
||||||
return {
|
return {"agent": {"messages": [AIMessage(content=content)]}}
|
||||||
"agent": {
|
|
||||||
"messages": [AIMessage(content=content)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _create_error_chunk(self, content: str) -> Dict[str, Any]:
|
def _create_error_chunk(self, content: str) -> Dict[str, Any]:
|
||||||
"""Create an error chunk in the format expected by print_agent_output."""
|
"""Create an error chunk in the format expected by print_agent_output."""
|
||||||
message = ChunkMessage(content=content, status="error")
|
message = ChunkMessage(content=content, status="error")
|
||||||
return {
|
return {"tools": {"messages": [message]}}
|
||||||
"tools": {
|
|
||||||
"messages": [message]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _estimate_tokens(content: Optional[Union[str, BaseMessage]]) -> int:
|
def _estimate_tokens(content: Optional[Union[str, BaseMessage]]) -> int:
|
||||||
|
|
@ -271,9 +272,11 @@ Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
if not text:
|
if not text:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return len(text.encode('utf-8')) // 4
|
return len(text.encode("utf-8")) // 4
|
||||||
|
|
||||||
def _trim_chat_history(self, initial_messages: List[Any], chat_history: List[Any]) -> List[Any]:
|
def _trim_chat_history(
|
||||||
|
self, initial_messages: List[Any], chat_history: List[Any]
|
||||||
|
) -> List[Any]:
|
||||||
"""Trim chat history based on message count and token limits while preserving initial messages.
|
"""Trim chat history based on message count and token limits while preserving initial messages.
|
||||||
|
|
||||||
Applies both message count and token limits (if configured) to chat_history,
|
Applies both message count and token limits (if configured) to chat_history,
|
||||||
|
|
@ -288,7 +291,7 @@ Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
"""
|
"""
|
||||||
# First apply message count limit
|
# First apply message count limit
|
||||||
if len(chat_history) > self.max_history_messages:
|
if len(chat_history) > self.max_history_messages:
|
||||||
chat_history = chat_history[-self.max_history_messages:]
|
chat_history = chat_history[-self.max_history_messages :]
|
||||||
|
|
||||||
# Skip token limiting if max_tokens is None
|
# Skip token limiting if max_tokens is None
|
||||||
if self.max_tokens is None:
|
if self.max_tokens is None:
|
||||||
|
|
@ -299,14 +302,18 @@ Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
|
|
||||||
# Remove messages from start of chat_history until under token limit
|
# Remove messages from start of chat_history until under token limit
|
||||||
while chat_history:
|
while chat_history:
|
||||||
total_tokens = initial_tokens + sum(self._estimate_tokens(msg) for msg in chat_history)
|
total_tokens = initial_tokens + sum(
|
||||||
|
self._estimate_tokens(msg) for msg in chat_history
|
||||||
|
)
|
||||||
if total_tokens <= self.max_tokens:
|
if total_tokens <= self.max_tokens:
|
||||||
break
|
break
|
||||||
chat_history.pop(0)
|
chat_history.pop(0)
|
||||||
|
|
||||||
return initial_messages + chat_history
|
return initial_messages + chat_history
|
||||||
|
|
||||||
def stream(self, messages_dict: Dict[str, List[Any]], config: Dict[str, Any] = None) -> Generator[Dict[str, Any], None, None]:
|
def stream(
|
||||||
|
self, messages_dict: Dict[str, List[Any]], config: Dict[str, Any] = None
|
||||||
|
) -> Generator[Dict[str, Any], None, None]:
|
||||||
"""Stream agent responses in a format compatible with print_agent_output."""
|
"""Stream agent responses in a format compatible with print_agent_output."""
|
||||||
initial_messages = messages_dict.get("messages", [])
|
initial_messages = messages_dict.get("messages", [])
|
||||||
chat_history = []
|
chat_history = []
|
||||||
|
|
@ -318,7 +325,14 @@ Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
chat_history.append(HumanMessage(content=base_prompt))
|
chat_history.append(HumanMessage(content=base_prompt))
|
||||||
|
|
||||||
full_history = self._trim_chat_history(initial_messages, chat_history)
|
full_history = self._trim_chat_history(initial_messages, chat_history)
|
||||||
response = self.model.invoke([SystemMessage("Execute efficiently yet completely as a fully autonomous agent.")] + full_history)
|
response = self.model.invoke(
|
||||||
|
[
|
||||||
|
SystemMessage(
|
||||||
|
"Execute efficiently yet completely as a fully autonomous agent."
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ full_history
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Code generated by agent: {response.content}")
|
logger.debug(f"Code generated by agent: {response.content}")
|
||||||
|
|
@ -328,9 +342,14 @@ Output **ONLY THE CODE** and **NO MARKDOWN BACKTICKS**"""
|
||||||
yield {}
|
yield {}
|
||||||
|
|
||||||
except ToolExecutionError as e:
|
except ToolExecutionError as e:
|
||||||
chat_history.append(HumanMessage(content=f"Your tool call caused an error: {e}\n\nPlease correct your tool call and try again."))
|
chat_history.append(
|
||||||
|
HumanMessage(
|
||||||
|
content=f"Your tool call caused an error: {e}\n\nPlease correct your tool call and try again."
|
||||||
|
)
|
||||||
|
)
|
||||||
yield self._create_error_chunk(str(e))
|
yield self._create_error_chunk(str(e))
|
||||||
|
|
||||||
|
|
||||||
def _extract_tool_call(code: str, functions_list: str) -> str:
|
def _extract_tool_call(code: str, functions_list: str) -> str:
|
||||||
from ra_aid.tools.expert import get_model
|
from ra_aid.tools.expert import get_model
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from langchain_core.callbacks import CallbackManagerForLLMRun
|
from langchain_core.callbacks import CallbackManagerForLLMRun
|
||||||
from langchain_core.messages import BaseMessage
|
from langchain_core.messages import BaseMessage
|
||||||
from langchain_core.outputs import ChatResult
|
from langchain_core.outputs import ChatResult
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from typing import Any, List, Optional, Dict
|
|
||||||
|
|
||||||
|
|
||||||
# Docs: https://api-docs.deepseek.com/guides/reasoning_model
|
# Docs: https://api-docs.deepseek.com/guides/reasoning_model
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
from .formatting import print_stage_header, print_task_header, print_error, print_interrupt, console
|
from .formatting import (
|
||||||
|
console,
|
||||||
|
print_error,
|
||||||
|
print_interrupt,
|
||||||
|
print_stage_header,
|
||||||
|
print_task_header,
|
||||||
|
)
|
||||||
from .output import print_agent_output
|
from .output import print_agent_output
|
||||||
|
|
||||||
__all__ = ['print_stage_header', 'print_task_header', 'print_agent_output', 'console', 'print_error', 'print_interrupt']
|
__all__ = [
|
||||||
|
"print_stage_header",
|
||||||
|
"print_task_header",
|
||||||
|
"print_agent_output",
|
||||||
|
"console",
|
||||||
|
"print_error",
|
||||||
|
"print_interrupt",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ COWBOY_MESSAGES = [
|
||||||
"This ain't my first rodeo! 🤠",
|
"This ain't my first rodeo! 🤠",
|
||||||
"Lock and load, partner! 🤠",
|
"Lock and load, partner! 🤠",
|
||||||
"I'm just a baby 👶",
|
"I'm just a baby 👶",
|
||||||
"I'll try not to destroy everything 😏"
|
"I'll try not to destroy everything 😏",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_cowboy_message() -> str:
|
def get_cowboy_message() -> str:
|
||||||
"""Randomly select and return a cowboy message.
|
"""Randomly select and return a cowboy message.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def print_stage_header(stage: str) -> None:
|
def print_stage_header(stage: str) -> None:
|
||||||
"""Print a stage header with stage-specific styling and icons.
|
"""Print a stage header with stage-specific styling and icons.
|
||||||
|
|
||||||
|
|
@ -12,14 +13,14 @@ def print_stage_header(stage: str) -> None:
|
||||||
"""
|
"""
|
||||||
# Define stage icons mapping - using single-width emojis to prevent line wrapping issues
|
# Define stage icons mapping - using single-width emojis to prevent line wrapping issues
|
||||||
icons = {
|
icons = {
|
||||||
'research stage': '🔎',
|
"research stage": "🔎",
|
||||||
'planning stage': '📝',
|
"planning stage": "📝",
|
||||||
'implementation stage': '🔧', # Changed from 🛠️ to prevent wrapping
|
"implementation stage": "🔧", # Changed from 🛠️ to prevent wrapping
|
||||||
'task completed': '✅',
|
"task completed": "✅",
|
||||||
'debug stage': '🐛',
|
"debug stage": "🐛",
|
||||||
'testing stage': '🧪',
|
"testing stage": "🧪",
|
||||||
'research subtasks': '📚',
|
"research subtasks": "📚",
|
||||||
'skipping implementation stage': '⏭️'
|
"skipping implementation stage": "⏭️",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Format stage name to Title Case and normalize for mapping lookup
|
# Format stage name to Title Case and normalize for mapping lookup
|
||||||
|
|
@ -27,12 +28,13 @@ def print_stage_header(stage: str) -> None:
|
||||||
stage_key = stage.lower()
|
stage_key = stage.lower()
|
||||||
|
|
||||||
# Get appropriate icon with fallback
|
# Get appropriate icon with fallback
|
||||||
icon = icons.get(stage_key, '🚀')
|
icon = icons.get(stage_key, "🚀")
|
||||||
|
|
||||||
# Create styled panel with icon
|
# Create styled panel with icon
|
||||||
panel_content = f"{icon} {stage_title}"
|
panel_content = f"{icon} {stage_title}"
|
||||||
console.print(Panel(panel_content, style="green bold", padding=0))
|
console.print(Panel(panel_content, style="green bold", padding=0))
|
||||||
|
|
||||||
|
|
||||||
def print_task_header(task: str) -> None:
|
def print_task_header(task: str) -> None:
|
||||||
"""Print a task header with yellow styling and wrench emoji. Content is rendered as Markdown.
|
"""Print a task header with yellow styling and wrench emoji. Content is rendered as Markdown.
|
||||||
|
|
||||||
|
|
@ -41,6 +43,7 @@ def print_task_header(task: str) -> None:
|
||||||
"""
|
"""
|
||||||
console.print(Panel(Markdown(task), title="🔧 Task", border_style="yellow bold"))
|
console.print(Panel(Markdown(task), title="🔧 Task", border_style="yellow bold"))
|
||||||
|
|
||||||
|
|
||||||
def print_error(message: str) -> None:
|
def print_error(message: str) -> None:
|
||||||
"""Print an error message in a red-bordered panel with warning emoji.
|
"""Print an error message in a red-bordered panel with warning emoji.
|
||||||
|
|
||||||
|
|
@ -49,6 +52,7 @@ def print_error(message: str) -> None:
|
||||||
"""
|
"""
|
||||||
console.print(Panel(Markdown(message), title="Error", border_style="red bold"))
|
console.print(Panel(Markdown(message), title="Error", border_style="red bold"))
|
||||||
|
|
||||||
|
|
||||||
def print_interrupt(message: str) -> None:
|
def print_interrupt(message: str) -> None:
|
||||||
"""Print an interrupt message in a yellow-bordered panel with stop emoji.
|
"""Print an interrupt message in a yellow-bordered panel with stop emoji.
|
||||||
|
|
||||||
|
|
@ -56,5 +60,6 @@ def print_interrupt(message: str) -> None:
|
||||||
message: The interrupt message to display (supports Markdown formatting)
|
message: The interrupt message to display (supports Markdown formatting)
|
||||||
"""
|
"""
|
||||||
print() # Add spacing for ^C
|
print() # Add spacing for ^C
|
||||||
console.print(Panel(Markdown(message), title="⛔ Interrupt", border_style="yellow bold"))
|
console.print(
|
||||||
|
Panel(Markdown(message), title="⛔ Interrupt", border_style="yellow bold")
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,42 @@
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from rich.console import Console
|
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
from langchain_core.messages import AIMessage
|
from langchain_core.messages import AIMessage
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
# Import shared console instance
|
# Import shared console instance
|
||||||
from .formatting import console
|
from .formatting import console
|
||||||
|
|
||||||
|
|
||||||
def print_agent_output(chunk: Dict[str, Any]) -> None:
|
def print_agent_output(chunk: Dict[str, Any]) -> None:
|
||||||
"""Print only the agent's message content, not tool calls.
|
"""Print only the agent's message content, not tool calls.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
chunk: A dictionary containing agent or tool messages
|
chunk: A dictionary containing agent or tool messages
|
||||||
"""
|
"""
|
||||||
if 'agent' in chunk and 'messages' in chunk['agent']:
|
if "agent" in chunk and "messages" in chunk["agent"]:
|
||||||
messages = chunk['agent']['messages']
|
messages = chunk["agent"]["messages"]
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if isinstance(msg, AIMessage):
|
if isinstance(msg, AIMessage):
|
||||||
# Handle text content
|
# Handle text content
|
||||||
if isinstance(msg.content, list):
|
if isinstance(msg.content, list):
|
||||||
for content in msg.content:
|
for content in msg.content:
|
||||||
if content['type'] == 'text' and content['text'].strip():
|
if content["type"] == "text" and content["text"].strip():
|
||||||
console.print(Panel(Markdown(content['text']), title="🤖 Assistant"))
|
console.print(
|
||||||
|
Panel(Markdown(content["text"]), title="🤖 Assistant")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if msg.content.strip():
|
if msg.content.strip():
|
||||||
console.print(Panel(Markdown(msg.content.strip()), title="🤖 Assistant"))
|
console.print(
|
||||||
elif 'tools' in chunk and 'messages' in chunk['tools']:
|
Panel(Markdown(msg.content.strip()), title="🤖 Assistant")
|
||||||
for msg in chunk['tools']['messages']:
|
)
|
||||||
if msg.status == 'error' and msg.content:
|
elif "tools" in chunk and "messages" in chunk["tools"]:
|
||||||
console.print(Panel(Markdown(msg.content.strip()), title="❌ Tool Error", border_style="red bold"))
|
for msg in chunk["tools"]["messages"]:
|
||||||
|
if msg.status == "error" and msg.content:
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
Markdown(msg.content.strip()),
|
||||||
|
title="❌ Tool Error",
|
||||||
|
border_style="red bold",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
"""Module for checking system dependencies required by RA.Aid."""
|
"""Module for checking system dependencies required by RA.Aid."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from ra_aid import print_error
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from ra_aid import print_error
|
||||||
|
|
||||||
|
|
||||||
class Dependency(ABC):
|
class Dependency(ABC):
|
||||||
"""Base class for system dependencies."""
|
"""Base class for system dependencies."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def check(self):
|
def check(self):
|
||||||
"""Check if the dependency is installed."""
|
"""Check if the dependency is installed."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RipGrepDependency(Dependency):
|
class RipGrepDependency(Dependency):
|
||||||
"""Dependency checker for ripgrep."""
|
"""Dependency checker for ripgrep."""
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
"""Check if ripgrep is installed."""
|
"""Check if ripgrep is installed."""
|
||||||
result = os.system('rg --version > /dev/null 2>&1')
|
result = os.system("rg --version > /dev/null 2>&1")
|
||||||
if result != 0:
|
if result != 0:
|
||||||
print_error("Required dependency 'ripgrep' is not installed.")
|
print_error("Required dependency 'ripgrep' is not installed.")
|
||||||
print("Please install ripgrep:")
|
print("Please install ripgrep:")
|
||||||
|
|
@ -25,6 +31,7 @@ class RipGrepDependency(Dependency):
|
||||||
print(" - Other: https://github.com/BurntSushi/ripgrep#installation")
|
print(" - Other: https://github.com/BurntSushi/ripgrep#installation")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def check_dependencies():
|
def check_dependencies():
|
||||||
"""Check if required system dependencies are installed."""
|
"""Check if required system dependencies are installed."""
|
||||||
dependencies = [RipGrepDependency()] # Create instances
|
dependencies = [RipGrepDependency()] # Create instances
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,10 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from typing import Any, List
|
||||||
from typing import Tuple, List, Any
|
|
||||||
|
|
||||||
from ra_aid import print_error
|
|
||||||
from ra_aid.provider_strategy import ProviderFactory, ValidationResult
|
from ra_aid.provider_strategy import ProviderFactory, ValidationResult
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ValidationResult:
|
|
||||||
"""Result of validation."""
|
|
||||||
valid: bool
|
|
||||||
missing_vars: List[str]
|
|
||||||
|
|
||||||
def validate_provider(provider: str) -> ValidationResult:
|
def validate_provider(provider: str) -> ValidationResult:
|
||||||
"""Validate provider configuration."""
|
"""Validate provider configuration."""
|
||||||
|
|
@ -20,9 +13,12 @@ def validate_provider(provider: str) -> ValidationResult:
|
||||||
return ValidationResult(valid=False, missing_vars=["No provider specified"])
|
return ValidationResult(valid=False, missing_vars=["No provider specified"])
|
||||||
strategy = ProviderFactory.create(provider)
|
strategy = ProviderFactory.create(provider)
|
||||||
if not strategy:
|
if not strategy:
|
||||||
return ValidationResult(valid=False, missing_vars=[f"Unknown provider: {provider}"])
|
return ValidationResult(
|
||||||
|
valid=False, missing_vars=[f"Unknown provider: {provider}"]
|
||||||
|
)
|
||||||
return strategy.validate()
|
return strategy.validate()
|
||||||
|
|
||||||
|
|
||||||
def copy_base_to_expert_vars(base_provider: str, expert_provider: str) -> None:
|
def copy_base_to_expert_vars(base_provider: str, expert_provider: str) -> None:
|
||||||
"""Copy base provider environment variables to expert provider if not set.
|
"""Copy base provider environment variables to expert provider if not set.
|
||||||
|
|
||||||
|
|
@ -32,28 +28,24 @@ def copy_base_to_expert_vars(base_provider: str, expert_provider: str) -> None:
|
||||||
"""
|
"""
|
||||||
# Map of base to expert environment variables for each provider
|
# Map of base to expert environment variables for each provider
|
||||||
provider_vars = {
|
provider_vars = {
|
||||||
'openai': {
|
"openai": {
|
||||||
'OPENAI_API_KEY': 'EXPERT_OPENAI_API_KEY',
|
"OPENAI_API_KEY": "EXPERT_OPENAI_API_KEY",
|
||||||
'OPENAI_API_BASE': 'EXPERT_OPENAI_API_BASE'
|
"OPENAI_API_BASE": "EXPERT_OPENAI_API_BASE",
|
||||||
},
|
},
|
||||||
'openai-compatible': {
|
"openai-compatible": {
|
||||||
'OPENAI_API_KEY': 'EXPERT_OPENAI_API_KEY',
|
"OPENAI_API_KEY": "EXPERT_OPENAI_API_KEY",
|
||||||
'OPENAI_API_BASE': 'EXPERT_OPENAI_API_BASE'
|
"OPENAI_API_BASE": "EXPERT_OPENAI_API_BASE",
|
||||||
},
|
},
|
||||||
'anthropic': {
|
"anthropic": {
|
||||||
'ANTHROPIC_API_KEY': 'EXPERT_ANTHROPIC_API_KEY',
|
"ANTHROPIC_API_KEY": "EXPERT_ANTHROPIC_API_KEY",
|
||||||
'ANTHROPIC_MODEL': 'EXPERT_ANTHROPIC_MODEL'
|
"ANTHROPIC_MODEL": "EXPERT_ANTHROPIC_MODEL",
|
||||||
},
|
},
|
||||||
'openrouter': {
|
"openrouter": {"OPENROUTER_API_KEY": "EXPERT_OPENROUTER_API_KEY"},
|
||||||
'OPENROUTER_API_KEY': 'EXPERT_OPENROUTER_API_KEY'
|
"gemini": {
|
||||||
|
"GEMINI_API_KEY": "EXPERT_GEMINI_API_KEY",
|
||||||
|
"GEMINI_MODEL": "EXPERT_GEMINI_MODEL",
|
||||||
},
|
},
|
||||||
'gemini': {
|
"deepseek": {"DEEPSEEK_API_KEY": "EXPERT_DEEPSEEK_API_KEY"},
|
||||||
'GEMINI_API_KEY': 'EXPERT_GEMINI_API_KEY',
|
|
||||||
'GEMINI_MODEL': 'EXPERT_GEMINI_MODEL'
|
|
||||||
},
|
|
||||||
'deepseek': {
|
|
||||||
'DEEPSEEK_API_KEY': 'EXPERT_DEEPSEEK_API_KEY'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the variables to copy based on the expert provider
|
# Get the variables to copy based on the expert provider
|
||||||
|
|
@ -63,6 +55,7 @@ def copy_base_to_expert_vars(base_provider: str, expert_provider: str) -> None:
|
||||||
if not os.environ.get(expert_var) and os.environ.get(base_var):
|
if not os.environ.get(expert_var) and os.environ.get(base_var):
|
||||||
os.environ[expert_var] = os.environ[base_var]
|
os.environ[expert_var] = os.environ[base_var]
|
||||||
|
|
||||||
|
|
||||||
def validate_expert_provider(provider: str) -> ValidationResult:
|
def validate_expert_provider(provider: str) -> ValidationResult:
|
||||||
"""Validate expert provider configuration with fallback."""
|
"""Validate expert provider configuration with fallback."""
|
||||||
if not provider:
|
if not provider:
|
||||||
|
|
@ -70,7 +63,9 @@ def validate_expert_provider(provider: str) -> ValidationResult:
|
||||||
|
|
||||||
strategy = ProviderFactory.create(provider)
|
strategy = ProviderFactory.create(provider)
|
||||||
if not strategy:
|
if not strategy:
|
||||||
return ValidationResult(valid=False, missing_vars=[f"Unknown expert provider: {provider}"])
|
return ValidationResult(
|
||||||
|
valid=False, missing_vars=[f"Unknown expert provider: {provider}"]
|
||||||
|
)
|
||||||
|
|
||||||
# Copy base vars to expert vars for fallback
|
# Copy base vars to expert vars for fallback
|
||||||
copy_base_to_expert_vars(provider, provider)
|
copy_base_to_expert_vars(provider, provider)
|
||||||
|
|
@ -87,20 +82,25 @@ def validate_expert_provider(provider: str) -> ValidationResult:
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
||||||
def validate_web_research() -> ValidationResult:
|
def validate_web_research() -> ValidationResult:
|
||||||
"""Validate web research configuration."""
|
"""Validate web research configuration."""
|
||||||
key = "TAVILY_API_KEY"
|
key = "TAVILY_API_KEY"
|
||||||
return ValidationResult(
|
return ValidationResult(
|
||||||
valid=bool(os.environ.get(key)),
|
valid=bool(os.environ.get(key)),
|
||||||
missing_vars=[] if os.environ.get(key) else [f"{key} environment variable is not set"]
|
missing_vars=[]
|
||||||
|
if os.environ.get(key)
|
||||||
|
else [f"{key} environment variable is not set"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_missing_dependencies(missing_vars: List[str]) -> None:
|
def print_missing_dependencies(missing_vars: List[str]) -> None:
|
||||||
"""Print missing dependencies and exit."""
|
"""Print missing dependencies and exit."""
|
||||||
for var in missing_vars:
|
for var in missing_vars:
|
||||||
print(f"Error: {var}", file=sys.stderr)
|
print(f"Error: {var}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def validate_research_only_provider(args: Any) -> None:
|
def validate_research_only_provider(args: Any) -> None:
|
||||||
"""Validate provider and model for research-only mode.
|
"""Validate provider and model for research-only mode.
|
||||||
|
|
||||||
|
|
@ -111,16 +111,17 @@ def validate_research_only_provider(args: Any) -> None:
|
||||||
SystemExit: If provider or model validation fails
|
SystemExit: If provider or model validation fails
|
||||||
"""
|
"""
|
||||||
# Get provider from args
|
# Get provider from args
|
||||||
provider = args.provider if args and hasattr(args, 'provider') else None
|
provider = args.provider if args and hasattr(args, "provider") else None
|
||||||
if not provider:
|
if not provider:
|
||||||
sys.exit("No provider specified")
|
sys.exit("No provider specified")
|
||||||
|
|
||||||
# For non-Anthropic providers in research-only mode, model must be specified
|
# For non-Anthropic providers in research-only mode, model must be specified
|
||||||
if provider != 'anthropic':
|
if provider != "anthropic":
|
||||||
model = args.model if hasattr(args, 'model') and args.model else None
|
model = args.model if hasattr(args, "model") and args.model else None
|
||||||
if not model:
|
if not model:
|
||||||
sys.exit("Model is required for non-Anthropic providers")
|
sys.exit("Model is required for non-Anthropic providers")
|
||||||
|
|
||||||
|
|
||||||
def validate_research_only(args: Any) -> tuple[bool, list[str], bool, list[str]]:
|
def validate_research_only(args: Any) -> tuple[bool, list[str], bool, list[str]]:
|
||||||
"""Validate environment variables for research-only mode.
|
"""Validate environment variables for research-only mode.
|
||||||
|
|
||||||
|
|
@ -141,14 +142,15 @@ def validate_research_only(args: Any) -> tuple[bool, list[str], bool, list[str]]
|
||||||
web_research_missing = []
|
web_research_missing = []
|
||||||
|
|
||||||
# Validate web research dependencies
|
# Validate web research dependencies
|
||||||
tavily_key = os.environ.get('TAVILY_API_KEY')
|
tavily_key = os.environ.get("TAVILY_API_KEY")
|
||||||
if not tavily_key:
|
if not tavily_key:
|
||||||
web_research_missing.append('TAVILY_API_KEY environment variable is not set')
|
web_research_missing.append("TAVILY_API_KEY environment variable is not set")
|
||||||
else:
|
else:
|
||||||
web_research_enabled = True
|
web_research_enabled = True
|
||||||
|
|
||||||
return expert_enabled, expert_missing, web_research_enabled, web_research_missing
|
return expert_enabled, expert_missing, web_research_enabled, web_research_missing
|
||||||
|
|
||||||
|
|
||||||
def validate_environment(args: Any) -> tuple[bool, list[str], bool, list[str]]:
|
def validate_environment(args: Any) -> tuple[bool, list[str], bool, list[str]]:
|
||||||
"""Validate environment variables for providers and web research tools.
|
"""Validate environment variables for providers and web research tools.
|
||||||
|
|
||||||
|
|
@ -163,9 +165,9 @@ def validate_environment(args: Any) -> tuple[bool, list[str], bool, list[str]]:
|
||||||
- web_research_missing: List of missing web research dependencies
|
- web_research_missing: List of missing web research dependencies
|
||||||
"""
|
"""
|
||||||
# For research-only mode, use separate validation
|
# For research-only mode, use separate validation
|
||||||
if hasattr(args, 'research_only') and args.research_only:
|
if hasattr(args, "research_only") and args.research_only:
|
||||||
# Only validate provider and model when testing provider validation
|
# Only validate provider and model when testing provider validation
|
||||||
if hasattr(args, 'model') and args.model is None:
|
if hasattr(args, "model") and args.model is None:
|
||||||
validate_research_only_provider(args)
|
validate_research_only_provider(args)
|
||||||
return validate_research_only(args)
|
return validate_research_only(args)
|
||||||
|
|
||||||
|
|
@ -176,7 +178,7 @@ def validate_environment(args: Any) -> tuple[bool, list[str], bool, list[str]]:
|
||||||
web_research_missing = []
|
web_research_missing = []
|
||||||
|
|
||||||
# Get provider from args
|
# Get provider from args
|
||||||
provider = args.provider if args and hasattr(args, 'provider') else None
|
provider = args.provider if args and hasattr(args, "provider") else None
|
||||||
if not provider:
|
if not provider:
|
||||||
sys.exit("No provider specified")
|
sys.exit("No provider specified")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
"""Custom exceptions for RA.Aid."""
|
"""Custom exceptions for RA.Aid."""
|
||||||
|
|
||||||
|
|
||||||
class AgentInterrupt(Exception):
|
class AgentInterrupt(Exception):
|
||||||
"""Exception raised when an agent's execution is interrupted.
|
"""Exception raised when an agent's execution is interrupted.
|
||||||
|
|
||||||
This exception is used for internal agent interruption handling,
|
This exception is used for internal agent interruption handling,
|
||||||
separate from KeyboardInterrupt which is reserved for top-level handling.
|
separate from KeyboardInterrupt which is reserved for top-level handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,4 +17,5 @@ class ToolExecutionError(Exception):
|
||||||
This exception is used to distinguish tool execution failures
|
This exception is used to distinguish tool execution failures
|
||||||
from other types of errors in the agent system.
|
from other types of errors in the agent system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,25 @@ from typing import List, Optional, Tuple
|
||||||
|
|
||||||
class FileListerError(Exception):
|
class FileListerError(Exception):
|
||||||
"""Base exception for file listing related errors."""
|
"""Base exception for file listing related errors."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GitCommandError(FileListerError):
|
class GitCommandError(FileListerError):
|
||||||
"""Raised when a git command fails."""
|
"""Raised when a git command fails."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DirectoryNotFoundError(FileListerError):
|
class DirectoryNotFoundError(FileListerError):
|
||||||
"""Raised when the specified directory does not exist."""
|
"""Raised when the specified directory does not exist."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DirectoryAccessError(FileListerError):
|
class DirectoryAccessError(FileListerError):
|
||||||
"""Raised when the directory cannot be accessed due to permissions."""
|
"""Raised when the directory cannot be accessed due to permissions."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -51,7 +55,7 @@ def is_git_repo(directory: str) -> bool:
|
||||||
["git", "rev-parse", "--git-dir"],
|
["git", "rev-parse", "--git-dir"],
|
||||||
cwd=str(path),
|
cwd=str(path),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True
|
text=True,
|
||||||
)
|
)
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
@ -65,7 +69,9 @@ def is_git_repo(directory: str) -> bool:
|
||||||
raise FileListerError(f"Error checking git repository: {e}")
|
raise FileListerError(f"Error checking git repository: {e}")
|
||||||
|
|
||||||
|
|
||||||
def get_file_listing(directory: str, limit: Optional[int] = None) -> Tuple[List[str], int]:
|
def get_file_listing(
|
||||||
|
directory: str, limit: Optional[int] = None
|
||||||
|
) -> Tuple[List[str], int]:
|
||||||
"""
|
"""
|
||||||
Get a list of tracked files in a git repository.
|
Get a list of tracked files in a git repository.
|
||||||
|
|
||||||
|
|
@ -99,15 +105,11 @@ def get_file_listing(directory: str, limit: Optional[int] = None) -> Tuple[List[
|
||||||
cwd=directory,
|
cwd=directory,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=True
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process the output
|
# Process the output
|
||||||
files = [
|
files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
||||||
line.strip()
|
|
||||||
for line in result.stdout.splitlines()
|
|
||||||
if line.strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
# Deduplicate and sort for consistency
|
# Deduplicate and sort for consistency
|
||||||
files = list(dict.fromkeys(files)) # Remove duplicates while preserving order
|
files = list(dict.fromkeys(files)) # Remove duplicates while preserving order
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import os
|
import os
|
||||||
from typing import Optional, Dict, Any
|
from typing import Any, Dict, Optional
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langchain_anthropic import ChatAnthropic
|
from langchain_anthropic import ChatAnthropic
|
||||||
from langchain_core.language_models import BaseChatModel
|
from langchain_core.language_models import BaseChatModel
|
||||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
from ra_aid.chat_models.deepseek_chat import ChatDeepseekReasoner
|
from ra_aid.chat_models.deepseek_chat import ChatDeepseekReasoner
|
||||||
from ra_aid.logging_config import get_logger
|
from ra_aid.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_env_var(name: str, expert: bool = False) -> Optional[str]:
|
def get_env_var(name: str, expert: bool = False) -> Optional[str]:
|
||||||
"""Get environment variable with optional expert prefix and fallback."""
|
"""Get environment variable with optional expert prefix and fallback."""
|
||||||
prefix = "EXPERT_" if expert else ""
|
prefix = "EXPERT_" if expert else ""
|
||||||
|
|
@ -129,7 +132,7 @@ def create_llm_client(
|
||||||
provider,
|
provider,
|
||||||
model_name,
|
model_name,
|
||||||
temperature,
|
temperature,
|
||||||
is_expert
|
is_expert,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle temperature settings
|
# Handle temperature settings
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(verbose: bool = False) -> None:
|
def setup_logging(verbose: bool = False) -> None:
|
||||||
logger = logging.getLogger("ra_aid")
|
logger = logging.getLogger("ra_aid")
|
||||||
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||||
|
|
@ -14,5 +15,6 @@ def setup_logging(verbose: bool = False) -> None:
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
||||||
return logging.getLogger(f"ra_aid.{name}" if name else "ra_aid")
|
return logging.getLogger(f"ra_aid.{name}" if name else "ra_aid")
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ List of model tokens
|
||||||
DEFAULT_TOKEN_LIMIT = 100000
|
DEFAULT_TOKEN_LIMIT = 100000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
models_tokens = {
|
models_tokens = {
|
||||||
"openai": {
|
"openai": {
|
||||||
"gpt-3.5-turbo-0125": 16385,
|
"gpt-3.5-turbo-0125": 16385,
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ Module for running interactive subprocesses with output capture.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
# Add macOS detection
|
# Add macOS detection
|
||||||
IS_MACOS = os.uname().sysname == "Darwin"
|
IS_MACOS = os.uname().sysname == "Darwin"
|
||||||
|
|
||||||
|
|
||||||
def run_interactive_command(cmd: List[str]) -> Tuple[bytes, int]:
|
def run_interactive_command(cmd: List[str]) -> Tuple[bytes, int]:
|
||||||
"""
|
"""
|
||||||
Runs an interactive command with a pseudo-tty, capturing combined output.
|
Runs an interactive command with a pseudo-tty, capturing combined output.
|
||||||
|
|
@ -45,7 +45,7 @@ def run_interactive_command(cmd: List[str]) -> Tuple[bytes, int]:
|
||||||
retcode_file.close()
|
retcode_file.close()
|
||||||
|
|
||||||
# Quote arguments for safety
|
# Quote arguments for safety
|
||||||
quoted_cmd = ' '.join(shlex.quote(c) for c in cmd)
|
quoted_cmd = " ".join(shlex.quote(c) for c in cmd)
|
||||||
# Use script to capture output with TTY and save return code
|
# Use script to capture output with TTY and save return code
|
||||||
shell_cmd = f"{quoted_cmd}; echo $? > {shlex.quote(retcode_path)}"
|
shell_cmd = f"{quoted_cmd}; echo $? > {shlex.quote(retcode_path)}"
|
||||||
|
|
||||||
|
|
@ -56,22 +56,24 @@ def run_interactive_command(cmd: List[str]) -> Tuple[bytes, int]:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Disable pagers by setting environment variables
|
# Disable pagers by setting environment variables
|
||||||
os.environ['GIT_PAGER'] = ''
|
os.environ["GIT_PAGER"] = ""
|
||||||
os.environ['PAGER'] = ''
|
os.environ["PAGER"] = ""
|
||||||
|
|
||||||
# Run command with script for TTY and output capture
|
# Run command with script for TTY and output capture
|
||||||
if IS_MACOS:
|
if IS_MACOS:
|
||||||
os.system(f"script -q {shlex.quote(output_path)} {shell_cmd}")
|
os.system(f"script -q {shlex.quote(output_path)} {shell_cmd}")
|
||||||
else:
|
else:
|
||||||
os.system(f"script -q -c {shlex.quote(shell_cmd)} {shlex.quote(output_path)}")
|
os.system(
|
||||||
|
f"script -q -c {shlex.quote(shell_cmd)} {shlex.quote(output_path)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Read and clean the output
|
# Read and clean the output
|
||||||
with open(output_path, "rb") as f:
|
with open(output_path, "rb") as f:
|
||||||
output = f.read()
|
output = f.read()
|
||||||
|
|
||||||
# Clean ANSI escape sequences and control characters
|
# Clean ANSI escape sequences and control characters
|
||||||
output = re.sub(rb'\x1b\[[0-9;]*[a-zA-Z]', b'', output) # ANSI escape sequences
|
output = re.sub(rb"\x1b\[[0-9;]*[a-zA-Z]", b"", output) # ANSI escape sequences
|
||||||
output = re.sub(rb'[\x00-\x08\x0b\x0c\x0e-\x1f]', b'', output) # Control chars
|
output = re.sub(rb"[\x00-\x08\x0b\x0c\x0e-\x1f]", b"", output) # Control chars
|
||||||
|
|
||||||
# Get the return code
|
# Get the return code
|
||||||
with open(retcode_path, "r") as f:
|
with open(retcode_path, "r") as f:
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
__all__ = ['ProjectInfo', 'ProjectInfoError', 'get_project_info', 'format_project_info', 'display_project_status']
|
__all__ = [
|
||||||
|
"ProjectInfo",
|
||||||
|
"ProjectInfoError",
|
||||||
|
"get_project_info",
|
||||||
|
"format_project_info",
|
||||||
|
"display_project_status",
|
||||||
|
]
|
||||||
|
|
||||||
from ra_aid.project_state import is_new_project, ProjectStateError
|
from ra_aid.file_listing import FileListerError, get_file_listing
|
||||||
from ra_aid.file_listing import get_file_listing, FileListerError
|
from ra_aid.project_state import ProjectStateError, is_new_project
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -21,6 +28,7 @@ class ProjectInfo:
|
||||||
files: List of tracked files in the project
|
files: List of tracked files in the project
|
||||||
total_files: Total number of tracked files (before any limit)
|
total_files: Total number of tracked files (before any limit)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
is_new: bool
|
is_new: bool
|
||||||
files: List[str]
|
files: List[str]
|
||||||
total_files: int
|
total_files: int
|
||||||
|
|
@ -28,6 +36,7 @@ class ProjectInfo:
|
||||||
|
|
||||||
class ProjectInfoError(Exception):
|
class ProjectInfoError(Exception):
|
||||||
"""Base exception for project info related errors."""
|
"""Base exception for project info related errors."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,13 +63,9 @@ def get_project_info(directory: str, file_limit: Optional[int] = None) -> Projec
|
||||||
# Get file listing
|
# Get file listing
|
||||||
files, total = get_file_listing(directory, limit=file_limit)
|
files, total = get_file_listing(directory, limit=file_limit)
|
||||||
|
|
||||||
return ProjectInfo(
|
return ProjectInfo(is_new=new_status, files=files, total_files=total)
|
||||||
is_new=new_status,
|
|
||||||
files=files,
|
|
||||||
total_files=total
|
|
||||||
)
|
|
||||||
|
|
||||||
except (ProjectStateError, FileListerError) as e:
|
except (ProjectStateError, FileListerError):
|
||||||
# Re-raise known errors
|
# Re-raise known errors
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -85,7 +90,11 @@ def format_project_info(info: ProjectInfo) -> str:
|
||||||
return f"Project Status: {status}\nTotal Files: 0\nFiles: None"
|
return f"Project Status: {status}\nTotal Files: 0\nFiles: None"
|
||||||
|
|
||||||
# Format file count with truncation notice if needed
|
# Format file count with truncation notice if needed
|
||||||
file_count = f"{len(info.files)} of {info.total_files}" if len(info.files) < info.total_files else str(info.total_files)
|
file_count = (
|
||||||
|
f"{len(info.files)} of {info.total_files}"
|
||||||
|
if len(info.files) < info.total_files
|
||||||
|
else str(info.total_files)
|
||||||
|
)
|
||||||
file_count_line = f"Total Files: {file_count}"
|
file_count_line = f"Total Files: {file_count}"
|
||||||
|
|
||||||
# Format file listing
|
# Format file listing
|
||||||
|
|
@ -93,7 +102,9 @@ def format_project_info(info: ProjectInfo) -> str:
|
||||||
|
|
||||||
# Add truncation notice if list was truncated
|
# Add truncation notice if list was truncated
|
||||||
if len(info.files) < info.total_files:
|
if len(info.files) < info.total_files:
|
||||||
files_section += f"\n[Note: Showing {len(info.files)} of {info.total_files} total files]"
|
files_section += (
|
||||||
|
f"\n[Note: Showing {len(info.files)} of {info.total_files} total files]"
|
||||||
|
)
|
||||||
|
|
||||||
return f"Project Status: {status}\n{file_count_line}\n{files_section}"
|
return f"Project Status: {status}\n{file_count_line}\n{files_section}"
|
||||||
|
|
||||||
|
|
@ -108,7 +119,11 @@ def display_project_status(info: ProjectInfo) -> None:
|
||||||
status = "**New/empty project**" if info.is_new else "**Existing project**"
|
status = "**New/empty project**" if info.is_new else "**Existing project**"
|
||||||
|
|
||||||
# Format file count (with truncation notice if needed)
|
# Format file count (with truncation notice if needed)
|
||||||
file_count = f"{len(info.files)} of {info.total_files}" if len(info.files) < info.total_files else str(info.total_files)
|
file_count = (
|
||||||
|
f"{len(info.files)} of {info.total_files}"
|
||||||
|
if len(info.files) < info.total_files
|
||||||
|
else str(info.total_files)
|
||||||
|
)
|
||||||
|
|
||||||
# Build status text with markdown
|
# Build status text with markdown
|
||||||
status_text = f"""
|
status_text = f"""
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,19 @@ from typing import Set
|
||||||
|
|
||||||
class ProjectStateError(Exception):
|
class ProjectStateError(Exception):
|
||||||
"""Base exception for project state related errors."""
|
"""Base exception for project state related errors."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DirectoryNotFoundError(ProjectStateError):
|
class DirectoryNotFoundError(ProjectStateError):
|
||||||
"""Raised when the specified directory does not exist."""
|
"""Raised when the specified directory does not exist."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DirectoryAccessError(ProjectStateError):
|
class DirectoryAccessError(ProjectStateError):
|
||||||
"""Raised when the directory cannot be accessed due to permissions."""
|
"""Raised when the directory cannot be accessed due to permissions."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -47,18 +50,18 @@ def is_new_project(directory: str) -> bool:
|
||||||
raise DirectoryNotFoundError(f"Path is not a directory: {directory}")
|
raise DirectoryNotFoundError(f"Path is not a directory: {directory}")
|
||||||
|
|
||||||
# Get all files/dirs in the directory, excluding contents of .git
|
# Get all files/dirs in the directory, excluding contents of .git
|
||||||
allowed_items: Set[str] = {'.git', '.gitignore'}
|
_allowed_items: Set[str] = {".git", ".gitignore"}
|
||||||
try:
|
try:
|
||||||
contents = set()
|
contents = set()
|
||||||
for item in path.iterdir():
|
for item in path.iterdir():
|
||||||
# Only consider top-level items
|
# Only consider top-level items
|
||||||
if item.name != '.git':
|
if item.name != ".git":
|
||||||
contents.add(item.name)
|
contents.add(item.name)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
raise DirectoryAccessError(f"Cannot access directory {directory}: {e}")
|
raise DirectoryAccessError(f"Cannot access directory {directory}: {e}")
|
||||||
|
|
||||||
# Directory is new if empty or only contains .gitignore
|
# Directory is new if empty or only contains .gitignore
|
||||||
return len(contents) == 0 or contents.issubset({'.gitignore'})
|
return len(contents) == 0 or contents.issubset({".gitignore"})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, ProjectStateError):
|
if isinstance(e, ProjectStateError):
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
"""Provider validation strategies."""
|
"""Provider validation strategies."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, List, Any
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ValidationResult:
|
class ValidationResult:
|
||||||
"""Result of validation."""
|
"""Result of validation."""
|
||||||
|
|
||||||
valid: bool
|
valid: bool
|
||||||
missing_vars: List[str]
|
missing_vars: List[str]
|
||||||
|
|
||||||
|
|
||||||
class ProviderStrategy(ABC):
|
class ProviderStrategy(ABC):
|
||||||
"""Abstract base class for provider validation strategies."""
|
"""Abstract base class for provider validation strategies."""
|
||||||
|
|
||||||
|
|
@ -20,6 +23,7 @@ class ProviderStrategy(ABC):
|
||||||
"""Validate provider environment variables."""
|
"""Validate provider environment variables."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OpenAIStrategy(ProviderStrategy):
|
class OpenAIStrategy(ProviderStrategy):
|
||||||
"""OpenAI provider validation strategy."""
|
"""OpenAI provider validation strategy."""
|
||||||
|
|
||||||
|
|
@ -28,41 +32,50 @@ class OpenAIStrategy(ProviderStrategy):
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
# Check if we're validating expert config
|
# Check if we're validating expert config
|
||||||
if args and hasattr(args, 'expert_provider') and args.expert_provider == 'openai':
|
if (
|
||||||
key = os.environ.get('EXPERT_OPENAI_API_KEY')
|
args
|
||||||
if not key or key == '':
|
and hasattr(args, "expert_provider")
|
||||||
|
and args.expert_provider == "openai"
|
||||||
|
):
|
||||||
|
key = os.environ.get("EXPERT_OPENAI_API_KEY")
|
||||||
|
if not key or key == "":
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
base_key = os.environ.get('OPENAI_API_KEY')
|
base_key = os.environ.get("OPENAI_API_KEY")
|
||||||
if base_key:
|
if base_key:
|
||||||
os.environ['EXPERT_OPENAI_API_KEY'] = base_key
|
os.environ["EXPERT_OPENAI_API_KEY"] = base_key
|
||||||
key = base_key
|
key = base_key
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('EXPERT_OPENAI_API_KEY environment variable is not set')
|
missing.append("EXPERT_OPENAI_API_KEY environment variable is not set")
|
||||||
|
|
||||||
# Check expert model only for research-only mode
|
# Check expert model only for research-only mode
|
||||||
if hasattr(args, 'research_only') and args.research_only:
|
if hasattr(args, "research_only") and args.research_only:
|
||||||
model = args.expert_model if hasattr(args, 'expert_model') else None
|
model = args.expert_model if hasattr(args, "expert_model") else None
|
||||||
if not model:
|
if not model:
|
||||||
model = os.environ.get('EXPERT_OPENAI_MODEL')
|
model = os.environ.get("EXPERT_OPENAI_MODEL")
|
||||||
if not model:
|
if not model:
|
||||||
model = os.environ.get('OPENAI_MODEL')
|
model = os.environ.get("OPENAI_MODEL")
|
||||||
if not model:
|
if not model:
|
||||||
missing.append('Model is required for OpenAI provider in research-only mode')
|
missing.append(
|
||||||
|
"Model is required for OpenAI provider in research-only mode"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
key = os.environ.get('OPENAI_API_KEY')
|
key = os.environ.get("OPENAI_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('OPENAI_API_KEY environment variable is not set')
|
missing.append("OPENAI_API_KEY environment variable is not set")
|
||||||
|
|
||||||
# Check model only for research-only mode
|
# Check model only for research-only mode
|
||||||
if hasattr(args, 'research_only') and args.research_only:
|
if hasattr(args, "research_only") and args.research_only:
|
||||||
model = args.model if hasattr(args, 'model') else None
|
model = args.model if hasattr(args, "model") else None
|
||||||
if not model:
|
if not model:
|
||||||
model = os.environ.get('OPENAI_MODEL')
|
model = os.environ.get("OPENAI_MODEL")
|
||||||
if not model:
|
if not model:
|
||||||
missing.append('Model is required for OpenAI provider in research-only mode')
|
missing.append(
|
||||||
|
"Model is required for OpenAI provider in research-only mode"
|
||||||
|
)
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
||||||
class OpenAICompatibleStrategy(ProviderStrategy):
|
class OpenAICompatibleStrategy(ProviderStrategy):
|
||||||
"""OpenAI-compatible provider validation strategy."""
|
"""OpenAI-compatible provider validation strategy."""
|
||||||
|
|
||||||
|
|
@ -71,84 +84,97 @@ class OpenAICompatibleStrategy(ProviderStrategy):
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
# Check if we're validating expert config
|
# Check if we're validating expert config
|
||||||
if args and hasattr(args, 'expert_provider') and args.expert_provider == 'openai-compatible':
|
if (
|
||||||
key = os.environ.get('EXPERT_OPENAI_API_KEY')
|
args
|
||||||
base = os.environ.get('EXPERT_OPENAI_API_BASE')
|
and hasattr(args, "expert_provider")
|
||||||
|
and args.expert_provider == "openai-compatible"
|
||||||
|
):
|
||||||
|
key = os.environ.get("EXPERT_OPENAI_API_KEY")
|
||||||
|
base = os.environ.get("EXPERT_OPENAI_API_BASE")
|
||||||
|
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
if not key or key == '':
|
if not key or key == "":
|
||||||
base_key = os.environ.get('OPENAI_API_KEY')
|
base_key = os.environ.get("OPENAI_API_KEY")
|
||||||
if base_key:
|
if base_key:
|
||||||
os.environ['EXPERT_OPENAI_API_KEY'] = base_key
|
os.environ["EXPERT_OPENAI_API_KEY"] = base_key
|
||||||
key = base_key
|
key = base_key
|
||||||
if not base or base == '':
|
if not base or base == "":
|
||||||
base_base = os.environ.get('OPENAI_API_BASE')
|
base_base = os.environ.get("OPENAI_API_BASE")
|
||||||
if base_base:
|
if base_base:
|
||||||
os.environ['EXPERT_OPENAI_API_BASE'] = base_base
|
os.environ["EXPERT_OPENAI_API_BASE"] = base_base
|
||||||
base = base_base
|
base = base_base
|
||||||
|
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('EXPERT_OPENAI_API_KEY environment variable is not set')
|
missing.append("EXPERT_OPENAI_API_KEY environment variable is not set")
|
||||||
if not base:
|
if not base:
|
||||||
missing.append('EXPERT_OPENAI_API_BASE environment variable is not set')
|
missing.append("EXPERT_OPENAI_API_BASE environment variable is not set")
|
||||||
|
|
||||||
# Check expert model only for research-only mode
|
# Check expert model only for research-only mode
|
||||||
if hasattr(args, 'research_only') and args.research_only:
|
if hasattr(args, "research_only") and args.research_only:
|
||||||
model = args.expert_model if hasattr(args, 'expert_model') else None
|
model = args.expert_model if hasattr(args, "expert_model") else None
|
||||||
if not model:
|
if not model:
|
||||||
model = os.environ.get('EXPERT_OPENAI_MODEL')
|
model = os.environ.get("EXPERT_OPENAI_MODEL")
|
||||||
if not model:
|
if not model:
|
||||||
model = os.environ.get('OPENAI_MODEL')
|
model = os.environ.get("OPENAI_MODEL")
|
||||||
if not model:
|
if not model:
|
||||||
missing.append('Model is required for OpenAI-compatible provider in research-only mode')
|
missing.append(
|
||||||
|
"Model is required for OpenAI-compatible provider in research-only mode"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
key = os.environ.get('OPENAI_API_KEY')
|
key = os.environ.get("OPENAI_API_KEY")
|
||||||
base = os.environ.get('OPENAI_API_BASE')
|
base = os.environ.get("OPENAI_API_BASE")
|
||||||
|
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('OPENAI_API_KEY environment variable is not set')
|
missing.append("OPENAI_API_KEY environment variable is not set")
|
||||||
if not base:
|
if not base:
|
||||||
missing.append('OPENAI_API_BASE environment variable is not set')
|
missing.append("OPENAI_API_BASE environment variable is not set")
|
||||||
|
|
||||||
# Check model only for research-only mode
|
# Check model only for research-only mode
|
||||||
if hasattr(args, 'research_only') and args.research_only:
|
if hasattr(args, "research_only") and args.research_only:
|
||||||
model = args.model if hasattr(args, 'model') else None
|
model = args.model if hasattr(args, "model") else None
|
||||||
if not model:
|
if not model:
|
||||||
model = os.environ.get('OPENAI_MODEL')
|
model = os.environ.get("OPENAI_MODEL")
|
||||||
if not model:
|
if not model:
|
||||||
missing.append('Model is required for OpenAI-compatible provider in research-only mode')
|
missing.append(
|
||||||
|
"Model is required for OpenAI-compatible provider in research-only mode"
|
||||||
|
)
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
||||||
class AnthropicStrategy(ProviderStrategy):
|
class AnthropicStrategy(ProviderStrategy):
|
||||||
"""Anthropic provider validation strategy."""
|
"""Anthropic provider validation strategy."""
|
||||||
|
|
||||||
VALID_MODELS = [
|
VALID_MODELS = ["claude-"]
|
||||||
"claude-"
|
|
||||||
]
|
|
||||||
|
|
||||||
def validate(self, args: Optional[Any] = None) -> ValidationResult:
|
def validate(self, args: Optional[Any] = None) -> ValidationResult:
|
||||||
"""Validate Anthropic environment variables and model."""
|
"""Validate Anthropic environment variables and model."""
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
# Check if we're validating expert config
|
# Check if we're validating expert config
|
||||||
is_expert = args and hasattr(args, 'expert_provider') and args.expert_provider == 'anthropic'
|
is_expert = (
|
||||||
|
args
|
||||||
|
and hasattr(args, "expert_provider")
|
||||||
|
and args.expert_provider == "anthropic"
|
||||||
|
)
|
||||||
|
|
||||||
# Check API key
|
# Check API key
|
||||||
if is_expert:
|
if is_expert:
|
||||||
key = os.environ.get('EXPERT_ANTHROPIC_API_KEY')
|
key = os.environ.get("EXPERT_ANTHROPIC_API_KEY")
|
||||||
if not key or key == '':
|
if not key or key == "":
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
base_key = os.environ.get('ANTHROPIC_API_KEY')
|
base_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||||
if base_key:
|
if base_key:
|
||||||
os.environ['EXPERT_ANTHROPIC_API_KEY'] = base_key
|
os.environ["EXPERT_ANTHROPIC_API_KEY"] = base_key
|
||||||
key = base_key
|
key = base_key
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('EXPERT_ANTHROPIC_API_KEY environment variable is not set')
|
missing.append(
|
||||||
|
"EXPERT_ANTHROPIC_API_KEY environment variable is not set"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
key = os.environ.get('ANTHROPIC_API_KEY')
|
key = os.environ.get("ANTHROPIC_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('ANTHROPIC_API_KEY environment variable is not set')
|
missing.append("ANTHROPIC_API_KEY environment variable is not set")
|
||||||
|
|
||||||
# Check model
|
# Check model
|
||||||
model_matched = False
|
model_matched = False
|
||||||
|
|
@ -156,25 +182,25 @@ class AnthropicStrategy(ProviderStrategy):
|
||||||
|
|
||||||
# First check command line argument
|
# First check command line argument
|
||||||
if is_expert:
|
if is_expert:
|
||||||
if hasattr(args, 'expert_model') and args.expert_model:
|
if hasattr(args, "expert_model") and args.expert_model:
|
||||||
model_to_check = args.expert_model
|
model_to_check = args.expert_model
|
||||||
else:
|
else:
|
||||||
# If no expert model, check environment variable
|
# If no expert model, check environment variable
|
||||||
model_to_check = os.environ.get('EXPERT_ANTHROPIC_MODEL')
|
model_to_check = os.environ.get("EXPERT_ANTHROPIC_MODEL")
|
||||||
if not model_to_check or model_to_check == '':
|
if not model_to_check or model_to_check == "":
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
base_model = os.environ.get('ANTHROPIC_MODEL')
|
base_model = os.environ.get("ANTHROPIC_MODEL")
|
||||||
if base_model:
|
if base_model:
|
||||||
os.environ['EXPERT_ANTHROPIC_MODEL'] = base_model
|
os.environ["EXPERT_ANTHROPIC_MODEL"] = base_model
|
||||||
model_to_check = base_model
|
model_to_check = base_model
|
||||||
else:
|
else:
|
||||||
if hasattr(args, 'model') and args.model:
|
if hasattr(args, "model") and args.model:
|
||||||
model_to_check = args.model
|
model_to_check = args.model
|
||||||
else:
|
else:
|
||||||
model_to_check = os.environ.get('ANTHROPIC_MODEL')
|
model_to_check = os.environ.get("ANTHROPIC_MODEL")
|
||||||
|
|
||||||
if not model_to_check:
|
if not model_to_check:
|
||||||
missing.append('ANTHROPIC_MODEL environment variable is not set')
|
missing.append("ANTHROPIC_MODEL environment variable is not set")
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
# Validate model format
|
# Validate model format
|
||||||
|
|
@ -184,10 +210,13 @@ class AnthropicStrategy(ProviderStrategy):
|
||||||
break
|
break
|
||||||
|
|
||||||
if not model_matched:
|
if not model_matched:
|
||||||
missing.append(f'Invalid Anthropic model: {model_to_check}. Must match one of these patterns: {", ".join(self.VALID_MODELS)}')
|
missing.append(
|
||||||
|
f'Invalid Anthropic model: {model_to_check}. Must match one of these patterns: {", ".join(self.VALID_MODELS)}'
|
||||||
|
)
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
||||||
class OpenRouterStrategy(ProviderStrategy):
|
class OpenRouterStrategy(ProviderStrategy):
|
||||||
"""OpenRouter provider validation strategy."""
|
"""OpenRouter provider validation strategy."""
|
||||||
|
|
||||||
|
|
@ -196,23 +225,30 @@ class OpenRouterStrategy(ProviderStrategy):
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
# Check if we're validating expert config
|
# Check if we're validating expert config
|
||||||
if args and hasattr(args, 'expert_provider') and args.expert_provider == 'openrouter':
|
if (
|
||||||
key = os.environ.get('EXPERT_OPENROUTER_API_KEY')
|
args
|
||||||
if not key or key == '':
|
and hasattr(args, "expert_provider")
|
||||||
|
and args.expert_provider == "openrouter"
|
||||||
|
):
|
||||||
|
key = os.environ.get("EXPERT_OPENROUTER_API_KEY")
|
||||||
|
if not key or key == "":
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
base_key = os.environ.get('OPENROUTER_API_KEY')
|
base_key = os.environ.get("OPENROUTER_API_KEY")
|
||||||
if base_key:
|
if base_key:
|
||||||
os.environ['EXPERT_OPENROUTER_API_KEY'] = base_key
|
os.environ["EXPERT_OPENROUTER_API_KEY"] = base_key
|
||||||
key = base_key
|
key = base_key
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('EXPERT_OPENROUTER_API_KEY environment variable is not set')
|
missing.append(
|
||||||
|
"EXPERT_OPENROUTER_API_KEY environment variable is not set"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
key = os.environ.get('OPENROUTER_API_KEY')
|
key = os.environ.get("OPENROUTER_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('OPENROUTER_API_KEY environment variable is not set')
|
missing.append("OPENROUTER_API_KEY environment variable is not set")
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
||||||
class GeminiStrategy(ProviderStrategy):
|
class GeminiStrategy(ProviderStrategy):
|
||||||
"""Gemini provider validation strategy."""
|
"""Gemini provider validation strategy."""
|
||||||
|
|
||||||
|
|
@ -221,20 +257,24 @@ class GeminiStrategy(ProviderStrategy):
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
# Check if we're validating expert config
|
# Check if we're validating expert config
|
||||||
if args and hasattr(args, 'expert_provider') and args.expert_provider == 'gemini':
|
if (
|
||||||
key = os.environ.get('EXPERT_GEMINI_API_KEY')
|
args
|
||||||
if not key or key == '':
|
and hasattr(args, "expert_provider")
|
||||||
|
and args.expert_provider == "gemini"
|
||||||
|
):
|
||||||
|
key = os.environ.get("EXPERT_GEMINI_API_KEY")
|
||||||
|
if not key or key == "":
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
base_key = os.environ.get('GEMINI_API_KEY')
|
base_key = os.environ.get("GEMINI_API_KEY")
|
||||||
if base_key:
|
if base_key:
|
||||||
os.environ['EXPERT_GEMINI_API_KEY'] = base_key
|
os.environ["EXPERT_GEMINI_API_KEY"] = base_key
|
||||||
key = base_key
|
key = base_key
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('EXPERT_GEMINI_API_KEY environment variable is not set')
|
missing.append("EXPERT_GEMINI_API_KEY environment variable is not set")
|
||||||
else:
|
else:
|
||||||
key = os.environ.get('GEMINI_API_KEY')
|
key = os.environ.get("GEMINI_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('GEMINI_API_KEY environment variable is not set')
|
missing.append("GEMINI_API_KEY environment variable is not set")
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
@ -246,20 +286,26 @@ class DeepSeekStrategy(ProviderStrategy):
|
||||||
"""Validate DeepSeek environment variables."""
|
"""Validate DeepSeek environment variables."""
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
if args and hasattr(args, 'expert_provider') and args.expert_provider == 'deepseek':
|
if (
|
||||||
key = os.environ.get('EXPERT_DEEPSEEK_API_KEY')
|
args
|
||||||
if not key or key == '':
|
and hasattr(args, "expert_provider")
|
||||||
|
and args.expert_provider == "deepseek"
|
||||||
|
):
|
||||||
|
key = os.environ.get("EXPERT_DEEPSEEK_API_KEY")
|
||||||
|
if not key or key == "":
|
||||||
# Try to copy from base if not set
|
# Try to copy from base if not set
|
||||||
base_key = os.environ.get('DEEPSEEK_API_KEY')
|
base_key = os.environ.get("DEEPSEEK_API_KEY")
|
||||||
if base_key:
|
if base_key:
|
||||||
os.environ['EXPERT_DEEPSEEK_API_KEY'] = base_key
|
os.environ["EXPERT_DEEPSEEK_API_KEY"] = base_key
|
||||||
key = base_key
|
key = base_key
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('EXPERT_DEEPSEEK_API_KEY environment variable is not set')
|
missing.append(
|
||||||
|
"EXPERT_DEEPSEEK_API_KEY environment variable is not set"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
key = os.environ.get('DEEPSEEK_API_KEY')
|
key = os.environ.get("DEEPSEEK_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
missing.append('DEEPSEEK_API_KEY environment variable is not set')
|
missing.append("DEEPSEEK_API_KEY environment variable is not set")
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
@ -271,12 +317,13 @@ class OllamaStrategy(ProviderStrategy):
|
||||||
"""Validate Ollama environment variables."""
|
"""Validate Ollama environment variables."""
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
base_url = os.environ.get('OLLAMA_BASE_URL')
|
base_url = os.environ.get("OLLAMA_BASE_URL")
|
||||||
if not base_url:
|
if not base_url:
|
||||||
missing.append('OLLAMA_BASE_URL environment variable is not set')
|
missing.append("OLLAMA_BASE_URL environment variable is not set")
|
||||||
|
|
||||||
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
return ValidationResult(valid=len(missing) == 0, missing_vars=missing)
|
||||||
|
|
||||||
|
|
||||||
class ProviderFactory:
|
class ProviderFactory:
|
||||||
"""Factory for creating provider validation strategies."""
|
"""Factory for creating provider validation strategies."""
|
||||||
|
|
||||||
|
|
@ -292,13 +339,13 @@ class ProviderFactory:
|
||||||
Provider validation strategy or None if provider not found
|
Provider validation strategy or None if provider not found
|
||||||
"""
|
"""
|
||||||
strategies = {
|
strategies = {
|
||||||
'openai': OpenAIStrategy(),
|
"openai": OpenAIStrategy(),
|
||||||
'openai-compatible': OpenAICompatibleStrategy(),
|
"openai-compatible": OpenAICompatibleStrategy(),
|
||||||
'anthropic': AnthropicStrategy(),
|
"anthropic": AnthropicStrategy(),
|
||||||
'openrouter': OpenRouterStrategy(),
|
"openrouter": OpenRouterStrategy(),
|
||||||
'gemini': GeminiStrategy(),
|
"gemini": GeminiStrategy(),
|
||||||
'ollama': OllamaStrategy(),
|
"ollama": OllamaStrategy(),
|
||||||
'deepseek': DeepSeekStrategy()
|
"deepseek": DeepSeekStrategy(),
|
||||||
}
|
}
|
||||||
strategy = strategies.get(provider)
|
strategy = strategies.get(provider)
|
||||||
return strategy
|
return strategy
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,42 @@
|
||||||
"""Unit tests for environment validation."""
|
"""Unit tests for environment validation."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.env import validate_environment
|
from ra_aid.env import validate_environment
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MockArgs:
|
class MockArgs:
|
||||||
"""Mock arguments for testing."""
|
"""Mock arguments for testing."""
|
||||||
|
|
||||||
research_only: bool
|
research_only: bool
|
||||||
provider: str
|
provider: str
|
||||||
expert_provider: str = None
|
expert_provider: str = None
|
||||||
|
|
||||||
|
|
||||||
TEST_CASES = [
|
TEST_CASES = [
|
||||||
pytest.param(
|
pytest.param(
|
||||||
"research_only_no_model",
|
"research_only_no_model",
|
||||||
MockArgs(research_only=True, provider="openai"),
|
MockArgs(research_only=True, provider="openai"),
|
||||||
(False, [], False, ["TAVILY_API_KEY environment variable is not set"]),
|
(False, [], False, ["TAVILY_API_KEY environment variable is not set"]),
|
||||||
{},
|
{},
|
||||||
id="research_only_no_model"
|
id="research_only_no_model",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
"research_only_with_model",
|
"research_only_with_model",
|
||||||
MockArgs(research_only=True, provider="openai"),
|
MockArgs(research_only=True, provider="openai"),
|
||||||
(False, [], True, []),
|
(False, [], True, []),
|
||||||
{"TAVILY_API_KEY": "test_key"},
|
{"TAVILY_API_KEY": "test_key"},
|
||||||
id="research_only_with_model"
|
id="research_only_with_model",
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_name,args,expected,env_vars", TEST_CASES)
|
@pytest.mark.parametrize("test_name,args,expected,env_vars", TEST_CASES)
|
||||||
def test_validate_environment_research_only(
|
def test_validate_environment_research_only(
|
||||||
test_name: str,
|
test_name: str, args: MockArgs, expected: tuple, env_vars: dict, monkeypatch
|
||||||
args: MockArgs,
|
|
||||||
expected: tuple,
|
|
||||||
env_vars: dict,
|
|
||||||
monkeypatch
|
|
||||||
):
|
):
|
||||||
"""Test validate_environment with research_only flag."""
|
"""Test validate_environment with research_only flag."""
|
||||||
# Clear any existing environment variables
|
# Clear any existing environment variables
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from .processing import truncate_output
|
from .processing import truncate_output
|
||||||
|
|
||||||
__all__ = ['truncate_output']
|
__all__ = ["truncate_output"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def truncate_output(output: str, max_lines: Optional[int] = 5000) -> str:
|
def truncate_output(output: str, max_lines: Optional[int] = 5000) -> str:
|
||||||
"""Truncate output string to keep only the most recent lines if it exceeds max_lines.
|
"""Truncate output string to keep only the most recent lines if it exceeds max_lines.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,41 @@
|
||||||
from ra_aid.tools import (
|
from ra_aid.tools import (
|
||||||
ask_expert, ask_human, run_shell_command, run_programming_task,
|
ask_expert,
|
||||||
emit_research_notes, emit_plan, emit_related_files,
|
ask_human,
|
||||||
emit_expert_context, emit_key_facts, delete_key_facts,
|
delete_key_facts,
|
||||||
emit_key_snippets, delete_key_snippets, deregister_related_files, read_file_tool,
|
delete_key_snippets,
|
||||||
fuzzy_find_project_files, ripgrep_search, list_directory_tree,
|
deregister_related_files,
|
||||||
monorepo_detected, ui_detected,
|
emit_expert_context,
|
||||||
task_completed, plan_implementation_completed, web_search_tavily
|
emit_key_facts,
|
||||||
|
emit_key_snippets,
|
||||||
|
emit_plan,
|
||||||
|
emit_related_files,
|
||||||
|
emit_research_notes,
|
||||||
|
fuzzy_find_project_files,
|
||||||
|
list_directory_tree,
|
||||||
|
monorepo_detected,
|
||||||
|
plan_implementation_completed,
|
||||||
|
read_file_tool,
|
||||||
|
ripgrep_search,
|
||||||
|
run_programming_task,
|
||||||
|
run_shell_command,
|
||||||
|
task_completed,
|
||||||
|
ui_detected,
|
||||||
|
web_search_tavily,
|
||||||
|
)
|
||||||
|
from ra_aid.tools.agent import (
|
||||||
|
request_implementation,
|
||||||
|
request_research,
|
||||||
|
request_research_and_implementation,
|
||||||
|
request_task_implementation,
|
||||||
|
request_web_research,
|
||||||
)
|
)
|
||||||
from ra_aid.tools.memory import one_shot_completed
|
from ra_aid.tools.memory import one_shot_completed
|
||||||
from ra_aid.tools.agent import request_research, request_implementation, request_research_and_implementation, request_task_implementation, request_web_research
|
|
||||||
|
|
||||||
# Read-only tools that don't modify system state
|
# Read-only tools that don't modify system state
|
||||||
def get_read_only_tools(human_interaction: bool = False, web_research_enabled: bool = False) -> list:
|
def get_read_only_tools(
|
||||||
|
human_interaction: bool = False, web_research_enabled: bool = False
|
||||||
|
) -> list:
|
||||||
"""Get the list of read-only tools, optionally including human interaction tools.
|
"""Get the list of read-only tools, optionally including human interaction tools.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -32,7 +56,7 @@ def get_read_only_tools(human_interaction: bool = False, web_research_enabled: b
|
||||||
read_file_tool,
|
read_file_tool,
|
||||||
fuzzy_find_project_files,
|
fuzzy_find_project_files,
|
||||||
ripgrep_search,
|
ripgrep_search,
|
||||||
run_shell_command # can modify files, but we still need it for read-only tasks.
|
run_shell_command, # can modify files, but we still need it for read-only tasks.
|
||||||
]
|
]
|
||||||
|
|
||||||
if web_research_enabled:
|
if web_research_enabled:
|
||||||
|
|
@ -43,6 +67,7 @@ def get_read_only_tools(human_interaction: bool = False, web_research_enabled: b
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
|
||||||
# Define constant tool groups
|
# Define constant tool groups
|
||||||
READ_ONLY_TOOLS = get_read_only_tools()
|
READ_ONLY_TOOLS = get_read_only_tools()
|
||||||
MODIFICATION_TOOLS = [run_programming_task]
|
MODIFICATION_TOOLS = [run_programming_task]
|
||||||
|
|
@ -52,10 +77,16 @@ RESEARCH_TOOLS = [
|
||||||
emit_research_notes,
|
emit_research_notes,
|
||||||
one_shot_completed,
|
one_shot_completed,
|
||||||
monorepo_detected,
|
monorepo_detected,
|
||||||
ui_detected
|
ui_detected,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_research_tools(research_only: bool = False, expert_enabled: bool = True, human_interaction: bool = False, web_research_enabled: bool = False) -> list:
|
|
||||||
|
def get_research_tools(
|
||||||
|
research_only: bool = False,
|
||||||
|
expert_enabled: bool = True,
|
||||||
|
human_interaction: bool = False,
|
||||||
|
web_research_enabled: bool = False,
|
||||||
|
) -> list:
|
||||||
"""Get the list of research tools based on mode and whether expert is enabled.
|
"""Get the list of research tools based on mode and whether expert is enabled.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -83,7 +114,10 @@ def get_research_tools(research_only: bool = False, expert_enabled: bool = True,
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
def get_planning_tools(expert_enabled: bool = True, web_research_enabled: bool = False) -> list:
|
|
||||||
|
def get_planning_tools(
|
||||||
|
expert_enabled: bool = True, web_research_enabled: bool = False
|
||||||
|
) -> list:
|
||||||
"""Get the list of planning tools based on whether expert is enabled.
|
"""Get the list of planning tools based on whether expert is enabled.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -97,7 +131,7 @@ def get_planning_tools(expert_enabled: bool = True, web_research_enabled: bool =
|
||||||
planning_tools = [
|
planning_tools = [
|
||||||
emit_plan,
|
emit_plan,
|
||||||
request_task_implementation,
|
request_task_implementation,
|
||||||
plan_implementation_completed
|
plan_implementation_completed,
|
||||||
]
|
]
|
||||||
tools.extend(planning_tools)
|
tools.extend(planning_tools)
|
||||||
|
|
||||||
|
|
@ -107,7 +141,10 @@ def get_planning_tools(expert_enabled: bool = True, web_research_enabled: bool =
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
def get_implementation_tools(expert_enabled: bool = True, web_research_enabled: bool = False) -> list:
|
|
||||||
|
def get_implementation_tools(
|
||||||
|
expert_enabled: bool = True, web_research_enabled: bool = False
|
||||||
|
) -> list:
|
||||||
"""Get the list of implementation tools based on whether expert is enabled.
|
"""Get the list of implementation tools based on whether expert is enabled.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -119,9 +156,7 @@ def get_implementation_tools(expert_enabled: bool = True, web_research_enabled:
|
||||||
|
|
||||||
# Add modification tools since it's not research-only
|
# Add modification tools since it's not research-only
|
||||||
tools.extend(MODIFICATION_TOOLS)
|
tools.extend(MODIFICATION_TOOLS)
|
||||||
tools.extend([
|
tools.extend([task_completed])
|
||||||
task_completed
|
|
||||||
])
|
|
||||||
|
|
||||||
# Add expert tools if enabled
|
# Add expert tools if enabled
|
||||||
if expert_enabled:
|
if expert_enabled:
|
||||||
|
|
@ -129,6 +164,7 @@ def get_implementation_tools(expert_enabled: bool = True, web_research_enabled:
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
|
||||||
def get_web_research_tools(expert_enabled: bool = True) -> list:
|
def get_web_research_tools(expert_enabled: bool = True) -> list:
|
||||||
"""Get the list of tools available for web research.
|
"""Get the list of tools available for web research.
|
||||||
|
|
||||||
|
|
@ -140,11 +176,7 @@ def get_web_research_tools(expert_enabled: bool = True) -> list:
|
||||||
Returns:
|
Returns:
|
||||||
list: List of tools configured for web research
|
list: List of tools configured for web research
|
||||||
"""
|
"""
|
||||||
tools = [
|
tools = [web_search_tavily, emit_research_notes, task_completed]
|
||||||
web_search_tavily,
|
|
||||||
emit_research_notes,
|
|
||||||
task_completed
|
|
||||||
]
|
|
||||||
|
|
||||||
if expert_enabled:
|
if expert_enabled:
|
||||||
tools.append(emit_expert_context)
|
tools.append(emit_expert_context)
|
||||||
|
|
@ -152,7 +184,10 @@ def get_web_research_tools(expert_enabled: bool = True) -> list:
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
def get_chat_tools(expert_enabled: bool = True, web_research_enabled: bool = False) -> list:
|
|
||||||
|
def get_chat_tools(
|
||||||
|
expert_enabled: bool = True, web_research_enabled: bool = False
|
||||||
|
) -> list:
|
||||||
"""Get the list of tools available in chat mode.
|
"""Get the list of tools available in chat mode.
|
||||||
|
|
||||||
Chat mode includes research and implementation capabilities but excludes
|
Chat mode includes research and implementation capabilities but excludes
|
||||||
|
|
@ -169,7 +204,7 @@ def get_chat_tools(expert_enabled: bool = True, web_research_enabled: bool = Fal
|
||||||
emit_key_facts,
|
emit_key_facts,
|
||||||
delete_key_facts,
|
delete_key_facts,
|
||||||
delete_key_snippets,
|
delete_key_snippets,
|
||||||
deregister_related_files
|
deregister_related_files,
|
||||||
]
|
]
|
||||||
|
|
||||||
if web_research_enabled:
|
if web_research_enabled:
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,62 @@
|
||||||
|
from .expert import ask_expert, emit_expert_context
|
||||||
|
from .file_str_replace import file_str_replace
|
||||||
|
from .fuzzy_find import fuzzy_find_project_files
|
||||||
|
from .human import ask_human
|
||||||
|
from .list_directory import list_directory_tree
|
||||||
|
from .memory import (
|
||||||
|
delete_key_facts,
|
||||||
|
delete_key_snippets,
|
||||||
|
delete_tasks,
|
||||||
|
deregister_related_files,
|
||||||
|
emit_key_facts,
|
||||||
|
emit_key_snippets,
|
||||||
|
emit_plan,
|
||||||
|
emit_related_files,
|
||||||
|
emit_research_notes,
|
||||||
|
emit_task,
|
||||||
|
get_memory_value,
|
||||||
|
plan_implementation_completed,
|
||||||
|
request_implementation,
|
||||||
|
swap_task_order,
|
||||||
|
task_completed,
|
||||||
|
)
|
||||||
|
from .programmer import run_programming_task
|
||||||
|
from .read_file import read_file_tool
|
||||||
|
from .research import existing_project_detected, monorepo_detected, ui_detected
|
||||||
|
from .ripgrep import ripgrep_search
|
||||||
from .shell import run_shell_command
|
from .shell import run_shell_command
|
||||||
from .web_search_tavily import web_search_tavily
|
from .web_search_tavily import web_search_tavily
|
||||||
from .research import monorepo_detected, existing_project_detected, ui_detected
|
|
||||||
from .human import ask_human
|
|
||||||
from .programmer import run_programming_task
|
|
||||||
from .expert import ask_expert, emit_expert_context
|
|
||||||
from .read_file import read_file_tool
|
|
||||||
from .file_str_replace import file_str_replace
|
|
||||||
from .write_file import write_file_tool
|
from .write_file import write_file_tool
|
||||||
from .fuzzy_find import fuzzy_find_project_files
|
|
||||||
from .list_directory import list_directory_tree
|
|
||||||
from .ripgrep import ripgrep_search
|
|
||||||
from .memory import (
|
|
||||||
delete_tasks, emit_research_notes, emit_plan, emit_task, get_memory_value, emit_key_facts,
|
|
||||||
request_implementation, delete_key_facts,
|
|
||||||
emit_key_snippets, delete_key_snippets, emit_related_files, swap_task_order, task_completed,
|
|
||||||
plan_implementation_completed, deregister_related_files
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'ask_expert',
|
"ask_expert",
|
||||||
'delete_key_facts',
|
"delete_key_facts",
|
||||||
'delete_key_snippets',
|
"delete_key_snippets",
|
||||||
'web_search_tavily',
|
"web_search_tavily",
|
||||||
'deregister_related_files',
|
"deregister_related_files",
|
||||||
'emit_expert_context',
|
"emit_expert_context",
|
||||||
'emit_key_facts',
|
"emit_key_facts",
|
||||||
'emit_key_snippets',
|
"emit_key_snippets",
|
||||||
'emit_plan',
|
"emit_plan",
|
||||||
'emit_related_files',
|
"emit_related_files",
|
||||||
'emit_research_notes',
|
"emit_research_notes",
|
||||||
'emit_task',
|
"emit_task",
|
||||||
'fuzzy_find_project_files',
|
"fuzzy_find_project_files",
|
||||||
'get_memory_value',
|
"get_memory_value",
|
||||||
'list_directory_tree',
|
"list_directory_tree",
|
||||||
'read_file_tool',
|
"read_file_tool",
|
||||||
'request_implementation',
|
"request_implementation",
|
||||||
'run_programming_task',
|
"run_programming_task",
|
||||||
'run_shell_command',
|
"run_shell_command",
|
||||||
'write_file_tool',
|
"write_file_tool",
|
||||||
'ripgrep_search',
|
"ripgrep_search",
|
||||||
'file_str_replace',
|
"file_str_replace",
|
||||||
'delete_tasks',
|
"delete_tasks",
|
||||||
'swap_task_order',
|
"swap_task_order",
|
||||||
'monorepo_detected',
|
"monorepo_detected",
|
||||||
'existing_project_detected',
|
"existing_project_detected",
|
||||||
'ui_detected',
|
"ui_detected",
|
||||||
'ask_human',
|
"ask_human",
|
||||||
'task_completed',
|
"task_completed",
|
||||||
'plan_implementation_completed'
|
"plan_implementation_completed",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
"""Tools for spawning and managing sub-agents."""
|
"""Tools for spawning and managing sub-agents."""
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from typing import Dict, Any, Union, List
|
|
||||||
from typing_extensions import TypeAlias
|
|
||||||
from ..agent_utils import AgentInterrupt
|
|
||||||
from ra_aid.exceptions import AgentInterrupt
|
|
||||||
ResearchResult = Dict[str, Union[str, bool, Dict[int, Any], List[Any], None]]
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from ra_aid.tools.memory import _global_memory
|
|
||||||
from ra_aid.console.formatting import print_error
|
from ra_aid.console.formatting import print_error
|
||||||
from .memory import get_memory_value, get_related_files, get_work_log
|
from ra_aid.exceptions import AgentInterrupt
|
||||||
from .human import ask_human
|
from ra_aid.tools.memory import _global_memory
|
||||||
from ..llm import initialize_llm
|
|
||||||
from ..console import print_task_header
|
from ..console import print_task_header
|
||||||
|
from ..llm import initialize_llm
|
||||||
|
from .human import ask_human
|
||||||
|
from .memory import get_memory_value, get_related_files, get_work_log
|
||||||
|
|
||||||
|
ResearchResult = Dict[str, Union[str, bool, Dict[int, Any], List[Any], None]]
|
||||||
|
|
||||||
CANCELLED_BY_USER_REASON = "The operation was explicitly cancelled by the user. This typically is an indication that the action requested was not aligned with the user request."
|
CANCELLED_BY_USER_REASON = "The operation was explicitly cancelled by the user. This typically is an indication that the action requested was not aligned with the user request."
|
||||||
|
|
||||||
|
|
@ -20,6 +22,7 @@ RESEARCH_AGENT_RECURSION_LIMIT = 3
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@tool("request_research")
|
@tool("request_research")
|
||||||
def request_research(query: str) -> ResearchResult:
|
def request_research(query: str) -> ResearchResult:
|
||||||
"""Spawn a research-only agent to investigate the given query.
|
"""Spawn a research-only agent to investigate the given query.
|
||||||
|
|
@ -31,11 +34,14 @@ def request_research(query: str) -> ResearchResult:
|
||||||
query: The research question or project description
|
query: The research question or project description
|
||||||
"""
|
"""
|
||||||
# Initialize model from config
|
# Initialize model from config
|
||||||
config = _global_memory.get('config', {})
|
config = _global_memory.get("config", {})
|
||||||
model = initialize_llm(config.get('provider', 'anthropic'), config.get('model', 'claude-3-5-sonnet-20241022'))
|
model = initialize_llm(
|
||||||
|
config.get("provider", "anthropic"),
|
||||||
|
config.get("model", "claude-3-5-sonnet-20241022"),
|
||||||
|
)
|
||||||
|
|
||||||
# Check recursion depth
|
# Check recursion depth
|
||||||
current_depth = _global_memory.get('agent_depth', 0)
|
current_depth = _global_memory.get("agent_depth", 0)
|
||||||
if current_depth >= RESEARCH_AGENT_RECURSION_LIMIT:
|
if current_depth >= RESEARCH_AGENT_RECURSION_LIMIT:
|
||||||
print_error("Maximum research recursion depth reached")
|
print_error("Maximum research recursion depth reached")
|
||||||
return {
|
return {
|
||||||
|
|
@ -45,7 +51,7 @@ def request_research(query: str) -> ResearchResult:
|
||||||
"research_notes": get_memory_value("research_notes"),
|
"research_notes": get_memory_value("research_notes"),
|
||||||
"key_snippets": get_memory_value("key_snippets"),
|
"key_snippets": get_memory_value("key_snippets"),
|
||||||
"success": False,
|
"success": False,
|
||||||
"reason": "max_depth_exceeded"
|
"reason": "max_depth_exceeded",
|
||||||
}
|
}
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
|
|
@ -54,14 +60,15 @@ def request_research(query: str) -> ResearchResult:
|
||||||
try:
|
try:
|
||||||
# Run research agent
|
# Run research agent
|
||||||
from ..agent_utils import run_research_agent
|
from ..agent_utils import run_research_agent
|
||||||
result = run_research_agent(
|
|
||||||
|
_result = run_research_agent(
|
||||||
query,
|
query,
|
||||||
model,
|
model,
|
||||||
expert_enabled=True,
|
expert_enabled=True,
|
||||||
research_only=True,
|
research_only=True,
|
||||||
hil=config.get('hil', False),
|
hil=config.get("hil", False),
|
||||||
console_message=query,
|
console_message=query,
|
||||||
config=config
|
config=config,
|
||||||
)
|
)
|
||||||
except AgentInterrupt:
|
except AgentInterrupt:
|
||||||
print()
|
print()
|
||||||
|
|
@ -76,13 +83,16 @@ def request_research(query: str) -> ResearchResult:
|
||||||
reason = f"error: {str(e)}"
|
reason = f"error: {str(e)}"
|
||||||
finally:
|
finally:
|
||||||
# Get completion message if available
|
# Get completion message if available
|
||||||
completion_message = _global_memory.get('completion_message', 'Task was completed successfully.' if success else None)
|
completion_message = _global_memory.get(
|
||||||
|
"completion_message",
|
||||||
|
"Task was completed successfully." if success else None,
|
||||||
|
)
|
||||||
|
|
||||||
work_log = get_work_log()
|
work_log = get_work_log()
|
||||||
|
|
||||||
# Clear completion state from global memory
|
# Clear completion state from global memory
|
||||||
_global_memory['completion_message'] = ''
|
_global_memory["completion_message"] = ""
|
||||||
_global_memory['task_completed'] = False
|
_global_memory["task_completed"] = False
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"completion_message": completion_message,
|
"completion_message": completion_message,
|
||||||
|
|
@ -91,12 +101,13 @@ def request_research(query: str) -> ResearchResult:
|
||||||
"research_notes": get_memory_value("research_notes"),
|
"research_notes": get_memory_value("research_notes"),
|
||||||
"key_snippets": get_memory_value("key_snippets"),
|
"key_snippets": get_memory_value("key_snippets"),
|
||||||
"success": success,
|
"success": success,
|
||||||
"reason": reason
|
"reason": reason,
|
||||||
}
|
}
|
||||||
if work_log is not None:
|
if work_log is not None:
|
||||||
response_data["work_log"] = work_log
|
response_data["work_log"] = work_log
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
|
||||||
@tool("request_web_research")
|
@tool("request_web_research")
|
||||||
def request_web_research(query: str) -> ResearchResult:
|
def request_web_research(query: str) -> ResearchResult:
|
||||||
"""Spawn a web research agent to investigate the given query using web search.
|
"""Spawn a web research agent to investigate the given query using web search.
|
||||||
|
|
@ -105,8 +116,11 @@ def request_web_research(query: str) -> ResearchResult:
|
||||||
query: The research question or project description
|
query: The research question or project description
|
||||||
"""
|
"""
|
||||||
# Initialize model from config
|
# Initialize model from config
|
||||||
config = _global_memory.get('config', {})
|
config = _global_memory.get("config", {})
|
||||||
model = initialize_llm(config.get('provider', 'anthropic'), config.get('model', 'claude-3-5-sonnet-20241022'))
|
model = initialize_llm(
|
||||||
|
config.get("provider", "anthropic"),
|
||||||
|
config.get("model", "claude-3-5-sonnet-20241022"),
|
||||||
|
)
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
reason = None
|
reason = None
|
||||||
|
|
@ -114,13 +128,14 @@ def request_web_research(query: str) -> ResearchResult:
|
||||||
try:
|
try:
|
||||||
# Run web research agent
|
# Run web research agent
|
||||||
from ..agent_utils import run_web_research_agent
|
from ..agent_utils import run_web_research_agent
|
||||||
result = run_web_research_agent(
|
|
||||||
|
_result = run_web_research_agent(
|
||||||
query,
|
query,
|
||||||
model,
|
model,
|
||||||
expert_enabled=True,
|
expert_enabled=True,
|
||||||
hil=config.get('hil', False),
|
hil=config.get("hil", False),
|
||||||
console_message=query,
|
console_message=query,
|
||||||
config=config
|
config=config,
|
||||||
)
|
)
|
||||||
except AgentInterrupt:
|
except AgentInterrupt:
|
||||||
print()
|
print()
|
||||||
|
|
@ -135,25 +150,29 @@ def request_web_research(query: str) -> ResearchResult:
|
||||||
reason = f"error: {str(e)}"
|
reason = f"error: {str(e)}"
|
||||||
finally:
|
finally:
|
||||||
# Get completion message if available
|
# Get completion message if available
|
||||||
completion_message = _global_memory.get('completion_message', 'Task was completed successfully.' if success else None)
|
completion_message = _global_memory.get(
|
||||||
|
"completion_message",
|
||||||
|
"Task was completed successfully." if success else None,
|
||||||
|
)
|
||||||
|
|
||||||
work_log = get_work_log()
|
work_log = get_work_log()
|
||||||
|
|
||||||
# Clear completion state from global memory
|
# Clear completion state from global memory
|
||||||
_global_memory['completion_message'] = ''
|
_global_memory["completion_message"] = ""
|
||||||
_global_memory['task_completed'] = False
|
_global_memory["task_completed"] = False
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"completion_message": completion_message,
|
"completion_message": completion_message,
|
||||||
"key_snippets": get_memory_value("key_snippets"),
|
"key_snippets": get_memory_value("key_snippets"),
|
||||||
"research_notes": get_memory_value("research_notes"),
|
"research_notes": get_memory_value("research_notes"),
|
||||||
"success": success,
|
"success": success,
|
||||||
"reason": reason
|
"reason": reason,
|
||||||
}
|
}
|
||||||
if work_log is not None:
|
if work_log is not None:
|
||||||
response_data["work_log"] = work_log
|
response_data["work_log"] = work_log
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
|
||||||
@tool("request_research_and_implementation")
|
@tool("request_research_and_implementation")
|
||||||
def request_research_and_implementation(query: str) -> Dict[str, Any]:
|
def request_research_and_implementation(query: str) -> Dict[str, Any]:
|
||||||
"""Spawn a research agent to investigate and implement the given query.
|
"""Spawn a research agent to investigate and implement the given query.
|
||||||
|
|
@ -165,20 +184,24 @@ def request_research_and_implementation(query: str) -> Dict[str, Any]:
|
||||||
query: The research question or project description
|
query: The research question or project description
|
||||||
"""
|
"""
|
||||||
# Initialize model from config
|
# Initialize model from config
|
||||||
config = _global_memory.get('config', {})
|
config = _global_memory.get("config", {})
|
||||||
model = initialize_llm(config.get('provider', 'anthropic'), config.get('model', 'claude-3-5-sonnet-20241022'))
|
model = initialize_llm(
|
||||||
|
config.get("provider", "anthropic"),
|
||||||
|
config.get("model", "claude-3-5-sonnet-20241022"),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run research agent
|
# Run research agent
|
||||||
from ..agent_utils import run_research_agent
|
from ..agent_utils import run_research_agent
|
||||||
result = run_research_agent(
|
|
||||||
|
_result = run_research_agent(
|
||||||
query,
|
query,
|
||||||
model,
|
model,
|
||||||
expert_enabled=True,
|
expert_enabled=True,
|
||||||
research_only=False,
|
research_only=False,
|
||||||
hil=config.get('hil', False),
|
hil=config.get("hil", False),
|
||||||
console_message=query,
|
console_message=query,
|
||||||
config=config
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
|
|
@ -196,14 +219,16 @@ def request_research_and_implementation(query: str) -> Dict[str, Any]:
|
||||||
reason = f"error: {str(e)}"
|
reason = f"error: {str(e)}"
|
||||||
|
|
||||||
# Get completion message if available
|
# Get completion message if available
|
||||||
completion_message = _global_memory.get('completion_message', 'Task was completed successfully.' if success else None)
|
completion_message = _global_memory.get(
|
||||||
|
"completion_message", "Task was completed successfully." if success else None
|
||||||
|
)
|
||||||
|
|
||||||
work_log = get_work_log()
|
work_log = get_work_log()
|
||||||
|
|
||||||
# Clear completion state from global memory
|
# Clear completion state from global memory
|
||||||
_global_memory['completion_message'] = ''
|
_global_memory["completion_message"] = ""
|
||||||
_global_memory['task_completed'] = False
|
_global_memory["task_completed"] = False
|
||||||
_global_memory['plan_completed'] = False
|
_global_memory["plan_completed"] = False
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"completion_message": completion_message,
|
"completion_message": completion_message,
|
||||||
|
|
@ -212,12 +237,13 @@ def request_research_and_implementation(query: str) -> Dict[str, Any]:
|
||||||
"research_notes": get_memory_value("research_notes"),
|
"research_notes": get_memory_value("research_notes"),
|
||||||
"key_snippets": get_memory_value("key_snippets"),
|
"key_snippets": get_memory_value("key_snippets"),
|
||||||
"success": success,
|
"success": success,
|
||||||
"reason": reason
|
"reason": reason,
|
||||||
}
|
}
|
||||||
if work_log is not None:
|
if work_log is not None:
|
||||||
response_data["work_log"] = work_log
|
response_data["work_log"] = work_log
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
|
||||||
@tool("request_task_implementation")
|
@tool("request_task_implementation")
|
||||||
def request_task_implementation(task_spec: str) -> Dict[str, Any]:
|
def request_task_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
"""Spawn an implementation agent to execute the given task.
|
"""Spawn an implementation agent to execute the given task.
|
||||||
|
|
@ -226,27 +252,33 @@ def request_task_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
task_spec: REQUIRED The full task specification (markdown format, typically one part of the overall plan)
|
task_spec: REQUIRED The full task specification (markdown format, typically one part of the overall plan)
|
||||||
"""
|
"""
|
||||||
# Initialize model from config
|
# Initialize model from config
|
||||||
config = _global_memory.get('config', {})
|
config = _global_memory.get("config", {})
|
||||||
model = initialize_llm(config.get('provider', 'anthropic'), config.get('model', 'claude-3-5-sonnet-20241022'))
|
model = initialize_llm(
|
||||||
|
config.get("provider", "anthropic"),
|
||||||
|
config.get("model", "claude-3-5-sonnet-20241022"),
|
||||||
|
)
|
||||||
|
|
||||||
# Get required parameters
|
# Get required parameters
|
||||||
tasks = [_global_memory['tasks'][task_id] for task_id in sorted(_global_memory['tasks'])]
|
tasks = [
|
||||||
plan = _global_memory.get('plan', '')
|
_global_memory["tasks"][task_id] for task_id in sorted(_global_memory["tasks"])
|
||||||
related_files = list(_global_memory['related_files'].values())
|
]
|
||||||
|
plan = _global_memory.get("plan", "")
|
||||||
|
related_files = list(_global_memory["related_files"].values())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print_task_header(task_spec)
|
print_task_header(task_spec)
|
||||||
# Run implementation agent
|
# Run implementation agent
|
||||||
from ..agent_utils import run_task_implementation_agent
|
from ..agent_utils import run_task_implementation_agent
|
||||||
result = run_task_implementation_agent(
|
|
||||||
base_task=_global_memory.get('base_task', ''),
|
_result = run_task_implementation_agent(
|
||||||
|
base_task=_global_memory.get("base_task", ""),
|
||||||
tasks=tasks,
|
tasks=tasks,
|
||||||
task=task_spec,
|
task=task_spec,
|
||||||
plan=plan,
|
plan=plan,
|
||||||
related_files=related_files,
|
related_files=related_files,
|
||||||
model=model,
|
model=model,
|
||||||
expert_enabled=True,
|
expert_enabled=True,
|
||||||
config=config
|
config=config,
|
||||||
)
|
)
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
|
|
@ -264,14 +296,16 @@ def request_task_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
reason = f"error: {str(e)}"
|
reason = f"error: {str(e)}"
|
||||||
|
|
||||||
# Get completion message if available
|
# Get completion message if available
|
||||||
completion_message = _global_memory.get('completion_message', 'Task was completed successfully.' if success else None)
|
completion_message = _global_memory.get(
|
||||||
|
"completion_message", "Task was completed successfully." if success else None
|
||||||
|
)
|
||||||
|
|
||||||
# Get and reset work log if at root depth
|
# Get and reset work log if at root depth
|
||||||
work_log = get_work_log()
|
work_log = get_work_log()
|
||||||
|
|
||||||
# Clear completion state from global memory
|
# Clear completion state from global memory
|
||||||
_global_memory['completion_message'] = ''
|
_global_memory["completion_message"] = ""
|
||||||
_global_memory['task_completed'] = False
|
_global_memory["task_completed"] = False
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"key_facts": get_memory_value("key_facts"),
|
"key_facts": get_memory_value("key_facts"),
|
||||||
|
|
@ -279,12 +313,13 @@ def request_task_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
"key_snippets": get_memory_value("key_snippets"),
|
"key_snippets": get_memory_value("key_snippets"),
|
||||||
"completion_message": completion_message,
|
"completion_message": completion_message,
|
||||||
"success": success,
|
"success": success,
|
||||||
"reason": reason
|
"reason": reason,
|
||||||
}
|
}
|
||||||
if work_log is not None:
|
if work_log is not None:
|
||||||
response_data["work_log"] = work_log
|
response_data["work_log"] = work_log
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
|
||||||
@tool("request_implementation")
|
@tool("request_implementation")
|
||||||
def request_implementation(task_spec: str) -> Dict[str, Any]:
|
def request_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
"""Spawn a planning agent to create an implementation plan for the given task.
|
"""Spawn a planning agent to create an implementation plan for the given task.
|
||||||
|
|
@ -293,18 +328,22 @@ def request_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
task_spec: The task specification to plan implementation for
|
task_spec: The task specification to plan implementation for
|
||||||
"""
|
"""
|
||||||
# Initialize model from config
|
# Initialize model from config
|
||||||
config = _global_memory.get('config', {})
|
config = _global_memory.get("config", {})
|
||||||
model = initialize_llm(config.get('provider', 'anthropic'), config.get('model', 'claude-3-5-sonnet-20241022'))
|
model = initialize_llm(
|
||||||
|
config.get("provider", "anthropic"),
|
||||||
|
config.get("model", "claude-3-5-sonnet-20241022"),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run planning agent
|
# Run planning agent
|
||||||
from ..agent_utils import run_planning_agent
|
from ..agent_utils import run_planning_agent
|
||||||
result = run_planning_agent(
|
|
||||||
|
_result = run_planning_agent(
|
||||||
task_spec,
|
task_spec,
|
||||||
model,
|
model,
|
||||||
config=config,
|
config=config,
|
||||||
expert_enabled=True,
|
expert_enabled=True,
|
||||||
hil=config.get('hil', False)
|
hil=config.get("hil", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
|
|
@ -322,15 +361,17 @@ def request_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
reason = f"error: {str(e)}"
|
reason = f"error: {str(e)}"
|
||||||
|
|
||||||
# Get completion message if available
|
# Get completion message if available
|
||||||
completion_message = _global_memory.get('completion_message', 'Task was completed successfully.' if success else None)
|
completion_message = _global_memory.get(
|
||||||
|
"completion_message", "Task was completed successfully." if success else None
|
||||||
|
)
|
||||||
|
|
||||||
# Get and reset work log if at root depth
|
# Get and reset work log if at root depth
|
||||||
work_log = get_work_log()
|
work_log = get_work_log()
|
||||||
|
|
||||||
# Clear completion state from global memory
|
# Clear completion state from global memory
|
||||||
_global_memory['completion_message'] = ''
|
_global_memory["completion_message"] = ""
|
||||||
_global_memory['task_completed'] = False
|
_global_memory["task_completed"] = False
|
||||||
_global_memory['plan_completed'] = False
|
_global_memory["plan_completed"] = False
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"completion_message": completion_message,
|
"completion_message": completion_message,
|
||||||
|
|
@ -338,7 +379,7 @@ def request_implementation(task_spec: str) -> Dict[str, Any]:
|
||||||
"related_files": get_related_files(),
|
"related_files": get_related_files(),
|
||||||
"key_snippets": get_memory_value("key_snippets"),
|
"key_snippets": get_memory_value("key_snippets"),
|
||||||
"success": success,
|
"success": success,
|
||||||
"reason": reason
|
"reason": reason,
|
||||||
}
|
}
|
||||||
if work_log is not None:
|
if work_log is not None:
|
||||||
response_data["work_log"] = work_log
|
response_data["work_log"] = work_log
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,45 @@
|
||||||
from typing import List
|
|
||||||
import os
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
from ..llm import initialize_expert_llm
|
from ..llm import initialize_expert_llm
|
||||||
from .memory import get_memory_value, _global_memory
|
from .memory import _global_memory, get_memory_value
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
_model = None
|
_model = None
|
||||||
|
|
||||||
|
|
||||||
def get_model():
|
def get_model():
|
||||||
global _model
|
global _model
|
||||||
try:
|
try:
|
||||||
if _model is None:
|
if _model is None:
|
||||||
provider = _global_memory['config']['expert_provider'] or 'openai'
|
provider = _global_memory["config"]["expert_provider"] or "openai"
|
||||||
model = _global_memory['config']['expert_model'] or 'o1'
|
model = _global_memory["config"]["expert_model"] or "o1"
|
||||||
_model = initialize_expert_llm(provider, model)
|
_model = initialize_expert_llm(provider, model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_model = None
|
_model = None
|
||||||
console.print(Panel(f"Failed to initialize expert model: {e}", title="Error", border_style="red"))
|
console.print(
|
||||||
|
Panel(
|
||||||
|
f"Failed to initialize expert model: {e}",
|
||||||
|
title="Error",
|
||||||
|
border_style="red",
|
||||||
|
)
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
return _model
|
return _model
|
||||||
|
|
||||||
|
|
||||||
# Keep track of context globally
|
# Keep track of context globally
|
||||||
expert_context = {
|
expert_context = {
|
||||||
'text': [], # Additional textual context
|
"text": [], # Additional textual context
|
||||||
'files': [] # File paths to include
|
"files": [], # File paths to include
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@tool("emit_expert_context")
|
@tool("emit_expert_context")
|
||||||
def emit_expert_context(context: str) -> str:
|
def emit_expert_context(context: str) -> str:
|
||||||
"""Add context for the next expert question.
|
"""Add context for the next expert question.
|
||||||
|
|
@ -46,13 +57,14 @@ def emit_expert_context(context: str) -> str:
|
||||||
Args:
|
Args:
|
||||||
context: The context to add
|
context: The context to add
|
||||||
"""
|
"""
|
||||||
expert_context['text'].append(context)
|
expert_context["text"].append(context)
|
||||||
|
|
||||||
# Create and display status panel
|
# Create and display status panel
|
||||||
panel_content = f"Added expert context ({len(context)} characters)"
|
panel_content = f"Added expert context ({len(context)} characters)"
|
||||||
console.print(Panel(panel_content, title="Expert Context", border_style="blue"))
|
console.print(Panel(panel_content, title="Expert Context", border_style="blue"))
|
||||||
|
|
||||||
return f"Context added."
|
return "Context added."
|
||||||
|
|
||||||
|
|
||||||
def read_files_with_limit(file_paths: List[str], max_lines: int = 10000) -> str:
|
def read_files_with_limit(file_paths: List[str], max_lines: int = 10000) -> str:
|
||||||
"""Read multiple files and concatenate contents, stopping at line limit.
|
"""Read multiple files and concatenate contents, stopping at line limit.
|
||||||
|
|
@ -75,24 +87,27 @@ def read_files_with_limit(file_paths: List[str], max_lines: int = 10000) -> str:
|
||||||
console.print(f"Warning: File not found: {path}", style="yellow")
|
console.print(f"Warning: File not found: {path}", style="yellow")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
file_content = []
|
file_content = []
|
||||||
for i, line in enumerate(f):
|
for i, line in enumerate(f):
|
||||||
if total_lines + i >= max_lines:
|
if total_lines + i >= max_lines:
|
||||||
file_content.append(f"\n... truncated after {max_lines} lines ...")
|
file_content.append(
|
||||||
|
f"\n... truncated after {max_lines} lines ..."
|
||||||
|
)
|
||||||
break
|
break
|
||||||
file_content.append(line)
|
file_content.append(line)
|
||||||
|
|
||||||
if file_content:
|
if file_content:
|
||||||
contents.append(f'\n## File: {path}\n')
|
contents.append(f"\n## File: {path}\n")
|
||||||
contents.append(''.join(file_content))
|
contents.append("".join(file_content))
|
||||||
total_lines += len(file_content)
|
total_lines += len(file_content)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"Error reading file {path}: {str(e)}", style="red")
|
console.print(f"Error reading file {path}: {str(e)}", style="red")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return ''.join(contents)
|
return "".join(contents)
|
||||||
|
|
||||||
|
|
||||||
def read_related_files(file_paths: List[str]) -> str:
|
def read_related_files(file_paths: List[str]) -> str:
|
||||||
"""Read the provided files and return their contents.
|
"""Read the provided files and return their contents.
|
||||||
|
|
@ -104,10 +119,11 @@ def read_related_files(file_paths: List[str]) -> str:
|
||||||
String containing concatenated file contents, or empty string if no paths
|
String containing concatenated file contents, or empty string if no paths
|
||||||
"""
|
"""
|
||||||
if not file_paths:
|
if not file_paths:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
return read_files_with_limit(file_paths, max_lines=10000)
|
return read_files_with_limit(file_paths, max_lines=10000)
|
||||||
|
|
||||||
|
|
||||||
@tool("ask_expert")
|
@tool("ask_expert")
|
||||||
def ask_expert(question: str) -> str:
|
def ask_expert(question: str) -> str:
|
||||||
"""Ask a question to an expert AI model.
|
"""Ask a question to an expert AI model.
|
||||||
|
|
@ -128,58 +144,58 @@ def ask_expert(question: str) -> str:
|
||||||
global expert_context
|
global expert_context
|
||||||
|
|
||||||
# Get all content first
|
# Get all content first
|
||||||
file_paths = list(_global_memory['related_files'].values())
|
file_paths = list(_global_memory["related_files"].values())
|
||||||
related_contents = read_related_files(file_paths)
|
related_contents = read_related_files(file_paths)
|
||||||
key_snippets = get_memory_value('key_snippets')
|
key_snippets = get_memory_value("key_snippets")
|
||||||
key_facts = get_memory_value('key_facts')
|
key_facts = get_memory_value("key_facts")
|
||||||
research_notes = get_memory_value('research_notes')
|
research_notes = get_memory_value("research_notes")
|
||||||
|
|
||||||
# Build display query (just question)
|
# Build display query (just question)
|
||||||
display_query = "# Question\n" + question
|
display_query = "# Question\n" + question
|
||||||
|
|
||||||
# Show only question in panel
|
# Show only question in panel
|
||||||
console.print(Panel(
|
console.print(
|
||||||
Markdown(display_query),
|
Panel(Markdown(display_query), title="🤔 Expert Query", border_style="yellow")
|
||||||
title="🤔 Expert Query",
|
)
|
||||||
border_style="yellow"
|
|
||||||
))
|
|
||||||
|
|
||||||
# Clear context after panel display
|
# Clear context after panel display
|
||||||
expert_context['text'].clear()
|
expert_context["text"].clear()
|
||||||
expert_context['files'].clear()
|
expert_context["files"].clear()
|
||||||
|
|
||||||
# Build full query in specified order
|
# Build full query in specified order
|
||||||
query_parts = []
|
query_parts = []
|
||||||
|
|
||||||
if related_contents:
|
if related_contents:
|
||||||
query_parts.extend(['# Related Files', related_contents])
|
query_parts.extend(["# Related Files", related_contents])
|
||||||
|
|
||||||
if related_contents:
|
if related_contents:
|
||||||
query_parts.extend(['# Research Notes', research_notes])
|
query_parts.extend(["# Research Notes", research_notes])
|
||||||
|
|
||||||
if key_snippets and len(key_snippets) > 0:
|
if key_snippets and len(key_snippets) > 0:
|
||||||
query_parts.extend(['# Key Snippets', key_snippets])
|
query_parts.extend(["# Key Snippets", key_snippets])
|
||||||
|
|
||||||
if key_facts and len(key_facts) > 0:
|
if key_facts and len(key_facts) > 0:
|
||||||
query_parts.extend(['# Key Facts About This Project', key_facts])
|
query_parts.extend(["# Key Facts About This Project", key_facts])
|
||||||
|
|
||||||
if expert_context['text']:
|
if expert_context["text"]:
|
||||||
query_parts.extend(['\n# Additional Context', '\n'.join(expert_context['text'])])
|
query_parts.extend(
|
||||||
|
["\n# Additional Context", "\n".join(expert_context["text"])]
|
||||||
|
)
|
||||||
|
|
||||||
query_parts.extend(['# Question', question])
|
query_parts.extend(["# Question", question])
|
||||||
query_parts.extend(['\n # Addidional Requirements', "Do not expand the scope unnecessarily."])
|
query_parts.extend(
|
||||||
|
["\n # Addidional Requirements", "Do not expand the scope unnecessarily."]
|
||||||
|
)
|
||||||
|
|
||||||
# Join all parts
|
# Join all parts
|
||||||
full_query = '\n'.join(query_parts)
|
full_query = "\n".join(query_parts)
|
||||||
|
|
||||||
# Get response using full query
|
# Get response using full query
|
||||||
response = get_model().invoke(full_query)
|
response = get_model().invoke(full_query)
|
||||||
|
|
||||||
# Format and display response
|
# Format and display response
|
||||||
console.print(Panel(
|
console.print(
|
||||||
Markdown(response.content),
|
Panel(Markdown(response.content), title="Expert Response", border_style="blue")
|
||||||
title="Expert Response",
|
)
|
||||||
border_style="blue"
|
|
||||||
))
|
|
||||||
|
|
||||||
return response.content
|
return response.content
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
from langchain_core.tools import tool
|
|
||||||
from typing import Dict
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
from ra_aid.console import console
|
from ra_aid.console import console
|
||||||
from ra_aid.console.formatting import print_error
|
from ra_aid.console.formatting import print_error
|
||||||
|
|
||||||
|
|
||||||
def truncate_display_str(s: str, max_length: int = 30) -> str:
|
def truncate_display_str(s: str, max_length: int = 30) -> str:
|
||||||
"""Truncate a string for display purposes if it exceeds max length.
|
"""Truncate a string for display purposes if it exceeds max length.
|
||||||
|
|
||||||
|
|
@ -19,6 +22,7 @@ def truncate_display_str(s: str, max_length: int = 30) -> str:
|
||||||
return s
|
return s
|
||||||
return s[:max_length] + "..."
|
return s[:max_length] + "..."
|
||||||
|
|
||||||
|
|
||||||
def format_string_for_display(s: str, threshold: int = 30) -> str:
|
def format_string_for_display(s: str, threshold: int = 30) -> str:
|
||||||
"""Format a string for display, showing either quoted string or length.
|
"""Format a string for display, showing either quoted string or length.
|
||||||
|
|
||||||
|
|
@ -31,14 +35,11 @@ def format_string_for_display(s: str, threshold: int = 30) -> str:
|
||||||
"""
|
"""
|
||||||
if len(s) <= threshold:
|
if len(s) <= threshold:
|
||||||
return f"'{s}'"
|
return f"'{s}'"
|
||||||
return f'[{len(s)} characters]'
|
return f"[{len(s)} characters]"
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def file_str_replace(
|
def file_str_replace(filepath: str, old_str: str, new_str: str) -> Dict[str, any]:
|
||||||
filepath: str,
|
|
||||||
old_str: str,
|
|
||||||
new_str: str
|
|
||||||
) -> Dict[str, any]:
|
|
||||||
"""Replace an exact string match in a file with a new string.
|
"""Replace an exact string match in a file with a new string.
|
||||||
Only performs replacement if the old string appears exactly once.
|
Only performs replacement if the old string appears exactly once.
|
||||||
|
|
||||||
|
|
@ -74,14 +75,16 @@ def file_str_replace(
|
||||||
new_content = content.replace(old_str, new_str)
|
new_content = content.replace(old_str, new_str)
|
||||||
path.write_text(new_content)
|
path.write_text(new_content)
|
||||||
|
|
||||||
console.print(Panel(
|
console.print(
|
||||||
f"Replaced in {filepath}:\n{format_string_for_display(old_str)} → {format_string_for_display(new_str)}",
|
Panel(
|
||||||
title="✓ String Replaced",
|
f"Replaced in {filepath}:\n{format_string_for_display(old_str)} → {format_string_for_display(new_str)}",
|
||||||
border_style="bright_blue"
|
title="✓ String Replaced",
|
||||||
))
|
border_style="bright_blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Successfully replaced '{old_str}' with '{new_str}' in {filepath}"
|
"message": f"Successfully replaced '{old_str}' with '{new_str}' in {filepath}",
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
from typing import List, Tuple
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
from git import Repo
|
from typing import List, Tuple
|
||||||
|
|
||||||
from fuzzywuzzy import process
|
from fuzzywuzzy import process
|
||||||
|
from git import Repo
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
DEFAULT_EXCLUDE_PATTERNS = [
|
DEFAULT_EXCLUDE_PATTERNS = [
|
||||||
'*.pyc',
|
"*.pyc",
|
||||||
'__pycache__/*',
|
"__pycache__/*",
|
||||||
'.git/*',
|
".git/*",
|
||||||
'*.so',
|
"*.so",
|
||||||
'*.o',
|
"*.o",
|
||||||
'*.class'
|
"*.class",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def fuzzy_find_project_files(
|
def fuzzy_find_project_files(
|
||||||
search_term: str,
|
search_term: str,
|
||||||
|
|
@ -26,7 +28,7 @@ def fuzzy_find_project_files(
|
||||||
threshold: int = 60,
|
threshold: int = 60,
|
||||||
max_results: int = 10,
|
max_results: int = 10,
|
||||||
include_paths: List[str] = None,
|
include_paths: List[str] = None,
|
||||||
exclude_patterns: List[str] = None
|
exclude_patterns: List[str] = None,
|
||||||
) -> List[Tuple[str, int]]:
|
) -> List[Tuple[str, int]]:
|
||||||
"""Fuzzy find files in a git repository matching the search term.
|
"""Fuzzy find files in a git repository matching the search term.
|
||||||
|
|
||||||
|
|
@ -73,33 +75,19 @@ def fuzzy_find_project_files(
|
||||||
if include_paths:
|
if include_paths:
|
||||||
filtered_files = []
|
filtered_files = []
|
||||||
for pattern in include_paths:
|
for pattern in include_paths:
|
||||||
filtered_files.extend(
|
filtered_files.extend(f for f in all_files if fnmatch.fnmatch(f, pattern))
|
||||||
f for f in all_files
|
|
||||||
if fnmatch.fnmatch(f, pattern)
|
|
||||||
)
|
|
||||||
all_files = filtered_files
|
all_files = filtered_files
|
||||||
|
|
||||||
# Apply exclude patterns
|
# Apply exclude patterns
|
||||||
patterns = DEFAULT_EXCLUDE_PATTERNS + (exclude_patterns or [])
|
patterns = DEFAULT_EXCLUDE_PATTERNS + (exclude_patterns or [])
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
all_files = [
|
all_files = [f for f in all_files if not fnmatch.fnmatch(f, pattern)]
|
||||||
f for f in all_files
|
|
||||||
if not fnmatch.fnmatch(f, pattern)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Perform fuzzy matching
|
# Perform fuzzy matching
|
||||||
matches = process.extract(
|
matches = process.extract(search_term, all_files, limit=max_results)
|
||||||
search_term,
|
|
||||||
all_files,
|
|
||||||
limit=max_results
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by threshold
|
# Filter by threshold
|
||||||
filtered_matches = [
|
filtered_matches = [(path, score) for path, score in matches if score >= threshold]
|
||||||
(path, score)
|
|
||||||
for path, score in matches
|
|
||||||
if score >= threshold
|
|
||||||
]
|
|
||||||
|
|
||||||
# Build info panel content
|
# Build info panel content
|
||||||
info_sections = []
|
info_sections = []
|
||||||
|
|
@ -110,7 +98,7 @@ def fuzzy_find_project_files(
|
||||||
f"**Search Term**: `{search_term}`",
|
f"**Search Term**: `{search_term}`",
|
||||||
f"**Repository**: `{repo_path}`",
|
f"**Repository**: `{repo_path}`",
|
||||||
f"**Threshold**: {threshold}",
|
f"**Threshold**: {threshold}",
|
||||||
f"**Max Results**: {max_results}"
|
f"**Max Results**: {max_results}",
|
||||||
]
|
]
|
||||||
if include_paths:
|
if include_paths:
|
||||||
params_section.append("\n**Include Patterns**:")
|
params_section.append("\n**Include Patterns**:")
|
||||||
|
|
@ -126,7 +114,7 @@ def fuzzy_find_project_files(
|
||||||
stats_section = [
|
stats_section = [
|
||||||
"## Results Statistics",
|
"## Results Statistics",
|
||||||
f"**Total Files Scanned**: {len(all_files)}",
|
f"**Total Files Scanned**: {len(all_files)}",
|
||||||
f"**Matches Found**: {len(filtered_matches)}"
|
f"**Matches Found**: {len(filtered_matches)}",
|
||||||
]
|
]
|
||||||
info_sections.append("\n".join(stats_section))
|
info_sections.append("\n".join(stats_section))
|
||||||
|
|
||||||
|
|
@ -140,10 +128,12 @@ def fuzzy_find_project_files(
|
||||||
info_sections.append("## Results\n*No matches found*")
|
info_sections.append("## Results\n*No matches found*")
|
||||||
|
|
||||||
# Display the panel
|
# Display the panel
|
||||||
console.print(Panel(
|
console.print(
|
||||||
Markdown("\n\n".join(info_sections)),
|
Panel(
|
||||||
title="🔍 Fuzzy Find Results",
|
Markdown("\n\n".join(info_sections)),
|
||||||
border_style="bright_blue"
|
title="🔍 Fuzzy Find Results",
|
||||||
))
|
border_style="bright_blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return filtered_matches
|
return filtered_matches
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,41 @@
|
||||||
"""Utilities for executing and managing user-defined test commands."""
|
"""Utilities for executing and managing user-defined test commands."""
|
||||||
|
|
||||||
from typing import Dict, Any, Tuple
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from ra_aid.logging_config import get_logger
|
||||||
from ra_aid.tools.human import ask_human
|
from ra_aid.tools.human import ask_human
|
||||||
from ra_aid.tools.shell import run_shell_command
|
from ra_aid.tools.shell import run_shell_command
|
||||||
from ra_aid.logging_config import get_logger
|
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TestState:
|
class TestState:
|
||||||
"""State for test execution."""
|
"""State for test execution."""
|
||||||
|
|
||||||
prompt: str
|
prompt: str
|
||||||
test_attempts: int
|
test_attempts: int
|
||||||
auto_test: bool
|
auto_test: bool
|
||||||
should_break: bool = False
|
should_break: bool = False
|
||||||
|
|
||||||
|
|
||||||
class TestCommandExecutor:
|
class TestCommandExecutor:
|
||||||
"""Class for executing and managing test commands."""
|
"""Class for executing and managing test commands."""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any], original_prompt: str, test_attempts: int = 0, auto_test: bool = False):
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: Dict[str, Any],
|
||||||
|
original_prompt: str,
|
||||||
|
test_attempts: int = 0,
|
||||||
|
auto_test: bool = False,
|
||||||
|
):
|
||||||
"""Initialize the test command executor.
|
"""Initialize the test command executor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -38,7 +49,7 @@ class TestCommandExecutor:
|
||||||
prompt=original_prompt,
|
prompt=original_prompt,
|
||||||
test_attempts=test_attempts,
|
test_attempts=test_attempts,
|
||||||
auto_test=auto_test,
|
auto_test=auto_test,
|
||||||
should_break=False
|
should_break=False,
|
||||||
)
|
)
|
||||||
self.max_retries = config.get("max_test_cmd_retries", 5)
|
self.max_retries = config.get("max_test_cmd_retries", 5)
|
||||||
|
|
||||||
|
|
@ -46,13 +57,17 @@ class TestCommandExecutor:
|
||||||
"""Display test failure message."""
|
"""Display test failure message."""
|
||||||
console.print(
|
console.print(
|
||||||
Panel(
|
Panel(
|
||||||
Markdown(f"Test failed. Attempt number {self.state.test_attempts} of {self.max_retries}. Retrying and informing of failure output"),
|
Markdown(
|
||||||
|
f"Test failed. Attempt number {self.state.test_attempts} of {self.max_retries}. Retrying and informing of failure output"
|
||||||
|
),
|
||||||
title="🔎 User Defined Test",
|
title="🔎 User Defined Test",
|
||||||
border_style="red bold"
|
border_style="red bold",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_test_failure(self, original_prompt: str, test_result: Dict[str, Any]) -> None:
|
def handle_test_failure(
|
||||||
|
self, original_prompt: str, test_result: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
"""Handle test command failure.
|
"""Handle test command failure.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -70,7 +85,7 @@ class TestCommandExecutor:
|
||||||
cmd: Test command to execute
|
cmd: Test command to execute
|
||||||
original_prompt: Original prompt text
|
original_prompt: Original prompt text
|
||||||
"""
|
"""
|
||||||
timeout = self.config.get('timeout', 30)
|
timeout = self.config.get("timeout", 30)
|
||||||
try:
|
try:
|
||||||
logger.info(f"Executing test command: {cmd} with timeout {timeout}s")
|
logger.info(f"Executing test command: {cmd} with timeout {timeout}s")
|
||||||
test_result = run_shell_command(cmd, timeout=timeout)
|
test_result = run_shell_command(cmd, timeout=timeout)
|
||||||
|
|
@ -83,14 +98,18 @@ class TestCommandExecutor:
|
||||||
self.state.should_break = True
|
self.state.should_break = True
|
||||||
logger.info("Test command executed successfully")
|
logger.info("Test command executed successfully")
|
||||||
|
|
||||||
except subprocess.TimeoutExpired as e:
|
except subprocess.TimeoutExpired:
|
||||||
logger.warning(f"Test command timed out after {timeout}s: {cmd}")
|
logger.warning(f"Test command timed out after {timeout}s: {cmd}")
|
||||||
self.state.test_attempts += 1
|
self.state.test_attempts += 1
|
||||||
self.state.prompt = f"{original_prompt}. Previous attempt timed out after {timeout} seconds"
|
self.state.prompt = (
|
||||||
|
f"{original_prompt}. Previous attempt timed out after {timeout} seconds"
|
||||||
|
)
|
||||||
self.display_test_failure()
|
self.display_test_failure()
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error(f"Test command failed with exit code {e.returncode}: {cmd}\nOutput: {e.output}")
|
logger.error(
|
||||||
|
f"Test command failed with exit code {e.returncode}: {cmd}\nOutput: {e.output}"
|
||||||
|
)
|
||||||
self.state.test_attempts += 1
|
self.state.test_attempts += 1
|
||||||
self.state.prompt = f"{original_prompt}. Previous attempt failed with exit code {e.returncode}: {e.output}"
|
self.state.prompt = f"{original_prompt}. Previous attempt failed with exit code {e.returncode}: {e.output}"
|
||||||
self.display_test_failure()
|
self.display_test_failure()
|
||||||
|
|
@ -100,7 +119,9 @@ class TestCommandExecutor:
|
||||||
self.state.test_attempts += 1
|
self.state.test_attempts += 1
|
||||||
self.state.should_break = True
|
self.state.should_break = True
|
||||||
|
|
||||||
def handle_user_response(self, response: str, cmd: str, original_prompt: str) -> None:
|
def handle_user_response(
|
||||||
|
self, response: str, cmd: str, original_prompt: str
|
||||||
|
) -> None:
|
||||||
"""Handle user's response to test prompt.
|
"""Handle user's response to test prompt.
|
||||||
Args:
|
Args:
|
||||||
response: User's response (y/n/a)
|
response: User's response (y/n/a)
|
||||||
|
|
@ -144,23 +165,46 @@ class TestCommandExecutor:
|
||||||
"""
|
"""
|
||||||
if not self.config.get("test_cmd"):
|
if not self.config.get("test_cmd"):
|
||||||
self.state.should_break = True
|
self.state.should_break = True
|
||||||
return self.state.should_break, self.state.prompt, self.state.auto_test, self.state.test_attempts
|
return (
|
||||||
|
self.state.should_break,
|
||||||
|
self.state.prompt,
|
||||||
|
self.state.auto_test,
|
||||||
|
self.state.test_attempts,
|
||||||
|
)
|
||||||
|
|
||||||
cmd = self.config["test_cmd"]
|
cmd = self.config["test_cmd"]
|
||||||
|
|
||||||
if not self.state.auto_test:
|
if not self.state.auto_test:
|
||||||
print()
|
print()
|
||||||
response = ask_human.invoke({"question": "Would you like to run the test command? (y=yes, n=no, a=enable auto-test)"})
|
response = ask_human.invoke(
|
||||||
|
{
|
||||||
|
"question": "Would you like to run the test command? (y=yes, n=no, a=enable auto-test)"
|
||||||
|
}
|
||||||
|
)
|
||||||
self.handle_user_response(response, cmd, self.state.prompt)
|
self.handle_user_response(response, cmd, self.state.prompt)
|
||||||
else:
|
else:
|
||||||
if self.check_max_retries():
|
if self.check_max_retries():
|
||||||
logger.error(f"Maximum number of test retries ({self.max_retries}) reached. Stopping test execution.")
|
logger.error(
|
||||||
console.print(Panel(f"Maximum retries ({self.max_retries}) reached. Test execution stopped.", title="⚠️ Test Execution", border_style="yellow bold"))
|
f"Maximum number of test retries ({self.max_retries}) reached. Stopping test execution."
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
f"Maximum retries ({self.max_retries}) reached. Test execution stopped.",
|
||||||
|
title="⚠️ Test Execution",
|
||||||
|
border_style="yellow bold",
|
||||||
|
)
|
||||||
|
)
|
||||||
self.state.should_break = True
|
self.state.should_break = True
|
||||||
else:
|
else:
|
||||||
self.run_test_command(cmd, self.state.prompt)
|
self.run_test_command(cmd, self.state.prompt)
|
||||||
|
|
||||||
return self.state.should_break, self.state.prompt, self.state.auto_test, self.state.test_attempts
|
return (
|
||||||
|
self.state.should_break,
|
||||||
|
self.state.prompt,
|
||||||
|
self.state.auto_test,
|
||||||
|
self.state.test_attempts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def execute_test_command(
|
def execute_test_command(
|
||||||
config: Dict[str, Any],
|
config: Dict[str, Any],
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,24 @@ from langchain_core.tools import tool
|
||||||
from prompt_toolkit import PromptSession
|
from prompt_toolkit import PromptSession
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def create_keybindings():
|
def create_keybindings():
|
||||||
"""Create custom key bindings for Ctrl+D submission."""
|
"""Create custom key bindings for Ctrl+D submission."""
|
||||||
bindings = KeyBindings()
|
bindings = KeyBindings()
|
||||||
|
|
||||||
@bindings.add('c-d')
|
@bindings.add("c-d")
|
||||||
def submit(event):
|
def submit(event):
|
||||||
"""Trigger submission when Ctrl+D is pressed."""
|
"""Trigger submission when Ctrl+D is pressed."""
|
||||||
event.current_buffer.validate_and_handle()
|
event.current_buffer.validate_and_handle()
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def ask_human(question: str) -> str:
|
def ask_human(question: str) -> str:
|
||||||
"""Ask the human user a question with a nicely formatted display.
|
"""Ask the human user a question with a nicely formatted display.
|
||||||
|
|
@ -28,24 +30,26 @@ def ask_human(question: str) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
The user's response as a string
|
The user's response as a string
|
||||||
"""
|
"""
|
||||||
console.print(Panel(
|
console.print(
|
||||||
Markdown(question + "\n\n*Multiline input is supported; use Ctrl+D to submit. Use Ctrl+C to exit the program.*"),
|
Panel(
|
||||||
title="💭 Question for Human",
|
Markdown(
|
||||||
border_style="yellow bold"
|
question
|
||||||
))
|
+ "\n\n*Multiline input is supported; use Ctrl+D to submit. Use Ctrl+C to exit the program.*"
|
||||||
|
),
|
||||||
|
title="💭 Question for Human",
|
||||||
|
border_style="yellow bold",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
session = PromptSession(
|
session = PromptSession(
|
||||||
multiline=True,
|
multiline=True,
|
||||||
key_bindings=create_keybindings(),
|
key_bindings=create_keybindings(),
|
||||||
prompt_continuation='. ',
|
prompt_continuation=". ",
|
||||||
)
|
)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
response = session.prompt(
|
response = session.prompt("> ", wrap_lines=True)
|
||||||
"> ",
|
|
||||||
wrap_lines=True
|
|
||||||
)
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,45 @@
|
||||||
|
import datetime
|
||||||
|
import fnmatch
|
||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import datetime
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import pathspec
|
import pathspec
|
||||||
from rich.tree import Tree
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
import fnmatch
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.tree import Tree
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DirScanConfig:
|
class DirScanConfig:
|
||||||
"""Configuration for directory scanning"""
|
"""Configuration for directory scanning"""
|
||||||
|
|
||||||
max_depth: int
|
max_depth: int
|
||||||
follow_links: bool
|
follow_links: bool
|
||||||
show_size: bool
|
show_size: bool
|
||||||
show_modified: bool
|
show_modified: bool
|
||||||
exclude_patterns: List[str]
|
exclude_patterns: List[str]
|
||||||
|
|
||||||
|
|
||||||
def format_size(size_bytes: int) -> str:
|
def format_size(size_bytes: int) -> str:
|
||||||
"""Format file size in human readable format"""
|
"""Format file size in human readable format"""
|
||||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
for unit in ["B", "KB", "MB", "GB"]:
|
||||||
if size_bytes < 1024:
|
if size_bytes < 1024:
|
||||||
return f"{size_bytes:.1f}{unit}"
|
return f"{size_bytes:.1f}{unit}"
|
||||||
size_bytes /= 1024
|
size_bytes /= 1024
|
||||||
return f"{size_bytes:.1f}TB"
|
return f"{size_bytes:.1f}TB"
|
||||||
|
|
||||||
|
|
||||||
def format_time(timestamp: float) -> str:
|
def format_time(timestamp: float) -> str:
|
||||||
"""Format timestamp as readable date"""
|
"""Format timestamp as readable date"""
|
||||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||||
return dt.strftime("%Y-%m-%d %H:%M")
|
return dt.strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
|
||||||
# Default patterns to exclude
|
# Default patterns to exclude
|
||||||
DEFAULT_EXCLUDE_PATTERNS = [
|
DEFAULT_EXCLUDE_PATTERNS = [
|
||||||
".*", # Hidden files
|
".*", # Hidden files
|
||||||
|
|
@ -54,6 +60,7 @@ DEFAULT_EXCLUDE_PATTERNS = [
|
||||||
"*.cache", # Cache files
|
"*.cache", # Cache files
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def load_gitignore_patterns(path: Path) -> pathspec.PathSpec:
|
def load_gitignore_patterns(path: Path) -> pathspec.PathSpec:
|
||||||
"""Load gitignore patterns from .gitignore file or use defaults.
|
"""Load gitignore patterns from .gitignore file or use defaults.
|
||||||
|
|
||||||
|
|
@ -63,7 +70,7 @@ def load_gitignore_patterns(path: Path) -> pathspec.PathSpec:
|
||||||
Returns:
|
Returns:
|
||||||
PathSpec object configured with the loaded patterns
|
PathSpec object configured with the loaded patterns
|
||||||
"""
|
"""
|
||||||
gitignore_path = path / '.gitignore'
|
gitignore_path = path / ".gitignore"
|
||||||
patterns = []
|
patterns = []
|
||||||
|
|
||||||
def modify_path(p: str) -> str:
|
def modify_path(p: str) -> str:
|
||||||
|
|
@ -99,20 +106,23 @@ def load_gitignore_patterns(path: Path) -> pathspec.PathSpec:
|
||||||
|
|
||||||
return pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, patterns)
|
return pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, patterns)
|
||||||
|
|
||||||
|
|
||||||
def should_ignore(path: str, spec: pathspec.PathSpec) -> bool:
|
def should_ignore(path: str, spec: pathspec.PathSpec) -> bool:
|
||||||
"""Check if a path should be ignored based on gitignore patterns"""
|
"""Check if a path should be ignored based on gitignore patterns"""
|
||||||
return spec.match_file(path)
|
return spec.match_file(path)
|
||||||
|
|
||||||
|
|
||||||
def should_exclude(name: str, patterns: List[str]) -> bool:
|
def should_exclude(name: str, patterns: List[str]) -> bool:
|
||||||
"""Check if a file/directory name matches any exclude patterns"""
|
"""Check if a file/directory name matches any exclude patterns"""
|
||||||
return any(fnmatch.fnmatch(name, pattern) for pattern in patterns)
|
return any(fnmatch.fnmatch(name, pattern) for pattern in patterns)
|
||||||
|
|
||||||
|
|
||||||
def build_tree(
|
def build_tree(
|
||||||
path: Path,
|
path: Path,
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
config: DirScanConfig,
|
config: DirScanConfig,
|
||||||
current_depth: int = 0,
|
current_depth: int = 0,
|
||||||
spec: Optional[pathspec.PathSpec] = None
|
spec: Optional[pathspec.PathSpec] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Recursively build a Rich tree representation of the directory"""
|
"""Recursively build a Rich tree representation of the directory"""
|
||||||
if current_depth >= config.max_depth:
|
if current_depth >= config.max_depth:
|
||||||
|
|
@ -139,9 +149,7 @@ def build_tree(
|
||||||
try:
|
try:
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
# Add directory node
|
# Add directory node
|
||||||
branch = tree.add(
|
branch = tree.add(f"📁 {entry.name}/")
|
||||||
f"📁 {entry.name}/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Recursively process subdirectory
|
# Recursively process subdirectory
|
||||||
build_tree(entry, branch, config, current_depth + 1, spec)
|
build_tree(entry, branch, config, current_depth + 1, spec)
|
||||||
|
|
@ -165,6 +173,7 @@ def build_tree(
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
tree.add("🔒 (Permission denied)")
|
tree.add("🔒 (Permission denied)")
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def list_directory_tree(
|
def list_directory_tree(
|
||||||
path: str = ".",
|
path: str = ".",
|
||||||
|
|
@ -173,7 +182,7 @@ def list_directory_tree(
|
||||||
follow_links: bool = False,
|
follow_links: bool = False,
|
||||||
show_size: bool = False, # Default to not showing size
|
show_size: bool = False, # Default to not showing size
|
||||||
show_modified: bool = False, # Default to not showing modified time
|
show_modified: bool = False, # Default to not showing modified time
|
||||||
exclude_patterns: List[str] = None
|
exclude_patterns: List[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""List directory contents in a tree format with optional metadata.
|
"""List directory contents in a tree format with optional metadata.
|
||||||
|
|
||||||
|
|
@ -204,7 +213,7 @@ def list_directory_tree(
|
||||||
follow_links=follow_links,
|
follow_links=follow_links,
|
||||||
show_size=show_size,
|
show_size=show_size,
|
||||||
show_modified=show_modified,
|
show_modified=show_modified,
|
||||||
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS + (exclude_patterns or [])
|
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS + (exclude_patterns or []),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build tree
|
# Build tree
|
||||||
|
|
@ -216,10 +225,12 @@ def list_directory_tree(
|
||||||
tree_str = capture.get()
|
tree_str = capture.get()
|
||||||
|
|
||||||
# Display panel
|
# Display panel
|
||||||
console.print(Panel(
|
console.print(
|
||||||
Markdown(f"```\n{tree_str}\n```"),
|
Panel(
|
||||||
title="📂 Directory Tree",
|
Markdown(f"```\n{tree_str}\n```"),
|
||||||
border_style="bright_blue"
|
title="📂 Directory Tree",
|
||||||
))
|
border_style="bright_blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return tree_str
|
return tree_str
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import os
|
import os
|
||||||
from typing import Dict, List, Any, Union, Optional, Set
|
from typing import Any, Dict, List, Optional, Set, Union
|
||||||
from typing_extensions import TypedDict
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from langchain_core.tools import tool
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
|
||||||
class WorkLogEntry(TypedDict):
|
class WorkLogEntry(TypedDict):
|
||||||
|
|
@ -249,7 +250,7 @@ def emit_key_snippets(snippets: List[SnippetInfo]) -> str:
|
||||||
|
|
||||||
# Format display text as markdown
|
# Format display text as markdown
|
||||||
display_text = [
|
display_text = [
|
||||||
f"**Source Location**:",
|
"**Source Location**:",
|
||||||
f"- File: `{snippet_info['filepath']}`",
|
f"- File: `{snippet_info['filepath']}`",
|
||||||
f"- Line: `{snippet_info['line_number']}`",
|
f"- Line: `{snippet_info['line_number']}`",
|
||||||
"", # Empty line before code block
|
"", # Empty line before code block
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,51 @@
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Union
|
from typing import Dict, List, Union
|
||||||
from ra_aid.logging_config import get_logger
|
|
||||||
from ra_aid.tools.memory import _global_memory
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
|
from ra_aid.logging_config import get_logger
|
||||||
from ra_aid.proc.interactive import run_interactive_command
|
from ra_aid.proc.interactive import run_interactive_command
|
||||||
from ra_aid.text.processing import truncate_output
|
from ra_aid.text.processing import truncate_output
|
||||||
|
from ra_aid.tools.memory import _global_memory
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def run_programming_task(instructions: str, files: List[str] = []) -> Dict[str, Union[str, int, bool]]:
|
def run_programming_task(
|
||||||
|
instructions: str, files: List[str] = []
|
||||||
|
) -> Dict[str, Union[str, int, bool]]:
|
||||||
"""Assign a programming task to a human programmer. Use this instead of trying to write code to files yourself.
|
"""Assign a programming task to a human programmer. Use this instead of trying to write code to files yourself.
|
||||||
|
|
||||||
Before using this tool, ensure all related files have been emitted with emit_related_files.
|
Before using this tool, ensure all related files have been emitted with emit_related_files.
|
||||||
|
|
||||||
The programmer sees only what you provide, no conversation history.
|
The programmer sees only what you provide, no conversation history.
|
||||||
|
|
||||||
Give detailed instructions but do not write their code.
|
Give detailed instructions but do not write their code.
|
||||||
|
|
||||||
They are intelligent and can edit multiple files.
|
They are intelligent and can edit multiple files.
|
||||||
|
|
||||||
If new files are created, emit them after finishing.
|
If new files are created, emit them after finishing.
|
||||||
|
|
||||||
They can add/modify files, but not remove. Use run_shell_command to remove files. If referencing files you’ll delete, remove them after they finish.
|
They can add/modify files, but not remove. Use run_shell_command to remove files. If referencing files you’ll delete, remove them after they finish.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
instructions: REQUIRED Programming task instructions (markdown format, use newlines and as many tokens as needed)
|
instructions: REQUIRED Programming task instructions (markdown format, use newlines and as many tokens as needed)
|
||||||
files: Optional; if not provided, uses related_files
|
files: Optional; if not provided, uses related_files
|
||||||
|
|
||||||
Returns: { "output": stdout+stderr, "return_code": 0 if success, "success": True/False }
|
Returns: { "output": stdout+stderr, "return_code": 0 if success, "success": True/False }
|
||||||
"""
|
"""
|
||||||
# Get related files if no specific files provided
|
# Get related files if no specific files provided
|
||||||
file_paths = list(_global_memory['related_files'].values()) if 'related_files' in _global_memory else []
|
file_paths = (
|
||||||
|
list(_global_memory["related_files"].values())
|
||||||
|
if "related_files" in _global_memory
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
# Build command
|
# Build command
|
||||||
command = [
|
command = [
|
||||||
|
|
@ -50,14 +59,14 @@ Returns: { "output": stdout+stderr, "return_code": 0 if success, "success": True
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add config file if specified
|
# Add config file if specified
|
||||||
if 'config' in _global_memory and _global_memory['config'].get('aider_config'):
|
if "config" in _global_memory and _global_memory["config"].get("aider_config"):
|
||||||
command.extend(['--config', _global_memory['config']['aider_config']])
|
command.extend(["--config", _global_memory["config"]["aider_config"]])
|
||||||
|
|
||||||
# if environment variable AIDER_FLAGS exists then parse
|
# if environment variable AIDER_FLAGS exists then parse
|
||||||
if 'AIDER_FLAGS' in os.environ:
|
if "AIDER_FLAGS" in os.environ:
|
||||||
# wrap in try catch in case of any error and log the error
|
# wrap in try catch in case of any error and log the error
|
||||||
try:
|
try:
|
||||||
command.extend(parse_aider_flags(os.environ['AIDER_FLAGS']))
|
command.extend(parse_aider_flags(os.environ["AIDER_FLAGS"]))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error parsing AIDER_FLAGS: {e}")
|
print(f"Error parsing AIDER_FLAGS: {e}")
|
||||||
|
|
||||||
|
|
@ -72,19 +81,21 @@ Returns: { "output": stdout+stderr, "return_code": 0 if success, "success": True
|
||||||
command.extend(files_to_use)
|
command.extend(files_to_use)
|
||||||
|
|
||||||
# Create a pretty display of what we're doing
|
# Create a pretty display of what we're doing
|
||||||
task_display = [
|
task_display = ["## Instructions\n", f"{instructions}\n"]
|
||||||
"## Instructions\n",
|
|
||||||
f"{instructions}\n"
|
|
||||||
]
|
|
||||||
|
|
||||||
if files_to_use:
|
if files_to_use:
|
||||||
task_display.extend([
|
task_display.extend(
|
||||||
"\n## Files\n",
|
["\n## Files\n", *[f"- `{file}`\n" for file in files_to_use]]
|
||||||
*[f"- `{file}`\n" for file in files_to_use]
|
)
|
||||||
])
|
|
||||||
|
|
||||||
markdown_content = "".join(task_display)
|
markdown_content = "".join(task_display)
|
||||||
console.print(Panel(Markdown(markdown_content), title="🤖 Aider Task", border_style="bright_blue"))
|
console.print(
|
||||||
|
Panel(
|
||||||
|
Markdown(markdown_content),
|
||||||
|
title="🤖 Aider Task",
|
||||||
|
border_style="bright_blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
logger.debug(f"command: {command}")
|
logger.debug(f"command: {command}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -97,7 +108,7 @@ Returns: { "output": stdout+stderr, "return_code": 0 if success, "success": True
|
||||||
return {
|
return {
|
||||||
"output": truncate_output(output.decode() if output else ""),
|
"output": truncate_output(output.decode() if output else ""),
|
||||||
"return_code": return_code,
|
"return_code": return_code,
|
||||||
"success": return_code == 0
|
"success": return_code == 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -107,11 +118,8 @@ Returns: { "output": stdout+stderr, "return_code": 0 if success, "success": True
|
||||||
error_text.append(str(e), style="red")
|
error_text.append(str(e), style="red")
|
||||||
console.print(error_text)
|
console.print(error_text)
|
||||||
|
|
||||||
return {
|
return {"output": str(e), "return_code": 1, "success": False}
|
||||||
"output": str(e),
|
|
||||||
"return_code": 1,
|
|
||||||
"success": False
|
|
||||||
}
|
|
||||||
|
|
||||||
def parse_aider_flags(aider_flags: str) -> List[str]:
|
def parse_aider_flags(aider_flags: str) -> List[str]:
|
||||||
"""Parse a string of aider flags into a list of flags.
|
"""Parse a string of aider flags into a list of flags.
|
||||||
|
|
@ -140,5 +148,6 @@ def parse_aider_flags(aider_flags: str) -> List[str]:
|
||||||
# Add '--' prefix if not present and filter out empty flags
|
# Add '--' prefix if not present and filter out empty flags
|
||||||
return [f"--{flag.lstrip('-')}" for flag in flags if flag.strip()]
|
return [f"--{flag.lstrip('-')}" for flag in flags if flag.strip()]
|
||||||
|
|
||||||
|
|
||||||
# Export the functions
|
# Export the functions
|
||||||
__all__ = ['run_programming_task']
|
__all__ = ["run_programming_task"]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import os.path
|
|
||||||
import logging
|
import logging
|
||||||
|
import os.path
|
||||||
import time
|
import time
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
from ra_aid.text.processing import truncate_output
|
from ra_aid.text.processing import truncate_output
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
@ -12,11 +14,9 @@ console = Console()
|
||||||
# Standard buffer size for file reading
|
# Standard buffer size for file reading
|
||||||
CHUNK_SIZE = 8192
|
CHUNK_SIZE = 8192
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def read_file_tool(
|
def read_file_tool(filepath: str, encoding: str = "utf-8") -> Dict[str, str]:
|
||||||
filepath: str,
|
|
||||||
encoding: str = 'utf-8'
|
|
||||||
) -> Dict[str, str]:
|
|
||||||
"""Read and return the contents of a text file.
|
"""Read and return the contents of a text file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -33,7 +33,7 @@ def read_file_tool(
|
||||||
line_count = 0
|
line_count = 0
|
||||||
total_bytes = 0
|
total_bytes = 0
|
||||||
|
|
||||||
with open(filepath, 'r', encoding=encoding) as f:
|
with open(filepath, "r", encoding=encoding) as f:
|
||||||
while True:
|
while True:
|
||||||
chunk = f.read(CHUNK_SIZE)
|
chunk = f.read(CHUNK_SIZE)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
|
|
@ -41,27 +41,31 @@ def read_file_tool(
|
||||||
|
|
||||||
content.append(chunk)
|
content.append(chunk)
|
||||||
total_bytes += len(chunk)
|
total_bytes += len(chunk)
|
||||||
line_count += chunk.count('\n')
|
line_count += chunk.count("\n")
|
||||||
|
|
||||||
logging.debug(f"Read chunk: {len(chunk)} bytes, running total: {total_bytes} bytes")
|
logging.debug(
|
||||||
|
f"Read chunk: {len(chunk)} bytes, running total: {total_bytes} bytes"
|
||||||
|
)
|
||||||
|
|
||||||
full_content = ''.join(content)
|
full_content = "".join(content)
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
logging.debug(f"File read complete: {total_bytes} bytes in {elapsed:.2f}s")
|
logging.debug(f"File read complete: {total_bytes} bytes in {elapsed:.2f}s")
|
||||||
logging.debug(f"Pre-truncation stats: {total_bytes} bytes, {line_count} lines")
|
logging.debug(f"Pre-truncation stats: {total_bytes} bytes, {line_count} lines")
|
||||||
|
|
||||||
console.print(Panel(
|
console.print(
|
||||||
f"Read {line_count} lines ({total_bytes} bytes) from {filepath} in {elapsed:.2f}s",
|
Panel(
|
||||||
title="📄 File Read",
|
f"Read {line_count} lines ({total_bytes} bytes) from {filepath} in {elapsed:.2f}s",
|
||||||
border_style="bright_blue"
|
title="📄 File Read",
|
||||||
))
|
border_style="bright_blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Truncate if needed
|
# Truncate if needed
|
||||||
truncated = truncate_output(full_content) if full_content else ""
|
truncated = truncate_output(full_content) if full_content else ""
|
||||||
|
|
||||||
return {"content": truncated}
|
return {"content": truncated}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ This module provides utilities for:
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
__all__ = ['get_function_info']
|
__all__ = ["get_function_info"]
|
||||||
|
|
||||||
|
|
||||||
def get_function_info(func):
|
def get_function_info(func):
|
||||||
|
|
@ -32,5 +32,3 @@ def get_function_info(func):
|
||||||
{docstring}
|
{docstring}
|
||||||
\"\"\""""
|
\"\"\""""
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from rich.panel import Panel
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@tool("existing_project_detected")
|
@tool("existing_project_detected")
|
||||||
def existing_project_detected() -> dict:
|
def existing_project_detected() -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -11,7 +12,7 @@ def existing_project_detected() -> dict:
|
||||||
"""
|
"""
|
||||||
console.print(Panel("📁 Existing Project Detected", style="bright_blue", padding=0))
|
console.print(Panel("📁 Existing Project Detected", style="bright_blue", padding=0))
|
||||||
return {
|
return {
|
||||||
'hint': (
|
"hint": (
|
||||||
"You are working within an existing codebase that already has established patterns and standards. "
|
"You are working within an existing codebase that already has established patterns and standards. "
|
||||||
"Integrate any new functionality by adhering to the project's conventions:\n\n"
|
"Integrate any new functionality by adhering to the project's conventions:\n\n"
|
||||||
"- Carefully discover existing folder structure, naming conventions, and architecture.\n"
|
"- Carefully discover existing folder structure, naming conventions, and architecture.\n"
|
||||||
|
|
@ -23,6 +24,7 @@ def existing_project_detected() -> dict:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@tool("monorepo_detected")
|
@tool("monorepo_detected")
|
||||||
def monorepo_detected() -> dict:
|
def monorepo_detected() -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -30,7 +32,7 @@ def monorepo_detected() -> dict:
|
||||||
"""
|
"""
|
||||||
console.print(Panel("📦 Monorepo Detected", style="bright_blue", padding=0))
|
console.print(Panel("📦 Monorepo Detected", style="bright_blue", padding=0))
|
||||||
return {
|
return {
|
||||||
'hint': (
|
"hint": (
|
||||||
"You are researching in a monorepo environment that manages multiple packages or services under one roof. "
|
"You are researching in a monorepo environment that manages multiple packages or services under one roof. "
|
||||||
"Ensure new work fits cohesively within the broader structure:\n\n"
|
"Ensure new work fits cohesively within the broader structure:\n\n"
|
||||||
"- Search all packages for shared libraries, utilities, and patterns, and reuse them to avoid redundancy.\n"
|
"- Search all packages for shared libraries, utilities, and patterns, and reuse them to avoid redundancy.\n"
|
||||||
|
|
@ -45,6 +47,7 @@ def monorepo_detected() -> dict:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@tool("ui_detected")
|
@tool("ui_detected")
|
||||||
def ui_detected() -> dict:
|
def ui_detected() -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -52,7 +55,7 @@ def ui_detected() -> dict:
|
||||||
"""
|
"""
|
||||||
console.print(Panel("🎯 UI Detected", style="bright_blue", padding=0))
|
console.print(Panel("🎯 UI Detected", style="bright_blue", padding=0))
|
||||||
return {
|
return {
|
||||||
'hint': (
|
"hint": (
|
||||||
"You are working with a user interface component where established UI conventions, styles, and frameworks are likely in place. "
|
"You are working with a user interface component where established UI conventions, styles, and frameworks are likely in place. "
|
||||||
"Any modifications or additions should blend seamlessly with the existing design and user experience:\n\n"
|
"Any modifications or additions should blend seamlessly with the existing design and user experience:\n\n"
|
||||||
"- Locate and note existing UI design conventions, including layout, typography, color schemes, and interaction patterns.\n"
|
"- Locate and note existing UI design conventions, including layout, typography, color schemes, and interaction patterns.\n"
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,29 @@
|
||||||
from typing import Dict, Union, List
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
from ra_aid.proc.interactive import run_interactive_command
|
from ra_aid.proc.interactive import run_interactive_command
|
||||||
from ra_aid.text.processing import truncate_output
|
from ra_aid.text.processing import truncate_output
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
DEFAULT_EXCLUDE_DIRS = [
|
DEFAULT_EXCLUDE_DIRS = [
|
||||||
'.git',
|
".git",
|
||||||
'node_modules',
|
"node_modules",
|
||||||
'vendor',
|
"vendor",
|
||||||
'.venv',
|
".venv",
|
||||||
'__pycache__',
|
"__pycache__",
|
||||||
'.cache',
|
".cache",
|
||||||
'dist',
|
"dist",
|
||||||
'build',
|
"build",
|
||||||
'env',
|
"env",
|
||||||
'.env',
|
".env",
|
||||||
'venv',
|
"venv",
|
||||||
'.idea',
|
".idea",
|
||||||
'.vscode'
|
".vscode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,6 +66,7 @@ FILE_TYPE_MAP = {
|
||||||
"psql": "postgres",
|
"psql": "postgres",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def ripgrep_search(
|
def ripgrep_search(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
|
|
@ -72,7 +75,7 @@ def ripgrep_search(
|
||||||
case_sensitive: bool = True,
|
case_sensitive: bool = True,
|
||||||
include_hidden: bool = False,
|
include_hidden: bool = False,
|
||||||
follow_links: bool = False,
|
follow_links: bool = False,
|
||||||
exclude_dirs: List[str] = None
|
exclude_dirs: List[str] = None,
|
||||||
) -> Dict[str, Union[str, int, bool]]:
|
) -> Dict[str, Union[str, int, bool]]:
|
||||||
"""Execute a ripgrep (rg) search with formatting and common options.
|
"""Execute a ripgrep (rg) search with formatting and common options.
|
||||||
|
|
||||||
|
|
@ -91,16 +94,16 @@ def ripgrep_search(
|
||||||
- success: Boolean indicating if search succeeded
|
- success: Boolean indicating if search succeeded
|
||||||
"""
|
"""
|
||||||
# Build rg command with options
|
# Build rg command with options
|
||||||
cmd = ['rg', '--color', 'always']
|
cmd = ["rg", "--color", "always"]
|
||||||
|
|
||||||
if not case_sensitive:
|
if not case_sensitive:
|
||||||
cmd.append('-i')
|
cmd.append("-i")
|
||||||
|
|
||||||
if include_hidden:
|
if include_hidden:
|
||||||
cmd.append('--hidden')
|
cmd.append("--hidden")
|
||||||
|
|
||||||
if follow_links:
|
if follow_links:
|
||||||
cmd.append('--follow')
|
cmd.append("--follow")
|
||||||
|
|
||||||
if file_type:
|
if file_type:
|
||||||
if FILE_TYPE_MAP.get(file_type):
|
if FILE_TYPE_MAP.get(file_type):
|
||||||
|
|
@ -110,7 +113,7 @@ def ripgrep_search(
|
||||||
# Add exclusions
|
# Add exclusions
|
||||||
exclusions = DEFAULT_EXCLUDE_DIRS + (exclude_dirs or [])
|
exclusions = DEFAULT_EXCLUDE_DIRS + (exclude_dirs or [])
|
||||||
for dir in exclusions:
|
for dir in exclusions:
|
||||||
cmd.extend(['--glob', f'!{dir}'])
|
cmd.extend(["--glob", f"!{dir}"])
|
||||||
|
|
||||||
# Add the search pattern
|
# Add the search pattern
|
||||||
cmd.append(pattern)
|
cmd.append(pattern)
|
||||||
|
|
@ -123,7 +126,7 @@ def ripgrep_search(
|
||||||
"## Search Parameters",
|
"## Search Parameters",
|
||||||
f"**Pattern**: `{pattern}`",
|
f"**Pattern**: `{pattern}`",
|
||||||
f"**Case Sensitive**: {case_sensitive}",
|
f"**Case Sensitive**: {case_sensitive}",
|
||||||
f"**File Type**: {file_type or 'all'}"
|
f"**File Type**: {file_type or 'all'}",
|
||||||
]
|
]
|
||||||
if include_hidden:
|
if include_hidden:
|
||||||
params.append("**Including Hidden Files**: yes")
|
params.append("**Including Hidden Files**: yes")
|
||||||
|
|
@ -136,7 +139,13 @@ def ripgrep_search(
|
||||||
info_sections.append("\n".join(params))
|
info_sections.append("\n".join(params))
|
||||||
|
|
||||||
# Execute command
|
# Execute command
|
||||||
console.print(Panel(Markdown(f"Searching for: **{pattern}**"), title="🔎 Ripgrep Search", border_style="bright_blue"))
|
console.print(
|
||||||
|
Panel(
|
||||||
|
Markdown(f"Searching for: **{pattern}**"),
|
||||||
|
title="🔎 Ripgrep Search",
|
||||||
|
border_style="bright_blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
print()
|
print()
|
||||||
output, return_code = run_interactive_command(cmd)
|
output, return_code = run_interactive_command(cmd)
|
||||||
|
|
@ -146,14 +155,10 @@ def ripgrep_search(
|
||||||
return {
|
return {
|
||||||
"output": truncate_output(decoded_output),
|
"output": truncate_output(decoded_output),
|
||||||
"return_code": return_code,
|
"return_code": return_code,
|
||||||
"success": return_code == 0
|
"success": return_code == 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
console.print(Panel(error_msg, title="❌ Error", border_style="red"))
|
console.print(Panel(error_msg, title="❌ Error", border_style="red"))
|
||||||
return {
|
return {"output": error_msg, "return_code": 1, "success": False}
|
||||||
"output": error_msg,
|
|
||||||
"return_code": 1,
|
|
||||||
"success": False
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
from ra_aid.tools.memory import _global_memory
|
|
||||||
|
from ra_aid.console.cowboy_messages import get_cowboy_message
|
||||||
from ra_aid.proc.interactive import run_interactive_command
|
from ra_aid.proc.interactive import run_interactive_command
|
||||||
from ra_aid.text.processing import truncate_output
|
from ra_aid.text.processing import truncate_output
|
||||||
from ra_aid.console.cowboy_messages import get_cowboy_message
|
from ra_aid.tools.memory import _global_memory
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def run_shell_command(command: str) -> Dict[str, Union[str, int, bool]]:
|
def run_shell_command(command: str) -> Dict[str, Union[str, int, bool]]:
|
||||||
"""Execute a shell command and return its output.
|
"""Execute a shell command and return its output.
|
||||||
|
|
@ -28,7 +31,7 @@ def run_shell_command(command: str) -> Dict[str, Union[str, int, bool]]:
|
||||||
4. Add flags e.g. git --no-pager in order to reduce interaction required by the human.
|
4. Add flags e.g. git --no-pager in order to reduce interaction required by the human.
|
||||||
"""
|
"""
|
||||||
# Check if we need approval
|
# Check if we need approval
|
||||||
cowboy_mode = _global_memory.get('config', {}).get('cowboy_mode', False)
|
cowboy_mode = _global_memory.get("config", {}).get("cowboy_mode", False)
|
||||||
|
|
||||||
if cowboy_mode:
|
if cowboy_mode:
|
||||||
console.print("")
|
console.print("")
|
||||||
|
|
@ -45,7 +48,7 @@ def run_shell_command(command: str) -> Dict[str, Union[str, int, bool]]:
|
||||||
choices=choices,
|
choices=choices,
|
||||||
default="y",
|
default="y",
|
||||||
show_choices=True,
|
show_choices=True,
|
||||||
show_default=True
|
show_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response == "n":
|
if response == "n":
|
||||||
|
|
@ -53,28 +56,24 @@ def run_shell_command(command: str) -> Dict[str, Union[str, int, bool]]:
|
||||||
return {
|
return {
|
||||||
"output": "Command execution cancelled by user",
|
"output": "Command execution cancelled by user",
|
||||||
"return_code": 1,
|
"return_code": 1,
|
||||||
"success": False
|
"success": False,
|
||||||
}
|
}
|
||||||
elif response == "c":
|
elif response == "c":
|
||||||
_global_memory['config']['cowboy_mode'] = True
|
_global_memory["config"]["cowboy_mode"] = True
|
||||||
console.print("")
|
console.print("")
|
||||||
console.print(" " + get_cowboy_message())
|
console.print(" " + get_cowboy_message())
|
||||||
console.print("")
|
console.print("")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print()
|
print()
|
||||||
output, return_code = run_interactive_command(['/bin/bash', '-c', command])
|
output, return_code = run_interactive_command(["/bin/bash", "-c", command])
|
||||||
print()
|
print()
|
||||||
return {
|
return {
|
||||||
"output": truncate_output(output.decode()) if output else "",
|
"output": truncate_output(output.decode()) if output else "",
|
||||||
"return_code": return_code,
|
"return_code": return_code,
|
||||||
"success": return_code == 0
|
"success": return_code == 0,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print()
|
print()
|
||||||
console.print(Panel(str(e), title="❌ Error", border_style="red"))
|
console.print(Panel(str(e), title="❌ Error", border_style="red"))
|
||||||
return {
|
return {"output": str(e), "return_code": 1, "success": False}
|
||||||
"output": str(e),
|
|
||||||
"return_code": 1,
|
|
||||||
"success": False
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import os
|
import os
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from tavily import TavilyClient
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
|
from tavily import TavilyClient
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def web_search_tavily(query: str) -> Dict:
|
def web_search_tavily(query: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -20,6 +22,8 @@ def web_search_tavily(query: str) -> Dict:
|
||||||
Dict containing search results from Tavily
|
Dict containing search results from Tavily
|
||||||
"""
|
"""
|
||||||
client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
|
client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
|
||||||
console.print(Panel(Markdown(query), title="🔍 Searching Tavily", border_style="bright_blue"))
|
console.print(
|
||||||
|
Panel(Markdown(query), title="🔍 Searching Tavily", border_style="bright_blue")
|
||||||
|
)
|
||||||
search_result = client.search(query=query)
|
search_result = client.search(query=query)
|
||||||
return search_result
|
return search_result
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def write_file_tool(
|
def write_file_tool(
|
||||||
filepath: str,
|
filepath: str, content: str, encoding: str = "utf-8", verbose: bool = True
|
||||||
content: str,
|
|
||||||
encoding: str = 'utf-8',
|
|
||||||
verbose: bool = True
|
|
||||||
) -> Dict[str, any]:
|
) -> Dict[str, any]:
|
||||||
"""Write content to a text file.
|
"""Write content to a text file.
|
||||||
|
|
||||||
|
|
@ -40,7 +39,7 @@ def write_file_tool(
|
||||||
"elapsed_time": 0,
|
"elapsed_time": 0,
|
||||||
"error": None,
|
"error": None,
|
||||||
"filepath": None,
|
"filepath": None,
|
||||||
"message": None
|
"message": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -51,7 +50,7 @@ def write_file_tool(
|
||||||
|
|
||||||
logging.debug(f"Starting to write file: {filepath}")
|
logging.debug(f"Starting to write file: {filepath}")
|
||||||
|
|
||||||
with open(filepath, 'w', encoding=encoding) as f:
|
with open(filepath, "w", encoding=encoding) as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
result["bytes_written"] = len(content.encode(encoding))
|
result["bytes_written"] = len(content.encode(encoding))
|
||||||
|
|
||||||
|
|
@ -61,14 +60,18 @@ def write_file_tool(
|
||||||
result["filepath"] = filepath
|
result["filepath"] = filepath
|
||||||
result["message"] = "Operation completed successfully"
|
result["message"] = "Operation completed successfully"
|
||||||
|
|
||||||
logging.debug(f"File write complete: {result['bytes_written']} bytes in {elapsed:.2f}s")
|
logging.debug(
|
||||||
|
f"File write complete: {result['bytes_written']} bytes in {elapsed:.2f}s"
|
||||||
|
)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
console.print(Panel(
|
console.print(
|
||||||
f"Wrote {result['bytes_written']} bytes to {filepath} in {elapsed:.2f}s",
|
Panel(
|
||||||
title="💾 File Write",
|
f"Wrote {result['bytes_written']} bytes to {filepath} in {elapsed:.2f}s",
|
||||||
border_style="bright_green"
|
title="💾 File Write",
|
||||||
))
|
border_style="bright_green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
|
|
@ -82,10 +85,12 @@ def write_file_tool(
|
||||||
result["message"] = error_msg
|
result["message"] = error_msg
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
console.print(Panel(
|
console.print(
|
||||||
f"Failed to write {filepath}\nError: {error_msg}",
|
Panel(
|
||||||
title="❌ File Write Error",
|
f"Failed to write {filepath}\nError: {error_msg}",
|
||||||
border_style="red"
|
title="❌ File Write Error",
|
||||||
))
|
border_style="red",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ Usage:
|
||||||
python extract_changelog.py VERSION
|
python extract_changelog.py VERSION
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Dict, Any, List
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from git import Repo
|
from git import Repo
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
|
@ -44,15 +44,13 @@ def setup_logging(log_dir: Path, verbose: bool = False) -> None:
|
||||||
file_handler = logging.FileHandler(log_file)
|
file_handler = logging.FileHandler(log_file)
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
file_formatter = logging.Formatter(
|
file_formatter = logging.Formatter(
|
||||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
)
|
)
|
||||||
file_handler.setFormatter(file_formatter)
|
file_handler.setFormatter(file_formatter)
|
||||||
root_logger.addHandler(file_handler)
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
console_handler = RichHandler(
|
console_handler = RichHandler(
|
||||||
rich_tracebacks=True,
|
rich_tracebacks=True, show_time=False, show_path=False
|
||||||
show_time=False,
|
|
||||||
show_path=False
|
|
||||||
)
|
)
|
||||||
console_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
|
console_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
|
@ -62,6 +60,7 @@ def load_dataset_safely() -> Optional[Any]:
|
||||||
"""Load SWE-bench Lite dataset with error handling."""
|
"""Load SWE-bench Lite dataset with error handling."""
|
||||||
try:
|
try:
|
||||||
from datasets import load_dataset
|
from datasets import load_dataset
|
||||||
|
|
||||||
dataset = load_dataset("princeton-nlp/SWE-bench_Lite")
|
dataset = load_dataset("princeton-nlp/SWE-bench_Lite")
|
||||||
return dataset
|
return dataset
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -122,18 +121,14 @@ def uv_run_raaid(repo_dir: Path, prompt: str) -> Optional[str]:
|
||||||
streaming output directly to the console (capture_output=False).
|
streaming output directly to the console (capture_output=False).
|
||||||
Returns the patch if successful, else None.
|
Returns the patch if successful, else None.
|
||||||
"""
|
"""
|
||||||
cmd = [
|
cmd = ["uv", "run", "ra-aid", "--cowboy-mode", "-m", prompt]
|
||||||
"uv", "run", "ra-aid",
|
|
||||||
"--cowboy-mode",
|
|
||||||
"-m", prompt
|
|
||||||
]
|
|
||||||
# We are NOT capturing output, so it streams live:
|
# We are NOT capturing output, so it streams live:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
cwd=repo_dir,
|
cwd=repo_dir,
|
||||||
text=True,
|
text=True,
|
||||||
check=False, # We manually handle exit code
|
check=False, # We manually handle exit code
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logging.error("ra-aid returned non-zero exit code.")
|
logging.error("ra-aid returned non-zero exit code.")
|
||||||
|
|
@ -160,7 +155,7 @@ def get_git_patch(repo_dir: Path) -> Optional[str]:
|
||||||
patch_text = repo.git.diff(unified=3)
|
patch_text = repo.git.diff(unified=3)
|
||||||
if not patch_text.strip():
|
if not patch_text.strip():
|
||||||
return None
|
return None
|
||||||
if not any(line.startswith('+') for line in patch_text.splitlines()):
|
if not any(line.startswith("+") for line in patch_text.splitlines()):
|
||||||
return None
|
return None
|
||||||
return patch_text
|
return patch_text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -214,7 +209,9 @@ def setup_venv_and_deps(repo_dir: Path, repo_name: str, force_venv: bool) -> Non
|
||||||
uv_pip_install(repo_dir, ["-e", "."])
|
uv_pip_install(repo_dir, ["-e", "."])
|
||||||
|
|
||||||
|
|
||||||
def build_prompt(problem_statement: str, fail_tests: List[str], pass_tests: List[str]) -> str:
|
def build_prompt(
|
||||||
|
problem_statement: str, fail_tests: List[str], pass_tests: List[str]
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Construct the prompt text from problem_statement, FAIL_TO_PASS, PASS_TO_PASS.
|
Construct the prompt text from problem_statement, FAIL_TO_PASS, PASS_TO_PASS.
|
||||||
"""
|
"""
|
||||||
|
|
@ -232,10 +229,7 @@ def build_prompt(problem_statement: str, fail_tests: List[str], pass_tests: List
|
||||||
|
|
||||||
|
|
||||||
def process_instance(
|
def process_instance(
|
||||||
instance: Dict[str, Any],
|
instance: Dict[str, Any], projects_dir: Path, reuse_repo: bool, force_venv: bool
|
||||||
projects_dir: Path,
|
|
||||||
reuse_repo: bool,
|
|
||||||
force_venv: bool
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Process a single dataset instance without a progress bar/spinner.
|
Process a single dataset instance without a progress bar/spinner.
|
||||||
|
|
@ -291,7 +285,7 @@ def process_instance(
|
||||||
return {
|
return {
|
||||||
"instance_id": inst_id,
|
"instance_id": inst_id,
|
||||||
"model_patch": patch if patch else "",
|
"model_patch": patch if patch else "",
|
||||||
"model_name_or_path": "ra-aid"
|
"model_name_or_path": "ra-aid",
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -299,7 +293,7 @@ def process_instance(
|
||||||
return {
|
return {
|
||||||
"instance_id": inst_id,
|
"instance_id": inst_id,
|
||||||
"model_patch": "",
|
"model_patch": "",
|
||||||
"model_name_or_path": "ra-aid"
|
"model_name_or_path": "ra-aid",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -308,41 +302,33 @@ def main() -> None:
|
||||||
description="Generate predictions for SWE-bench Lite using uv + ra-aid (no progress bar)."
|
description="Generate predictions for SWE-bench Lite using uv + ra-aid (no progress bar)."
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"output_dir",
|
"output_dir", type=Path, help="Directory to store prediction file"
|
||||||
type=Path,
|
|
||||||
help="Directory to store prediction file"
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--projects-dir",
|
"--projects-dir",
|
||||||
type=Path,
|
type=Path,
|
||||||
required=True,
|
required=True,
|
||||||
help="Directory where projects will be cloned."
|
help="Directory where projects will be cloned.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--num-instances",
|
"--num-instances",
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="Number of instances to process (default: all)"
|
help="Number of instances to process (default: all)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--reuse-repo",
|
"--reuse-repo",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="If set, do not delete an existing repo directory. We'll reuse it."
|
help="If set, do not delete an existing repo directory. We'll reuse it.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--force-venv",
|
"--force-venv",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="If set, recreate the .venv even if it exists."
|
help="If set, recreate the .venv even if it exists.",
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--verbose",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable verbose logging"
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
from datasets import load_dataset
|
|
||||||
|
|
||||||
# Create base/log dirs and set up logging
|
# Create base/log dirs and set up logging
|
||||||
base_dir, log_dir = create_output_dirs()
|
base_dir, log_dir = create_output_dirs()
|
||||||
setup_logging(log_dir, args.verbose)
|
setup_logging(log_dir, args.verbose)
|
||||||
|
|
@ -373,7 +359,9 @@ def main() -> None:
|
||||||
break
|
break
|
||||||
|
|
||||||
logging.info(f"=== Instance {i+1}/{limit}, ID={inst.get('instance_id')} ===")
|
logging.info(f"=== Instance {i+1}/{limit}, ID={inst.get('instance_id')} ===")
|
||||||
pred = process_instance(inst, args.projects_dir, args.reuse_repo, args.force_venv)
|
pred = process_instance(
|
||||||
|
inst, args.projects_dir, args.reuse_repo, args.force_venv
|
||||||
|
)
|
||||||
predictions.append(pred)
|
predictions.append(pred)
|
||||||
|
|
||||||
# Save predictions
|
# Save predictions
|
||||||
|
|
@ -389,6 +377,6 @@ if __name__ == "__main__":
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nOperation cancelled by user.")
|
print("\nOperation cancelled by user.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("Unhandled error occurred.")
|
logging.exception("Unhandled error occurred.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch
|
from langchain_core.messages import AIMessage, HumanMessage
|
||||||
from langchain_core.messages import HumanMessage, AIMessage
|
|
||||||
from ra_aid.agents.ciayn_agent import CiaynAgent
|
from ra_aid.agents.ciayn_agent import CiaynAgent, validate_function_call_pattern
|
||||||
from ra_aid.agents.ciayn_agent import validate_function_call_pattern
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_model():
|
def mock_model():
|
||||||
|
|
@ -11,23 +13,25 @@ def mock_model():
|
||||||
model.invoke = Mock()
|
model.invoke = Mock()
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def agent(mock_model):
|
def agent(mock_model):
|
||||||
"""Create a CiaynAgent instance with mock model."""
|
"""Create a CiaynAgent instance with mock model."""
|
||||||
tools = [] # Empty tools list for testing trimming functionality
|
tools = [] # Empty tools list for testing trimming functionality
|
||||||
return CiaynAgent(mock_model, tools, max_history_messages=3)
|
return CiaynAgent(mock_model, tools, max_history_messages=3)
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_preserves_initial_messages(agent):
|
def test_trim_chat_history_preserves_initial_messages(agent):
|
||||||
"""Test that initial messages are preserved during trimming."""
|
"""Test that initial messages are preserved during trimming."""
|
||||||
initial_messages = [
|
initial_messages = [
|
||||||
HumanMessage(content="Initial 1"),
|
HumanMessage(content="Initial 1"),
|
||||||
AIMessage(content="Initial 2")
|
AIMessage(content="Initial 2"),
|
||||||
]
|
]
|
||||||
chat_history = [
|
chat_history = [
|
||||||
HumanMessage(content="Chat 1"),
|
HumanMessage(content="Chat 1"),
|
||||||
AIMessage(content="Chat 2"),
|
AIMessage(content="Chat 2"),
|
||||||
HumanMessage(content="Chat 3"),
|
HumanMessage(content="Chat 3"),
|
||||||
AIMessage(content="Chat 4")
|
AIMessage(content="Chat 4"),
|
||||||
]
|
]
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
@ -38,13 +42,11 @@ def test_trim_chat_history_preserves_initial_messages(agent):
|
||||||
assert len(result[2:]) == 3
|
assert len(result[2:]) == 3
|
||||||
assert result[2:] == chat_history[-3:]
|
assert result[2:] == chat_history[-3:]
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_under_limit(agent):
|
def test_trim_chat_history_under_limit(agent):
|
||||||
"""Test trimming when chat history is under the maximum limit."""
|
"""Test trimming when chat history is under the maximum limit."""
|
||||||
initial_messages = [HumanMessage(content="Initial")]
|
initial_messages = [HumanMessage(content="Initial")]
|
||||||
chat_history = [
|
chat_history = [HumanMessage(content="Chat 1"), AIMessage(content="Chat 2")]
|
||||||
HumanMessage(content="Chat 1"),
|
|
||||||
AIMessage(content="Chat 2")
|
|
||||||
]
|
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
||||||
|
|
@ -52,6 +54,7 @@ def test_trim_chat_history_under_limit(agent):
|
||||||
assert len(result) == 3
|
assert len(result) == 3
|
||||||
assert result == initial_messages + chat_history
|
assert result == initial_messages + chat_history
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_over_limit(agent):
|
def test_trim_chat_history_over_limit(agent):
|
||||||
"""Test trimming when chat history exceeds the maximum limit."""
|
"""Test trimming when chat history exceeds the maximum limit."""
|
||||||
initial_messages = [HumanMessage(content="Initial")]
|
initial_messages = [HumanMessage(content="Initial")]
|
||||||
|
|
@ -60,7 +63,7 @@ def test_trim_chat_history_over_limit(agent):
|
||||||
AIMessage(content="Chat 2"),
|
AIMessage(content="Chat 2"),
|
||||||
HumanMessage(content="Chat 3"),
|
HumanMessage(content="Chat 3"),
|
||||||
AIMessage(content="Chat 4"),
|
AIMessage(content="Chat 4"),
|
||||||
HumanMessage(content="Chat 5")
|
HumanMessage(content="Chat 5"),
|
||||||
]
|
]
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
@ -70,6 +73,7 @@ def test_trim_chat_history_over_limit(agent):
|
||||||
assert result[0] == initial_messages[0] # Initial message preserved
|
assert result[0] == initial_messages[0] # Initial message preserved
|
||||||
assert result[1:] == chat_history[-3:] # Last 3 chat messages kept
|
assert result[1:] == chat_history[-3:] # Last 3 chat messages kept
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_empty_initial(agent):
|
def test_trim_chat_history_empty_initial(agent):
|
||||||
"""Test trimming with empty initial messages."""
|
"""Test trimming with empty initial messages."""
|
||||||
initial_messages = []
|
initial_messages = []
|
||||||
|
|
@ -77,7 +81,7 @@ def test_trim_chat_history_empty_initial(agent):
|
||||||
HumanMessage(content="Chat 1"),
|
HumanMessage(content="Chat 1"),
|
||||||
AIMessage(content="Chat 2"),
|
AIMessage(content="Chat 2"),
|
||||||
HumanMessage(content="Chat 3"),
|
HumanMessage(content="Chat 3"),
|
||||||
AIMessage(content="Chat 4")
|
AIMessage(content="Chat 4"),
|
||||||
]
|
]
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
@ -86,11 +90,12 @@ def test_trim_chat_history_empty_initial(agent):
|
||||||
assert len(result) == 3
|
assert len(result) == 3
|
||||||
assert result == chat_history[-3:]
|
assert result == chat_history[-3:]
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_empty_chat(agent):
|
def test_trim_chat_history_empty_chat(agent):
|
||||||
"""Test trimming with empty chat history."""
|
"""Test trimming with empty chat history."""
|
||||||
initial_messages = [
|
initial_messages = [
|
||||||
HumanMessage(content="Initial 1"),
|
HumanMessage(content="Initial 1"),
|
||||||
AIMessage(content="Initial 2")
|
AIMessage(content="Initial 2"),
|
||||||
]
|
]
|
||||||
chat_history = []
|
chat_history = []
|
||||||
|
|
||||||
|
|
@ -100,15 +105,16 @@ def test_trim_chat_history_empty_chat(agent):
|
||||||
assert result == initial_messages
|
assert result == initial_messages
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_token_limit():
|
def test_trim_chat_history_token_limit():
|
||||||
"""Test trimming based on token limit."""
|
"""Test trimming based on token limit."""
|
||||||
agent = CiaynAgent(Mock(), [], max_history_messages=10, max_tokens=20)
|
agent = CiaynAgent(Mock(), [], max_history_messages=10, max_tokens=20)
|
||||||
|
|
||||||
initial_messages = [HumanMessage(content="Initial")] # ~2 tokens
|
initial_messages = [HumanMessage(content="Initial")] # ~2 tokens
|
||||||
chat_history = [
|
chat_history = [
|
||||||
HumanMessage(content="A" * 40), # ~10 tokens
|
HumanMessage(content="A" * 40), # ~10 tokens
|
||||||
AIMessage(content="B" * 40), # ~10 tokens
|
AIMessage(content="B" * 40), # ~10 tokens
|
||||||
HumanMessage(content="C" * 40) # ~10 tokens
|
HumanMessage(content="C" * 40), # ~10 tokens
|
||||||
]
|
]
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
@ -118,6 +124,7 @@ def test_trim_chat_history_token_limit():
|
||||||
assert result[0] == initial_messages[0]
|
assert result[0] == initial_messages[0]
|
||||||
assert result[1] == chat_history[-1]
|
assert result[1] == chat_history[-1]
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_no_token_limit():
|
def test_trim_chat_history_no_token_limit():
|
||||||
"""Test trimming with no token limit set."""
|
"""Test trimming with no token limit set."""
|
||||||
agent = CiaynAgent(Mock(), [], max_history_messages=2, max_tokens=None)
|
agent = CiaynAgent(Mock(), [], max_history_messages=2, max_tokens=None)
|
||||||
|
|
@ -126,7 +133,7 @@ def test_trim_chat_history_no_token_limit():
|
||||||
chat_history = [
|
chat_history = [
|
||||||
HumanMessage(content="A" * 1000),
|
HumanMessage(content="A" * 1000),
|
||||||
AIMessage(content="B" * 1000),
|
AIMessage(content="B" * 1000),
|
||||||
HumanMessage(content="C" * 1000)
|
HumanMessage(content="C" * 1000),
|
||||||
]
|
]
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
@ -136,16 +143,17 @@ def test_trim_chat_history_no_token_limit():
|
||||||
assert result[0] == initial_messages[0]
|
assert result[0] == initial_messages[0]
|
||||||
assert result[1:] == chat_history[-2:]
|
assert result[1:] == chat_history[-2:]
|
||||||
|
|
||||||
|
|
||||||
def test_trim_chat_history_both_limits():
|
def test_trim_chat_history_both_limits():
|
||||||
"""Test trimming with both message count and token limits."""
|
"""Test trimming with both message count and token limits."""
|
||||||
agent = CiaynAgent(Mock(), [], max_history_messages=3, max_tokens=15)
|
agent = CiaynAgent(Mock(), [], max_history_messages=3, max_tokens=15)
|
||||||
|
|
||||||
initial_messages = [HumanMessage(content="Init")] # ~1 token
|
initial_messages = [HumanMessage(content="Init")] # ~1 token
|
||||||
chat_history = [
|
chat_history = [
|
||||||
HumanMessage(content="A" * 40), # ~10 tokens
|
HumanMessage(content="A" * 40), # ~10 tokens
|
||||||
AIMessage(content="B" * 40), # ~10 tokens
|
AIMessage(content="B" * 40), # ~10 tokens
|
||||||
HumanMessage(content="C" * 40), # ~10 tokens
|
HumanMessage(content="C" * 40), # ~10 tokens
|
||||||
AIMessage(content="D" * 40) # ~10 tokens
|
AIMessage(content="D" * 40), # ~10 tokens
|
||||||
]
|
]
|
||||||
|
|
||||||
result = agent._trim_chat_history(initial_messages, chat_history)
|
result = agent._trim_chat_history(initial_messages, chat_history)
|
||||||
|
|
@ -158,46 +166,58 @@ def test_trim_chat_history_both_limits():
|
||||||
|
|
||||||
|
|
||||||
class TestFunctionCallValidation:
|
class TestFunctionCallValidation:
|
||||||
@pytest.mark.parametrize("test_input", [
|
@pytest.mark.parametrize(
|
||||||
"basic_func()",
|
"test_input",
|
||||||
"func_with_arg(\"test\")",
|
[
|
||||||
"complex_func(1, \"two\", three)",
|
"basic_func()",
|
||||||
"nested_parens(func(\"test\"))",
|
'func_with_arg("test")',
|
||||||
"under_score()",
|
'complex_func(1, "two", three)',
|
||||||
"with-dash()"
|
'nested_parens(func("test"))',
|
||||||
])
|
"under_score()",
|
||||||
|
"with-dash()",
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_valid_function_calls(self, test_input):
|
def test_valid_function_calls(self, test_input):
|
||||||
"""Test function call patterns that should pass validation."""
|
"""Test function call patterns that should pass validation."""
|
||||||
assert not validate_function_call_pattern(test_input)
|
assert not validate_function_call_pattern(test_input)
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_input", [
|
@pytest.mark.parametrize(
|
||||||
"",
|
"test_input",
|
||||||
"Invalid!function()",
|
[
|
||||||
"missing_parens",
|
"",
|
||||||
"unmatched(parens))",
|
"Invalid!function()",
|
||||||
"multiple()calls()",
|
"missing_parens",
|
||||||
"no spaces()()"
|
"unmatched(parens))",
|
||||||
])
|
"multiple()calls()",
|
||||||
|
"no spaces()()",
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_invalid_function_calls(self, test_input):
|
def test_invalid_function_calls(self, test_input):
|
||||||
"""Test function call patterns that should fail validation."""
|
"""Test function call patterns that should fail validation."""
|
||||||
assert validate_function_call_pattern(test_input)
|
assert validate_function_call_pattern(test_input)
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_input", [
|
@pytest.mark.parametrize(
|
||||||
" leading_space()",
|
"test_input",
|
||||||
"trailing_space() ",
|
[
|
||||||
"func (arg)",
|
" leading_space()",
|
||||||
"func( spaced args )"
|
"trailing_space() ",
|
||||||
])
|
"func (arg)",
|
||||||
|
"func( spaced args )",
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_whitespace_handling(self, test_input):
|
def test_whitespace_handling(self, test_input):
|
||||||
"""Test whitespace variations in function calls."""
|
"""Test whitespace variations in function calls."""
|
||||||
assert not validate_function_call_pattern(test_input)
|
assert not validate_function_call_pattern(test_input)
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_input", [
|
@pytest.mark.parametrize(
|
||||||
"""multiline(
|
"test_input",
|
||||||
|
[
|
||||||
|
"""multiline(
|
||||||
arg
|
arg
|
||||||
)""",
|
)""",
|
||||||
"func(\n arg1,\n arg2\n)"
|
"func(\n arg1,\n arg2\n)",
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_multiline_responses(self, test_input):
|
def test_multiline_responses(self, test_input):
|
||||||
"""Test function calls spanning multiple lines."""
|
"""Test function calls spanning multiple lines."""
|
||||||
assert not validate_function_call_pattern(test_input)
|
assert not validate_function_call_pattern(test_input)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import pytest
|
from ra_aid.console.cowboy_messages import COWBOY_MESSAGES, get_cowboy_message
|
||||||
from ra_aid.console.cowboy_messages import get_cowboy_message, COWBOY_MESSAGES
|
|
||||||
|
|
||||||
def test_get_cowboy_message_returns_string():
|
def test_get_cowboy_message_returns_string():
|
||||||
"""Test that get_cowboy_message returns a non-empty string"""
|
"""Test that get_cowboy_message returns a non-empty string"""
|
||||||
|
|
@ -7,12 +7,14 @@ def test_get_cowboy_message_returns_string():
|
||||||
assert isinstance(message, str)
|
assert isinstance(message, str)
|
||||||
assert len(message) > 0
|
assert len(message) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_cowboy_message_contains_emoji():
|
def test_cowboy_message_contains_emoji():
|
||||||
"""Test that returned message contains at least one of the expected emojis"""
|
"""Test that returned message contains at least one of the expected emojis"""
|
||||||
message = get_cowboy_message()
|
message = get_cowboy_message()
|
||||||
expected_emojis = ['🤠', '👶', '😏']
|
expected_emojis = ["🤠", "👶", "😏"]
|
||||||
assert any(emoji in message for emoji in expected_emojis)
|
assert any(emoji in message for emoji in expected_emojis)
|
||||||
|
|
||||||
|
|
||||||
def test_message_from_predefined_list():
|
def test_message_from_predefined_list():
|
||||||
"""Test that returned message is from our predefined list"""
|
"""Test that returned message is from our predefined list"""
|
||||||
message = get_cowboy_message()
|
message = get_cowboy_message()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"""Tests for the interactive subprocess module."""
|
"""Tests for the interactive subprocess module."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.proc.interactive import run_interactive_command
|
from ra_aid.proc.interactive import run_interactive_command
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,7 +17,9 @@ def test_basic_command():
|
||||||
|
|
||||||
def test_shell_pipeline():
|
def test_shell_pipeline():
|
||||||
"""Test running a shell pipeline command."""
|
"""Test running a shell pipeline command."""
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", "echo 'hello world' | grep 'world'"])
|
output, retcode = run_interactive_command(
|
||||||
|
["/bin/bash", "-c", "echo 'hello world' | grep 'world'"]
|
||||||
|
)
|
||||||
assert b"world" in output
|
assert b"world" in output
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
||||||
|
|
@ -24,7 +27,9 @@ def test_shell_pipeline():
|
||||||
def test_stderr_capture():
|
def test_stderr_capture():
|
||||||
"""Test that stderr is properly captured in combined output."""
|
"""Test that stderr is properly captured in combined output."""
|
||||||
# Use a command that definitely writes to stderr
|
# Use a command that definitely writes to stderr
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", "ls /nonexistent/path"])
|
output, retcode = run_interactive_command(
|
||||||
|
["/bin/bash", "-c", "ls /nonexistent/path"]
|
||||||
|
)
|
||||||
assert b"No such file or directory" in output
|
assert b"No such file or directory" in output
|
||||||
assert retcode != 0 # ls returns 0 upon success
|
assert retcode != 0 # ls returns 0 upon success
|
||||||
|
|
||||||
|
|
@ -46,22 +51,29 @@ def test_interactive_command():
|
||||||
|
|
||||||
This test verifies that output appears in real-time using process substitution.
|
This test verifies that output appears in real-time using process substitution.
|
||||||
We use a command that prints to both stdout and stderr to verify capture."""
|
We use a command that prints to both stdout and stderr to verify capture."""
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", "echo stdout; echo stderr >&2"])
|
output, retcode = run_interactive_command(
|
||||||
|
["/bin/bash", "-c", "echo stdout; echo stderr >&2"]
|
||||||
|
)
|
||||||
assert b"stdout" in output
|
assert b"stdout" in output
|
||||||
assert b"stderr" in output
|
assert b"stderr" in output
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
||||||
|
|
||||||
def test_large_output():
|
def test_large_output():
|
||||||
"""Test handling of commands that produce large output."""
|
"""Test handling of commands that produce large output."""
|
||||||
# Generate a large output with predictable content
|
# Generate a large output with predictable content
|
||||||
cmd = "for i in {1..10000}; do echo \"Line $i of test output\"; done"
|
cmd = 'for i in {1..10000}; do echo "Line $i of test output"; done'
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", cmd])
|
output, retcode = run_interactive_command(["/bin/bash", "-c", cmd])
|
||||||
|
|
||||||
# Clean up specific artifacts (e.g., ^D)
|
# Clean up specific artifacts (e.g., ^D)
|
||||||
output_cleaned = output.lstrip(b'^D') # Remove the leading ^D if present
|
output_cleaned = output.lstrip(b"^D") # Remove the leading ^D if present
|
||||||
|
|
||||||
# Split and filter lines
|
# Split and filter lines
|
||||||
lines = [line.strip() for line in output_cleaned.splitlines() if b"Script" not in line and line.strip()]
|
lines = [
|
||||||
|
line.strip()
|
||||||
|
for line in output_cleaned.splitlines()
|
||||||
|
if b"Script" not in line and line.strip()
|
||||||
|
]
|
||||||
|
|
||||||
# Verify we got all 10000 lines
|
# Verify we got all 10000 lines
|
||||||
assert len(lines) == 10000, f"Expected 10000 lines, but got {len(lines)}"
|
assert len(lines) == 10000, f"Expected 10000 lines, but got {len(lines)}"
|
||||||
|
|
@ -78,14 +90,18 @@ def test_large_output():
|
||||||
def test_unicode_handling():
|
def test_unicode_handling():
|
||||||
"""Test handling of unicode characters."""
|
"""Test handling of unicode characters."""
|
||||||
test_string = "Hello "
|
test_string = "Hello "
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", f"echo '{test_string}'"])
|
output, retcode = run_interactive_command(
|
||||||
|
["/bin/bash", "-c", f"echo '{test_string}'"]
|
||||||
|
)
|
||||||
assert test_string.encode() in output
|
assert test_string.encode() in output
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_commands():
|
def test_multiple_commands():
|
||||||
"""Test running multiple commands in sequence."""
|
"""Test running multiple commands in sequence."""
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", "echo 'first'; echo 'second'"])
|
output, retcode = run_interactive_command(
|
||||||
|
["/bin/bash", "-c", "echo 'first'; echo 'second'"]
|
||||||
|
)
|
||||||
assert b"first" in output
|
assert b"first" in output
|
||||||
assert b"second" in output
|
assert b"second" in output
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
@ -94,15 +110,21 @@ def test_multiple_commands():
|
||||||
def test_cat_medium_file():
|
def test_cat_medium_file():
|
||||||
"""Test that cat command properly captures output for medium-length files."""
|
"""Test that cat command properly captures output for medium-length files."""
|
||||||
# Create a temporary file with known content
|
# Create a temporary file with known content
|
||||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
||||||
for i in range(500):
|
for i in range(500):
|
||||||
f.write(f"This is test line {i}\n")
|
f.write(f"This is test line {i}\n")
|
||||||
temp_path = f.name
|
temp_path = f.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", f"cat {temp_path}"])
|
output, retcode = run_interactive_command(
|
||||||
|
["/bin/bash", "-c", f"cat {temp_path}"]
|
||||||
|
)
|
||||||
# Split by newlines and filter out script header/footer lines
|
# Split by newlines and filter out script header/footer lines
|
||||||
lines = [line for line in output.splitlines() if b"Script" not in line and line.strip()]
|
lines = [
|
||||||
|
line
|
||||||
|
for line in output.splitlines()
|
||||||
|
if b"Script" not in line and line.strip()
|
||||||
|
]
|
||||||
assert len(lines) == 500
|
assert len(lines) == 500
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
||||||
|
|
@ -120,24 +142,29 @@ def test_realtime_output():
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", cmd])
|
output, retcode = run_interactive_command(["/bin/bash", "-c", cmd])
|
||||||
|
|
||||||
# Filter out script header/footer lines
|
# Filter out script header/footer lines
|
||||||
lines = [line for line in output.splitlines() if b"Script" not in line and line.strip()]
|
lines = [
|
||||||
|
line for line in output.splitlines() if b"Script" not in line and line.strip()
|
||||||
|
]
|
||||||
|
|
||||||
assert b"first" in lines[0]
|
assert b"first" in lines[0]
|
||||||
assert b"second" in lines[1]
|
assert b"second" in lines[1]
|
||||||
assert b"third" in lines[2]
|
assert b"third" in lines[2]
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
||||||
|
|
||||||
def test_tty_available():
|
def test_tty_available():
|
||||||
"""Test that commands have access to a TTY."""
|
"""Test that commands have access to a TTY."""
|
||||||
# Run the tty command
|
# Run the tty command
|
||||||
output, retcode = run_interactive_command(["/bin/bash", "-c", "tty"])
|
output, retcode = run_interactive_command(["/bin/bash", "-c", "tty"])
|
||||||
|
|
||||||
# Clean up specific artifacts (e.g., ^D)
|
# Clean up specific artifacts (e.g., ^D)
|
||||||
output_cleaned = output.lstrip(b'^D') # Remove leading ^D if present
|
output_cleaned = output.lstrip(b"^D") # Remove leading ^D if present
|
||||||
|
|
||||||
# Debug: Print cleaned output
|
# Debug: Print cleaned output
|
||||||
print(f"Cleaned TTY Output: {output_cleaned}")
|
print(f"Cleaned TTY Output: {output_cleaned}")
|
||||||
|
|
||||||
# Check if the output contains a valid TTY path
|
# Check if the output contains a valid TTY path
|
||||||
assert b"/dev/pts/" in output_cleaned or b"/dev/ttys" in output_cleaned, f"Unexpected TTY output: {output_cleaned}"
|
assert (
|
||||||
|
b"/dev/pts/" in output_cleaned or b"/dev/ttys" in output_cleaned
|
||||||
|
), f"Unexpected TTY output: {output_cleaned}"
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
"""Unit tests for agent_utils.py."""
|
"""Unit tests for agent_utils.py."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from langchain_core.language_models import BaseChatModel
|
|
||||||
import litellm
|
import litellm
|
||||||
|
import pytest
|
||||||
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||||
|
|
||||||
from ra_aid.models_tokens import DEFAULT_TOKEN_LIMIT
|
from ra_aid.agent_utils import (
|
||||||
from ra_aid.agent_utils import state_modifier, AgentState
|
AgentState,
|
||||||
|
create_agent,
|
||||||
from ra_aid.agent_utils import create_agent, get_model_token_limit
|
get_model_token_limit,
|
||||||
from ra_aid.models_tokens import models_tokens
|
state_modifier,
|
||||||
|
)
|
||||||
|
from ra_aid.models_tokens import DEFAULT_TOKEN_LIMIT, models_tokens
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -60,7 +63,6 @@ def test_get_model_token_limit_missing_config(mock_memory):
|
||||||
assert token_limit is None
|
assert token_limit is None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_model_token_limit_litellm_success():
|
def test_get_model_token_limit_litellm_success():
|
||||||
"""Test get_model_token_limit successfully getting limit from litellm."""
|
"""Test get_model_token_limit successfully getting limit from litellm."""
|
||||||
config = {"provider": "anthropic", "model": "claude-2"}
|
config = {"provider": "anthropic", "model": "claude-2"}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
"""Tests for default provider and model configuration."""
|
"""Tests for default provider and model configuration."""
|
||||||
|
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ra_aid.env import validate_environment
|
import pytest
|
||||||
|
|
||||||
from ra_aid.__main__ import parse_arguments
|
from ra_aid.__main__ import parse_arguments
|
||||||
|
from ra_aid.env import validate_environment
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MockArgs:
|
class MockArgs:
|
||||||
"""Mock arguments for testing."""
|
"""Mock arguments for testing."""
|
||||||
provider: str
|
|
||||||
|
provider: Optional[str] = None
|
||||||
expert_provider: Optional[str] = None
|
expert_provider: Optional[str] = None
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
expert_model: Optional[str] = None
|
expert_model: Optional[str] = None
|
||||||
|
|
@ -19,6 +21,7 @@ class MockArgs:
|
||||||
research_only: bool = False
|
research_only: bool = False
|
||||||
chat: bool = False
|
chat: bool = False
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def clean_env(monkeypatch):
|
def clean_env(monkeypatch):
|
||||||
"""Remove all provider-related environment variables."""
|
"""Remove all provider-related environment variables."""
|
||||||
|
|
@ -37,6 +40,7 @@ def clean_env(monkeypatch):
|
||||||
monkeypatch.delenv(var, raising=False)
|
monkeypatch.delenv(var, raising=False)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def test_default_anthropic_provider(clean_env, monkeypatch):
|
def test_default_anthropic_provider(clean_env, monkeypatch):
|
||||||
"""Test that Anthropic is the default provider when no environment variables are set."""
|
"""Test that Anthropic is the default provider when no environment variables are set."""
|
||||||
args = parse_arguments(["-m", "test message"])
|
args = parse_arguments(["-m", "test message"])
|
||||||
|
|
@ -44,62 +48,41 @@ def test_default_anthropic_provider(clean_env, monkeypatch):
|
||||||
assert args.model == "claude-3-5-sonnet-20241022"
|
assert args.model == "claude-3-5-sonnet-20241022"
|
||||||
|
|
||||||
|
|
||||||
"""Unit tests for provider and model validation in research-only mode."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from argparse import Namespace
|
|
||||||
from ra_aid.env import validate_environment
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MockArgs:
|
|
||||||
"""Mock command line arguments."""
|
|
||||||
research_only: bool = False
|
|
||||||
provider: str = None
|
|
||||||
model: str = None
|
|
||||||
expert_provider: str = None
|
|
||||||
|
|
||||||
|
|
||||||
TEST_CASES = [
|
TEST_CASES = [
|
||||||
pytest.param(
|
pytest.param(
|
||||||
"research_only_no_provider",
|
"research_only_no_provider",
|
||||||
MockArgs(research_only=True),
|
MockArgs(research_only=True),
|
||||||
{},
|
{},
|
||||||
"No provider specified",
|
"No provider specified",
|
||||||
id="research_only_no_provider"
|
id="research_only_no_provider",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
"research_only_anthropic",
|
"research_only_anthropic",
|
||||||
MockArgs(research_only=True, provider="anthropic"),
|
MockArgs(research_only=True, provider="anthropic"),
|
||||||
{},
|
{},
|
||||||
None,
|
None,
|
||||||
id="research_only_anthropic"
|
id="research_only_anthropic",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
"research_only_non_anthropic_no_model",
|
"research_only_non_anthropic_no_model",
|
||||||
MockArgs(research_only=True, provider="openai"),
|
MockArgs(research_only=True, provider="openai"),
|
||||||
{},
|
{},
|
||||||
"Model is required for non-Anthropic providers",
|
"Model is required for non-Anthropic providers",
|
||||||
id="research_only_non_anthropic_no_model"
|
id="research_only_non_anthropic_no_model",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
"research_only_non_anthropic_with_model",
|
"research_only_non_anthropic_with_model",
|
||||||
MockArgs(research_only=True, provider="openai", model="gpt-4"),
|
MockArgs(research_only=True, provider="openai", model="gpt-4"),
|
||||||
{},
|
{},
|
||||||
None,
|
None,
|
||||||
id="research_only_non_anthropic_with_model"
|
id="research_only_non_anthropic_with_model",
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_name,args,env_vars,expected_error", TEST_CASES)
|
@pytest.mark.parametrize("test_name,args,env_vars,expected_error", TEST_CASES)
|
||||||
def test_research_only_provider_validation(
|
def test_research_only_provider_validation(
|
||||||
test_name: str,
|
test_name: str, args: MockArgs, env_vars: dict, expected_error: str, monkeypatch
|
||||||
args: MockArgs,
|
|
||||||
env_vars: dict,
|
|
||||||
expected_error: str,
|
|
||||||
monkeypatch
|
|
||||||
):
|
):
|
||||||
"""Test provider and model validation in research-only mode."""
|
"""Test provider and model validation in research-only mode."""
|
||||||
# Set test environment variables
|
# Set test environment variables
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import os
|
import os
|
||||||
import pytest
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.env import validate_environment
|
from ra_aid.env import validate_environment
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MockArgs:
|
class MockArgs:
|
||||||
provider: str
|
provider: str
|
||||||
|
|
@ -16,30 +18,44 @@ class MockArgs:
|
||||||
planner_provider: Optional[str] = None
|
planner_provider: Optional[str] = None
|
||||||
planner_model: Optional[str] = None
|
planner_model: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def clean_env(monkeypatch):
|
def clean_env(monkeypatch):
|
||||||
"""Remove relevant environment variables before each test"""
|
"""Remove relevant environment variables before each test"""
|
||||||
env_vars = [
|
env_vars = [
|
||||||
'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY',
|
"ANTHROPIC_API_KEY",
|
||||||
'OPENAI_API_BASE', 'EXPERT_ANTHROPIC_API_KEY', 'EXPERT_OPENAI_API_KEY',
|
"OPENAI_API_KEY",
|
||||||
'EXPERT_OPENROUTER_API_KEY', 'EXPERT_OPENAI_API_BASE', 'TAVILY_API_KEY', 'ANTHROPIC_MODEL'
|
"OPENROUTER_API_KEY",
|
||||||
|
"OPENAI_API_BASE",
|
||||||
|
"EXPERT_ANTHROPIC_API_KEY",
|
||||||
|
"EXPERT_OPENAI_API_KEY",
|
||||||
|
"EXPERT_OPENROUTER_API_KEY",
|
||||||
|
"EXPERT_OPENAI_API_BASE",
|
||||||
|
"TAVILY_API_KEY",
|
||||||
|
"ANTHROPIC_MODEL",
|
||||||
]
|
]
|
||||||
for var in env_vars:
|
for var in env_vars:
|
||||||
monkeypatch.delenv(var, raising=False)
|
monkeypatch.delenv(var, raising=False)
|
||||||
|
|
||||||
|
|
||||||
def test_anthropic_validation(clean_env, monkeypatch):
|
def test_anthropic_validation(clean_env, monkeypatch):
|
||||||
args = MockArgs(provider="anthropic", expert_provider="openai", model="claude-3-haiku-20240307")
|
args = MockArgs(
|
||||||
|
provider="anthropic", expert_provider="openai", model="claude-3-haiku-20240307"
|
||||||
|
)
|
||||||
|
|
||||||
# Should fail without API key
|
# Should fail without API key
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
validate_environment(args)
|
validate_environment(args)
|
||||||
|
|
||||||
# Should pass with API key and model
|
# Should pass with API key and model
|
||||||
monkeypatch.setenv('ANTHROPIC_API_KEY', 'test-key')
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert not expert_enabled
|
assert not expert_enabled
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
|
|
||||||
|
|
||||||
def test_openai_validation(clean_env, monkeypatch):
|
def test_openai_validation(clean_env, monkeypatch):
|
||||||
args = MockArgs(provider="openai", expert_provider="openai")
|
args = MockArgs(provider="openai", expert_provider="openai")
|
||||||
|
|
@ -49,13 +65,16 @@ def test_openai_validation(clean_env, monkeypatch):
|
||||||
validate_environment(args)
|
validate_environment(args)
|
||||||
|
|
||||||
# Should pass with API key and enable expert mode with fallback
|
# Should pass with API key and enable expert mode with fallback
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'test-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'test-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "test-key"
|
||||||
|
|
||||||
|
|
||||||
def test_openai_compatible_validation(clean_env, monkeypatch):
|
def test_openai_compatible_validation(clean_env, monkeypatch):
|
||||||
args = MockArgs(provider="openai-compatible", expert_provider="openai-compatible")
|
args = MockArgs(provider="openai-compatible", expert_provider="openai-compatible")
|
||||||
|
|
@ -65,126 +84,158 @@ def test_openai_compatible_validation(clean_env, monkeypatch):
|
||||||
validate_environment(args)
|
validate_environment(args)
|
||||||
|
|
||||||
# Should fail with only API key
|
# Should fail with only API key
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'test-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
validate_environment(args)
|
validate_environment(args)
|
||||||
|
|
||||||
# Should pass with both API key and base URL
|
# Should pass with both API key and base URL
|
||||||
monkeypatch.setenv('OPENAI_API_BASE', 'http://test')
|
monkeypatch.setenv("OPENAI_API_BASE", "http://test")
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'test-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "test-key"
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_BASE') == 'http://test'
|
assert os.environ.get("EXPERT_OPENAI_API_BASE") == "http://test"
|
||||||
|
|
||||||
|
|
||||||
def test_expert_fallback(clean_env, monkeypatch):
|
def test_expert_fallback(clean_env, monkeypatch):
|
||||||
args = MockArgs(provider="openai", expert_provider="openai")
|
args = MockArgs(provider="openai", expert_provider="openai")
|
||||||
|
|
||||||
# Set only base API key
|
# Set only base API key
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'test-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||||
|
|
||||||
# Should enable expert mode with fallback
|
# Should enable expert mode with fallback
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'test-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "test-key"
|
||||||
|
|
||||||
# Should use explicit expert key if available
|
# Should use explicit expert key if available
|
||||||
monkeypatch.setenv('EXPERT_OPENAI_API_KEY', 'expert-key')
|
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "expert-key")
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'expert-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "expert-key"
|
||||||
|
|
||||||
|
|
||||||
def test_cross_provider_fallback(clean_env, monkeypatch):
|
def test_cross_provider_fallback(clean_env, monkeypatch):
|
||||||
"""Test that fallback works even when providers differ"""
|
"""Test that fallback works even when providers differ"""
|
||||||
args = MockArgs(provider="openai", expert_provider="anthropic", expert_model="claude-3-haiku-20240307")
|
args = MockArgs(
|
||||||
|
provider="openai",
|
||||||
|
expert_provider="anthropic",
|
||||||
|
expert_model="claude-3-haiku-20240307",
|
||||||
|
)
|
||||||
|
|
||||||
# Set base API key for main provider and expert provider
|
# Set base API key for main provider and expert provider
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'openai-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "openai-key")
|
||||||
monkeypatch.setenv('ANTHROPIC_API_KEY', 'anthropic-key')
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key")
|
||||||
monkeypatch.setenv('ANTHROPIC_MODEL', 'claude-3-haiku-20240307')
|
monkeypatch.setenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307")
|
||||||
|
|
||||||
# Should enable expert mode with fallback to ANTHROPIC base key
|
# Should enable expert mode with fallback to ANTHROPIC base key
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
|
|
||||||
# Try with openai-compatible expert provider
|
# Try with openai-compatible expert provider
|
||||||
args = MockArgs(provider="anthropic", expert_provider="openai-compatible")
|
args = MockArgs(provider="anthropic", expert_provider="openai-compatible")
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'openai-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "openai-key")
|
||||||
monkeypatch.setenv('OPENAI_API_BASE', 'http://test')
|
monkeypatch.setenv("OPENAI_API_BASE", "http://test")
|
||||||
|
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'openai-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "openai-key"
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_BASE') == 'http://test'
|
assert os.environ.get("EXPERT_OPENAI_API_BASE") == "http://test"
|
||||||
|
|
||||||
|
|
||||||
def test_no_warning_on_fallback(clean_env, monkeypatch):
|
def test_no_warning_on_fallback(clean_env, monkeypatch):
|
||||||
"""Test that no warning is issued when fallback succeeds"""
|
"""Test that no warning is issued when fallback succeeds"""
|
||||||
args = MockArgs(provider="openai", expert_provider="openai")
|
args = MockArgs(provider="openai", expert_provider="openai")
|
||||||
|
|
||||||
# Set only base API key
|
# Set only base API key
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'test-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
||||||
|
|
||||||
# Should enable expert mode with fallback and no warnings
|
# Should enable expert mode with fallback and no warnings
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'test-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "test-key"
|
||||||
|
|
||||||
# Should use explicit expert key if available
|
# Should use explicit expert key if available
|
||||||
monkeypatch.setenv('EXPERT_OPENAI_API_KEY', 'expert-key')
|
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "expert-key")
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'expert-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "expert-key"
|
||||||
|
|
||||||
|
|
||||||
def test_different_providers_no_expert_key(clean_env, monkeypatch):
|
def test_different_providers_no_expert_key(clean_env, monkeypatch):
|
||||||
"""Test behavior when providers differ and only base keys are available"""
|
"""Test behavior when providers differ and only base keys are available"""
|
||||||
args = MockArgs(provider="anthropic", expert_provider="openai", model="claude-3-haiku-20240307")
|
args = MockArgs(
|
||||||
|
provider="anthropic", expert_provider="openai", model="claude-3-haiku-20240307"
|
||||||
|
)
|
||||||
|
|
||||||
# Set only base keys
|
# Set only base keys
|
||||||
monkeypatch.setenv('ANTHROPIC_API_KEY', 'anthropic-key')
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key")
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'openai-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "openai-key")
|
||||||
|
|
||||||
# Should enable expert mode and use base OPENAI key
|
# Should enable expert mode and use base OPENAI key
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
|
|
||||||
|
|
||||||
def test_mixed_provider_openai_compatible(clean_env, monkeypatch):
|
def test_mixed_provider_openai_compatible(clean_env, monkeypatch):
|
||||||
"""Test behavior with openai-compatible expert and different main provider"""
|
"""Test behavior with openai-compatible expert and different main provider"""
|
||||||
args = MockArgs(provider="anthropic", expert_provider="openai-compatible", model="claude-3-haiku-20240307")
|
args = MockArgs(
|
||||||
|
provider="anthropic",
|
||||||
|
expert_provider="openai-compatible",
|
||||||
|
model="claude-3-haiku-20240307",
|
||||||
|
)
|
||||||
|
|
||||||
# Set all required keys and URLs
|
# Set all required keys and URLs
|
||||||
monkeypatch.setenv('ANTHROPIC_API_KEY', 'anthropic-key')
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key")
|
||||||
monkeypatch.setenv('OPENAI_API_KEY', 'openai-key')
|
monkeypatch.setenv("OPENAI_API_KEY", "openai-key")
|
||||||
monkeypatch.setenv('OPENAI_API_BASE', 'http://test')
|
monkeypatch.setenv("OPENAI_API_BASE", "http://test")
|
||||||
|
|
||||||
# Should enable expert mode and use base openai key and URL
|
# Should enable expert mode and use base openai key and URL
|
||||||
expert_enabled, expert_missing, web_research_enabled, web_research_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_research_enabled, web_research_missing = (
|
||||||
|
validate_environment(args)
|
||||||
|
)
|
||||||
assert expert_enabled
|
assert expert_enabled
|
||||||
assert not expert_missing
|
assert not expert_missing
|
||||||
assert not web_research_enabled
|
assert not web_research_enabled
|
||||||
assert 'TAVILY_API_KEY environment variable is not set' in web_research_missing
|
assert "TAVILY_API_KEY environment variable is not set" in web_research_missing
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_KEY') == 'openai-key'
|
assert os.environ.get("EXPERT_OPENAI_API_KEY") == "openai-key"
|
||||||
assert os.environ.get('EXPERT_OPENAI_API_BASE') == 'http://test'
|
assert os.environ.get("EXPERT_OPENAI_API_BASE") == "http://test"
|
||||||
|
|
|
||||||
|
|
@ -1,178 +1,184 @@
|
||||||
import os
|
import os
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, Mock
|
|
||||||
from langchain_openai.chat_models import ChatOpenAI
|
|
||||||
from langchain_anthropic.chat_models import ChatAnthropic
|
|
||||||
from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
|
|
||||||
from langchain_core.messages import HumanMessage
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from ra_aid.agents.ciayn_agent import CiaynAgent
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_anthropic.chat_models import ChatAnthropic
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
|
||||||
|
from langchain_openai.chat_models import ChatOpenAI
|
||||||
|
|
||||||
|
from ra_aid.agents.ciayn_agent import CiaynAgent
|
||||||
from ra_aid.env import validate_environment
|
from ra_aid.env import validate_environment
|
||||||
from ra_aid.llm import (
|
from ra_aid.llm import (
|
||||||
initialize_llm,
|
create_llm_client,
|
||||||
initialize_expert_llm,
|
|
||||||
get_env_var,
|
get_env_var,
|
||||||
get_provider_config,
|
get_provider_config,
|
||||||
create_llm_client
|
initialize_expert_llm,
|
||||||
|
initialize_llm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def clean_env(monkeypatch):
|
def clean_env(monkeypatch):
|
||||||
"""Remove relevant environment variables before each test"""
|
"""Remove relevant environment variables before each test"""
|
||||||
env_vars = [
|
env_vars = [
|
||||||
'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY',
|
"ANTHROPIC_API_KEY",
|
||||||
'OPENAI_API_BASE', 'EXPERT_ANTHROPIC_API_KEY', 'EXPERT_OPENAI_API_KEY',
|
"OPENAI_API_KEY",
|
||||||
'EXPERT_OPENROUTER_API_KEY', 'EXPERT_OPENAI_API_BASE', 'GEMINI_API_KEY', 'EXPERT_GEMINI_API_KEY'
|
"OPENROUTER_API_KEY",
|
||||||
|
"OPENAI_API_BASE",
|
||||||
|
"EXPERT_ANTHROPIC_API_KEY",
|
||||||
|
"EXPERT_OPENAI_API_KEY",
|
||||||
|
"EXPERT_OPENROUTER_API_KEY",
|
||||||
|
"EXPERT_OPENAI_API_BASE",
|
||||||
|
"GEMINI_API_KEY",
|
||||||
|
"EXPERT_GEMINI_API_KEY",
|
||||||
]
|
]
|
||||||
for var in env_vars:
|
for var in env_vars:
|
||||||
monkeypatch.delenv(var, raising=False)
|
monkeypatch.delenv(var, raising=False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_openai():
|
def mock_openai():
|
||||||
"""
|
"""
|
||||||
Mock ChatOpenAI class for testing OpenAI provider initialization.
|
Mock ChatOpenAI class for testing OpenAI provider initialization.
|
||||||
Prevents actual API calls during testing.
|
Prevents actual API calls during testing.
|
||||||
"""
|
"""
|
||||||
with patch('ra_aid.llm.ChatOpenAI') as mock:
|
with patch("ra_aid.llm.ChatOpenAI") as mock:
|
||||||
mock.return_value = Mock(spec=ChatOpenAI)
|
mock.return_value = Mock(spec=ChatOpenAI)
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_expert_defaults(clean_env, mock_openai, monkeypatch):
|
def test_initialize_expert_defaults(clean_env, mock_openai, monkeypatch):
|
||||||
"""Test expert LLM initialization with default parameters."""
|
"""Test expert LLM initialization with default parameters."""
|
||||||
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "test-key")
|
||||||
llm = initialize_expert_llm()
|
_llm = initialize_expert_llm()
|
||||||
|
|
||||||
|
mock_openai.assert_called_once_with(api_key="test-key", model="o1", temperature=0)
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
|
||||||
api_key="test-key",
|
|
||||||
model="o1",
|
|
||||||
temperature=0
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_initialize_expert_openai_custom(clean_env, mock_openai, monkeypatch):
|
def test_initialize_expert_openai_custom(clean_env, mock_openai, monkeypatch):
|
||||||
"""Test expert OpenAI initialization with custom parameters."""
|
"""Test expert OpenAI initialization with custom parameters."""
|
||||||
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "test-key")
|
||||||
llm = initialize_expert_llm("openai", "gpt-4-preview")
|
_llm = initialize_expert_llm("openai", "gpt-4-preview")
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
mock_openai.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model="gpt-4-preview", temperature=0
|
||||||
model="gpt-4-preview",
|
|
||||||
temperature=0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_expert_gemini(clean_env, mock_gemini, monkeypatch):
|
def test_initialize_expert_gemini(clean_env, mock_gemini, monkeypatch):
|
||||||
"""Test expert Gemini initialization."""
|
"""Test expert Gemini initialization."""
|
||||||
monkeypatch.setenv("EXPERT_GEMINI_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_GEMINI_API_KEY", "test-key")
|
||||||
llm = initialize_expert_llm("gemini", "gemini-2.0-flash-thinking-exp-1219")
|
_llm = initialize_expert_llm("gemini", "gemini-2.0-flash-thinking-exp-1219")
|
||||||
|
|
||||||
mock_gemini.assert_called_once_with(
|
mock_gemini.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model="gemini-2.0-flash-thinking-exp-1219", temperature=0
|
||||||
model="gemini-2.0-flash-thinking-exp-1219",
|
|
||||||
temperature=0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_expert_anthropic(clean_env, mock_anthropic, monkeypatch):
|
def test_initialize_expert_anthropic(clean_env, mock_anthropic, monkeypatch):
|
||||||
"""Test expert Anthropic initialization."""
|
"""Test expert Anthropic initialization."""
|
||||||
monkeypatch.setenv("EXPERT_ANTHROPIC_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_ANTHROPIC_API_KEY", "test-key")
|
||||||
llm = initialize_expert_llm("anthropic", "claude-3")
|
_llm = initialize_expert_llm("anthropic", "claude-3")
|
||||||
|
|
||||||
mock_anthropic.assert_called_once_with(
|
mock_anthropic.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model_name="claude-3", temperature=0
|
||||||
model_name="claude-3",
|
|
||||||
temperature=0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_expert_openrouter(clean_env, mock_openai, monkeypatch):
|
def test_initialize_expert_openrouter(clean_env, mock_openai, monkeypatch):
|
||||||
"""Test expert OpenRouter initialization."""
|
"""Test expert OpenRouter initialization."""
|
||||||
monkeypatch.setenv("EXPERT_OPENROUTER_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_OPENROUTER_API_KEY", "test-key")
|
||||||
llm = initialize_expert_llm("openrouter", "models/mistral-large")
|
_llm = initialize_expert_llm("openrouter", "models/mistral-large")
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
mock_openai.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url="https://openrouter.ai/api/v1",
|
||||||
model="models/mistral-large",
|
model="models/mistral-large",
|
||||||
temperature=0
|
temperature=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_expert_openai_compatible(clean_env, mock_openai, monkeypatch):
|
def test_initialize_expert_openai_compatible(clean_env, mock_openai, monkeypatch):
|
||||||
"""Test expert OpenAI-compatible initialization."""
|
"""Test expert OpenAI-compatible initialization."""
|
||||||
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "test-key")
|
||||||
monkeypatch.setenv("EXPERT_OPENAI_API_BASE", "http://test-url")
|
monkeypatch.setenv("EXPERT_OPENAI_API_BASE", "http://test-url")
|
||||||
llm = initialize_expert_llm("openai-compatible", "local-model")
|
_llm = initialize_expert_llm("openai-compatible", "local-model")
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
mock_openai.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="http://test-url",
|
base_url="http://test-url",
|
||||||
model="local-model",
|
model="local-model",
|
||||||
temperature=0
|
temperature=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_expert_unsupported_provider(clean_env):
|
def test_initialize_expert_unsupported_provider(clean_env):
|
||||||
"""Test error handling for unsupported provider in expert mode."""
|
"""Test error handling for unsupported provider in expert mode."""
|
||||||
with pytest.raises(ValueError, match=r"Unsupported provider: unknown"):
|
with pytest.raises(ValueError, match=r"Unsupported provider: unknown"):
|
||||||
initialize_expert_llm("unknown", "model")
|
initialize_expert_llm("unknown", "model")
|
||||||
|
|
||||||
|
|
||||||
def test_estimate_tokens():
|
def test_estimate_tokens():
|
||||||
"""Test token estimation functionality."""
|
"""Test token estimation functionality."""
|
||||||
# Test empty/None cases
|
# Test empty/None cases
|
||||||
assert CiaynAgent._estimate_tokens(None) == 0
|
assert CiaynAgent._estimate_tokens(None) == 0
|
||||||
assert CiaynAgent._estimate_tokens('') == 0
|
assert CiaynAgent._estimate_tokens("") == 0
|
||||||
|
|
||||||
# Test string content
|
# Test string content
|
||||||
assert CiaynAgent._estimate_tokens('test') == 1 # 4 bytes
|
assert CiaynAgent._estimate_tokens("test") == 1 # 4 bytes
|
||||||
assert CiaynAgent._estimate_tokens('hello world') == 2 # 11 bytes
|
assert CiaynAgent._estimate_tokens("hello world") == 2 # 11 bytes
|
||||||
assert CiaynAgent._estimate_tokens('🚀') == 1 # 4 bytes
|
assert CiaynAgent._estimate_tokens("🚀") == 1 # 4 bytes
|
||||||
|
|
||||||
# Test message content
|
# Test message content
|
||||||
msg = HumanMessage(content='test message')
|
msg = HumanMessage(content="test message")
|
||||||
assert CiaynAgent._estimate_tokens(msg) == 3 # 11 bytes
|
assert CiaynAgent._estimate_tokens(msg) == 3 # 11 bytes
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_openai(clean_env, mock_openai):
|
def test_initialize_openai(clean_env, mock_openai):
|
||||||
"""Test OpenAI provider initialization"""
|
"""Test OpenAI provider initialization"""
|
||||||
os.environ["OPENAI_API_KEY"] = "test-key"
|
os.environ["OPENAI_API_KEY"] = "test-key"
|
||||||
model = initialize_llm("openai", "gpt-4")
|
_model = initialize_llm("openai", "gpt-4")
|
||||||
|
|
||||||
|
mock_openai.assert_called_once_with(api_key="test-key", model="gpt-4")
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
|
||||||
api_key="test-key",
|
|
||||||
model="gpt-4"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_initialize_gemini(clean_env, mock_gemini):
|
def test_initialize_gemini(clean_env, mock_gemini):
|
||||||
"""Test Gemini provider initialization"""
|
"""Test Gemini provider initialization"""
|
||||||
os.environ["GEMINI_API_KEY"] = "test-key"
|
os.environ["GEMINI_API_KEY"] = "test-key"
|
||||||
model = initialize_llm("gemini", "gemini-2.0-flash-thinking-exp-1219")
|
_model = initialize_llm("gemini", "gemini-2.0-flash-thinking-exp-1219")
|
||||||
|
|
||||||
mock_gemini.assert_called_once_with(
|
mock_gemini.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model="gemini-2.0-flash-thinking-exp-1219"
|
||||||
model="gemini-2.0-flash-thinking-exp-1219"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_anthropic(clean_env, mock_anthropic):
|
def test_initialize_anthropic(clean_env, mock_anthropic):
|
||||||
"""Test Anthropic provider initialization"""
|
"""Test Anthropic provider initialization"""
|
||||||
os.environ["ANTHROPIC_API_KEY"] = "test-key"
|
os.environ["ANTHROPIC_API_KEY"] = "test-key"
|
||||||
model = initialize_llm("anthropic", "claude-3")
|
_model = initialize_llm("anthropic", "claude-3")
|
||||||
|
|
||||||
|
mock_anthropic.assert_called_once_with(api_key="test-key", model_name="claude-3")
|
||||||
|
|
||||||
mock_anthropic.assert_called_once_with(
|
|
||||||
api_key="test-key",
|
|
||||||
model_name="claude-3"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_initialize_openrouter(clean_env, mock_openai):
|
def test_initialize_openrouter(clean_env, mock_openai):
|
||||||
"""Test OpenRouter provider initialization"""
|
"""Test OpenRouter provider initialization"""
|
||||||
os.environ["OPENROUTER_API_KEY"] = "test-key"
|
os.environ["OPENROUTER_API_KEY"] = "test-key"
|
||||||
model = initialize_llm("openrouter", "mistral-large")
|
_model = initialize_llm("openrouter", "mistral-large")
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
mock_openai.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url="https://openrouter.ai/api/v1",
|
||||||
model="mistral-large"
|
model="mistral-large",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_openai_compatible(clean_env, mock_openai):
|
def test_initialize_openai_compatible(clean_env, mock_openai):
|
||||||
"""Test OpenAI-compatible provider initialization"""
|
"""Test OpenAI-compatible provider initialization"""
|
||||||
os.environ["OPENAI_API_KEY"] = "test-key"
|
os.environ["OPENAI_API_KEY"] = "test-key"
|
||||||
os.environ["OPENAI_API_BASE"] = "https://custom-endpoint/v1"
|
os.environ["OPENAI_API_BASE"] = "https://custom-endpoint/v1"
|
||||||
model = initialize_llm("openai-compatible", "local-model")
|
_model = initialize_llm("openai-compatible", "local-model")
|
||||||
|
|
||||||
mock_openai.assert_called_once_with(
|
mock_openai.assert_called_once_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
|
|
@ -181,12 +187,14 @@ def test_initialize_openai_compatible(clean_env, mock_openai):
|
||||||
temperature=0.3,
|
temperature=0.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_unsupported_provider(clean_env):
|
def test_initialize_unsupported_provider(clean_env):
|
||||||
"""Test initialization with unsupported provider raises ValueError"""
|
"""Test initialization with unsupported provider raises ValueError"""
|
||||||
with pytest.raises(ValueError) as exc_info:
|
with pytest.raises(ValueError) as exc_info:
|
||||||
initialize_llm("unsupported", "model")
|
initialize_llm("unsupported", "model")
|
||||||
assert str(exc_info.value) == "Unsupported provider: unsupported"
|
assert str(exc_info.value) == "Unsupported provider: unsupported"
|
||||||
|
|
||||||
|
|
||||||
def test_temperature_defaults(clean_env, mock_openai, mock_anthropic, mock_gemini):
|
def test_temperature_defaults(clean_env, mock_openai, mock_anthropic, mock_gemini):
|
||||||
"""Test default temperature behavior for different providers."""
|
"""Test default temperature behavior for different providers."""
|
||||||
os.environ["OPENAI_API_KEY"] = "test-key"
|
os.environ["OPENAI_API_KEY"] = "test-key"
|
||||||
|
|
@ -199,27 +207,19 @@ def test_temperature_defaults(clean_env, mock_openai, mock_anthropic, mock_gemin
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="http://test-url",
|
base_url="http://test-url",
|
||||||
model="test-model",
|
model="test-model",
|
||||||
temperature=0.3
|
temperature=0.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test other providers don't set temperature by default
|
# Test other providers don't set temperature by default
|
||||||
initialize_llm("openai", "test-model")
|
initialize_llm("openai", "test-model")
|
||||||
mock_openai.assert_called_with(
|
mock_openai.assert_called_with(api_key="test-key", model="test-model")
|
||||||
api_key="test-key",
|
|
||||||
model="test-model"
|
|
||||||
)
|
|
||||||
|
|
||||||
initialize_llm("anthropic", "test-model")
|
initialize_llm("anthropic", "test-model")
|
||||||
mock_anthropic.assert_called_with(
|
mock_anthropic.assert_called_with(api_key="test-key", model_name="test-model")
|
||||||
api_key="test-key",
|
|
||||||
model_name="test-model"
|
|
||||||
)
|
|
||||||
|
|
||||||
initialize_llm("gemini", "test-model")
|
initialize_llm("gemini", "test-model")
|
||||||
mock_gemini.assert_called_with(
|
mock_gemini.assert_called_with(api_key="test-key", model="test-model")
|
||||||
api_key="test-key",
|
|
||||||
model="test-model"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_explicit_temperature(clean_env, mock_openai, mock_anthropic, mock_gemini):
|
def test_explicit_temperature(clean_env, mock_openai, mock_anthropic, mock_gemini):
|
||||||
"""Test explicit temperature setting for each provider."""
|
"""Test explicit temperature setting for each provider."""
|
||||||
|
|
@ -233,25 +233,19 @@ def test_explicit_temperature(clean_env, mock_openai, mock_anthropic, mock_gemin
|
||||||
# Test OpenAI
|
# Test OpenAI
|
||||||
initialize_llm("openai", "test-model", temperature=test_temp)
|
initialize_llm("openai", "test-model", temperature=test_temp)
|
||||||
mock_openai.assert_called_with(
|
mock_openai.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model="test-model", temperature=test_temp
|
||||||
model="test-model",
|
|
||||||
temperature=test_temp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test Gemini
|
# Test Gemini
|
||||||
initialize_llm("gemini", "test-model", temperature=test_temp)
|
initialize_llm("gemini", "test-model", temperature=test_temp)
|
||||||
mock_gemini.assert_called_with(
|
mock_gemini.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model="test-model", temperature=test_temp
|
||||||
model="test-model",
|
|
||||||
temperature=test_temp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test Anthropic
|
# Test Anthropic
|
||||||
initialize_llm("anthropic", "test-model", temperature=test_temp)
|
initialize_llm("anthropic", "test-model", temperature=test_temp)
|
||||||
mock_anthropic.assert_called_with(
|
mock_anthropic.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key", model_name="test-model", temperature=test_temp
|
||||||
model_name="test-model",
|
|
||||||
temperature=test_temp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test OpenRouter
|
# Test OpenRouter
|
||||||
|
|
@ -260,77 +254,80 @@ def test_explicit_temperature(clean_env, mock_openai, mock_anthropic, mock_gemin
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url="https://openrouter.ai/api/v1",
|
||||||
model="test-model",
|
model="test-model",
|
||||||
temperature=test_temp
|
temperature=test_temp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_temperature_validation(clean_env, mock_openai):
|
def test_temperature_validation(clean_env, mock_openai):
|
||||||
"""Test temperature validation in command line arguments."""
|
"""Test temperature validation in command line arguments."""
|
||||||
from ra_aid.__main__ import parse_arguments
|
from ra_aid.__main__ import parse_arguments
|
||||||
|
|
||||||
# Test temperature below minimum
|
# Test temperature below minimum
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
parse_arguments(['--message', 'test', '--temperature', '-0.1'])
|
parse_arguments(["--message", "test", "--temperature", "-0.1"])
|
||||||
|
|
||||||
# Test temperature above maximum
|
# Test temperature above maximum
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
parse_arguments(['--message', 'test', '--temperature', '2.1'])
|
parse_arguments(["--message", "test", "--temperature", "2.1"])
|
||||||
|
|
||||||
# Test valid temperature
|
# Test valid temperature
|
||||||
args = parse_arguments(['--message', 'test', '--temperature', '0.7'])
|
args = parse_arguments(["--message", "test", "--temperature", "0.7"])
|
||||||
assert args.temperature == 0.7
|
assert args.temperature == 0.7
|
||||||
|
|
||||||
|
|
||||||
def test_provider_name_validation():
|
def test_provider_name_validation():
|
||||||
"""Test provider name validation and normalization."""
|
"""Test provider name validation and normalization."""
|
||||||
# Test all supported providers
|
# Test all supported providers
|
||||||
providers = ["openai", "anthropic", "openrouter", "openai-compatible", "gemini"]
|
providers = ["openai", "anthropic", "openrouter", "openai-compatible", "gemini"]
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
try:
|
try:
|
||||||
with patch(f'ra_aid.llm.ChatOpenAI'), patch('ra_aid.llm.ChatAnthropic'):
|
with patch("ra_aid.llm.ChatOpenAI"), patch("ra_aid.llm.ChatAnthropic"):
|
||||||
initialize_llm(provider, "test-model")
|
initialize_llm(provider, "test-model")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pytest.fail(f"Valid provider {provider} raised ValueError")
|
pytest.fail(f"Valid provider {provider} raised ValueError")
|
||||||
|
|
||||||
# Test case sensitivity
|
# Test case sensitivity
|
||||||
with patch('ra_aid.llm.ChatOpenAI'):
|
with patch("ra_aid.llm.ChatOpenAI"):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
initialize_llm("OpenAI", "test-model")
|
initialize_llm("OpenAI", "test-model")
|
||||||
|
|
||||||
def test_initialize_llm_cross_provider(clean_env, mock_openai, mock_anthropic, mock_gemini, monkeypatch):
|
|
||||||
|
def test_initialize_llm_cross_provider(
|
||||||
|
clean_env, mock_openai, mock_anthropic, mock_gemini, monkeypatch
|
||||||
|
):
|
||||||
"""Test initializing different providers in sequence."""
|
"""Test initializing different providers in sequence."""
|
||||||
# Initialize OpenAI
|
# Initialize OpenAI
|
||||||
monkeypatch.setenv("OPENAI_API_KEY", "openai-key")
|
monkeypatch.setenv("OPENAI_API_KEY", "openai-key")
|
||||||
llm1 = initialize_llm("openai", "gpt-4")
|
_llm1 = initialize_llm("openai", "gpt-4")
|
||||||
|
|
||||||
# Initialize Anthropic
|
# Initialize Anthropic
|
||||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key")
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "anthropic-key")
|
||||||
llm2 = initialize_llm("anthropic", "claude-3")
|
_llm2 = initialize_llm("anthropic", "claude-3")
|
||||||
|
|
||||||
# Initialize Gemini
|
# Initialize Gemini
|
||||||
monkeypatch.setenv("GEMINI_API_KEY", "gemini-key")
|
monkeypatch.setenv("GEMINI_API_KEY", "gemini-key")
|
||||||
llm3 = initialize_llm("gemini", "gemini-2.0-flash-thinking-exp-1219")
|
_llm3 = initialize_llm("gemini", "gemini-2.0-flash-thinking-exp-1219")
|
||||||
|
|
||||||
# Verify both were initialized correctly
|
# Verify both were initialized correctly
|
||||||
mock_openai.assert_called_once_with(
|
mock_openai.assert_called_once_with(api_key="openai-key", model="gpt-4")
|
||||||
api_key="openai-key",
|
|
||||||
model="gpt-4"
|
|
||||||
)
|
|
||||||
mock_anthropic.assert_called_once_with(
|
mock_anthropic.assert_called_once_with(
|
||||||
api_key="anthropic-key",
|
api_key="anthropic-key", model_name="claude-3"
|
||||||
model_name="claude-3"
|
|
||||||
)
|
)
|
||||||
mock_gemini.assert_called_once_with(
|
mock_gemini.assert_called_once_with(
|
||||||
api_key="gemini-key",
|
api_key="gemini-key", model="gemini-2.0-flash-thinking-exp-1219"
|
||||||
model="gemini-2.0-flash-thinking-exp-1219"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Args:
|
class Args:
|
||||||
"""Test arguments class."""
|
"""Test arguments class."""
|
||||||
|
|
||||||
provider: str
|
provider: str
|
||||||
expert_provider: str
|
expert_provider: str
|
||||||
model: str = None
|
model: str = None
|
||||||
expert_model: str = None
|
expert_model: str = None
|
||||||
|
|
||||||
|
|
||||||
def test_environment_variable_precedence(clean_env, mock_openai, monkeypatch):
|
def test_environment_variable_precedence(clean_env, mock_openai, monkeypatch):
|
||||||
"""Test environment variable precedence and fallback."""
|
"""Test environment variable precedence and fallback."""
|
||||||
# Test get_env_var helper with fallback
|
# Test get_env_var helper with fallback
|
||||||
|
|
@ -350,12 +347,8 @@ def test_environment_variable_precedence(clean_env, mock_openai, monkeypatch):
|
||||||
assert config["api_key"] == "expert-key"
|
assert config["api_key"] == "expert-key"
|
||||||
|
|
||||||
# Test LLM client creation with expert mode
|
# Test LLM client creation with expert mode
|
||||||
llm = create_llm_client("openai", "o1", is_expert=True)
|
_llm = create_llm_client("openai", "o1", is_expert=True)
|
||||||
mock_openai.assert_called_with(
|
mock_openai.assert_called_with(api_key="expert-key", model="o1", temperature=0)
|
||||||
api_key="expert-key",
|
|
||||||
model="o1",
|
|
||||||
temperature=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test environment validation
|
# Test environment validation
|
||||||
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "")
|
monkeypatch.setenv("EXPERT_OPENAI_API_KEY", "")
|
||||||
|
|
@ -366,123 +359,141 @@ def test_environment_variable_precedence(clean_env, mock_openai, monkeypatch):
|
||||||
monkeypatch.setenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307")
|
monkeypatch.setenv("ANTHROPIC_MODEL", "claude-3-haiku-20240307")
|
||||||
|
|
||||||
args = Args(provider="anthropic", expert_provider="openai")
|
args = Args(provider="anthropic", expert_provider="openai")
|
||||||
expert_enabled, expert_missing, web_enabled, web_missing = validate_environment(args)
|
expert_enabled, expert_missing, web_enabled, web_missing = validate_environment(
|
||||||
|
args
|
||||||
|
)
|
||||||
assert not expert_enabled
|
assert not expert_enabled
|
||||||
assert expert_missing
|
assert expert_missing
|
||||||
assert not web_enabled
|
assert not web_enabled
|
||||||
assert web_missing
|
assert web_missing
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_anthropic():
|
def mock_anthropic():
|
||||||
"""
|
"""
|
||||||
Mock ChatAnthropic class for testing Anthropic provider initialization.
|
Mock ChatAnthropic class for testing Anthropic provider initialization.
|
||||||
Prevents actual API calls during testing.
|
Prevents actual API calls during testing.
|
||||||
"""
|
"""
|
||||||
with patch('ra_aid.llm.ChatAnthropic') as mock:
|
with patch("ra_aid.llm.ChatAnthropic") as mock:
|
||||||
mock.return_value = Mock(spec=ChatAnthropic)
|
mock.return_value = Mock(spec=ChatAnthropic)
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_gemini():
|
def mock_gemini():
|
||||||
"""Mock ChatGoogleGenerativeAI class for testing Gemini provider initialization."""
|
"""Mock ChatGoogleGenerativeAI class for testing Gemini provider initialization."""
|
||||||
with patch('ra_aid.llm.ChatGoogleGenerativeAI') as mock:
|
with patch("ra_aid.llm.ChatGoogleGenerativeAI") as mock:
|
||||||
mock.return_value = Mock(spec=ChatGoogleGenerativeAI)
|
mock.return_value = Mock(spec=ChatGoogleGenerativeAI)
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_deepseek_reasoner():
|
def mock_deepseek_reasoner():
|
||||||
"""Mock ChatDeepseekReasoner for testing DeepSeek provider initialization."""
|
"""Mock ChatDeepseekReasoner for testing DeepSeek provider initialization."""
|
||||||
with patch('ra_aid.llm.ChatDeepseekReasoner') as mock:
|
with patch("ra_aid.llm.ChatDeepseekReasoner") as mock:
|
||||||
mock.return_value = Mock()
|
mock.return_value = Mock()
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
def test_initialize_deepseek(clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch):
|
|
||||||
|
def test_initialize_deepseek(
|
||||||
|
clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch
|
||||||
|
):
|
||||||
"""Test DeepSeek provider initialization with different models."""
|
"""Test DeepSeek provider initialization with different models."""
|
||||||
monkeypatch.setenv("DEEPSEEK_API_KEY", "test-key")
|
monkeypatch.setenv("DEEPSEEK_API_KEY", "test-key")
|
||||||
|
|
||||||
# Test with reasoner model
|
# Test with reasoner model
|
||||||
model = initialize_llm("deepseek", "deepseek-reasoner")
|
_model = initialize_llm("deepseek", "deepseek-reasoner")
|
||||||
mock_deepseek_reasoner.assert_called_with(
|
mock_deepseek_reasoner.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
temperature=1,
|
temperature=1,
|
||||||
model="deepseek-reasoner"
|
model="deepseek-reasoner",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with non-reasoner model
|
# Test with non-reasoner model
|
||||||
model = initialize_llm("deepseek", "deepseek-chat")
|
_model = initialize_llm("deepseek", "deepseek-chat")
|
||||||
mock_openai.assert_called_with(
|
mock_openai.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
temperature=1,
|
temperature=1,
|
||||||
model="deepseek-chat"
|
model="deepseek-chat",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_initialize_expert_deepseek(clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch):
|
|
||||||
|
def test_initialize_expert_deepseek(
|
||||||
|
clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch
|
||||||
|
):
|
||||||
"""Test expert DeepSeek provider initialization."""
|
"""Test expert DeepSeek provider initialization."""
|
||||||
monkeypatch.setenv("EXPERT_DEEPSEEK_API_KEY", "test-key")
|
monkeypatch.setenv("EXPERT_DEEPSEEK_API_KEY", "test-key")
|
||||||
|
|
||||||
# Test with reasoner model
|
# Test with reasoner model
|
||||||
model = initialize_expert_llm("deepseek", "deepseek-reasoner")
|
_model = initialize_expert_llm("deepseek", "deepseek-reasoner")
|
||||||
mock_deepseek_reasoner.assert_called_with(
|
mock_deepseek_reasoner.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
temperature=0,
|
temperature=0,
|
||||||
model="deepseek-reasoner"
|
model="deepseek-reasoner",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with non-reasoner model
|
# Test with non-reasoner model
|
||||||
model = initialize_expert_llm("deepseek", "deepseek-chat")
|
_model = initialize_expert_llm("deepseek", "deepseek-chat")
|
||||||
mock_openai.assert_called_with(
|
mock_openai.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
temperature=0,
|
temperature=0,
|
||||||
model="deepseek-chat"
|
model="deepseek-chat",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_initialize_openrouter_deepseek(clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch):
|
|
||||||
|
def test_initialize_openrouter_deepseek(
|
||||||
|
clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch
|
||||||
|
):
|
||||||
"""Test OpenRouter DeepSeek model initialization."""
|
"""Test OpenRouter DeepSeek model initialization."""
|
||||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key")
|
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key")
|
||||||
|
|
||||||
# Test with DeepSeek R1 model
|
# Test with DeepSeek R1 model
|
||||||
model = initialize_llm("openrouter", "deepseek/deepseek-r1")
|
_model = initialize_llm("openrouter", "deepseek/deepseek-r1")
|
||||||
mock_deepseek_reasoner.assert_called_with(
|
mock_deepseek_reasoner.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url="https://openrouter.ai/api/v1",
|
||||||
temperature=1,
|
temperature=1,
|
||||||
model="deepseek/deepseek-r1"
|
model="deepseek/deepseek-r1",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with non-DeepSeek model
|
# Test with non-DeepSeek model
|
||||||
model = initialize_llm("openrouter", "mistral/mistral-large")
|
_model = initialize_llm("openrouter", "mistral/mistral-large")
|
||||||
mock_openai.assert_called_with(
|
|
||||||
api_key="test-key",
|
|
||||||
base_url="https://openrouter.ai/api/v1",
|
|
||||||
model="mistral/mistral-large"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_initialize_expert_openrouter_deepseek(clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch):
|
|
||||||
"""Test expert OpenRouter DeepSeek model initialization."""
|
|
||||||
monkeypatch.setenv("EXPERT_OPENROUTER_API_KEY", "test-key")
|
|
||||||
|
|
||||||
# Test with DeepSeek R1 model via create_llm_client
|
|
||||||
model = create_llm_client("openrouter", "deepseek/deepseek-r1", is_expert=True)
|
|
||||||
mock_deepseek_reasoner.assert_called_with(
|
|
||||||
api_key="test-key",
|
|
||||||
base_url="https://openrouter.ai/api/v1",
|
|
||||||
temperature=0,
|
|
||||||
model="deepseek/deepseek-r1"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with non-DeepSeek model
|
|
||||||
model = create_llm_client("openrouter", "mistral/mistral-large", is_expert=True)
|
|
||||||
mock_openai.assert_called_with(
|
mock_openai.assert_called_with(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url="https://openrouter.ai/api/v1",
|
||||||
model="mistral/mistral-large",
|
model="mistral/mistral-large",
|
||||||
temperature=0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_expert_openrouter_deepseek(
|
||||||
|
clean_env, mock_openai, mock_deepseek_reasoner, monkeypatch
|
||||||
|
):
|
||||||
|
"""Test expert OpenRouter DeepSeek model initialization."""
|
||||||
|
monkeypatch.setenv("EXPERT_OPENROUTER_API_KEY", "test-key")
|
||||||
|
|
||||||
|
# Test with DeepSeek R1 model via create_llm_client
|
||||||
|
_model = create_llm_client("openrouter", "deepseek/deepseek-r1", is_expert=True)
|
||||||
|
mock_deepseek_reasoner.assert_called_with(
|
||||||
|
api_key="test-key",
|
||||||
|
base_url="https://openrouter.ai/api/v1",
|
||||||
|
temperature=0,
|
||||||
|
model="deepseek/deepseek-r1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with non-DeepSeek model
|
||||||
|
_model = create_llm_client("openrouter", "mistral/mistral-large", is_expert=True)
|
||||||
|
mock_openai.assert_called_with(
|
||||||
|
api_key="test-key",
|
||||||
|
base_url="https://openrouter.ai/api/v1",
|
||||||
|
model="mistral/mistral-large",
|
||||||
|
temperature=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_deepseek_environment_fallback(clean_env, mock_deepseek_reasoner, monkeypatch):
|
def test_deepseek_environment_fallback(clean_env, mock_deepseek_reasoner, monkeypatch):
|
||||||
"""Test DeepSeek environment variable fallback behavior."""
|
"""Test DeepSeek environment variable fallback behavior."""
|
||||||
# Test environment variable helper with fallback
|
# Test environment variable helper with fallback
|
||||||
|
|
@ -500,10 +511,10 @@ def test_deepseek_environment_fallback(clean_env, mock_deepseek_reasoner, monkey
|
||||||
assert config["api_key"] == "expert-key"
|
assert config["api_key"] == "expert-key"
|
||||||
|
|
||||||
# Test client creation with expert key
|
# Test client creation with expert key
|
||||||
model = create_llm_client("deepseek", "deepseek-reasoner", is_expert=True)
|
_model = create_llm_client("deepseek", "deepseek-reasoner", is_expert=True)
|
||||||
mock_deepseek_reasoner.assert_called_with(
|
mock_deepseek_reasoner.assert_called_with(
|
||||||
api_key="expert-key",
|
api_key="expert-key",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
temperature=0,
|
temperature=0,
|
||||||
model="deepseek-reasoner"
|
model="deepseek-reasoner",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
"""Unit tests for __main__.py argument parsing."""
|
"""Unit tests for __main__.py argument parsing."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ra_aid.__main__ import parse_arguments
|
from ra_aid.__main__ import parse_arguments
|
||||||
from ra_aid.tools.memory import _global_memory
|
|
||||||
from ra_aid.config import DEFAULT_RECURSION_LIMIT
|
from ra_aid.config import DEFAULT_RECURSION_LIMIT
|
||||||
|
from ra_aid.tools.memory import _global_memory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_dependencies(monkeypatch):
|
def mock_dependencies(monkeypatch):
|
||||||
"""Mock all dependencies needed for main()."""
|
"""Mock all dependencies needed for main()."""
|
||||||
monkeypatch.setattr('ra_aid.__main__.check_dependencies', lambda: None)
|
monkeypatch.setattr("ra_aid.__main__.check_dependencies", lambda: None)
|
||||||
|
|
||||||
monkeypatch.setattr('ra_aid.__main__.validate_environment',
|
monkeypatch.setattr(
|
||||||
lambda args: (True, [], True, []))
|
"ra_aid.__main__.validate_environment", lambda args: (True, [], True, [])
|
||||||
|
)
|
||||||
|
|
||||||
def mock_config_update(*args, **kwargs):
|
def mock_config_update(*args, **kwargs):
|
||||||
config = _global_memory.get("config", {})
|
config = _global_memory.get("config", {})
|
||||||
|
|
@ -21,27 +23,31 @@ def mock_dependencies(monkeypatch):
|
||||||
_global_memory["config"] = config
|
_global_memory["config"] = config
|
||||||
return None
|
return None
|
||||||
|
|
||||||
monkeypatch.setattr('ra_aid.__main__.initialize_llm',
|
monkeypatch.setattr("ra_aid.__main__.initialize_llm", mock_config_update)
|
||||||
mock_config_update)
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"ra_aid.__main__.run_research_agent", lambda *args, **kwargs: None
|
||||||
|
)
|
||||||
|
|
||||||
monkeypatch.setattr('ra_aid.__main__.run_research_agent',
|
|
||||||
lambda *args, **kwargs: None)
|
|
||||||
|
|
||||||
def test_recursion_limit_in_global_config(mock_dependencies):
|
def test_recursion_limit_in_global_config(mock_dependencies):
|
||||||
"""Test that recursion limit is correctly set in global config."""
|
"""Test that recursion limit is correctly set in global config."""
|
||||||
from ra_aid.__main__ import main
|
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ra_aid.__main__ import main
|
||||||
|
|
||||||
_global_memory.clear()
|
_global_memory.clear()
|
||||||
|
|
||||||
with patch.object(sys, 'argv', ['ra-aid', '-m', 'test message']):
|
with patch.object(sys, "argv", ["ra-aid", "-m", "test message"]):
|
||||||
main()
|
main()
|
||||||
assert _global_memory["config"]["recursion_limit"] == DEFAULT_RECURSION_LIMIT
|
assert _global_memory["config"]["recursion_limit"] == DEFAULT_RECURSION_LIMIT
|
||||||
|
|
||||||
_global_memory.clear()
|
_global_memory.clear()
|
||||||
|
|
||||||
with patch.object(sys, 'argv', ['ra-aid', '-m', 'test message', '--recursion-limit', '50']):
|
with patch.object(
|
||||||
|
sys, "argv", ["ra-aid", "-m", "test message", "--recursion-limit", "50"]
|
||||||
|
):
|
||||||
main()
|
main()
|
||||||
assert _global_memory["config"]["recursion_limit"] == 50
|
assert _global_memory["config"]["recursion_limit"] == 50
|
||||||
|
|
||||||
|
|
@ -60,23 +66,35 @@ def test_zero_recursion_limit():
|
||||||
|
|
||||||
def test_config_settings(mock_dependencies):
|
def test_config_settings(mock_dependencies):
|
||||||
"""Test that various settings are correctly applied in global config."""
|
"""Test that various settings are correctly applied in global config."""
|
||||||
from ra_aid.__main__ import main
|
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ra_aid.__main__ import main
|
||||||
|
|
||||||
_global_memory.clear()
|
_global_memory.clear()
|
||||||
|
|
||||||
with patch.object(sys, 'argv', [
|
with patch.object(
|
||||||
'ra-aid', '-m', 'test message',
|
sys,
|
||||||
'--cowboy-mode',
|
"argv",
|
||||||
'--research-only',
|
[
|
||||||
'--provider', 'anthropic',
|
"ra-aid",
|
||||||
'--model', 'claude-3-5-sonnet-20241022',
|
"-m",
|
||||||
'--expert-provider', 'openai',
|
"test message",
|
||||||
'--expert-model', 'gpt-4',
|
"--cowboy-mode",
|
||||||
'--temperature', '0.7',
|
"--research-only",
|
||||||
'--disable-limit-tokens'
|
"--provider",
|
||||||
]):
|
"anthropic",
|
||||||
|
"--model",
|
||||||
|
"claude-3-5-sonnet-20241022",
|
||||||
|
"--expert-provider",
|
||||||
|
"openai",
|
||||||
|
"--expert-model",
|
||||||
|
"gpt-4",
|
||||||
|
"--temperature",
|
||||||
|
"0.7",
|
||||||
|
"--disable-limit-tokens",
|
||||||
|
],
|
||||||
|
):
|
||||||
main()
|
main()
|
||||||
config = _global_memory["config"]
|
config = _global_memory["config"]
|
||||||
assert config["cowboy_mode"] is True
|
assert config["cowboy_mode"] is True
|
||||||
|
|
@ -90,20 +108,25 @@ def test_config_settings(mock_dependencies):
|
||||||
|
|
||||||
def test_temperature_validation(mock_dependencies):
|
def test_temperature_validation(mock_dependencies):
|
||||||
"""Test that temperature argument is correctly passed to initialize_llm."""
|
"""Test that temperature argument is correctly passed to initialize_llm."""
|
||||||
from ra_aid.__main__ import main, initialize_llm
|
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ra_aid.__main__ import main
|
||||||
|
|
||||||
_global_memory.clear()
|
_global_memory.clear()
|
||||||
|
|
||||||
with patch('ra_aid.__main__.initialize_llm') as mock_init_llm:
|
with patch("ra_aid.__main__.initialize_llm") as mock_init_llm:
|
||||||
with patch.object(sys, 'argv', ['ra-aid', '-m', 'test', '--temperature', '0.7']):
|
with patch.object(
|
||||||
|
sys, "argv", ["ra-aid", "-m", "test", "--temperature", "0.7"]
|
||||||
|
):
|
||||||
main()
|
main()
|
||||||
mock_init_llm.assert_called_once()
|
mock_init_llm.assert_called_once()
|
||||||
assert mock_init_llm.call_args.kwargs['temperature'] == 0.7
|
assert mock_init_llm.call_args.kwargs["temperature"] == 0.7
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
with patch.object(sys, 'argv', ['ra-aid', '-m', 'test', '--temperature', '2.1']):
|
with patch.object(
|
||||||
|
sys, "argv", ["ra-aid", "-m", "test", "--temperature", "2.1"]
|
||||||
|
):
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -125,19 +148,30 @@ def test_missing_message():
|
||||||
|
|
||||||
def test_research_model_provider_args(mock_dependencies):
|
def test_research_model_provider_args(mock_dependencies):
|
||||||
"""Test that research-specific model/provider args are correctly stored in config."""
|
"""Test that research-specific model/provider args are correctly stored in config."""
|
||||||
from ra_aid.__main__ import main
|
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ra_aid.__main__ import main
|
||||||
|
|
||||||
_global_memory.clear()
|
_global_memory.clear()
|
||||||
|
|
||||||
with patch.object(sys, 'argv', [
|
with patch.object(
|
||||||
'ra-aid', '-m', 'test message',
|
sys,
|
||||||
'--research-provider', 'anthropic',
|
"argv",
|
||||||
'--research-model', 'claude-3-haiku-20240307',
|
[
|
||||||
'--planner-provider', 'openai',
|
"ra-aid",
|
||||||
'--planner-model', 'gpt-4'
|
"-m",
|
||||||
]):
|
"test message",
|
||||||
|
"--research-provider",
|
||||||
|
"anthropic",
|
||||||
|
"--research-model",
|
||||||
|
"claude-3-haiku-20240307",
|
||||||
|
"--planner-provider",
|
||||||
|
"openai",
|
||||||
|
"--planner-model",
|
||||||
|
"gpt-4",
|
||||||
|
],
|
||||||
|
):
|
||||||
main()
|
main()
|
||||||
config = _global_memory["config"]
|
config = _global_memory["config"]
|
||||||
assert config["research_provider"] == "anthropic"
|
assert config["research_provider"] == "anthropic"
|
||||||
|
|
@ -148,17 +182,18 @@ def test_research_model_provider_args(mock_dependencies):
|
||||||
|
|
||||||
def test_planner_model_provider_args(mock_dependencies):
|
def test_planner_model_provider_args(mock_dependencies):
|
||||||
"""Test that planner provider/model args fall back to main config when not specified."""
|
"""Test that planner provider/model args fall back to main config when not specified."""
|
||||||
from ra_aid.__main__ import main
|
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ra_aid.__main__ import main
|
||||||
|
|
||||||
_global_memory.clear()
|
_global_memory.clear()
|
||||||
|
|
||||||
with patch.object(sys, 'argv', [
|
with patch.object(
|
||||||
'ra-aid', '-m', 'test message',
|
sys,
|
||||||
'--provider', 'openai',
|
"argv",
|
||||||
'--model', 'gpt-4'
|
["ra-aid", "-m", "test message", "--provider", "openai", "--model", "gpt-4"],
|
||||||
]):
|
):
|
||||||
main()
|
main()
|
||||||
config = _global_memory["config"]
|
config = _global_memory["config"]
|
||||||
assert config["planner_provider"] == "openai"
|
assert config["planner_provider"] == "openai"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ra_aid.tools.programmer import parse_aider_flags, run_programming_task
|
from ra_aid.tools.programmer import parse_aider_flags, run_programming_task
|
||||||
|
|
||||||
# Test cases for parse_aider_flags function
|
# Test cases for parse_aider_flags function
|
||||||
|
|
@ -7,65 +8,57 @@ test_cases = [
|
||||||
(
|
(
|
||||||
"yes-always,dark-mode",
|
"yes-always,dark-mode",
|
||||||
["--yes-always", "--dark-mode"],
|
["--yes-always", "--dark-mode"],
|
||||||
"basic comma separated flags without dashes"
|
"basic comma separated flags without dashes",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"--yes-always,--dark-mode",
|
"--yes-always,--dark-mode",
|
||||||
["--yes-always", "--dark-mode"],
|
["--yes-always", "--dark-mode"],
|
||||||
"comma separated flags with dashes"
|
"comma separated flags with dashes",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"yes-always, dark-mode",
|
"yes-always, dark-mode",
|
||||||
["--yes-always", "--dark-mode"],
|
["--yes-always", "--dark-mode"],
|
||||||
"comma separated flags with space"
|
"comma separated flags with space",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"--yes-always, --dark-mode",
|
"--yes-always, --dark-mode",
|
||||||
["--yes-always", "--dark-mode"],
|
["--yes-always", "--dark-mode"],
|
||||||
"comma separated flags with dashes and space"
|
"comma separated flags with dashes and space",
|
||||||
),
|
|
||||||
(
|
|
||||||
"",
|
|
||||||
[],
|
|
||||||
"empty string"
|
|
||||||
),
|
),
|
||||||
|
("", [], "empty string"),
|
||||||
(
|
(
|
||||||
" yes-always , dark-mode ",
|
" yes-always , dark-mode ",
|
||||||
["--yes-always", "--dark-mode"],
|
["--yes-always", "--dark-mode"],
|
||||||
"flags with extra whitespace"
|
"flags with extra whitespace",
|
||||||
),
|
),
|
||||||
(
|
("--yes-always", ["--yes-always"], "single flag with dashes"),
|
||||||
"--yes-always",
|
("yes-always", ["--yes-always"], "single flag without dashes"),
|
||||||
["--yes-always"],
|
|
||||||
"single flag with dashes"
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"yes-always",
|
|
||||||
["--yes-always"],
|
|
||||||
"single flag without dashes"
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("input_flags,expected,description", test_cases)
|
@pytest.mark.parametrize("input_flags,expected,description", test_cases)
|
||||||
def test_parse_aider_flags(input_flags, expected, description):
|
def test_parse_aider_flags(input_flags, expected, description):
|
||||||
"""Table-driven test for parse_aider_flags function."""
|
"""Table-driven test for parse_aider_flags function."""
|
||||||
result = parse_aider_flags(input_flags)
|
result = parse_aider_flags(input_flags)
|
||||||
assert result == expected, f"Failed test case: {description}"
|
assert result == expected, f"Failed test case: {description}"
|
||||||
|
|
||||||
|
|
||||||
def test_aider_config_flag(mocker):
|
def test_aider_config_flag(mocker):
|
||||||
"""Test that aider config flag is properly included in the command when specified."""
|
"""Test that aider config flag is properly included in the command when specified."""
|
||||||
mock_memory = {
|
mock_memory = {
|
||||||
'config': {'aider_config': '/path/to/config.yml'},
|
"config": {"aider_config": "/path/to/config.yml"},
|
||||||
'related_files': {}
|
"related_files": {},
|
||||||
}
|
}
|
||||||
mocker.patch('ra_aid.tools.programmer._global_memory', mock_memory)
|
mocker.patch("ra_aid.tools.programmer._global_memory", mock_memory)
|
||||||
|
|
||||||
# Mock the run_interactive_command to capture the command that would be run
|
# Mock the run_interactive_command to capture the command that would be run
|
||||||
mock_run = mocker.patch('ra_aid.tools.programmer.run_interactive_command', return_value=(b'', 0))
|
mock_run = mocker.patch(
|
||||||
|
"ra_aid.tools.programmer.run_interactive_command", return_value=(b"", 0)
|
||||||
|
)
|
||||||
|
|
||||||
run_programming_task("test instruction")
|
run_programming_task("test instruction")
|
||||||
|
|
||||||
args = mock_run.call_args[0][0] # Get the first positional arg (command list)
|
args = mock_run.call_args[0][0] # Get the first positional arg (command list)
|
||||||
assert '--config' in args
|
assert "--config" in args
|
||||||
config_index = args.index('--config')
|
config_index = args.index("--config")
|
||||||
assert args[config_index + 1] == '/path/to/config.yml'
|
assert args[config_index + 1] == "/path/to/config.yml"
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
"""Integration tests for provider validation and environment handling."""
|
"""Integration tests for provider validation and environment handling."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pytest
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.env import validate_environment
|
from ra_aid.env import validate_environment
|
||||||
from ra_aid.provider_strategy import (
|
from ra_aid.provider_strategy import (
|
||||||
ProviderFactory,
|
|
||||||
ValidationResult,
|
|
||||||
AnthropicStrategy,
|
AnthropicStrategy,
|
||||||
OpenAIStrategy,
|
|
||||||
OpenAICompatibleStrategy,
|
|
||||||
OpenRouterStrategy,
|
|
||||||
GeminiStrategy,
|
GeminiStrategy,
|
||||||
|
OpenAICompatibleStrategy,
|
||||||
|
OpenAIStrategy,
|
||||||
|
OpenRouterStrategy,
|
||||||
|
ProviderFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import pytest
|
|
||||||
from ra_aid.tool_configs import (
|
from ra_aid.tool_configs import (
|
||||||
|
get_implementation_tools,
|
||||||
|
get_planning_tools,
|
||||||
get_read_only_tools,
|
get_read_only_tools,
|
||||||
get_research_tools,
|
get_research_tools,
|
||||||
get_planning_tools,
|
get_web_research_tools,
|
||||||
get_implementation_tools,
|
|
||||||
get_web_research_tools
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_read_only_tools():
|
def test_get_read_only_tools():
|
||||||
# Test without human interaction
|
# Test without human interaction
|
||||||
tools = get_read_only_tools(human_interaction=False)
|
tools = get_read_only_tools(human_interaction=False)
|
||||||
|
|
@ -17,6 +17,7 @@ def test_get_read_only_tools():
|
||||||
tools_with_human = get_read_only_tools(human_interaction=True)
|
tools_with_human = get_read_only_tools(human_interaction=True)
|
||||||
assert len(tools_with_human) == len(tools) + 1
|
assert len(tools_with_human) == len(tools) + 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_research_tools():
|
def test_get_research_tools():
|
||||||
# Test basic research tools
|
# Test basic research tools
|
||||||
tools = get_research_tools()
|
tools = get_research_tools()
|
||||||
|
|
@ -31,6 +32,7 @@ def test_get_research_tools():
|
||||||
tools_research_only = get_research_tools(research_only=True)
|
tools_research_only = get_research_tools(research_only=True)
|
||||||
assert len(tools_research_only) < len(tools)
|
assert len(tools_research_only) < len(tools)
|
||||||
|
|
||||||
|
|
||||||
def test_get_planning_tools():
|
def test_get_planning_tools():
|
||||||
# Test with expert enabled
|
# Test with expert enabled
|
||||||
tools = get_planning_tools(expert_enabled=True)
|
tools = get_planning_tools(expert_enabled=True)
|
||||||
|
|
@ -41,6 +43,7 @@ def test_get_planning_tools():
|
||||||
tools_no_expert = get_planning_tools(expert_enabled=False)
|
tools_no_expert = get_planning_tools(expert_enabled=False)
|
||||||
assert len(tools_no_expert) < len(tools)
|
assert len(tools_no_expert) < len(tools)
|
||||||
|
|
||||||
|
|
||||||
def test_get_implementation_tools():
|
def test_get_implementation_tools():
|
||||||
# Test with expert enabled
|
# Test with expert enabled
|
||||||
tools = get_implementation_tools(expert_enabled=True)
|
tools = get_implementation_tools(expert_enabled=True)
|
||||||
|
|
@ -51,6 +54,7 @@ def test_get_implementation_tools():
|
||||||
tools_no_expert = get_implementation_tools(expert_enabled=False)
|
tools_no_expert = get_implementation_tools(expert_enabled=False)
|
||||||
assert len(tools_no_expert) < len(tools)
|
assert len(tools_no_expert) < len(tools)
|
||||||
|
|
||||||
|
|
||||||
def test_get_web_research_tools():
|
def test_get_web_research_tools():
|
||||||
# Test with expert enabled
|
# Test with expert enabled
|
||||||
tools = get_web_research_tools(expert_enabled=True)
|
tools = get_web_research_tools(expert_enabled=True)
|
||||||
|
|
@ -59,7 +63,13 @@ def test_get_web_research_tools():
|
||||||
|
|
||||||
# Get tool names and verify exact matches
|
# Get tool names and verify exact matches
|
||||||
tool_names = [tool.name for tool in tools]
|
tool_names = [tool.name for tool in tools]
|
||||||
expected_names = ['emit_expert_context', 'ask_expert', 'web_search_tavily', 'emit_research_notes', 'task_completed']
|
expected_names = [
|
||||||
|
"emit_expert_context",
|
||||||
|
"ask_expert",
|
||||||
|
"web_search_tavily",
|
||||||
|
"emit_research_notes",
|
||||||
|
"task_completed",
|
||||||
|
]
|
||||||
assert sorted(tool_names) == sorted(expected_names)
|
assert sorted(tool_names) == sorted(expected_names)
|
||||||
|
|
||||||
# Test without expert enabled
|
# Test without expert enabled
|
||||||
|
|
@ -69,4 +79,6 @@ def test_get_web_research_tools():
|
||||||
|
|
||||||
# Verify exact tool names when expert is disabled
|
# Verify exact tool names when expert is disabled
|
||||||
tool_names_no_expert = [tool.name for tool in tools_no_expert]
|
tool_names_no_expert = [tool.name for tool in tools_no_expert]
|
||||||
assert sorted(tool_names_no_expert) == sorted(['web_search_tavily', 'emit_research_notes', 'task_completed'])
|
assert sorted(tool_names_no_expert) == sorted(
|
||||||
|
["web_search_tavily", "emit_research_notes", "task_completed"]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"""Tests for utility functions."""
|
"""Tests for utility functions."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from ra_aid.text.processing import truncate_output
|
from ra_aid.text.processing import truncate_output
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,7 +70,7 @@ def test_ansi_sequences():
|
||||||
"\033[31mRed Line 1\033[0m\n",
|
"\033[31mRed Line 1\033[0m\n",
|
||||||
"\033[32mGreen Line 2\033[0m\n",
|
"\033[32mGreen Line 2\033[0m\n",
|
||||||
"\033[34mBlue Line 3\033[0m\n",
|
"\033[34mBlue Line 3\033[0m\n",
|
||||||
"\033[33mYellow Line 4\033[0m\n"
|
"\033[33mYellow Line 4\033[0m\n",
|
||||||
]
|
]
|
||||||
input_text = "".join(input_lines)
|
input_text = "".join(input_lines)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
"""Tests for test execution utilities."""
|
"""Tests for test execution utilities."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
from ra_aid.tools.handle_user_defined_test_cmd_execution import execute_test_command
|
from ra_aid.tools.handle_user_defined_test_cmd_execution import execute_test_command
|
||||||
|
|
||||||
# Test cases for execute_test_command
|
# Test cases for execute_test_command
|
||||||
test_cases = [
|
test_cases = [
|
||||||
# Format: (name, config, original_prompt, test_attempts, auto_test,
|
# Format: (name, config, original_prompt, test_attempts, auto_test,
|
||||||
# mock_responses, expected_result)
|
# mock_responses, expected_result)
|
||||||
|
|
||||||
# Case 1: No test command configured
|
# Case 1: No test command configured
|
||||||
(
|
(
|
||||||
"no_test_command",
|
"no_test_command",
|
||||||
|
|
@ -17,9 +18,8 @@ test_cases = [
|
||||||
0,
|
0,
|
||||||
False,
|
False,
|
||||||
{},
|
{},
|
||||||
(True, "original prompt", False, 0)
|
(True, "original prompt", False, 0),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 2: User declines to run test
|
# Case 2: User declines to run test
|
||||||
(
|
(
|
||||||
"user_declines_test",
|
"user_declines_test",
|
||||||
|
|
@ -28,9 +28,8 @@ test_cases = [
|
||||||
0,
|
0,
|
||||||
False,
|
False,
|
||||||
{"ask_human_response": "n"},
|
{"ask_human_response": "n"},
|
||||||
(True, "original prompt", False, 0)
|
(True, "original prompt", False, 0),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 3: User enables auto-test
|
# Case 3: User enables auto-test
|
||||||
(
|
(
|
||||||
"user_enables_auto_test",
|
"user_enables_auto_test",
|
||||||
|
|
@ -40,11 +39,10 @@ test_cases = [
|
||||||
False,
|
False,
|
||||||
{
|
{
|
||||||
"ask_human_response": "a",
|
"ask_human_response": "a",
|
||||||
"shell_cmd_result": {"success": True, "output": "All tests passed"}
|
"shell_cmd_result": {"success": True, "output": "All tests passed"},
|
||||||
},
|
},
|
||||||
(True, "original prompt", True, 1)
|
(True, "original prompt", True, 1),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 4: Auto-test success
|
# Case 4: Auto-test success
|
||||||
(
|
(
|
||||||
"auto_test_success",
|
"auto_test_success",
|
||||||
|
|
@ -53,9 +51,8 @@ test_cases = [
|
||||||
0,
|
0,
|
||||||
True,
|
True,
|
||||||
{"shell_cmd_result": {"success": True, "output": "All tests passed"}},
|
{"shell_cmd_result": {"success": True, "output": "All tests passed"}},
|
||||||
(True, "original prompt", True, 1)
|
(True, "original prompt", True, 1),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 5: Auto-test failure with retry
|
# Case 5: Auto-test failure with retry
|
||||||
(
|
(
|
||||||
"auto_test_failure_retry",
|
"auto_test_failure_retry",
|
||||||
|
|
@ -64,9 +61,13 @@ test_cases = [
|
||||||
0,
|
0,
|
||||||
True,
|
True,
|
||||||
{"shell_cmd_result": {"success": False, "output": "Test failed"}},
|
{"shell_cmd_result": {"success": False, "output": "Test failed"}},
|
||||||
(False, "original prompt. Previous attempt failed with: <test_cmd_stdout>Test failed</test_cmd_stdout>", True, 1)
|
(
|
||||||
|
False,
|
||||||
|
"original prompt. Previous attempt failed with: <test_cmd_stdout>Test failed</test_cmd_stdout>",
|
||||||
|
True,
|
||||||
|
1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 6: Max retries reached
|
# Case 6: Max retries reached
|
||||||
(
|
(
|
||||||
"max_retries_reached",
|
"max_retries_reached",
|
||||||
|
|
@ -75,9 +76,8 @@ test_cases = [
|
||||||
3,
|
3,
|
||||||
True,
|
True,
|
||||||
{},
|
{},
|
||||||
(True, "original prompt", True, 3)
|
(True, "original prompt", True, 3),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 7: User runs test manually
|
# Case 7: User runs test manually
|
||||||
(
|
(
|
||||||
"manual_test_success",
|
"manual_test_success",
|
||||||
|
|
@ -87,11 +87,10 @@ test_cases = [
|
||||||
False,
|
False,
|
||||||
{
|
{
|
||||||
"ask_human_response": "y",
|
"ask_human_response": "y",
|
||||||
"shell_cmd_result": {"success": True, "output": "All tests passed"}
|
"shell_cmd_result": {"success": True, "output": "All tests passed"},
|
||||||
},
|
},
|
||||||
(True, "original prompt", False, 1)
|
(True, "original prompt", False, 1),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 8: Manual test failure
|
# Case 8: Manual test failure
|
||||||
(
|
(
|
||||||
"manual_test_failure",
|
"manual_test_failure",
|
||||||
|
|
@ -101,11 +100,15 @@ test_cases = [
|
||||||
False,
|
False,
|
||||||
{
|
{
|
||||||
"ask_human_response": "y",
|
"ask_human_response": "y",
|
||||||
"shell_cmd_result": {"success": False, "output": "Test failed"}
|
"shell_cmd_result": {"success": False, "output": "Test failed"},
|
||||||
},
|
},
|
||||||
(False, "original prompt. Previous attempt failed with: <test_cmd_stdout>Test failed</test_cmd_stdout>", False, 1)
|
(
|
||||||
|
False,
|
||||||
|
"original prompt. Previous attempt failed with: <test_cmd_stdout>Test failed</test_cmd_stdout>",
|
||||||
|
False,
|
||||||
|
1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 9: Manual test error
|
# Case 9: Manual test error
|
||||||
(
|
(
|
||||||
"manual_test_error",
|
"manual_test_error",
|
||||||
|
|
@ -115,11 +118,10 @@ test_cases = [
|
||||||
False,
|
False,
|
||||||
{
|
{
|
||||||
"ask_human_response": "y",
|
"ask_human_response": "y",
|
||||||
"shell_cmd_result_error": Exception("Command failed")
|
"shell_cmd_result_error": Exception("Command failed"),
|
||||||
},
|
},
|
||||||
(True, "original prompt", False, 1)
|
(True, "original prompt", False, 1),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Case 10: Auto-test error
|
# Case 10: Auto-test error
|
||||||
(
|
(
|
||||||
"auto_test_error",
|
"auto_test_error",
|
||||||
|
|
@ -127,17 +129,16 @@ test_cases = [
|
||||||
"original prompt",
|
"original prompt",
|
||||||
0,
|
0,
|
||||||
True,
|
True,
|
||||||
{
|
{"shell_cmd_result_error": Exception("Command failed")},
|
||||||
"shell_cmd_result_error": Exception("Command failed")
|
(True, "original prompt", True, 1),
|
||||||
},
|
|
||||||
(True, "original prompt", True, 1)
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"name,config,original_prompt,test_attempts,auto_test,mock_responses,expected",
|
"name,config,original_prompt,test_attempts,auto_test,mock_responses,expected",
|
||||||
test_cases,
|
test_cases,
|
||||||
ids=[case[0] for case in test_cases]
|
ids=[case[0] for case in test_cases],
|
||||||
)
|
)
|
||||||
def test_execute_test_command(
|
def test_execute_test_command(
|
||||||
name: str,
|
name: str,
|
||||||
|
|
@ -159,11 +160,20 @@ def test_execute_test_command(
|
||||||
mock_responses: Mock response data
|
mock_responses: Mock response data
|
||||||
expected: Expected result tuple
|
expected: Expected result tuple
|
||||||
"""
|
"""
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.ask_human") as mock_ask_human, \
|
with (
|
||||||
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run_cmd, \
|
patch(
|
||||||
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.console") as mock_console, \
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.ask_human"
|
||||||
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.logger") as mock_logger:
|
) as mock_ask_human,
|
||||||
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run_cmd,
|
||||||
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.console"
|
||||||
|
) as _mock_console,
|
||||||
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.logger"
|
||||||
|
) as mock_logger,
|
||||||
|
):
|
||||||
# Configure mocks based on mock_responses
|
# Configure mocks based on mock_responses
|
||||||
if "ask_human_response" in mock_responses:
|
if "ask_human_response" in mock_responses:
|
||||||
mock_ask_human.invoke.return_value = mock_responses["ask_human_response"]
|
mock_ask_human.invoke.return_value = mock_responses["ask_human_response"]
|
||||||
|
|
@ -174,12 +184,7 @@ def test_execute_test_command(
|
||||||
mock_run_cmd.return_value = mock_responses["shell_cmd_result"]
|
mock_run_cmd.return_value = mock_responses["shell_cmd_result"]
|
||||||
|
|
||||||
# Execute test command
|
# Execute test command
|
||||||
result = execute_test_command(
|
result = execute_test_command(config, original_prompt, test_attempts, auto_test)
|
||||||
config,
|
|
||||||
original_prompt,
|
|
||||||
test_attempts,
|
|
||||||
auto_test
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify result matches expected
|
# Verify result matches expected
|
||||||
assert result == expected, f"Test case '{name}' failed"
|
assert result == expected, f"Test case '{name}' failed"
|
||||||
|
|
@ -191,28 +196,31 @@ def test_execute_test_command(
|
||||||
if auto_test and test_attempts < config.get("max_test_cmd_retries", 5):
|
if auto_test and test_attempts < config.get("max_test_cmd_retries", 5):
|
||||||
if config.get("test_cmd"):
|
if config.get("test_cmd"):
|
||||||
# Verify run_shell_command called with command and default timeout
|
# Verify run_shell_command called with command and default timeout
|
||||||
mock_run_cmd.assert_called_once_with(config["test_cmd"], timeout=config.get('timeout', 30))
|
mock_run_cmd.assert_called_once_with(
|
||||||
|
config["test_cmd"], timeout=config.get("timeout", 30)
|
||||||
|
)
|
||||||
|
|
||||||
# Verify logging for max retries
|
# Verify logging for max retries
|
||||||
if test_attempts >= config.get("max_test_cmd_retries", 5):
|
if test_attempts >= config.get("max_test_cmd_retries", 5):
|
||||||
mock_logger.warning.assert_called_once_with("Max test retries reached")
|
mock_logger.warning.assert_called_once_with("Max test retries reached")
|
||||||
|
|
||||||
|
|
||||||
def test_execute_test_command_error_handling() -> None:
|
def test_execute_test_command_error_handling() -> None:
|
||||||
"""Test error handling in execute_test_command."""
|
"""Test error handling in execute_test_command."""
|
||||||
config = {"test_cmd": "pytest"}
|
config = {"test_cmd": "pytest"}
|
||||||
|
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run_cmd, \
|
with (
|
||||||
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.logger") as mock_logger:
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run_cmd,
|
||||||
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.logger"
|
||||||
|
) as mock_logger,
|
||||||
|
):
|
||||||
# Simulate run_shell_command raising an exception
|
# Simulate run_shell_command raising an exception
|
||||||
mock_run_cmd.side_effect = Exception("Command failed")
|
mock_run_cmd.side_effect = Exception("Command failed")
|
||||||
|
|
||||||
result = execute_test_command(
|
result = execute_test_command(config, "original prompt", 0, True)
|
||||||
config,
|
|
||||||
"original prompt",
|
|
||||||
0,
|
|
||||||
True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Should handle error and continue
|
# Should handle error and continue
|
||||||
assert result == (True, "original prompt", True, 1)
|
assert result == (True, "original prompt", True, 1)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import os
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, mock_open
|
from ra_aid.tools.expert import (
|
||||||
from ra_aid.tools.expert import read_files_with_limit, emit_expert_context, expert_context
|
emit_expert_context,
|
||||||
|
expert_context,
|
||||||
|
read_files_with_limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_test_files(tmp_path):
|
def temp_test_files(tmp_path):
|
||||||
|
|
@ -17,6 +22,7 @@ def temp_test_files(tmp_path):
|
||||||
|
|
||||||
return tmp_path, [file1, file2, file3]
|
return tmp_path, [file1, file2, file3]
|
||||||
|
|
||||||
|
|
||||||
def test_read_files_with_limit_basic(temp_test_files):
|
def test_read_files_with_limit_basic(temp_test_files):
|
||||||
"""Test basic successful reading of multiple files."""
|
"""Test basic successful reading of multiple files."""
|
||||||
tmp_path, files = temp_test_files
|
tmp_path, files = temp_test_files
|
||||||
|
|
@ -28,12 +34,14 @@ def test_read_files_with_limit_basic(temp_test_files):
|
||||||
assert str(files[0]) in result
|
assert str(files[0]) in result
|
||||||
assert str(files[1]) in result
|
assert str(files[1]) in result
|
||||||
|
|
||||||
|
|
||||||
def test_read_files_with_limit_empty_file(temp_test_files):
|
def test_read_files_with_limit_empty_file(temp_test_files):
|
||||||
"""Test handling of empty files."""
|
"""Test handling of empty files."""
|
||||||
tmp_path, files = temp_test_files
|
tmp_path, files = temp_test_files
|
||||||
result = read_files_with_limit([str(files[2])]) # Empty file
|
result = read_files_with_limit([str(files[2])]) # Empty file
|
||||||
assert result == "" # Empty files should be excluded from output
|
assert result == "" # Empty files should be excluded from output
|
||||||
|
|
||||||
|
|
||||||
def test_read_files_with_limit_nonexistent_file(temp_test_files):
|
def test_read_files_with_limit_nonexistent_file(temp_test_files):
|
||||||
"""Test handling of nonexistent files."""
|
"""Test handling of nonexistent files."""
|
||||||
tmp_path, files = temp_test_files
|
tmp_path, files = temp_test_files
|
||||||
|
|
@ -43,6 +51,7 @@ def test_read_files_with_limit_nonexistent_file(temp_test_files):
|
||||||
assert "Line 1" in result # Should contain content from existing file
|
assert "Line 1" in result # Should contain content from existing file
|
||||||
assert "nonexistent.txt" not in result # Shouldn't include non-existent file
|
assert "nonexistent.txt" not in result # Shouldn't include non-existent file
|
||||||
|
|
||||||
|
|
||||||
def test_read_files_with_limit_line_limit(temp_test_files):
|
def test_read_files_with_limit_line_limit(temp_test_files):
|
||||||
"""Test enforcement of line limit."""
|
"""Test enforcement of line limit."""
|
||||||
tmp_path, files = temp_test_files
|
tmp_path, files = temp_test_files
|
||||||
|
|
@ -53,7 +62,8 @@ def test_read_files_with_limit_line_limit(temp_test_files):
|
||||||
assert "Line 2" in result
|
assert "Line 2" in result
|
||||||
assert "File 2 Line 1" not in result # Should be truncated before reaching file 2
|
assert "File 2 Line 1" not in result # Should be truncated before reaching file 2
|
||||||
|
|
||||||
@patch('builtins.open')
|
|
||||||
|
@patch("builtins.open")
|
||||||
def test_read_files_with_limit_permission_error(mock_open_func, temp_test_files):
|
def test_read_files_with_limit_permission_error(mock_open_func, temp_test_files):
|
||||||
"""Test handling of permission errors."""
|
"""Test handling of permission errors."""
|
||||||
mock_open_func.side_effect = PermissionError("Permission denied")
|
mock_open_func.side_effect = PermissionError("Permission denied")
|
||||||
|
|
@ -62,7 +72,8 @@ def test_read_files_with_limit_permission_error(mock_open_func, temp_test_files)
|
||||||
result = read_files_with_limit([str(files[0])])
|
result = read_files_with_limit([str(files[0])])
|
||||||
assert result == "" # Should return empty string on permission error
|
assert result == "" # Should return empty string on permission error
|
||||||
|
|
||||||
@patch('builtins.open')
|
|
||||||
|
@patch("builtins.open")
|
||||||
def test_read_files_with_limit_io_error(mock_open_func, temp_test_files):
|
def test_read_files_with_limit_io_error(mock_open_func, temp_test_files):
|
||||||
"""Test handling of IO errors."""
|
"""Test handling of IO errors."""
|
||||||
mock_open_func.side_effect = IOError("IO Error")
|
mock_open_func.side_effect = IOError("IO Error")
|
||||||
|
|
@ -71,35 +82,39 @@ def test_read_files_with_limit_io_error(mock_open_func, temp_test_files):
|
||||||
result = read_files_with_limit([str(files[0])])
|
result = read_files_with_limit([str(files[0])])
|
||||||
assert result == "" # Should return empty string on IO error
|
assert result == "" # Should return empty string on IO error
|
||||||
|
|
||||||
|
|
||||||
def test_read_files_with_limit_encoding_error(temp_test_files):
|
def test_read_files_with_limit_encoding_error(temp_test_files):
|
||||||
"""Test handling of encoding errors."""
|
"""Test handling of encoding errors."""
|
||||||
tmp_path, files = temp_test_files
|
tmp_path, files = temp_test_files
|
||||||
|
|
||||||
# Create a file with invalid UTF-8
|
# Create a file with invalid UTF-8
|
||||||
invalid_file = tmp_path / "invalid.txt"
|
invalid_file = tmp_path / "invalid.txt"
|
||||||
with open(invalid_file, 'wb') as f:
|
with open(invalid_file, "wb") as f:
|
||||||
f.write(b'\xFF\xFE\x00\x00') # Invalid UTF-8
|
f.write(b"\xff\xfe\x00\x00") # Invalid UTF-8
|
||||||
|
|
||||||
result = read_files_with_limit([str(invalid_file)])
|
result = read_files_with_limit([str(invalid_file)])
|
||||||
assert result == "" # Should return empty string on encoding error
|
assert result == "" # Should return empty string on encoding error
|
||||||
|
|
||||||
|
|
||||||
def test_expert_context_management():
|
def test_expert_context_management():
|
||||||
"""Test expert context global state management."""
|
"""Test expert context global state management."""
|
||||||
# Clear any existing context
|
# Clear any existing context
|
||||||
expert_context['text'].clear()
|
expert_context["text"].clear()
|
||||||
expert_context['files'].clear()
|
expert_context["files"].clear()
|
||||||
|
|
||||||
# Test adding context
|
# Test adding context
|
||||||
result1 = emit_expert_context.invoke("Test context 1")
|
result1 = emit_expert_context.invoke("Test context 1")
|
||||||
assert "Context added" in result1
|
assert "Context added" in result1
|
||||||
assert len(expert_context['text']) == 1
|
assert len(expert_context["text"]) == 1
|
||||||
assert expert_context['text'][0] == "Test context 1"
|
assert expert_context["text"][0] == "Test context 1"
|
||||||
|
|
||||||
# Test adding multiple contexts
|
# Test adding multiple contexts
|
||||||
result2 = emit_expert_context.invoke("Test context 2")
|
result2 = emit_expert_context.invoke("Test context 2")
|
||||||
assert "Context added" in result2
|
assert "Context added" in result2
|
||||||
assert len(expert_context['text']) == 2
|
assert len(expert_context["text"]) == 2
|
||||||
assert expert_context['text'][1] == "Test context 2"
|
assert expert_context["text"][1] == "Test context 2"
|
||||||
|
|
||||||
# Test context accumulation
|
# Test context accumulation
|
||||||
assert all(ctx in expert_context['text'] for ctx in ["Test context 1", "Test context 2"])
|
assert all(
|
||||||
|
ctx in expert_context["text"] for ctx in ["Test context 1", "Test context 2"]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import os
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, mock_open
|
|
||||||
from ra_aid.tools.file_str_replace import file_str_replace
|
from ra_aid.tools.file_str_replace import file_str_replace
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_test_dir(tmp_path):
|
def temp_test_dir(tmp_path):
|
||||||
"""Create a temporary test directory."""
|
"""Create a temporary test directory."""
|
||||||
|
|
@ -11,101 +13,98 @@ def temp_test_dir(tmp_path):
|
||||||
test_dir.mkdir(exist_ok=True)
|
test_dir.mkdir(exist_ok=True)
|
||||||
return test_dir
|
return test_dir
|
||||||
|
|
||||||
|
|
||||||
def test_basic_replacement(temp_test_dir):
|
def test_basic_replacement(temp_test_dir):
|
||||||
"""Test basic string replacement functionality."""
|
"""Test basic string replacement functionality."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
initial_content = "Hello world! This is a test."
|
initial_content = "Hello world! This is a test."
|
||||||
test_file.write_text(initial_content)
|
test_file.write_text(initial_content)
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "world", "new_str": "universe"}
|
||||||
"old_str": "world",
|
)
|
||||||
"new_str": "universe"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert test_file.read_text() == "Hello universe! This is a test."
|
assert test_file.read_text() == "Hello universe! This is a test."
|
||||||
assert "Successfully replaced" in result["message"]
|
assert "Successfully replaced" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_file_not_found():
|
def test_file_not_found():
|
||||||
"""Test handling of non-existent file."""
|
"""Test handling of non-existent file."""
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": "nonexistent.txt",
|
{"filepath": "nonexistent.txt", "old_str": "test", "new_str": "replacement"}
|
||||||
"old_str": "test",
|
)
|
||||||
"new_str": "replacement"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "File not found" in result["message"]
|
assert "File not found" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_string_not_found(temp_test_dir):
|
def test_string_not_found(temp_test_dir):
|
||||||
"""Test handling of string not present in file."""
|
"""Test handling of string not present in file."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
test_file.write_text("Hello world!")
|
test_file.write_text("Hello world!")
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "nonexistent", "new_str": "replacement"}
|
||||||
"old_str": "nonexistent",
|
)
|
||||||
"new_str": "replacement"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "String not found" in result["message"]
|
assert "String not found" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_occurrences(temp_test_dir):
|
def test_multiple_occurrences(temp_test_dir):
|
||||||
"""Test handling of multiple string occurrences."""
|
"""Test handling of multiple string occurrences."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
test_file.write_text("test test test")
|
test_file.write_text("test test test")
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "test", "new_str": "replacement"}
|
||||||
"old_str": "test",
|
)
|
||||||
"new_str": "replacement"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "appears" in result["message"]
|
assert "appears" in result["message"]
|
||||||
assert "must be unique" in result["message"]
|
assert "must be unique" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_empty_strings(temp_test_dir):
|
def test_empty_strings(temp_test_dir):
|
||||||
"""Test handling of empty strings."""
|
"""Test handling of empty strings."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
test_file.write_text("Hello world!")
|
test_file.write_text("Hello world!")
|
||||||
|
|
||||||
# Test empty old string
|
# Test empty old string
|
||||||
result1 = file_str_replace.invoke({
|
result1 = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "", "new_str": "replacement"}
|
||||||
"old_str": "",
|
)
|
||||||
"new_str": "replacement"
|
|
||||||
})
|
|
||||||
assert result1["success"] is False
|
assert result1["success"] is False
|
||||||
|
|
||||||
# Test empty new string
|
# Test empty new string
|
||||||
result2 = file_str_replace.invoke({
|
result2 = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "world", "new_str": ""}
|
||||||
"old_str": "world",
|
)
|
||||||
"new_str": ""
|
|
||||||
})
|
|
||||||
assert result2["success"] is True
|
assert result2["success"] is True
|
||||||
assert test_file.read_text() == "Hello !"
|
assert test_file.read_text() == "Hello !"
|
||||||
|
|
||||||
|
|
||||||
def test_special_characters(temp_test_dir):
|
def test_special_characters(temp_test_dir):
|
||||||
"""Test handling of special characters."""
|
"""Test handling of special characters."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
initial_content = "Hello\nworld!\t\r\nSpecial chars: $@#%"
|
initial_content = "Hello\nworld!\t\r\nSpecial chars: $@#%"
|
||||||
test_file.write_text(initial_content)
|
test_file.write_text(initial_content)
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{
|
||||||
"old_str": "Special chars: $@#%",
|
"filepath": str(test_file),
|
||||||
"new_str": "Replaced!"
|
"old_str": "Special chars: $@#%",
|
||||||
})
|
"new_str": "Replaced!",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert "Special chars: $@#%" not in test_file.read_text()
|
assert "Special chars: $@#%" not in test_file.read_text()
|
||||||
assert "Replaced!" in test_file.read_text()
|
assert "Replaced!" in test_file.read_text()
|
||||||
|
|
||||||
@patch('pathlib.Path.read_text')
|
|
||||||
|
@patch("pathlib.Path.read_text")
|
||||||
def test_io_error(mock_read_text, temp_test_dir):
|
def test_io_error(mock_read_text, temp_test_dir):
|
||||||
"""Test handling of IO errors during read."""
|
"""Test handling of IO errors during read."""
|
||||||
# Create and write to file first
|
# Create and write to file first
|
||||||
|
|
@ -115,15 +114,14 @@ def test_io_error(mock_read_text, temp_test_dir):
|
||||||
# Then mock read_text to raise error
|
# Then mock read_text to raise error
|
||||||
mock_read_text.side_effect = IOError("Failed to read file")
|
mock_read_text.side_effect = IOError("Failed to read file")
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "test", "new_str": "replacement"}
|
||||||
"old_str": "test",
|
)
|
||||||
"new_str": "replacement"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Failed to read file" in result["message"]
|
assert "Failed to read file" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_permission_error(temp_test_dir):
|
def test_permission_error(temp_test_dir):
|
||||||
"""Test handling of permission errors."""
|
"""Test handling of permission errors."""
|
||||||
test_file = temp_test_dir / "readonly.txt"
|
test_file = temp_test_dir / "readonly.txt"
|
||||||
|
|
@ -131,44 +129,40 @@ def test_permission_error(temp_test_dir):
|
||||||
os.chmod(test_file, 0o444) # Make file read-only
|
os.chmod(test_file, 0o444) # Make file read-only
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "test", "new_str": "replacement"}
|
||||||
"old_str": "test",
|
)
|
||||||
"new_str": "replacement"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Permission" in result["message"] or "Error" in result["message"]
|
assert "Permission" in result["message"] or "Error" in result["message"]
|
||||||
finally:
|
finally:
|
||||||
os.chmod(test_file, 0o644) # Restore permissions for cleanup
|
os.chmod(test_file, 0o644) # Restore permissions for cleanup
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_strings(temp_test_dir):
|
def test_unicode_strings(temp_test_dir):
|
||||||
"""Test handling of Unicode strings."""
|
"""Test handling of Unicode strings."""
|
||||||
test_file = temp_test_dir / "unicode.txt"
|
test_file = temp_test_dir / "unicode.txt"
|
||||||
initial_content = "Hello 世界! Unicode テスト"
|
initial_content = "Hello 世界! Unicode テスト"
|
||||||
test_file.write_text(initial_content)
|
test_file.write_text(initial_content)
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": "世界", "new_str": "ワールド"}
|
||||||
"old_str": "世界",
|
)
|
||||||
"new_str": "ワールド"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert "世界" not in test_file.read_text()
|
assert "世界" not in test_file.read_text()
|
||||||
assert "ワールド" in test_file.read_text()
|
assert "ワールド" in test_file.read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_long_string_truncation(temp_test_dir):
|
def test_long_string_truncation(temp_test_dir):
|
||||||
"""Test handling and truncation of very long strings."""
|
"""Test handling and truncation of very long strings."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
long_string = "x" * 100
|
long_string = "x" * 100
|
||||||
test_file.write_text(f"prefix {long_string} suffix")
|
test_file.write_text(f"prefix {long_string} suffix")
|
||||||
|
|
||||||
result = file_str_replace.invoke({
|
result = file_str_replace.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "old_str": long_string, "new_str": "replaced"}
|
||||||
"old_str": long_string,
|
)
|
||||||
"new_str": "replaced"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert test_file.read_text() == "prefix replaced suffix"
|
assert test_file.read_text() == "prefix replaced suffix"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import mark
|
|
||||||
from git import Repo
|
from git import Repo
|
||||||
from git.exc import InvalidGitRepositoryError
|
from git.exc import InvalidGitRepositoryError
|
||||||
|
|
||||||
from ra_aid.tools import fuzzy_find_project_files
|
from ra_aid.tools import fuzzy_find_project_files
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def git_repo(tmp_path):
|
def git_repo(tmp_path):
|
||||||
"""Create a temporary git repository with some test files"""
|
"""Create a temporary git repository with some test files"""
|
||||||
|
|
@ -27,108 +28,106 @@ def git_repo(tmp_path):
|
||||||
|
|
||||||
return tmp_path
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
def test_basic_fuzzy_search(git_repo):
|
def test_basic_fuzzy_search(git_repo):
|
||||||
"""Test basic fuzzy matching functionality"""
|
"""Test basic fuzzy matching functionality"""
|
||||||
results = fuzzy_find_project_files.invoke({"search_term": "utils", "repo_path": str(git_repo)})
|
results = fuzzy_find_project_files.invoke(
|
||||||
|
{"search_term": "utils", "repo_path": str(git_repo)}
|
||||||
|
)
|
||||||
|
|
||||||
assert len(results) >= 1
|
assert len(results) >= 1
|
||||||
assert any("lib/utils.py" in match[0] for match in results)
|
assert any("lib/utils.py" in match[0] for match in results)
|
||||||
assert all(isinstance(match[1], int) for match in results)
|
assert all(isinstance(match[1], int) for match in results)
|
||||||
|
|
||||||
|
|
||||||
def test_threshold_filtering(git_repo):
|
def test_threshold_filtering(git_repo):
|
||||||
"""Test threshold parameter behavior"""
|
"""Test threshold parameter behavior"""
|
||||||
# Should match with high threshold
|
# Should match with high threshold
|
||||||
results_high = fuzzy_find_project_files.invoke({
|
results_high = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "main",
|
{"search_term": "main", "threshold": 80, "repo_path": str(git_repo)}
|
||||||
"threshold": 80,
|
)
|
||||||
"repo_path": str(git_repo)
|
|
||||||
})
|
|
||||||
assert len(results_high) >= 1
|
assert len(results_high) >= 1
|
||||||
assert any("main.py" in match[0] for match in results_high)
|
assert any("main.py" in match[0] for match in results_high)
|
||||||
|
|
||||||
# Should not match with very high threshold
|
# Should not match with very high threshold
|
||||||
results_very_high = fuzzy_find_project_files.invoke({
|
results_very_high = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "mian",
|
{"search_term": "mian", "threshold": 99, "repo_path": str(git_repo)}
|
||||||
"threshold": 99,
|
)
|
||||||
"repo_path": str(git_repo)
|
|
||||||
})
|
|
||||||
assert len(results_very_high) == 0
|
assert len(results_very_high) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_max_results_limit(git_repo):
|
def test_max_results_limit(git_repo):
|
||||||
"""Test max_results parameter"""
|
"""Test max_results parameter"""
|
||||||
max_results = 1
|
max_results = 1
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "py",
|
{"search_term": "py", "max_results": max_results, "repo_path": str(git_repo)}
|
||||||
"max_results": max_results,
|
)
|
||||||
"repo_path": str(git_repo)
|
|
||||||
})
|
|
||||||
assert len(results) <= max_results
|
assert len(results) <= max_results
|
||||||
|
|
||||||
|
|
||||||
def test_include_paths_filter(git_repo):
|
def test_include_paths_filter(git_repo):
|
||||||
"""Test include_paths filtering"""
|
"""Test include_paths filtering"""
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "py",
|
{"search_term": "py", "include_paths": ["lib/*"], "repo_path": str(git_repo)}
|
||||||
"include_paths": ["lib/*"],
|
)
|
||||||
"repo_path": str(git_repo)
|
|
||||||
})
|
|
||||||
assert all("lib/" in match[0] for match in results)
|
assert all("lib/" in match[0] for match in results)
|
||||||
|
|
||||||
|
|
||||||
def test_exclude_patterns_filter(git_repo):
|
def test_exclude_patterns_filter(git_repo):
|
||||||
"""Test exclude_patterns filtering"""
|
"""Test exclude_patterns filtering"""
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "py",
|
{
|
||||||
"exclude_patterns": ["*test*"],
|
"search_term": "py",
|
||||||
"repo_path": str(git_repo)
|
"exclude_patterns": ["*test*"],
|
||||||
})
|
"repo_path": str(git_repo),
|
||||||
|
}
|
||||||
|
)
|
||||||
assert not any("test" in match[0] for match in results)
|
assert not any("test" in match[0] for match in results)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_threshold():
|
def test_invalid_threshold():
|
||||||
"""Test error handling for invalid threshold"""
|
"""Test error handling for invalid threshold"""
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
fuzzy_find_project_files.invoke({
|
fuzzy_find_project_files.invoke({"search_term": "test", "threshold": 101})
|
||||||
"search_term": "test",
|
|
||||||
"threshold": 101
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_non_git_repo(tmp_path):
|
def test_non_git_repo(tmp_path):
|
||||||
"""Test error handling outside git repo"""
|
"""Test error handling outside git repo"""
|
||||||
with pytest.raises(InvalidGitRepositoryError):
|
with pytest.raises(InvalidGitRepositoryError):
|
||||||
fuzzy_find_project_files.invoke({
|
fuzzy_find_project_files.invoke(
|
||||||
"search_term": "test",
|
{"search_term": "test", "repo_path": str(tmp_path)}
|
||||||
"repo_path": str(tmp_path)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
def test_exact_match(git_repo):
|
def test_exact_match(git_repo):
|
||||||
"""Test exact matching returns 100% score"""
|
"""Test exact matching returns 100% score"""
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "main.py",
|
{"search_term": "main.py", "repo_path": str(git_repo)}
|
||||||
"repo_path": str(git_repo)
|
)
|
||||||
})
|
|
||||||
assert len(results) >= 1
|
assert len(results) >= 1
|
||||||
assert any(match[1] == 100 for match in results)
|
assert any(match[1] == 100 for match in results)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_search_term(git_repo):
|
def test_empty_search_term(git_repo):
|
||||||
"""Test behavior with empty search term"""
|
"""Test behavior with empty search term"""
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "",
|
{"search_term": "", "repo_path": str(git_repo)}
|
||||||
"repo_path": str(git_repo)
|
)
|
||||||
})
|
|
||||||
assert len(results) == 0
|
assert len(results) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_untracked_files(git_repo):
|
def test_untracked_files(git_repo):
|
||||||
"""Test that untracked files are included in search results"""
|
"""Test that untracked files are included in search results"""
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "untracked",
|
{"search_term": "untracked", "repo_path": str(git_repo)}
|
||||||
"repo_path": str(git_repo)
|
)
|
||||||
})
|
|
||||||
assert len(results) >= 1
|
assert len(results) >= 1
|
||||||
assert any("untracked.txt" in match[0] for match in results)
|
assert any("untracked.txt" in match[0] for match in results)
|
||||||
|
|
||||||
|
|
||||||
def test_no_matches(git_repo):
|
def test_no_matches(git_repo):
|
||||||
"""Test behavior when no files match the search term"""
|
"""Test behavior when no files match the search term"""
|
||||||
results = fuzzy_find_project_files.invoke({
|
results = fuzzy_find_project_files.invoke(
|
||||||
"search_term": "nonexistentfile",
|
{"search_term": "nonexistentfile", "threshold": 80, "repo_path": str(git_repo)}
|
||||||
"threshold": 80,
|
)
|
||||||
"repo_path": str(git_repo)
|
|
||||||
})
|
|
||||||
assert len(results) == 0
|
assert len(results) == 0
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
"""Tests for user-defined test command execution utilities."""
|
"""Tests for user-defined test command execution utilities."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, Mock
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.tools.handle_user_defined_test_cmd_execution import (
|
from ra_aid.tools.handle_user_defined_test_cmd_execution import (
|
||||||
TestState,
|
|
||||||
TestCommandExecutor,
|
TestCommandExecutor,
|
||||||
execute_test_command
|
TestState,
|
||||||
|
execute_test_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_state():
|
def test_state():
|
||||||
"""Create a test state fixture."""
|
"""Create a test state fixture."""
|
||||||
return TestState(
|
return TestState(
|
||||||
prompt="test prompt",
|
prompt="test prompt", test_attempts=0, auto_test=False, should_break=False
|
||||||
test_attempts=0,
|
|
||||||
auto_test=False,
|
|
||||||
should_break=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_executor():
|
def test_executor():
|
||||||
"""Create a test executor fixture."""
|
"""Create a test executor fixture."""
|
||||||
config = {"test_cmd": "test", "max_test_cmd_retries": 3}
|
config = {"test_cmd": "test", "max_test_cmd_retries": 3}
|
||||||
return TestCommandExecutor(config, "test prompt")
|
return TestCommandExecutor(config, "test prompt")
|
||||||
|
|
||||||
|
|
||||||
def test_check_max_retries(test_executor):
|
def test_check_max_retries(test_executor):
|
||||||
"""Test max retries check."""
|
"""Test max retries check."""
|
||||||
test_executor.state.test_attempts = 2
|
test_executor.state.test_attempts = 2
|
||||||
|
|
@ -36,6 +38,7 @@ def test_check_max_retries(test_executor):
|
||||||
test_executor.state.test_attempts = 4
|
test_executor.state.test_attempts = 4
|
||||||
assert test_executor.check_max_retries()
|
assert test_executor.check_max_retries()
|
||||||
|
|
||||||
|
|
||||||
def test_handle_test_failure(test_executor):
|
def test_handle_test_failure(test_executor):
|
||||||
"""Test handling of test failures."""
|
"""Test handling of test failures."""
|
||||||
test_result = {"output": "error message"}
|
test_result = {"output": "error message"}
|
||||||
|
|
@ -44,36 +47,51 @@ def test_handle_test_failure(test_executor):
|
||||||
assert not test_executor.state.should_break
|
assert not test_executor.state.should_break
|
||||||
assert "error message" in test_executor.state.prompt
|
assert "error message" in test_executor.state.prompt
|
||||||
|
|
||||||
|
|
||||||
def test_run_test_command_success(test_executor):
|
def test_run_test_command_success(test_executor):
|
||||||
"""Test successful test command execution."""
|
"""Test successful test command execution."""
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run:
|
with patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run:
|
||||||
mock_run.return_value = {"success": True, "output": ""}
|
mock_run.return_value = {"success": True, "output": ""}
|
||||||
test_executor.run_test_command("test", "original")
|
test_executor.run_test_command("test", "original")
|
||||||
assert test_executor.state.should_break
|
assert test_executor.state.should_break
|
||||||
assert test_executor.state.test_attempts == 1
|
assert test_executor.state.test_attempts == 1
|
||||||
|
|
||||||
|
|
||||||
def test_run_test_command_failure(test_executor):
|
def test_run_test_command_failure(test_executor):
|
||||||
"""Test failed test command execution."""
|
"""Test failed test command execution."""
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run:
|
with patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run:
|
||||||
mock_run.return_value = {"success": False, "output": "error"}
|
mock_run.return_value = {"success": False, "output": "error"}
|
||||||
test_executor.run_test_command("test", "original")
|
test_executor.run_test_command("test", "original")
|
||||||
assert not test_executor.state.should_break
|
assert not test_executor.state.should_break
|
||||||
assert test_executor.state.test_attempts == 1
|
assert test_executor.state.test_attempts == 1
|
||||||
assert "error" in test_executor.state.prompt
|
assert "error" in test_executor.state.prompt
|
||||||
|
|
||||||
|
|
||||||
def test_run_test_command_error(test_executor):
|
def test_run_test_command_error(test_executor):
|
||||||
"""Test test command execution error."""
|
"""Test test command execution error."""
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run:
|
with patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run:
|
||||||
mock_run.side_effect = Exception("Generic error")
|
mock_run.side_effect = Exception("Generic error")
|
||||||
test_executor.run_test_command("test", "original")
|
test_executor.run_test_command("test", "original")
|
||||||
assert test_executor.state.should_break
|
assert test_executor.state.should_break
|
||||||
assert test_executor.state.test_attempts == 1
|
assert test_executor.state.test_attempts == 1
|
||||||
|
|
||||||
|
|
||||||
def test_run_test_command_timeout(test_executor):
|
def test_run_test_command_timeout(test_executor):
|
||||||
"""Test test command timeout handling."""
|
"""Test test command timeout handling."""
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run,\
|
with (
|
||||||
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.logger.warning") as mock_logger:
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run,
|
||||||
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.logger.warning"
|
||||||
|
) as mock_logger,
|
||||||
|
):
|
||||||
# Create a TimeoutExpired exception
|
# Create a TimeoutExpired exception
|
||||||
timeout_exc = subprocess.TimeoutExpired(cmd="test", timeout=30)
|
timeout_exc = subprocess.TimeoutExpired(cmd="test", timeout=30)
|
||||||
mock_run.side_effect = timeout_exc
|
mock_run.side_effect = timeout_exc
|
||||||
|
|
@ -88,16 +106,20 @@ def test_run_test_command_timeout(test_executor):
|
||||||
# Verify logging
|
# Verify logging
|
||||||
mock_logger.assert_called_once()
|
mock_logger.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_run_test_command_called_process_error(test_executor):
|
def test_run_test_command_called_process_error(test_executor):
|
||||||
"""Test handling of CalledProcessError exception."""
|
"""Test handling of CalledProcessError exception."""
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run,\
|
with (
|
||||||
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.logger.error") as mock_logger:
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command"
|
||||||
|
) as mock_run,
|
||||||
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.logger.error"
|
||||||
|
) as mock_logger,
|
||||||
|
):
|
||||||
# Create a CalledProcessError exception
|
# Create a CalledProcessError exception
|
||||||
process_error = subprocess.CalledProcessError(
|
process_error = subprocess.CalledProcessError(
|
||||||
returncode=1,
|
returncode=1, cmd="test", output="Command failed output"
|
||||||
cmd="test",
|
|
||||||
output="Command failed output"
|
|
||||||
)
|
)
|
||||||
mock_run.side_effect = process_error
|
mock_run.side_effect = process_error
|
||||||
|
|
||||||
|
|
@ -111,12 +133,14 @@ def test_run_test_command_called_process_error(test_executor):
|
||||||
# Verify logging
|
# Verify logging
|
||||||
mock_logger.assert_called_once()
|
mock_logger.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_handle_user_response_no(test_executor):
|
def test_handle_user_response_no(test_executor):
|
||||||
"""Test handling of 'no' response."""
|
"""Test handling of 'no' response."""
|
||||||
test_executor.handle_user_response("n", "test", "original")
|
test_executor.handle_user_response("n", "test", "original")
|
||||||
assert test_executor.state.should_break
|
assert test_executor.state.should_break
|
||||||
assert not test_executor.state.auto_test
|
assert not test_executor.state.auto_test
|
||||||
|
|
||||||
|
|
||||||
def test_handle_user_response_auto(test_executor):
|
def test_handle_user_response_auto(test_executor):
|
||||||
"""Test handling of 'auto' response."""
|
"""Test handling of 'auto' response."""
|
||||||
with patch.object(test_executor, "run_test_command") as mock_run:
|
with patch.object(test_executor, "run_test_command") as mock_run:
|
||||||
|
|
@ -124,6 +148,7 @@ def test_handle_user_response_auto(test_executor):
|
||||||
assert test_executor.state.auto_test
|
assert test_executor.state.auto_test
|
||||||
mock_run.assert_called_once_with("test", "original")
|
mock_run.assert_called_once_with("test", "original")
|
||||||
|
|
||||||
|
|
||||||
def test_handle_user_response_yes(test_executor):
|
def test_handle_user_response_yes(test_executor):
|
||||||
"""Test handling of 'yes' response."""
|
"""Test handling of 'yes' response."""
|
||||||
with patch.object(test_executor, "run_test_command") as mock_run:
|
with patch.object(test_executor, "run_test_command") as mock_run:
|
||||||
|
|
@ -131,12 +156,14 @@ def test_handle_user_response_yes(test_executor):
|
||||||
assert not test_executor.state.auto_test
|
assert not test_executor.state.auto_test
|
||||||
mock_run.assert_called_once_with("test", "original")
|
mock_run.assert_called_once_with("test", "original")
|
||||||
|
|
||||||
|
|
||||||
def test_execute_no_cmd():
|
def test_execute_no_cmd():
|
||||||
"""Test execution with no test command."""
|
"""Test execution with no test command."""
|
||||||
executor = TestCommandExecutor({}, "prompt")
|
executor = TestCommandExecutor({}, "prompt")
|
||||||
result = executor.execute()
|
result = executor.execute()
|
||||||
assert result == (True, "prompt", False, 0)
|
assert result == (True, "prompt", False, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_manual():
|
def test_execute_manual():
|
||||||
"""Test manual test execution."""
|
"""Test manual test execution."""
|
||||||
config = {"test_cmd": "test"}
|
config = {"test_cmd": "test"}
|
||||||
|
|
@ -148,14 +175,21 @@ def test_execute_manual():
|
||||||
executor.state.test_attempts = 1
|
executor.state.test_attempts = 1
|
||||||
executor.state.prompt = "new prompt"
|
executor.state.prompt = "new prompt"
|
||||||
|
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.ask_human") as mock_ask, \
|
with (
|
||||||
patch.object(executor, "handle_user_response", side_effect=mock_handle_response) as mock_handle:
|
patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.ask_human"
|
||||||
|
) as mock_ask,
|
||||||
|
patch.object(
|
||||||
|
executor, "handle_user_response", side_effect=mock_handle_response
|
||||||
|
) as mock_handle,
|
||||||
|
):
|
||||||
mock_ask.invoke.return_value = "y"
|
mock_ask.invoke.return_value = "y"
|
||||||
|
|
||||||
result = executor.execute()
|
result = executor.execute()
|
||||||
mock_handle.assert_called_once_with("y", "test", "prompt")
|
mock_handle.assert_called_once_with("y", "test", "prompt")
|
||||||
assert result == (True, "new prompt", False, 1)
|
assert result == (True, "new prompt", False, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_auto():
|
def test_execute_auto():
|
||||||
"""Test auto test execution."""
|
"""Test auto test execution."""
|
||||||
config = {"test_cmd": "test", "max_test_cmd_retries": 3}
|
config = {"test_cmd": "test", "max_test_cmd_retries": 3}
|
||||||
|
|
@ -170,10 +204,13 @@ def test_execute_auto():
|
||||||
assert result == (True, "prompt", True, 1)
|
assert result == (True, "prompt", True, 1)
|
||||||
mock_run.assert_called_once_with("test", "prompt")
|
mock_run.assert_called_once_with("test", "prompt")
|
||||||
|
|
||||||
|
|
||||||
def test_execute_test_command_function():
|
def test_execute_test_command_function():
|
||||||
"""Test the execute_test_command function."""
|
"""Test the execute_test_command function."""
|
||||||
config = {"test_cmd": "test"}
|
config = {"test_cmd": "test"}
|
||||||
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.TestCommandExecutor") as mock_executor_class:
|
with patch(
|
||||||
|
"ra_aid.tools.handle_user_defined_test_cmd_execution.TestCommandExecutor"
|
||||||
|
) as mock_executor_class:
|
||||||
mock_executor = Mock()
|
mock_executor = Mock()
|
||||||
mock_executor.execute.return_value = (True, "new prompt", True, 1)
|
mock_executor.execute.return_value = (True, "new prompt", True, 1)
|
||||||
mock_executor_class.return_value = mock_executor
|
mock_executor_class.return_value = mock_executor
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.tools import list_directory_tree
|
from ra_aid.tools import list_directory_tree
|
||||||
from ra_aid.tools.list_directory import load_gitignore_patterns, should_ignore
|
from ra_aid.tools.list_directory import load_gitignore_patterns, should_ignore
|
||||||
|
|
||||||
EXPECTED_YEAR = str(datetime.now().year)
|
EXPECTED_YEAR = str(datetime.now().year)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_dir():
|
def temp_dir():
|
||||||
"""Create a temporary directory for testing"""
|
"""Create a temporary directory for testing"""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
yield Path(tmpdir)
|
yield Path(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
def create_test_directory_structure(path: Path):
|
def create_test_directory_structure(path: Path):
|
||||||
"""Create a test directory structure"""
|
"""Create a test directory structure"""
|
||||||
# Create files
|
# Create files
|
||||||
|
|
@ -32,15 +35,14 @@ def create_test_directory_structure(path: Path):
|
||||||
(subdir2 / ".git").mkdir()
|
(subdir2 / ".git").mkdir()
|
||||||
(subdir2 / "__pycache__").mkdir()
|
(subdir2 / "__pycache__").mkdir()
|
||||||
|
|
||||||
|
|
||||||
def test_list_directory_basic(temp_dir):
|
def test_list_directory_basic(temp_dir):
|
||||||
"""Test basic directory listing functionality"""
|
"""Test basic directory listing functionality"""
|
||||||
create_test_directory_structure(temp_dir)
|
create_test_directory_structure(temp_dir)
|
||||||
|
|
||||||
result = list_directory_tree.invoke({
|
result = list_directory_tree.invoke(
|
||||||
"path": str(temp_dir),
|
{"path": str(temp_dir), "max_depth": 2, "follow_links": False}
|
||||||
"max_depth": 2,
|
)
|
||||||
"follow_links": False
|
|
||||||
})
|
|
||||||
|
|
||||||
# Check basic structure
|
# Check basic structure
|
||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
|
|
@ -58,50 +60,56 @@ def test_list_directory_basic(temp_dir):
|
||||||
assert "bytes" not in result.lower()
|
assert "bytes" not in result.lower()
|
||||||
assert "2024-" not in result
|
assert "2024-" not in result
|
||||||
|
|
||||||
|
|
||||||
def test_list_directory_with_details(temp_dir):
|
def test_list_directory_with_details(temp_dir):
|
||||||
"""Test directory listing with file details"""
|
"""Test directory listing with file details"""
|
||||||
create_test_directory_structure(temp_dir)
|
create_test_directory_structure(temp_dir)
|
||||||
|
|
||||||
result = list_directory_tree.invoke({
|
result = list_directory_tree.invoke(
|
||||||
"path": str(temp_dir),
|
{
|
||||||
"max_depth": 2,
|
"path": str(temp_dir),
|
||||||
"show_size": True,
|
"max_depth": 2,
|
||||||
"show_modified": True
|
"show_size": True,
|
||||||
})
|
"show_modified": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# File details should be present
|
# File details should be present
|
||||||
assert "bytes" in result.lower() or "kb" in result.lower() or "b" in result.lower()
|
assert "bytes" in result.lower() or "kb" in result.lower() or "b" in result.lower()
|
||||||
assert f"{EXPECTED_YEAR}-" in result
|
assert f"{EXPECTED_YEAR}-" in result
|
||||||
|
|
||||||
|
|
||||||
def test_list_directory_depth_limit(temp_dir):
|
def test_list_directory_depth_limit(temp_dir):
|
||||||
"""Test max_depth parameter"""
|
"""Test max_depth parameter"""
|
||||||
create_test_directory_structure(temp_dir)
|
create_test_directory_structure(temp_dir)
|
||||||
|
|
||||||
# Test with depth 1 (default)
|
# Test with depth 1 (default)
|
||||||
result = list_directory_tree.invoke({
|
result = list_directory_tree.invoke(
|
||||||
"path": str(temp_dir) # Use defaults
|
{
|
||||||
})
|
"path": str(temp_dir) # Use defaults
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
assert "subdir1" in result # Directory name should be visible
|
assert "subdir1" in result # Directory name should be visible
|
||||||
assert "subfile1.txt" not in result # But not its contents
|
assert "subfile1.txt" not in result # But not its contents
|
||||||
assert "subfile2.py" not in result
|
assert "subfile2.py" not in result
|
||||||
|
|
||||||
|
|
||||||
def test_list_directory_ignore_patterns(temp_dir):
|
def test_list_directory_ignore_patterns(temp_dir):
|
||||||
"""Test exclude patterns"""
|
"""Test exclude patterns"""
|
||||||
create_test_directory_structure(temp_dir)
|
create_test_directory_structure(temp_dir)
|
||||||
|
|
||||||
result = list_directory_tree.invoke({
|
result = list_directory_tree.invoke(
|
||||||
"path": str(temp_dir),
|
{"path": str(temp_dir), "max_depth": 2, "exclude_patterns": ["*.py"]}
|
||||||
"max_depth": 2,
|
)
|
||||||
"exclude_patterns": ["*.py"]
|
|
||||||
})
|
|
||||||
|
|
||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
assert "file1.txt" in result
|
assert "file1.txt" in result
|
||||||
assert "file2.py" not in result
|
assert "file2.py" not in result
|
||||||
assert "subfile2.py" not in result
|
assert "subfile2.py" not in result
|
||||||
|
|
||||||
|
|
||||||
def test_gitignore_patterns():
|
def test_gitignore_patterns():
|
||||||
"""Test gitignore pattern loading and matching"""
|
"""Test gitignore pattern loading and matching"""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
|
@ -117,10 +125,13 @@ def test_gitignore_patterns():
|
||||||
assert should_ignore("test.txt", spec) is False
|
assert should_ignore("test.txt", spec) is False
|
||||||
assert should_ignore("dir/test.log", spec) is True
|
assert should_ignore("dir/test.log", spec) is True
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_path():
|
def test_invalid_path():
|
||||||
"""Test error handling for invalid paths"""
|
"""Test error handling for invalid paths"""
|
||||||
with pytest.raises(ValueError, match="Path does not exist"):
|
with pytest.raises(ValueError, match="Path does not exist"):
|
||||||
list_directory_tree.invoke({"path": "/nonexistent/path"})
|
list_directory_tree.invoke({"path": "/nonexistent/path"})
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Path is not a directory"):
|
with pytest.raises(ValueError, match="Path is not a directory"):
|
||||||
list_directory_tree.invoke({"path": __file__}) # Try to list the test file itself
|
list_directory_tree.invoke(
|
||||||
|
{"path": __file__}
|
||||||
|
) # Try to list the test file itself
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,61 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ra_aid.tools.memory import (
|
from ra_aid.tools.memory import (
|
||||||
_global_memory,
|
_global_memory,
|
||||||
get_memory_value,
|
|
||||||
emit_key_facts,
|
|
||||||
delete_key_facts,
|
delete_key_facts,
|
||||||
emit_key_snippets,
|
|
||||||
delete_key_snippets,
|
delete_key_snippets,
|
||||||
emit_related_files,
|
|
||||||
get_related_files,
|
|
||||||
deregister_related_files,
|
|
||||||
emit_task,
|
|
||||||
delete_tasks,
|
delete_tasks,
|
||||||
swap_task_order,
|
deregister_related_files,
|
||||||
|
emit_key_facts,
|
||||||
|
emit_key_snippets,
|
||||||
|
emit_related_files,
|
||||||
|
emit_task,
|
||||||
|
get_memory_value,
|
||||||
|
get_related_files,
|
||||||
|
get_work_log,
|
||||||
log_work_event,
|
log_work_event,
|
||||||
reset_work_log,
|
reset_work_log,
|
||||||
get_work_log
|
swap_task_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def reset_memory():
|
def reset_memory():
|
||||||
"""Reset global memory before each test"""
|
"""Reset global memory before each test"""
|
||||||
_global_memory['key_facts'] = {}
|
_global_memory["key_facts"] = {}
|
||||||
_global_memory['key_fact_id_counter'] = 0
|
_global_memory["key_fact_id_counter"] = 0
|
||||||
_global_memory['key_snippets'] = {}
|
_global_memory["key_snippets"] = {}
|
||||||
_global_memory['key_snippet_id_counter'] = 0
|
_global_memory["key_snippet_id_counter"] = 0
|
||||||
_global_memory['research_notes'] = []
|
_global_memory["research_notes"] = []
|
||||||
_global_memory['plans'] = []
|
_global_memory["plans"] = []
|
||||||
_global_memory['tasks'] = {}
|
_global_memory["tasks"] = {}
|
||||||
_global_memory['task_id_counter'] = 0
|
_global_memory["task_id_counter"] = 0
|
||||||
_global_memory['related_files'] = {}
|
_global_memory["related_files"] = {}
|
||||||
_global_memory['related_file_id_counter'] = 0
|
_global_memory["related_file_id_counter"] = 0
|
||||||
_global_memory['work_log'] = []
|
_global_memory["work_log"] = []
|
||||||
yield
|
yield
|
||||||
# Clean up after test
|
# Clean up after test
|
||||||
_global_memory['key_facts'] = {}
|
_global_memory["key_facts"] = {}
|
||||||
_global_memory['key_fact_id_counter'] = 0
|
_global_memory["key_fact_id_counter"] = 0
|
||||||
_global_memory['key_snippets'] = {}
|
_global_memory["key_snippets"] = {}
|
||||||
_global_memory['key_snippet_id_counter'] = 0
|
_global_memory["key_snippet_id_counter"] = 0
|
||||||
_global_memory['research_notes'] = []
|
_global_memory["research_notes"] = []
|
||||||
_global_memory['plans'] = []
|
_global_memory["plans"] = []
|
||||||
_global_memory['tasks'] = {}
|
_global_memory["tasks"] = {}
|
||||||
_global_memory['task_id_counter'] = 0
|
_global_memory["task_id_counter"] = 0
|
||||||
_global_memory['related_files'] = {}
|
_global_memory["related_files"] = {}
|
||||||
_global_memory['related_file_id_counter'] = 0
|
_global_memory["related_file_id_counter"] = 0
|
||||||
_global_memory['work_log'] = []
|
_global_memory["work_log"] = []
|
||||||
|
|
||||||
|
|
||||||
def test_emit_key_facts_single_fact(reset_memory):
|
def test_emit_key_facts_single_fact(reset_memory):
|
||||||
"""Test emitting a single key fact using emit_key_facts"""
|
"""Test emitting a single key fact using emit_key_facts"""
|
||||||
# Test with single fact
|
# Test with single fact
|
||||||
result = emit_key_facts.invoke({"facts": ["First fact"]})
|
result = emit_key_facts.invoke({"facts": ["First fact"]})
|
||||||
assert result == "Facts stored."
|
assert result == "Facts stored."
|
||||||
assert _global_memory['key_facts'][0] == "First fact"
|
assert _global_memory["key_facts"][0] == "First fact"
|
||||||
assert _global_memory['key_fact_id_counter'] == 1
|
assert _global_memory["key_fact_id_counter"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_delete_key_facts_single_fact(reset_memory):
|
def test_delete_key_facts_single_fact(reset_memory):
|
||||||
"""Test deleting a single key fact using delete_key_facts"""
|
"""Test deleting a single key fact using delete_key_facts"""
|
||||||
|
|
@ -61,7 +65,8 @@ def test_delete_key_facts_single_fact(reset_memory):
|
||||||
# Delete the fact
|
# Delete the fact
|
||||||
result = delete_key_facts.invoke({"fact_ids": [0]})
|
result = delete_key_facts.invoke({"fact_ids": [0]})
|
||||||
assert result == "Facts deleted."
|
assert result == "Facts deleted."
|
||||||
assert 0 not in _global_memory['key_facts']
|
assert 0 not in _global_memory["key_facts"]
|
||||||
|
|
||||||
|
|
||||||
def test_delete_key_facts_invalid(reset_memory):
|
def test_delete_key_facts_invalid(reset_memory):
|
||||||
"""Test deleting non-existent facts returns empty list"""
|
"""Test deleting non-existent facts returns empty list"""
|
||||||
|
|
@ -75,31 +80,34 @@ def test_delete_key_facts_invalid(reset_memory):
|
||||||
result = delete_key_facts.invoke({"fact_ids": [0]})
|
result = delete_key_facts.invoke({"fact_ids": [0]})
|
||||||
assert result == "Facts deleted."
|
assert result == "Facts deleted."
|
||||||
|
|
||||||
|
|
||||||
def test_get_memory_value_key_facts(reset_memory):
|
def test_get_memory_value_key_facts(reset_memory):
|
||||||
"""Test get_memory_value with key facts dictionary"""
|
"""Test get_memory_value with key facts dictionary"""
|
||||||
# Empty key facts should return empty string
|
# Empty key facts should return empty string
|
||||||
assert get_memory_value('key_facts') == ""
|
assert get_memory_value("key_facts") == ""
|
||||||
|
|
||||||
# Add some facts
|
# Add some facts
|
||||||
emit_key_facts.invoke({"facts": ["First fact", "Second fact"]})
|
emit_key_facts.invoke({"facts": ["First fact", "Second fact"]})
|
||||||
|
|
||||||
# Should return markdown formatted list
|
# Should return markdown formatted list
|
||||||
expected = "## 🔑 Key Fact #0\n\nFirst fact\n\n## 🔑 Key Fact #1\n\nSecond fact"
|
expected = "## 🔑 Key Fact #0\n\nFirst fact\n\n## 🔑 Key Fact #1\n\nSecond fact"
|
||||||
assert get_memory_value('key_facts') == expected
|
assert get_memory_value("key_facts") == expected
|
||||||
|
|
||||||
|
|
||||||
def test_get_memory_value_other_types(reset_memory):
|
def test_get_memory_value_other_types(reset_memory):
|
||||||
"""Test get_memory_value remains compatible with other memory types"""
|
"""Test get_memory_value remains compatible with other memory types"""
|
||||||
# Add some research notes
|
# Add some research notes
|
||||||
_global_memory['research_notes'].append("Note 1")
|
_global_memory["research_notes"].append("Note 1")
|
||||||
_global_memory['research_notes'].append("Note 2")
|
_global_memory["research_notes"].append("Note 2")
|
||||||
|
|
||||||
assert get_memory_value('research_notes') == "Note 1\nNote 2"
|
assert get_memory_value("research_notes") == "Note 1\nNote 2"
|
||||||
|
|
||||||
# Test with empty list
|
# Test with empty list
|
||||||
assert get_memory_value('plans') == ""
|
assert get_memory_value("plans") == ""
|
||||||
|
|
||||||
# Test with non-existent key
|
# Test with non-existent key
|
||||||
assert get_memory_value('nonexistent') == ""
|
assert get_memory_value("nonexistent") == ""
|
||||||
|
|
||||||
|
|
||||||
def test_log_work_event(reset_memory):
|
def test_log_work_event(reset_memory):
|
||||||
"""Test logging work events with timestamps"""
|
"""Test logging work events with timestamps"""
|
||||||
|
|
@ -109,16 +117,17 @@ def test_log_work_event(reset_memory):
|
||||||
log_work_event("Completed task")
|
log_work_event("Completed task")
|
||||||
|
|
||||||
# Verify events are stored
|
# Verify events are stored
|
||||||
assert len(_global_memory['work_log']) == 3
|
assert len(_global_memory["work_log"]) == 3
|
||||||
|
|
||||||
# Check event structure
|
# Check event structure
|
||||||
event = _global_memory['work_log'][0]
|
event = _global_memory["work_log"][0]
|
||||||
assert isinstance(event['timestamp'], str)
|
assert isinstance(event["timestamp"], str)
|
||||||
assert event['event'] == "Started task"
|
assert event["event"] == "Started task"
|
||||||
|
|
||||||
# Verify order
|
# Verify order
|
||||||
assert _global_memory['work_log'][1]['event'] == "Made progress"
|
assert _global_memory["work_log"][1]["event"] == "Made progress"
|
||||||
assert _global_memory['work_log'][2]['event'] == "Completed task"
|
assert _global_memory["work_log"][2]["event"] == "Completed task"
|
||||||
|
|
||||||
|
|
||||||
def test_get_work_log(reset_memory):
|
def test_get_work_log(reset_memory):
|
||||||
"""Test work log formatting with heading-based markdown"""
|
"""Test work log formatting with heading-based markdown"""
|
||||||
|
|
@ -135,23 +144,26 @@ def test_get_work_log(reset_memory):
|
||||||
assert "First event" in log
|
assert "First event" in log
|
||||||
assert "Second event" in log
|
assert "Second event" in log
|
||||||
|
|
||||||
|
|
||||||
def test_reset_work_log(reset_memory):
|
def test_reset_work_log(reset_memory):
|
||||||
"""Test resetting the work log"""
|
"""Test resetting the work log"""
|
||||||
# Add some events
|
# Add some events
|
||||||
log_work_event("Test event")
|
log_work_event("Test event")
|
||||||
assert len(_global_memory['work_log']) == 1
|
assert len(_global_memory["work_log"]) == 1
|
||||||
|
|
||||||
# Reset log
|
# Reset log
|
||||||
reset_work_log()
|
reset_work_log()
|
||||||
|
|
||||||
# Verify log is empty
|
# Verify log is empty
|
||||||
assert len(_global_memory['work_log']) == 0
|
assert len(_global_memory["work_log"]) == 0
|
||||||
assert get_memory_value('work_log') == ""
|
assert get_memory_value("work_log") == ""
|
||||||
|
|
||||||
|
|
||||||
def test_empty_work_log(reset_memory):
|
def test_empty_work_log(reset_memory):
|
||||||
"""Test empty work log behavior"""
|
"""Test empty work log behavior"""
|
||||||
# Fresh work log should return empty string
|
# Fresh work log should return empty string
|
||||||
assert get_memory_value('work_log') == ""
|
assert get_memory_value("work_log") == ""
|
||||||
|
|
||||||
|
|
||||||
def test_emit_key_facts(reset_memory):
|
def test_emit_key_facts(reset_memory):
|
||||||
"""Test emitting multiple key facts at once"""
|
"""Test emitting multiple key facts at once"""
|
||||||
|
|
@ -163,12 +175,13 @@ def test_emit_key_facts(reset_memory):
|
||||||
assert result == "Facts stored."
|
assert result == "Facts stored."
|
||||||
|
|
||||||
# Verify facts stored in memory with correct IDs
|
# Verify facts stored in memory with correct IDs
|
||||||
assert _global_memory['key_facts'][0] == "First fact"
|
assert _global_memory["key_facts"][0] == "First fact"
|
||||||
assert _global_memory['key_facts'][1] == "Second fact"
|
assert _global_memory["key_facts"][1] == "Second fact"
|
||||||
assert _global_memory['key_facts'][2] == "Third fact"
|
assert _global_memory["key_facts"][2] == "Third fact"
|
||||||
|
|
||||||
# Verify counter incremented correctly
|
# Verify counter incremented correctly
|
||||||
assert _global_memory['key_fact_id_counter'] == 3
|
assert _global_memory["key_fact_id_counter"] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_delete_key_facts(reset_memory):
|
def test_delete_key_facts(reset_memory):
|
||||||
"""Test deleting multiple key facts"""
|
"""Test deleting multiple key facts"""
|
||||||
|
|
@ -182,10 +195,11 @@ def test_delete_key_facts(reset_memory):
|
||||||
assert result == "Facts deleted."
|
assert result == "Facts deleted."
|
||||||
|
|
||||||
# Verify correct facts removed from memory
|
# Verify correct facts removed from memory
|
||||||
assert 0 not in _global_memory['key_facts']
|
assert 0 not in _global_memory["key_facts"]
|
||||||
assert 1 not in _global_memory['key_facts']
|
assert 1 not in _global_memory["key_facts"]
|
||||||
assert 2 in _global_memory['key_facts'] # ID 2 should remain
|
assert 2 in _global_memory["key_facts"] # ID 2 should remain
|
||||||
assert _global_memory['key_facts'][2] == "Third fact"
|
assert _global_memory["key_facts"][2] == "Third fact"
|
||||||
|
|
||||||
|
|
||||||
def test_emit_key_snippets(reset_memory):
|
def test_emit_key_snippets(reset_memory):
|
||||||
"""Test emitting multiple code snippets at once"""
|
"""Test emitting multiple code snippets at once"""
|
||||||
|
|
@ -195,14 +209,14 @@ def test_emit_key_snippets(reset_memory):
|
||||||
"filepath": "test.py",
|
"filepath": "test.py",
|
||||||
"line_number": 10,
|
"line_number": 10,
|
||||||
"snippet": "def test():\n pass",
|
"snippet": "def test():\n pass",
|
||||||
"description": "Test function"
|
"description": "Test function",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filepath": "main.py",
|
"filepath": "main.py",
|
||||||
"line_number": 20,
|
"line_number": 20,
|
||||||
"snippet": "print('hello')",
|
"snippet": "print('hello')",
|
||||||
"description": None
|
"description": None,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Emit snippets
|
# Emit snippets
|
||||||
|
|
@ -212,11 +226,12 @@ def test_emit_key_snippets(reset_memory):
|
||||||
assert result == "Snippets stored."
|
assert result == "Snippets stored."
|
||||||
|
|
||||||
# Verify snippets stored correctly
|
# Verify snippets stored correctly
|
||||||
assert _global_memory['key_snippets'][0] == snippets[0]
|
assert _global_memory["key_snippets"][0] == snippets[0]
|
||||||
assert _global_memory['key_snippets'][1] == snippets[1]
|
assert _global_memory["key_snippets"][1] == snippets[1]
|
||||||
|
|
||||||
# Verify counter incremented correctly
|
# Verify counter incremented correctly
|
||||||
assert _global_memory['key_snippet_id_counter'] == 2
|
assert _global_memory["key_snippet_id_counter"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_delete_key_snippets(reset_memory):
|
def test_delete_key_snippets(reset_memory):
|
||||||
"""Test deleting multiple code snippets"""
|
"""Test deleting multiple code snippets"""
|
||||||
|
|
@ -226,20 +241,20 @@ def test_delete_key_snippets(reset_memory):
|
||||||
"filepath": "test1.py",
|
"filepath": "test1.py",
|
||||||
"line_number": 1,
|
"line_number": 1,
|
||||||
"snippet": "code1",
|
"snippet": "code1",
|
||||||
"description": None
|
"description": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filepath": "test2.py",
|
"filepath": "test2.py",
|
||||||
"line_number": 2,
|
"line_number": 2,
|
||||||
"snippet": "code2",
|
"snippet": "code2",
|
||||||
"description": None
|
"description": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filepath": "test3.py",
|
"filepath": "test3.py",
|
||||||
"line_number": 3,
|
"line_number": 3,
|
||||||
"snippet": "code3",
|
"snippet": "code3",
|
||||||
"description": None
|
"description": None,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
emit_key_snippets.invoke({"snippets": snippets})
|
emit_key_snippets.invoke({"snippets": snippets})
|
||||||
|
|
||||||
|
|
@ -250,10 +265,11 @@ def test_delete_key_snippets(reset_memory):
|
||||||
assert result == "Snippets deleted."
|
assert result == "Snippets deleted."
|
||||||
|
|
||||||
# Verify correct snippets removed
|
# Verify correct snippets removed
|
||||||
assert 0 not in _global_memory['key_snippets']
|
assert 0 not in _global_memory["key_snippets"]
|
||||||
assert 1 not in _global_memory['key_snippets']
|
assert 1 not in _global_memory["key_snippets"]
|
||||||
assert 2 in _global_memory['key_snippets']
|
assert 2 in _global_memory["key_snippets"]
|
||||||
assert _global_memory['key_snippets'][2]['filepath'] == "test3.py"
|
assert _global_memory["key_snippets"][2]["filepath"] == "test3.py"
|
||||||
|
|
||||||
|
|
||||||
def test_delete_key_snippets_empty(reset_memory):
|
def test_delete_key_snippets_empty(reset_memory):
|
||||||
"""Test deleting snippets with empty ID list"""
|
"""Test deleting snippets with empty ID list"""
|
||||||
|
|
@ -262,7 +278,7 @@ def test_delete_key_snippets_empty(reset_memory):
|
||||||
"filepath": "test.py",
|
"filepath": "test.py",
|
||||||
"line_number": 1,
|
"line_number": 1,
|
||||||
"snippet": "code",
|
"snippet": "code",
|
||||||
"description": None
|
"description": None,
|
||||||
}
|
}
|
||||||
emit_key_snippets.invoke({"snippets": [snippet]})
|
emit_key_snippets.invoke({"snippets": [snippet]})
|
||||||
|
|
||||||
|
|
@ -271,7 +287,8 @@ def test_delete_key_snippets_empty(reset_memory):
|
||||||
assert result == "Snippets deleted."
|
assert result == "Snippets deleted."
|
||||||
|
|
||||||
# Verify snippet still exists
|
# Verify snippet still exists
|
||||||
assert 0 in _global_memory['key_snippets']
|
assert 0 in _global_memory["key_snippets"]
|
||||||
|
|
||||||
|
|
||||||
def test_emit_related_files_basic(reset_memory, tmp_path):
|
def test_emit_related_files_basic(reset_memory, tmp_path):
|
||||||
"""Test basic adding of files with ID tracking"""
|
"""Test basic adding of files with ID tracking"""
|
||||||
|
|
@ -286,20 +303,22 @@ def test_emit_related_files_basic(reset_memory, tmp_path):
|
||||||
# Test adding single file
|
# Test adding single file
|
||||||
result = emit_related_files.invoke({"files": [str(test_file)]})
|
result = emit_related_files.invoke({"files": [str(test_file)]})
|
||||||
assert result == f"File ID #0: {test_file}"
|
assert result == f"File ID #0: {test_file}"
|
||||||
assert _global_memory['related_files'][0] == str(test_file)
|
assert _global_memory["related_files"][0] == str(test_file)
|
||||||
|
|
||||||
# Test adding multiple files
|
# Test adding multiple files
|
||||||
result = emit_related_files.invoke({"files": [str(main_file), str(utils_file)]})
|
result = emit_related_files.invoke({"files": [str(main_file), str(utils_file)]})
|
||||||
assert result == f"File ID #1: {main_file}\nFile ID #2: {utils_file}"
|
assert result == f"File ID #1: {main_file}\nFile ID #2: {utils_file}"
|
||||||
# Verify both files exist in related_files
|
# Verify both files exist in related_files
|
||||||
values = list(_global_memory['related_files'].values())
|
values = list(_global_memory["related_files"].values())
|
||||||
assert str(main_file) in values
|
assert str(main_file) in values
|
||||||
assert str(utils_file) in values
|
assert str(utils_file) in values
|
||||||
|
|
||||||
|
|
||||||
def test_get_related_files_empty(reset_memory):
|
def test_get_related_files_empty(reset_memory):
|
||||||
"""Test getting related files when none added"""
|
"""Test getting related files when none added"""
|
||||||
assert get_related_files() == []
|
assert get_related_files() == []
|
||||||
|
|
||||||
|
|
||||||
def test_emit_related_files_duplicates(reset_memory, tmp_path):
|
def test_emit_related_files_duplicates(reset_memory, tmp_path):
|
||||||
"""Test that duplicate files return existing IDs with proper formatting"""
|
"""Test that duplicate files return existing IDs with proper formatting"""
|
||||||
# Create test files
|
# Create test files
|
||||||
|
|
@ -313,17 +332,18 @@ def test_emit_related_files_duplicates(reset_memory, tmp_path):
|
||||||
# Add initial files
|
# Add initial files
|
||||||
result = emit_related_files.invoke({"files": [str(test_file), str(main_file)]})
|
result = emit_related_files.invoke({"files": [str(test_file), str(main_file)]})
|
||||||
assert result == f"File ID #0: {test_file}\nFile ID #1: {main_file}"
|
assert result == f"File ID #0: {test_file}\nFile ID #1: {main_file}"
|
||||||
first_id = 0 # ID of test.py
|
_first_id = 0 # ID of test.py
|
||||||
|
|
||||||
# Try adding duplicates
|
# Try adding duplicates
|
||||||
result = emit_related_files.invoke({"files": [str(test_file)]})
|
result = emit_related_files.invoke({"files": [str(test_file)]})
|
||||||
assert result == f"File ID #0: {test_file}" # Should return same ID
|
assert result == f"File ID #0: {test_file}" # Should return same ID
|
||||||
assert len(_global_memory['related_files']) == 2 # Count should not increase
|
assert len(_global_memory["related_files"]) == 2 # Count should not increase
|
||||||
|
|
||||||
# Try mix of new and duplicate files
|
# Try mix of new and duplicate files
|
||||||
result = emit_related_files.invoke({"files": [str(test_file), str(new_file)]})
|
result = emit_related_files.invoke({"files": [str(test_file), str(new_file)]})
|
||||||
assert result == f"File ID #0: {test_file}\nFile ID #2: {new_file}"
|
assert result == f"File ID #0: {test_file}\nFile ID #2: {new_file}"
|
||||||
assert len(_global_memory['related_files']) == 3
|
assert len(_global_memory["related_files"]) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_related_files_id_tracking(reset_memory, tmp_path):
|
def test_related_files_id_tracking(reset_memory, tmp_path):
|
||||||
"""Test ID assignment and counter functionality for related files"""
|
"""Test ID assignment and counter functionality for related files"""
|
||||||
|
|
@ -336,16 +356,17 @@ def test_related_files_id_tracking(reset_memory, tmp_path):
|
||||||
# Add first file
|
# Add first file
|
||||||
result = emit_related_files.invoke({"files": [str(file1)]})
|
result = emit_related_files.invoke({"files": [str(file1)]})
|
||||||
assert result == f"File ID #0: {file1}"
|
assert result == f"File ID #0: {file1}"
|
||||||
assert _global_memory['related_file_id_counter'] == 1
|
assert _global_memory["related_file_id_counter"] == 1
|
||||||
|
|
||||||
# Add second file
|
# Add second file
|
||||||
result = emit_related_files.invoke({"files": [str(file2)]})
|
result = emit_related_files.invoke({"files": [str(file2)]})
|
||||||
assert result == f"File ID #1: {file2}"
|
assert result == f"File ID #1: {file2}"
|
||||||
assert _global_memory['related_file_id_counter'] == 2
|
assert _global_memory["related_file_id_counter"] == 2
|
||||||
|
|
||||||
# Verify all files stored correctly
|
# Verify all files stored correctly
|
||||||
assert _global_memory['related_files'][0] == str(file1)
|
assert _global_memory["related_files"][0] == str(file1)
|
||||||
assert _global_memory['related_files'][1] == str(file2)
|
assert _global_memory["related_files"][1] == str(file2)
|
||||||
|
|
||||||
|
|
||||||
def test_deregister_related_files(reset_memory, tmp_path):
|
def test_deregister_related_files(reset_memory, tmp_path):
|
||||||
"""Test deleting related files"""
|
"""Test deleting related files"""
|
||||||
|
|
@ -363,16 +384,17 @@ def test_deregister_related_files(reset_memory, tmp_path):
|
||||||
# Delete middle file
|
# Delete middle file
|
||||||
result = deregister_related_files.invoke({"file_ids": [1]})
|
result = deregister_related_files.invoke({"file_ids": [1]})
|
||||||
assert result == "File references removed."
|
assert result == "File references removed."
|
||||||
assert 1 not in _global_memory['related_files']
|
assert 1 not in _global_memory["related_files"]
|
||||||
assert len(_global_memory['related_files']) == 2
|
assert len(_global_memory["related_files"]) == 2
|
||||||
|
|
||||||
# Delete multiple files including non-existent ID
|
# Delete multiple files including non-existent ID
|
||||||
result = deregister_related_files.invoke({"file_ids": [0, 2, 999]})
|
result = deregister_related_files.invoke({"file_ids": [0, 2, 999]})
|
||||||
assert result == "File references removed."
|
assert result == "File references removed."
|
||||||
assert len(_global_memory['related_files']) == 0
|
assert len(_global_memory["related_files"]) == 0
|
||||||
|
|
||||||
# Counter should remain unchanged after deletions
|
# Counter should remain unchanged after deletions
|
||||||
assert _global_memory['related_file_id_counter'] == 3
|
assert _global_memory["related_file_id_counter"] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_related_files_duplicates(reset_memory, tmp_path):
|
def test_related_files_duplicates(reset_memory, tmp_path):
|
||||||
"""Test duplicate file handling returns same ID"""
|
"""Test duplicate file handling returns same ID"""
|
||||||
|
|
@ -389,8 +411,9 @@ def test_related_files_duplicates(reset_memory, tmp_path):
|
||||||
assert result2 == f"File ID #0: {test_file}"
|
assert result2 == f"File ID #0: {test_file}"
|
||||||
|
|
||||||
# Verify only one entry exists
|
# Verify only one entry exists
|
||||||
assert len(_global_memory['related_files']) == 1
|
assert len(_global_memory["related_files"]) == 1
|
||||||
assert _global_memory['related_file_id_counter'] == 1
|
assert _global_memory["related_file_id_counter"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_emit_related_files_with_directory(reset_memory, tmp_path):
|
def test_emit_related_files_with_directory(reset_memory, tmp_path):
|
||||||
"""Test that directories and non-existent paths are rejected while valid files are added"""
|
"""Test that directories and non-existent paths are rejected while valid files are added"""
|
||||||
|
|
@ -402,22 +425,23 @@ def test_emit_related_files_with_directory(reset_memory, tmp_path):
|
||||||
nonexistent = tmp_path / "does_not_exist.txt"
|
nonexistent = tmp_path / "does_not_exist.txt"
|
||||||
|
|
||||||
# Try to emit directory, nonexistent path, and valid file
|
# Try to emit directory, nonexistent path, and valid file
|
||||||
result = emit_related_files.invoke({
|
result = emit_related_files.invoke(
|
||||||
"files": [str(test_dir), str(nonexistent), str(test_file)]
|
{"files": [str(test_dir), str(nonexistent), str(test_file)]}
|
||||||
})
|
)
|
||||||
|
|
||||||
# Verify specific error messages for directory and nonexistent path
|
# Verify specific error messages for directory and nonexistent path
|
||||||
assert f"Error: Path '{test_dir}' is a directory, not a file" in result
|
assert f"Error: Path '{test_dir}' is a directory, not a file" in result
|
||||||
assert f"Error: Path '{nonexistent}' does not exist" in result
|
assert f"Error: Path '{nonexistent}' does not exist" in result
|
||||||
|
|
||||||
# Verify directory and nonexistent not added but valid file was
|
# Verify directory and nonexistent not added but valid file was
|
||||||
assert len(_global_memory['related_files']) == 1
|
assert len(_global_memory["related_files"]) == 1
|
||||||
values = list(_global_memory['related_files'].values())
|
values = list(_global_memory["related_files"].values())
|
||||||
assert str(test_file) in values
|
assert str(test_file) in values
|
||||||
assert str(test_dir) not in values
|
assert str(test_dir) not in values
|
||||||
assert str(nonexistent) not in values
|
assert str(nonexistent) not in values
|
||||||
assert str(nonexistent) not in values
|
assert str(nonexistent) not in values
|
||||||
|
|
||||||
|
|
||||||
def test_related_files_formatting(reset_memory, tmp_path):
|
def test_related_files_formatting(reset_memory, tmp_path):
|
||||||
"""Test related files output string formatting"""
|
"""Test related files output string formatting"""
|
||||||
# Create test files
|
# Create test files
|
||||||
|
|
@ -430,14 +454,15 @@ def test_related_files_formatting(reset_memory, tmp_path):
|
||||||
emit_related_files.invoke({"files": [str(file1), str(file2)]})
|
emit_related_files.invoke({"files": [str(file1), str(file2)]})
|
||||||
|
|
||||||
# Get formatted output
|
# Get formatted output
|
||||||
output = get_memory_value('related_files')
|
output = get_memory_value("related_files")
|
||||||
# Expect just the IDs on separate lines
|
# Expect just the IDs on separate lines
|
||||||
expected = "0\n1"
|
expected = "0\n1"
|
||||||
assert output == expected
|
assert output == expected
|
||||||
|
|
||||||
# Test empty case
|
# Test empty case
|
||||||
_global_memory['related_files'] = {}
|
_global_memory["related_files"] = {}
|
||||||
assert get_memory_value('related_files') == ""
|
assert get_memory_value("related_files") == ""
|
||||||
|
|
||||||
|
|
||||||
def test_key_snippets_integration(reset_memory, tmp_path):
|
def test_key_snippets_integration(reset_memory, tmp_path):
|
||||||
"""Integration test for key snippets functionality"""
|
"""Integration test for key snippets functionality"""
|
||||||
|
|
@ -455,51 +480,51 @@ def test_key_snippets_integration(reset_memory, tmp_path):
|
||||||
"filepath": str(file1),
|
"filepath": str(file1),
|
||||||
"line_number": 10,
|
"line_number": 10,
|
||||||
"snippet": "def func1():\n pass",
|
"snippet": "def func1():\n pass",
|
||||||
"description": "First function"
|
"description": "First function",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filepath": str(file2),
|
"filepath": str(file2),
|
||||||
"line_number": 20,
|
"line_number": 20,
|
||||||
"snippet": "def func2():\n return True",
|
"snippet": "def func2():\n return True",
|
||||||
"description": "Second function"
|
"description": "Second function",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filepath": str(file3),
|
"filepath": str(file3),
|
||||||
"line_number": 30,
|
"line_number": 30,
|
||||||
"snippet": "class TestClass:\n pass",
|
"snippet": "class TestClass:\n pass",
|
||||||
"description": "Test class"
|
"description": "Test class",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add all snippets
|
# Add all snippets
|
||||||
result = emit_key_snippets.invoke({"snippets": snippets})
|
result = emit_key_snippets.invoke({"snippets": snippets})
|
||||||
assert result == "Snippets stored."
|
assert result == "Snippets stored."
|
||||||
assert _global_memory['key_snippet_id_counter'] == 3
|
assert _global_memory["key_snippet_id_counter"] == 3
|
||||||
# Verify related files were tracked with IDs
|
# Verify related files were tracked with IDs
|
||||||
assert len(_global_memory['related_files']) == 3
|
assert len(_global_memory["related_files"]) == 3
|
||||||
# Check files are stored with proper IDs
|
# Check files are stored with proper IDs
|
||||||
file_values = _global_memory['related_files'].values()
|
file_values = _global_memory["related_files"].values()
|
||||||
assert str(file1) in file_values
|
assert str(file1) in file_values
|
||||||
assert str(file2) in file_values
|
assert str(file2) in file_values
|
||||||
assert str(file3) in file_values
|
assert str(file3) in file_values
|
||||||
|
|
||||||
# Verify all snippets were stored correctly
|
# Verify all snippets were stored correctly
|
||||||
assert len(_global_memory['key_snippets']) == 3
|
assert len(_global_memory["key_snippets"]) == 3
|
||||||
assert _global_memory['key_snippets'][0] == snippets[0]
|
assert _global_memory["key_snippets"][0] == snippets[0]
|
||||||
assert _global_memory['key_snippets'][1] == snippets[1]
|
assert _global_memory["key_snippets"][1] == snippets[1]
|
||||||
assert _global_memory['key_snippets'][2] == snippets[2]
|
assert _global_memory["key_snippets"][2] == snippets[2]
|
||||||
|
|
||||||
# Delete some but not all snippets (0 and 2)
|
# Delete some but not all snippets (0 and 2)
|
||||||
result = delete_key_snippets.invoke({"snippet_ids": [0, 2]})
|
result = delete_key_snippets.invoke({"snippet_ids": [0, 2]})
|
||||||
assert result == "Snippets deleted."
|
assert result == "Snippets deleted."
|
||||||
|
|
||||||
# Verify remaining snippet is intact
|
# Verify remaining snippet is intact
|
||||||
assert len(_global_memory['key_snippets']) == 1
|
assert len(_global_memory["key_snippets"]) == 1
|
||||||
assert 1 in _global_memory['key_snippets']
|
assert 1 in _global_memory["key_snippets"]
|
||||||
assert _global_memory['key_snippets'][1] == snippets[1]
|
assert _global_memory["key_snippets"][1] == snippets[1]
|
||||||
|
|
||||||
# Counter should remain unchanged after deletions
|
# Counter should remain unchanged after deletions
|
||||||
assert _global_memory['key_snippet_id_counter'] == 3
|
assert _global_memory["key_snippet_id_counter"] == 3
|
||||||
|
|
||||||
# Add new snippet to verify counter continues correctly
|
# Add new snippet to verify counter continues correctly
|
||||||
file4 = tmp_path / "file4.py"
|
file4 = tmp_path / "file4.py"
|
||||||
|
|
@ -508,25 +533,26 @@ def test_key_snippets_integration(reset_memory, tmp_path):
|
||||||
"filepath": str(file4),
|
"filepath": str(file4),
|
||||||
"line_number": 40,
|
"line_number": 40,
|
||||||
"snippet": "def func4():\n return False",
|
"snippet": "def func4():\n return False",
|
||||||
"description": "Fourth function"
|
"description": "Fourth function",
|
||||||
}
|
}
|
||||||
result = emit_key_snippets.invoke({"snippets": [new_snippet]})
|
result = emit_key_snippets.invoke({"snippets": [new_snippet]})
|
||||||
assert result == "Snippets stored."
|
assert result == "Snippets stored."
|
||||||
assert _global_memory['key_snippet_id_counter'] == 4
|
assert _global_memory["key_snippet_id_counter"] == 4
|
||||||
# Verify new file was added to related files
|
# Verify new file was added to related files
|
||||||
file_values = _global_memory['related_files'].values()
|
file_values = _global_memory["related_files"].values()
|
||||||
assert str(file4) in file_values
|
assert str(file4) in file_values
|
||||||
assert len(_global_memory['related_files']) == 4
|
assert len(_global_memory["related_files"]) == 4
|
||||||
|
|
||||||
# Delete remaining snippets
|
# Delete remaining snippets
|
||||||
result = delete_key_snippets.invoke({"snippet_ids": [1, 3]})
|
result = delete_key_snippets.invoke({"snippet_ids": [1, 3]})
|
||||||
assert result == "Snippets deleted."
|
assert result == "Snippets deleted."
|
||||||
|
|
||||||
# Verify all snippets are gone
|
# Verify all snippets are gone
|
||||||
assert len(_global_memory['key_snippets']) == 0
|
assert len(_global_memory["key_snippets"]) == 0
|
||||||
|
|
||||||
# Counter should still maintain its value
|
# Counter should still maintain its value
|
||||||
assert _global_memory['key_snippet_id_counter'] == 4
|
assert _global_memory["key_snippet_id_counter"] == 4
|
||||||
|
|
||||||
|
|
||||||
def test_emit_task_with_id(reset_memory):
|
def test_emit_task_with_id(reset_memory):
|
||||||
"""Test emitting tasks with ID tracking"""
|
"""Test emitting tasks with ID tracking"""
|
||||||
|
|
@ -538,17 +564,18 @@ def test_emit_task_with_id(reset_memory):
|
||||||
assert result == "Task #0 stored."
|
assert result == "Task #0 stored."
|
||||||
|
|
||||||
# Verify task stored correctly with ID
|
# Verify task stored correctly with ID
|
||||||
assert _global_memory['tasks'][0] == task
|
assert _global_memory["tasks"][0] == task
|
||||||
|
|
||||||
# Verify counter incremented
|
# Verify counter incremented
|
||||||
assert _global_memory['task_id_counter'] == 1
|
assert _global_memory["task_id_counter"] == 1
|
||||||
|
|
||||||
# Add another task to verify counter continues correctly
|
# Add another task to verify counter continues correctly
|
||||||
task2 = "Fix bug"
|
task2 = "Fix bug"
|
||||||
result = emit_task.invoke({"task": task2})
|
result = emit_task.invoke({"task": task2})
|
||||||
assert result == "Task #1 stored."
|
assert result == "Task #1 stored."
|
||||||
assert _global_memory['tasks'][1] == task2
|
assert _global_memory["tasks"][1] == task2
|
||||||
assert _global_memory['task_id_counter'] == 2
|
assert _global_memory["task_id_counter"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_delete_tasks(reset_memory):
|
def test_delete_tasks(reset_memory):
|
||||||
"""Test deleting tasks"""
|
"""Test deleting tasks"""
|
||||||
|
|
@ -560,20 +587,21 @@ def test_delete_tasks(reset_memory):
|
||||||
# Test deleting single task
|
# Test deleting single task
|
||||||
result = delete_tasks.invoke({"task_ids": [1]})
|
result = delete_tasks.invoke({"task_ids": [1]})
|
||||||
assert result == "Tasks deleted."
|
assert result == "Tasks deleted."
|
||||||
assert 1 not in _global_memory['tasks']
|
assert 1 not in _global_memory["tasks"]
|
||||||
assert len(_global_memory['tasks']) == 2
|
assert len(_global_memory["tasks"]) == 2
|
||||||
|
|
||||||
# Test deleting multiple tasks including non-existent ID
|
# Test deleting multiple tasks including non-existent ID
|
||||||
result = delete_tasks.invoke({"task_ids": [0, 2, 999]})
|
result = delete_tasks.invoke({"task_ids": [0, 2, 999]})
|
||||||
assert result == "Tasks deleted."
|
assert result == "Tasks deleted."
|
||||||
assert len(_global_memory['tasks']) == 0
|
assert len(_global_memory["tasks"]) == 0
|
||||||
|
|
||||||
# Test deleting from empty tasks dict
|
# Test deleting from empty tasks dict
|
||||||
result = delete_tasks.invoke({"task_ids": [0]})
|
result = delete_tasks.invoke({"task_ids": [0]})
|
||||||
assert result == "Tasks deleted."
|
assert result == "Tasks deleted."
|
||||||
|
|
||||||
# Counter should remain unchanged after deletions
|
# Counter should remain unchanged after deletions
|
||||||
assert _global_memory['task_id_counter'] == 3
|
assert _global_memory["task_id_counter"] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_swap_task_order_valid_ids(reset_memory):
|
def test_swap_task_order_valid_ids(reset_memory):
|
||||||
"""Test basic task swapping functionality"""
|
"""Test basic task swapping functionality"""
|
||||||
|
|
@ -587,9 +615,10 @@ def test_swap_task_order_valid_ids(reset_memory):
|
||||||
assert result == "Tasks swapped."
|
assert result == "Tasks swapped."
|
||||||
|
|
||||||
# Verify tasks were swapped
|
# Verify tasks were swapped
|
||||||
assert _global_memory['tasks'][0] == "Task 3"
|
assert _global_memory["tasks"][0] == "Task 3"
|
||||||
assert _global_memory['tasks'][2] == "Task 1"
|
assert _global_memory["tasks"][2] == "Task 1"
|
||||||
assert _global_memory['tasks'][1] == "Task 2" # Unchanged
|
assert _global_memory["tasks"][1] == "Task 2" # Unchanged
|
||||||
|
|
||||||
|
|
||||||
def test_swap_task_order_invalid_ids(reset_memory):
|
def test_swap_task_order_invalid_ids(reset_memory):
|
||||||
"""Test error handling for invalid task IDs"""
|
"""Test error handling for invalid task IDs"""
|
||||||
|
|
@ -601,7 +630,8 @@ def test_swap_task_order_invalid_ids(reset_memory):
|
||||||
assert result == "Invalid task ID(s)"
|
assert result == "Invalid task ID(s)"
|
||||||
|
|
||||||
# Verify original task unchanged
|
# Verify original task unchanged
|
||||||
assert _global_memory['tasks'][0] == "Task 1"
|
assert _global_memory["tasks"][0] == "Task 1"
|
||||||
|
|
||||||
|
|
||||||
def test_swap_task_order_same_id(reset_memory):
|
def test_swap_task_order_same_id(reset_memory):
|
||||||
"""Test handling of attempt to swap a task with itself"""
|
"""Test handling of attempt to swap a task with itself"""
|
||||||
|
|
@ -613,13 +643,15 @@ def test_swap_task_order_same_id(reset_memory):
|
||||||
assert result == "Cannot swap task with itself"
|
assert result == "Cannot swap task with itself"
|
||||||
|
|
||||||
# Verify task unchanged
|
# Verify task unchanged
|
||||||
assert _global_memory['tasks'][0] == "Task 1"
|
assert _global_memory["tasks"][0] == "Task 1"
|
||||||
|
|
||||||
|
|
||||||
def test_swap_task_order_empty_tasks(reset_memory):
|
def test_swap_task_order_empty_tasks(reset_memory):
|
||||||
"""Test swapping behavior with empty tasks dictionary"""
|
"""Test swapping behavior with empty tasks dictionary"""
|
||||||
result = swap_task_order.invoke({"id1": 0, "id2": 1})
|
result = swap_task_order.invoke({"id1": 0, "id2": 1})
|
||||||
assert result == "Invalid task ID(s)"
|
assert result == "Invalid task ID(s)"
|
||||||
|
|
||||||
|
|
||||||
def test_swap_task_order_after_delete(reset_memory):
|
def test_swap_task_order_after_delete(reset_memory):
|
||||||
"""Test swapping after deleting a task"""
|
"""Test swapping after deleting a task"""
|
||||||
# Add test tasks
|
# Add test tasks
|
||||||
|
|
@ -639,5 +671,5 @@ def test_swap_task_order_after_delete(reset_memory):
|
||||||
assert result == "Tasks swapped."
|
assert result == "Tasks swapped."
|
||||||
|
|
||||||
# Verify swap worked
|
# Verify swap worked
|
||||||
assert _global_memory['tasks'][0] == "Task 3"
|
assert _global_memory["tasks"][0] == "Task 3"
|
||||||
assert _global_memory['tasks'][2] == "Task 1"
|
assert _global_memory["tasks"][2] == "Task 1"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import mark
|
|
||||||
from langchain.schema.runnable import Runnable
|
|
||||||
from ra_aid.tools import read_file_tool
|
from ra_aid.tools import read_file_tool
|
||||||
|
|
||||||
|
|
||||||
def test_basic_file_reading(tmp_path):
|
def test_basic_file_reading(tmp_path):
|
||||||
"""Test basic file reading functionality"""
|
"""Test basic file reading functionality"""
|
||||||
# Create a test file
|
# Create a test file
|
||||||
|
|
@ -15,8 +15,9 @@ def test_basic_file_reading(tmp_path):
|
||||||
|
|
||||||
# Verify return format and content
|
# Verify return format and content
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert 'content' in result
|
assert "content" in result
|
||||||
assert result['content'] == test_content
|
assert result["content"] == test_content
|
||||||
|
|
||||||
|
|
||||||
def test_no_truncation(tmp_path):
|
def test_no_truncation(tmp_path):
|
||||||
"""Test that files under max_lines are not truncated"""
|
"""Test that files under max_lines are not truncated"""
|
||||||
|
|
@ -31,8 +32,9 @@ def test_no_truncation(tmp_path):
|
||||||
|
|
||||||
# Verify no truncation occurred
|
# Verify no truncation occurred
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert '[lines of output truncated]' not in result['content']
|
assert "[lines of output truncated]" not in result["content"]
|
||||||
assert len(result['content'].splitlines()) == line_count
|
assert len(result["content"].splitlines()) == line_count
|
||||||
|
|
||||||
|
|
||||||
def test_with_truncation(tmp_path):
|
def test_with_truncation(tmp_path):
|
||||||
"""Test that files over max_lines are properly truncated"""
|
"""Test that files over max_lines are properly truncated"""
|
||||||
|
|
@ -47,14 +49,18 @@ def test_with_truncation(tmp_path):
|
||||||
|
|
||||||
# Verify truncation occurred correctly
|
# Verify truncation occurred correctly
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert '[1000 lines of output truncated]' in result['content']
|
assert "[1000 lines of output truncated]" in result["content"]
|
||||||
assert len(result['content'].splitlines()) == 5001 # 5000 content lines + 1 truncation message
|
assert (
|
||||||
|
len(result["content"].splitlines()) == 5001
|
||||||
|
) # 5000 content lines + 1 truncation message
|
||||||
|
|
||||||
|
|
||||||
def test_nonexistent_file():
|
def test_nonexistent_file():
|
||||||
"""Test error handling for non-existent files"""
|
"""Test error handling for non-existent files"""
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
read_file_tool.invoke({"filepath": "/nonexistent/file.txt"})
|
read_file_tool.invoke({"filepath": "/nonexistent/file.txt"})
|
||||||
|
|
||||||
|
|
||||||
def test_empty_file(tmp_path):
|
def test_empty_file(tmp_path):
|
||||||
"""Test reading an empty file"""
|
"""Test reading an empty file"""
|
||||||
# Create an empty test file
|
# Create an empty test file
|
||||||
|
|
@ -66,5 +72,5 @@ def test_empty_file(tmp_path):
|
||||||
|
|
||||||
# Verify return format and empty content
|
# Verify return format and empty content
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert 'content' in result
|
assert "content" in result
|
||||||
assert result['content'] == ""
|
assert result["content"] == ""
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import pytest
|
|
||||||
from ra_aid.tools.reflection import get_function_info
|
from ra_aid.tools.reflection import get_function_info
|
||||||
|
|
||||||
|
|
||||||
# Sample functions for testing get_function_info
|
# Sample functions for testing get_function_info
|
||||||
def simple_func():
|
def simple_func():
|
||||||
"""A simple function with no parameters."""
|
"""A simple function with no parameters."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def typed_func(a: int, b: str = "default") -> bool:
|
def typed_func(a: int, b: str = "default") -> bool:
|
||||||
"""A function with type hints and default value.
|
"""A function with type hints and default value.
|
||||||
|
|
||||||
|
|
@ -18,13 +19,16 @@ def typed_func(a: int, b: str = "default") -> bool:
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def complex_func(pos1, pos2, *args, kw1="default", **kwargs):
|
def complex_func(pos1, pos2, *args, kw1="default", **kwargs):
|
||||||
"""A function with complex signature."""
|
"""A function with complex signature."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def no_docstring_func(x):
|
def no_docstring_func(x):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestGetFunctionInfo:
|
class TestGetFunctionInfo:
|
||||||
def test_simple_function_info(self):
|
def test_simple_function_info(self):
|
||||||
"""Test info extraction for simple function."""
|
"""Test info extraction for simple function."""
|
||||||
|
|
@ -58,5 +62,3 @@ class TestGetFunctionInfo:
|
||||||
info = get_function_info(no_docstring_func)
|
info = get_function_info(no_docstring_func)
|
||||||
assert "no_docstring_func" in info
|
assert "no_docstring_func" in info
|
||||||
assert "No docstring provided" in info
|
assert "No docstring provided" in info
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,107 @@
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from ra_aid.tools.shell import run_shell_command
|
|
||||||
from ra_aid.tools.memory import _global_memory
|
from ra_aid.tools.memory import _global_memory
|
||||||
|
from ra_aid.tools.shell import run_shell_command
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_console():
|
def mock_console():
|
||||||
with patch('ra_aid.tools.shell.console') as mock:
|
with patch("ra_aid.tools.shell.console") as mock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_prompt():
|
def mock_prompt():
|
||||||
with patch('ra_aid.tools.shell.Prompt') as mock:
|
with patch("ra_aid.tools.shell.Prompt") as mock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_run_interactive():
|
def mock_run_interactive():
|
||||||
with patch('ra_aid.tools.shell.run_interactive_command') as mock:
|
with patch("ra_aid.tools.shell.run_interactive_command") as mock:
|
||||||
mock.return_value = (b"test output", 0)
|
mock.return_value = (b"test output", 0)
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
def test_shell_command_cowboy_mode(mock_console, mock_prompt, mock_run_interactive):
|
def test_shell_command_cowboy_mode(mock_console, mock_prompt, mock_run_interactive):
|
||||||
"""Test shell command execution in cowboy mode (no approval)"""
|
"""Test shell command execution in cowboy mode (no approval)"""
|
||||||
_global_memory['config'] = {'cowboy_mode': True}
|
_global_memory["config"] = {"cowboy_mode": True}
|
||||||
|
|
||||||
result = run_shell_command.invoke({"command": "echo test"})
|
result = run_shell_command.invoke({"command": "echo test"})
|
||||||
|
|
||||||
assert result['success'] is True
|
assert result["success"] is True
|
||||||
assert result['return_code'] == 0
|
assert result["return_code"] == 0
|
||||||
assert "test output" in result['output']
|
assert "test output" in result["output"]
|
||||||
mock_prompt.ask.assert_not_called()
|
mock_prompt.ask.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_shell_command_cowboy_message(mock_console, mock_prompt, mock_run_interactive):
|
def test_shell_command_cowboy_message(mock_console, mock_prompt, mock_run_interactive):
|
||||||
"""Test that cowboy mode displays a properly formatted cowboy message with correct spacing"""
|
"""Test that cowboy mode displays a properly formatted cowboy message with correct spacing"""
|
||||||
_global_memory['config'] = {'cowboy_mode': True}
|
_global_memory["config"] = {"cowboy_mode": True}
|
||||||
|
|
||||||
with patch('ra_aid.tools.shell.get_cowboy_message') as mock_get_message:
|
with patch("ra_aid.tools.shell.get_cowboy_message") as mock_get_message:
|
||||||
mock_get_message.return_value = '🤠 Test cowboy message!'
|
mock_get_message.return_value = "🤠 Test cowboy message!"
|
||||||
result = run_shell_command.invoke({"command": "echo test"})
|
result = run_shell_command.invoke({"command": "echo test"})
|
||||||
|
|
||||||
assert result['success'] is True
|
assert result["success"] is True
|
||||||
mock_console.print.assert_any_call("")
|
mock_console.print.assert_any_call("")
|
||||||
mock_console.print.assert_any_call(" 🤠 Test cowboy message!")
|
mock_console.print.assert_any_call(" 🤠 Test cowboy message!")
|
||||||
mock_console.print.assert_any_call("")
|
mock_console.print.assert_any_call("")
|
||||||
mock_get_message.assert_called_once()
|
mock_get_message.assert_called_once()
|
||||||
|
|
||||||
def test_shell_command_interactive_approved(mock_console, mock_prompt, mock_run_interactive):
|
|
||||||
|
def test_shell_command_interactive_approved(
|
||||||
|
mock_console, mock_prompt, mock_run_interactive
|
||||||
|
):
|
||||||
"""Test shell command execution with interactive approval"""
|
"""Test shell command execution with interactive approval"""
|
||||||
_global_memory['config'] = {'cowboy_mode': False}
|
_global_memory["config"] = {"cowboy_mode": False}
|
||||||
mock_prompt.ask.return_value = 'y'
|
mock_prompt.ask.return_value = "y"
|
||||||
|
|
||||||
result = run_shell_command.invoke({"command": "echo test"})
|
result = run_shell_command.invoke({"command": "echo test"})
|
||||||
|
|
||||||
assert result['success'] is True
|
assert result["success"] is True
|
||||||
assert result['return_code'] == 0
|
assert result["return_code"] == 0
|
||||||
assert "test output" in result['output']
|
assert "test output" in result["output"]
|
||||||
mock_prompt.ask.assert_called_once_with(
|
mock_prompt.ask.assert_called_once_with(
|
||||||
"Execute this command? (y=yes, n=no, c=enable cowboy mode for session)",
|
"Execute this command? (y=yes, n=no, c=enable cowboy mode for session)",
|
||||||
choices=["y", "n", "c"],
|
choices=["y", "n", "c"],
|
||||||
default="y",
|
default="y",
|
||||||
show_choices=True,
|
show_choices=True,
|
||||||
show_default=True
|
show_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_shell_command_interactive_rejected(mock_console, mock_prompt, mock_run_interactive):
|
|
||||||
|
def test_shell_command_interactive_rejected(
|
||||||
|
mock_console, mock_prompt, mock_run_interactive
|
||||||
|
):
|
||||||
"""Test shell command rejection in interactive mode"""
|
"""Test shell command rejection in interactive mode"""
|
||||||
_global_memory['config'] = {'cowboy_mode': False}
|
_global_memory["config"] = {"cowboy_mode": False}
|
||||||
mock_prompt.ask.return_value = 'n'
|
mock_prompt.ask.return_value = "n"
|
||||||
|
|
||||||
result = run_shell_command.invoke({"command": "echo test"})
|
result = run_shell_command.invoke({"command": "echo test"})
|
||||||
|
|
||||||
assert result['success'] is False
|
assert result["success"] is False
|
||||||
assert result['return_code'] == 1
|
assert result["return_code"] == 1
|
||||||
assert "cancelled by user" in result['output']
|
assert "cancelled by user" in result["output"]
|
||||||
mock_prompt.ask.assert_called_once_with(
|
mock_prompt.ask.assert_called_once_with(
|
||||||
"Execute this command? (y=yes, n=no, c=enable cowboy mode for session)",
|
"Execute this command? (y=yes, n=no, c=enable cowboy mode for session)",
|
||||||
choices=["y", "n", "c"],
|
choices=["y", "n", "c"],
|
||||||
default="y",
|
default="y",
|
||||||
show_choices=True,
|
show_choices=True,
|
||||||
show_default=True
|
show_default=True,
|
||||||
)
|
)
|
||||||
mock_run_interactive.assert_not_called()
|
mock_run_interactive.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_shell_command_execution_error(mock_console, mock_prompt, mock_run_interactive):
|
def test_shell_command_execution_error(mock_console, mock_prompt, mock_run_interactive):
|
||||||
"""Test handling of shell command execution errors"""
|
"""Test handling of shell command execution errors"""
|
||||||
_global_memory['config'] = {'cowboy_mode': True}
|
_global_memory["config"] = {"cowboy_mode": True}
|
||||||
mock_run_interactive.side_effect = Exception("Command failed")
|
mock_run_interactive.side_effect = Exception("Command failed")
|
||||||
|
|
||||||
result = run_shell_command.invoke({"command": "invalid command"})
|
result = run_shell_command.invoke({"command": "invalid command"})
|
||||||
|
|
||||||
assert result['success'] is False
|
assert result["success"] is False
|
||||||
assert result['return_code'] == 1
|
assert result["return_code"] == 1
|
||||||
assert "Command failed" in result['output']
|
assert "Command failed" in result["output"]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import os
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, mock_open
|
|
||||||
from ra_aid.tools.write_file import write_file_tool
|
from ra_aid.tools.write_file import write_file_tool
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_test_dir(tmp_path):
|
def temp_test_dir(tmp_path):
|
||||||
"""Create a temporary test directory."""
|
"""Create a temporary test directory."""
|
||||||
|
|
@ -11,15 +13,13 @@ def temp_test_dir(tmp_path):
|
||||||
test_dir.mkdir(exist_ok=True)
|
test_dir.mkdir(exist_ok=True)
|
||||||
return test_dir
|
return test_dir
|
||||||
|
|
||||||
|
|
||||||
def test_basic_write_functionality(temp_test_dir):
|
def test_basic_write_functionality(temp_test_dir):
|
||||||
"""Test basic successful file writing."""
|
"""Test basic successful file writing."""
|
||||||
test_file = temp_test_dir / "test.txt"
|
test_file = temp_test_dir / "test.txt"
|
||||||
content = "Hello, World!\nTest content"
|
content = "Hello, World!\nTest content"
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke({"filepath": str(test_file), "content": content})
|
||||||
"filepath": str(test_file),
|
|
||||||
"content": content
|
|
||||||
})
|
|
||||||
|
|
||||||
# Verify file contents
|
# Verify file contents
|
||||||
assert test_file.read_text() == content
|
assert test_file.read_text() == content
|
||||||
|
|
@ -28,91 +28,85 @@ def test_basic_write_functionality(temp_test_dir):
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["filepath"] == str(test_file)
|
assert result["filepath"] == str(test_file)
|
||||||
assert result["bytes_written"] == len(content.encode('utf-8'))
|
assert result["bytes_written"] == len(content.encode("utf-8"))
|
||||||
assert "Operation completed" in result["message"]
|
assert "Operation completed" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_directory_creation(temp_test_dir):
|
def test_directory_creation(temp_test_dir):
|
||||||
"""Test writing to a file in a non-existent directory."""
|
"""Test writing to a file in a non-existent directory."""
|
||||||
nested_dir = temp_test_dir / "nested" / "subdirs"
|
nested_dir = temp_test_dir / "nested" / "subdirs"
|
||||||
test_file = nested_dir / "test.txt"
|
test_file = nested_dir / "test.txt"
|
||||||
content = "Test content"
|
content = "Test content"
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke({"filepath": str(test_file), "content": content})
|
||||||
"filepath": str(test_file),
|
|
||||||
"content": content
|
|
||||||
})
|
|
||||||
|
|
||||||
assert test_file.exists()
|
assert test_file.exists()
|
||||||
assert test_file.read_text() == content
|
assert test_file.read_text() == content
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_different_encodings(temp_test_dir):
|
def test_different_encodings(temp_test_dir):
|
||||||
"""Test writing files with different encodings."""
|
"""Test writing files with different encodings."""
|
||||||
test_file = temp_test_dir / "encoded.txt"
|
test_file = temp_test_dir / "encoded.txt"
|
||||||
content = "Hello 世界" # Mixed ASCII and Unicode
|
content = "Hello 世界" # Mixed ASCII and Unicode
|
||||||
|
|
||||||
# Test UTF-8
|
# Test UTF-8
|
||||||
result_utf8 = write_file_tool.invoke({
|
result_utf8 = write_file_tool.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "content": content, "encoding": "utf-8"}
|
||||||
"content": content,
|
)
|
||||||
"encoding": 'utf-8'
|
|
||||||
})
|
|
||||||
assert result_utf8["success"] is True
|
assert result_utf8["success"] is True
|
||||||
assert test_file.read_text(encoding='utf-8') == content
|
assert test_file.read_text(encoding="utf-8") == content
|
||||||
|
|
||||||
# Test UTF-16
|
# Test UTF-16
|
||||||
result_utf16 = write_file_tool.invoke({
|
result_utf16 = write_file_tool.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "content": content, "encoding": "utf-16"}
|
||||||
"content": content,
|
)
|
||||||
"encoding": 'utf-16'
|
|
||||||
})
|
|
||||||
assert result_utf16["success"] is True
|
assert result_utf16["success"] is True
|
||||||
assert test_file.read_text(encoding='utf-16') == content
|
assert test_file.read_text(encoding="utf-16") == content
|
||||||
|
|
||||||
@patch('builtins.open')
|
|
||||||
|
@patch("builtins.open")
|
||||||
def test_permission_error(mock_open_func, temp_test_dir):
|
def test_permission_error(mock_open_func, temp_test_dir):
|
||||||
"""Test handling of permission errors."""
|
"""Test handling of permission errors."""
|
||||||
mock_open_func.side_effect = PermissionError("Permission denied")
|
mock_open_func.side_effect = PermissionError("Permission denied")
|
||||||
test_file = temp_test_dir / "noperm.txt"
|
test_file = temp_test_dir / "noperm.txt"
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "content": "test content"}
|
||||||
"content": "test content"
|
)
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Permission denied" in result["message"]
|
assert "Permission denied" in result["message"]
|
||||||
assert result["error"] is not None
|
assert result["error"] is not None
|
||||||
|
|
||||||
@patch('builtins.open')
|
|
||||||
|
@patch("builtins.open")
|
||||||
def test_io_error(mock_open_func, temp_test_dir):
|
def test_io_error(mock_open_func, temp_test_dir):
|
||||||
"""Test handling of IO errors."""
|
"""Test handling of IO errors."""
|
||||||
mock_open_func.side_effect = IOError("IO Error occurred")
|
mock_open_func.side_effect = IOError("IO Error occurred")
|
||||||
test_file = temp_test_dir / "ioerror.txt"
|
test_file = temp_test_dir / "ioerror.txt"
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "content": "test content"}
|
||||||
"content": "test content"
|
)
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "IO Error" in result["message"]
|
assert "IO Error" in result["message"]
|
||||||
assert result["error"] is not None
|
assert result["error"] is not None
|
||||||
|
|
||||||
|
|
||||||
def test_empty_content(temp_test_dir):
|
def test_empty_content(temp_test_dir):
|
||||||
"""Test writing empty content to a file."""
|
"""Test writing empty content to a file."""
|
||||||
test_file = temp_test_dir / "empty.txt"
|
test_file = temp_test_dir / "empty.txt"
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke({"filepath": str(test_file), "content": ""})
|
||||||
"filepath": str(test_file),
|
|
||||||
"content": ""
|
|
||||||
})
|
|
||||||
|
|
||||||
assert test_file.exists()
|
assert test_file.exists()
|
||||||
assert test_file.read_text() == ""
|
assert test_file.read_text() == ""
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["bytes_written"] == 0
|
assert result["bytes_written"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_overwrite_existing_file(temp_test_dir):
|
def test_overwrite_existing_file(temp_test_dir):
|
||||||
"""Test overwriting an existing file."""
|
"""Test overwriting an existing file."""
|
||||||
test_file = temp_test_dir / "overwrite.txt"
|
test_file = temp_test_dir / "overwrite.txt"
|
||||||
|
|
@ -122,43 +116,41 @@ def test_overwrite_existing_file(temp_test_dir):
|
||||||
|
|
||||||
# Overwrite with new content
|
# Overwrite with new content
|
||||||
new_content = "New content"
|
new_content = "New content"
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "content": new_content}
|
||||||
"content": new_content
|
)
|
||||||
})
|
|
||||||
|
|
||||||
assert test_file.read_text() == new_content
|
assert test_file.read_text() == new_content
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["bytes_written"] == len(new_content.encode('utf-8'))
|
assert result["bytes_written"] == len(new_content.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def test_large_file_write(temp_test_dir):
|
def test_large_file_write(temp_test_dir):
|
||||||
"""Test writing a large file and verify statistics."""
|
"""Test writing a large file and verify statistics."""
|
||||||
test_file = temp_test_dir / "large.txt"
|
test_file = temp_test_dir / "large.txt"
|
||||||
content = "Large content\n" * 1000 # Create substantial content
|
content = "Large content\n" * 1000 # Create substantial content
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke({"filepath": str(test_file), "content": content})
|
||||||
"filepath": str(test_file),
|
|
||||||
"content": content
|
|
||||||
})
|
|
||||||
|
|
||||||
assert test_file.exists()
|
assert test_file.exists()
|
||||||
assert test_file.read_text() == content
|
assert test_file.read_text() == content
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["bytes_written"] == len(content.encode('utf-8'))
|
assert result["bytes_written"] == len(content.encode("utf-8"))
|
||||||
assert os.path.getsize(test_file) == len(content.encode('utf-8'))
|
assert os.path.getsize(test_file) == len(content.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_path_characters(temp_test_dir):
|
def test_invalid_path_characters(temp_test_dir):
|
||||||
"""Test handling of invalid path characters."""
|
"""Test handling of invalid path characters."""
|
||||||
invalid_path = temp_test_dir / "invalid\0file.txt"
|
invalid_path = temp_test_dir / "invalid\0file.txt"
|
||||||
|
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke(
|
||||||
"filepath": str(invalid_path),
|
{"filepath": str(invalid_path), "content": "test content"}
|
||||||
"content": "test content"
|
)
|
||||||
})
|
|
||||||
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Invalid file path" in result["message"]
|
assert "Invalid file path" in result["message"]
|
||||||
|
|
||||||
|
|
||||||
def test_write_to_readonly_directory(temp_test_dir):
|
def test_write_to_readonly_directory(temp_test_dir):
|
||||||
"""Test writing to a readonly directory."""
|
"""Test writing to a readonly directory."""
|
||||||
readonly_dir = temp_test_dir / "readonly"
|
readonly_dir = temp_test_dir / "readonly"
|
||||||
|
|
@ -169,10 +161,9 @@ def test_write_to_readonly_directory(temp_test_dir):
|
||||||
os.chmod(readonly_dir, 0o444)
|
os.chmod(readonly_dir, 0o444)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = write_file_tool.invoke({
|
result = write_file_tool.invoke(
|
||||||
"filepath": str(test_file),
|
{"filepath": str(test_file), "content": "test content"}
|
||||||
"content": "test content"
|
)
|
||||||
})
|
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Permission" in result["message"]
|
assert "Permission" in result["message"]
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from scripts.extract_changelog import extract_version_content
|
from scripts.extract_changelog import extract_version_content
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def basic_changelog():
|
def basic_changelog():
|
||||||
return """## [1.2.0]
|
return """## [1.2.0]
|
||||||
|
|
@ -14,6 +16,7 @@ def basic_changelog():
|
||||||
- Change Y
|
- Change Y
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def complex_changelog():
|
def complex_changelog():
|
||||||
return """## [2.0.0]
|
return """## [2.0.0]
|
||||||
|
|
@ -30,6 +33,7 @@ def complex_changelog():
|
||||||
Some content
|
Some content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def test_basic_version_extraction(basic_changelog):
|
def test_basic_version_extraction(basic_changelog):
|
||||||
"""Test extracting a simple version entry"""
|
"""Test extracting a simple version entry"""
|
||||||
result = extract_version_content(basic_changelog, "1.2.0")
|
result = extract_version_content(basic_changelog, "1.2.0")
|
||||||
|
|
@ -39,6 +43,7 @@ def test_basic_version_extraction(basic_changelog):
|
||||||
- Feature B"""
|
- Feature B"""
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
def test_middle_version_extraction(complex_changelog):
|
def test_middle_version_extraction(complex_changelog):
|
||||||
"""Test extracting a version from middle of changelog"""
|
"""Test extracting a version from middle of changelog"""
|
||||||
result = extract_version_content(complex_changelog, "1.9.0")
|
result = extract_version_content(complex_changelog, "1.9.0")
|
||||||
|
|
@ -49,22 +54,26 @@ def test_middle_version_extraction(complex_changelog):
|
||||||
- Bug fix"""
|
- Bug fix"""
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
def test_version_not_found():
|
def test_version_not_found():
|
||||||
"""Test error handling when version doesn't exist"""
|
"""Test error handling when version doesn't exist"""
|
||||||
with pytest.raises(ValueError, match="Version 9.9.9 not found in changelog"):
|
with pytest.raises(ValueError, match="Version 9.9.9 not found in changelog"):
|
||||||
extract_version_content("## [1.0.0]\nSome content", "9.9.9")
|
extract_version_content("## [1.0.0]\nSome content", "9.9.9")
|
||||||
|
|
||||||
|
|
||||||
def test_empty_changelog():
|
def test_empty_changelog():
|
||||||
"""Test handling empty changelog"""
|
"""Test handling empty changelog"""
|
||||||
with pytest.raises(ValueError, match="Version 1.0.0 not found in changelog"):
|
with pytest.raises(ValueError, match="Version 1.0.0 not found in changelog"):
|
||||||
extract_version_content("", "1.0.0")
|
extract_version_content("", "1.0.0")
|
||||||
|
|
||||||
|
|
||||||
def test_malformed_changelog():
|
def test_malformed_changelog():
|
||||||
"""Test handling malformed changelog without proper version headers"""
|
"""Test handling malformed changelog without proper version headers"""
|
||||||
content = "Some content\nNo version headers here\n"
|
content = "Some content\nNo version headers here\n"
|
||||||
with pytest.raises(ValueError, match="Version 1.0.0 not found in changelog"):
|
with pytest.raises(ValueError, match="Version 1.0.0 not found in changelog"):
|
||||||
extract_version_content(content, "1.0.0")
|
extract_version_content(content, "1.0.0")
|
||||||
|
|
||||||
|
|
||||||
def test_version_with_special_chars():
|
def test_version_with_special_chars():
|
||||||
"""Test handling versions with special regex characters"""
|
"""Test handling versions with special regex characters"""
|
||||||
content = """## [1.0.0-beta.1]
|
content = """## [1.0.0-beta.1]
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
"""Tests for file listing functionality."""
|
"""Tests for file listing functionality."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ra_aid.file_listing import (
|
from ra_aid.file_listing import (
|
||||||
|
DirectoryAccessError,
|
||||||
|
DirectoryNotFoundError,
|
||||||
|
FileListerError,
|
||||||
|
GitCommandError,
|
||||||
get_file_listing,
|
get_file_listing,
|
||||||
is_git_repo,
|
is_git_repo,
|
||||||
GitCommandError,
|
|
||||||
DirectoryNotFoundError,
|
|
||||||
DirectoryAccessError,
|
|
||||||
FileListerError,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def empty_git_repo(tmp_path):
|
def empty_git_repo(tmp_path):
|
||||||
"""Create an empty git repository."""
|
"""Create an empty git repository."""
|
||||||
|
|
@ -30,7 +32,7 @@ def sample_git_repo(empty_git_repo):
|
||||||
"src/main.py",
|
"src/main.py",
|
||||||
"src/utils.py",
|
"src/utils.py",
|
||||||
"tests/test_main.py",
|
"tests/test_main.py",
|
||||||
"docs/index.html"
|
"docs/index.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
|
|
@ -43,10 +45,12 @@ def sample_git_repo(empty_git_repo):
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "commit", "-m", "Initial commit"],
|
["git", "commit", "-m", "Initial commit"],
|
||||||
cwd=empty_git_repo,
|
cwd=empty_git_repo,
|
||||||
env={"GIT_AUTHOR_NAME": "Test",
|
env={
|
||||||
"GIT_AUTHOR_EMAIL": "test@example.com",
|
"GIT_AUTHOR_NAME": "Test",
|
||||||
"GIT_COMMITTER_NAME": "Test",
|
"GIT_AUTHOR_EMAIL": "test@example.com",
|
||||||
"GIT_COMMITTER_EMAIL": "test@example.com"}
|
"GIT_COMMITTER_NAME": "Test",
|
||||||
|
"GIT_COMMITTER_EMAIL": "test@example.com",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return empty_git_repo
|
return empty_git_repo
|
||||||
|
|
@ -154,7 +158,10 @@ FILE_LISTING_TEST_CASES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "duplicate_files",
|
"name": "duplicate_files",
|
||||||
"git_output": "\n".join([SINGLE_FILE_NAME, SINGLE_FILE_NAME] + MULTI_FILE_NAMES[1:]) + "\n",
|
"git_output": "\n".join(
|
||||||
|
[SINGLE_FILE_NAME, SINGLE_FILE_NAME] + MULTI_FILE_NAMES[1:]
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
"expected_files": [SINGLE_FILE_NAME] + MULTI_FILE_NAMES[1:],
|
"expected_files": [SINGLE_FILE_NAME] + MULTI_FILE_NAMES[1:],
|
||||||
"expected_total": 3, # After deduplication
|
"expected_total": 3, # After deduplication
|
||||||
"limit": None,
|
"limit": None,
|
||||||
|
|
@ -217,6 +224,7 @@ FILE_LISTING_TEST_CASES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_mock_process(git_output: str) -> MagicMock:
|
def create_mock_process(git_output: str) -> MagicMock:
|
||||||
"""Create a mock process with the given git output."""
|
"""Create a mock process with the given git output."""
|
||||||
mock_process = MagicMock()
|
mock_process = MagicMock()
|
||||||
|
|
@ -224,12 +232,14 @@ def create_mock_process(git_output: str) -> MagicMock:
|
||||||
mock_process.returncode = 0
|
mock_process.returncode = 0
|
||||||
return mock_process
|
return mock_process
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_subprocess():
|
def mock_subprocess():
|
||||||
"""Fixture to mock subprocess.run."""
|
"""Fixture to mock subprocess.run."""
|
||||||
with patch("subprocess.run") as mock_run:
|
with patch("subprocess.run") as mock_run:
|
||||||
yield mock_run
|
yield mock_run
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_is_git_repo():
|
def mock_is_git_repo():
|
||||||
"""Fixture to mock is_git_repo function."""
|
"""Fixture to mock is_git_repo function."""
|
||||||
|
|
@ -237,6 +247,7 @@ def mock_is_git_repo():
|
||||||
mock.return_value = True
|
mock.return_value = True
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_case", FILE_LISTING_TEST_CASES, ids=lambda x: x["name"])
|
@pytest.mark.parametrize("test_case", FILE_LISTING_TEST_CASES, ids=lambda x: x["name"])
|
||||||
def test_get_file_listing(test_case, mock_subprocess, mock_is_git_repo):
|
def test_get_file_listing(test_case, mock_subprocess, mock_is_git_repo):
|
||||||
"""Test get_file_listing with various inputs."""
|
"""Test get_file_listing with various inputs."""
|
||||||
|
|
@ -245,6 +256,7 @@ def test_get_file_listing(test_case, mock_subprocess, mock_is_git_repo):
|
||||||
assert files == test_case["expected_files"]
|
assert files == test_case["expected_files"]
|
||||||
assert total == test_case["expected_total"]
|
assert total == test_case["expected_total"]
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_listing_non_git_repo(mock_is_git_repo):
|
def test_get_file_listing_non_git_repo(mock_is_git_repo):
|
||||||
"""Test get_file_listing with non-git repository."""
|
"""Test get_file_listing with non-git repository."""
|
||||||
mock_is_git_repo.return_value = False
|
mock_is_git_repo.return_value = False
|
||||||
|
|
@ -252,21 +264,23 @@ def test_get_file_listing_non_git_repo(mock_is_git_repo):
|
||||||
assert files == EMPTY_FILE_LIST
|
assert files == EMPTY_FILE_LIST
|
||||||
assert total == EMPTY_FILE_TOTAL
|
assert total == EMPTY_FILE_TOTAL
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_listing_git_error(mock_subprocess, mock_is_git_repo):
|
def test_get_file_listing_git_error(mock_subprocess, mock_is_git_repo):
|
||||||
"""Test get_file_listing when git command fails."""
|
"""Test get_file_listing when git command fails."""
|
||||||
mock_subprocess.side_effect = GitCommandError("Git command failed")
|
mock_subprocess.side_effect = GitCommandError("Git command failed")
|
||||||
with pytest.raises(GitCommandError):
|
with pytest.raises(GitCommandError):
|
||||||
get_file_listing(DUMMY_PATH)
|
get_file_listing(DUMMY_PATH)
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_listing_permission_error(mock_subprocess, mock_is_git_repo):
|
def test_get_file_listing_permission_error(mock_subprocess, mock_is_git_repo):
|
||||||
"""Test get_file_listing with permission error."""
|
"""Test get_file_listing with permission error."""
|
||||||
mock_subprocess.side_effect = PermissionError("Permission denied")
|
mock_subprocess.side_effect = PermissionError("Permission denied")
|
||||||
with pytest.raises(DirectoryAccessError):
|
with pytest.raises(DirectoryAccessError):
|
||||||
get_file_listing(DUMMY_PATH)
|
get_file_listing(DUMMY_PATH)
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_listing_unexpected_error(mock_subprocess, mock_is_git_repo):
|
def test_get_file_listing_unexpected_error(mock_subprocess, mock_is_git_repo):
|
||||||
"""Test get_file_listing with unexpected error."""
|
"""Test get_file_listing with unexpected error."""
|
||||||
mock_subprocess.side_effect = Exception("Unexpected error")
|
mock_subprocess.side_effect = Exception("Unexpected error")
|
||||||
with pytest.raises(FileListerError):
|
with pytest.raises(FileListerError):
|
||||||
get_file_listing(DUMMY_PATH)
|
get_file_listing(DUMMY_PATH)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,18 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ra_aid.project_info import (
|
import pytest
|
||||||
get_project_info,
|
|
||||||
ProjectInfo,
|
from ra_aid.project_info import ProjectInfo, get_project_info
|
||||||
ProjectInfoError
|
from ra_aid.project_state import DirectoryAccessError, DirectoryNotFoundError
|
||||||
)
|
|
||||||
from ra_aid.project_state import DirectoryNotFoundError, DirectoryAccessError
|
|
||||||
from ra_aid.file_listing import GitCommandError
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def empty_git_repo(tmp_path):
|
def empty_git_repo(tmp_path):
|
||||||
"""Create an empty git repository."""
|
"""Create an empty git repository."""
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True)
|
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True)
|
||||||
return tmp_path
|
return tmp_path
|
||||||
|
|
||||||
|
|
@ -31,7 +27,7 @@ def sample_git_repo(empty_git_repo):
|
||||||
"src/main.py",
|
"src/main.py",
|
||||||
"src/utils.py",
|
"src/utils.py",
|
||||||
"tests/test_main.py",
|
"tests/test_main.py",
|
||||||
"docs/index.html"
|
"docs/index.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
|
|
@ -44,10 +40,12 @@ def sample_git_repo(empty_git_repo):
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "commit", "-m", "Initial commit"],
|
["git", "commit", "-m", "Initial commit"],
|
||||||
cwd=empty_git_repo,
|
cwd=empty_git_repo,
|
||||||
env={"GIT_AUTHOR_NAME": "Test",
|
env={
|
||||||
"GIT_AUTHOR_EMAIL": "test@example.com",
|
"GIT_AUTHOR_NAME": "Test",
|
||||||
"GIT_COMMITTER_NAME": "Test",
|
"GIT_AUTHOR_EMAIL": "test@example.com",
|
||||||
"GIT_COMMITTER_EMAIL": "test@example.com"}
|
"GIT_COMMITTER_NAME": "Test",
|
||||||
|
"GIT_COMMITTER_EMAIL": "test@example.com",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return empty_git_repo
|
return empty_git_repo
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
"""Tests for project state detection functionality."""
|
"""Tests for project state detection functionality."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ra_aid.project_state import (
|
from ra_aid.project_state import (
|
||||||
is_new_project,
|
|
||||||
DirectoryNotFoundError,
|
|
||||||
DirectoryAccessError,
|
DirectoryAccessError,
|
||||||
ProjectStateError
|
DirectoryNotFoundError,
|
||||||
|
ProjectStateError,
|
||||||
|
is_new_project,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue