From 97d4af3681b1b4b2089b07a0ff43277618c9e370 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sat, 3 May 2025 18:57:18 +0200 Subject: [PATCH 1/6] Add auto mode for automated task execution without confirmation --- aider/coders/__init__.py | 2 + aider/coders/auto_coder.py | 77 ++++++++++++++++++++++++++++++++++++ aider/coders/auto_prompts.py | 29 ++++++++++++++ aider/commands.py | 11 ++++++ 4 files changed, 119 insertions(+) create mode 100644 aider/coders/auto_coder.py create mode 100644 aider/coders/auto_prompts.py diff --git a/aider/coders/__init__.py b/aider/coders/__init__.py index 88bcddfaa..69a2d40cb 100644 --- a/aider/coders/__init__.py +++ b/aider/coders/__init__.py @@ -1,5 +1,6 @@ from .architect_coder import ArchitectCoder from .ask_coder import AskCoder +from .auto_coder import AutoCoder from .base_coder import Coder from .context_coder import ContextCoder from .editblock_coder import EditBlockCoder @@ -31,4 +32,5 @@ __all__ = [ EditorWholeFileCoder, EditorDiffFencedCoder, ContextCoder, + AutoCoder, ] diff --git a/aider/coders/auto_coder.py b/aider/coders/auto_coder.py new file mode 100644 index 000000000..f8a763090 --- /dev/null +++ b/aider/coders/auto_coder.py @@ -0,0 +1,77 @@ +from .context_coder import ContextCoder +from .auto_prompts import AutoPrompts + + +class AutoCoder(ContextCoder): + """Automatically identify files and make changes without confirmation.""" + + edit_format = "auto" + gpt_prompts = AutoPrompts() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Set yes_to_all to bypass confirmations + self.io.yes = True + + # Ensure auto_accept_architect is True + self.auto_accept_architect = True + + # Enable auto-linting and auto-testing if configured + self.auto_lint = kwargs.get('auto_lint', True) + self.auto_test = kwargs.get('auto_test', False) + + def reply_completed(self): + # First use ContextCoder's functionality to identify relevant files + content = self.partial_response_content + if not content or not content.strip(): + return True + + # Get files mentioned in the response + current_rel_fnames = set(self.get_inchat_relative_files()) + mentioned_rel_fnames = set(self.get_file_mentions(content, ignore_current=True)) + + # If the files are different, automatically add the mentioned files + if mentioned_rel_fnames != current_rel_fnames: + self.abs_fnames = set() + for fname in mentioned_rel_fnames: + self.add_rel_fname(fname) + + # Now that we've added the files, we need to get the content again + # and apply the changes automatically + self.io.tool_output(f"Automatically added files: {', '.join(mentioned_rel_fnames)}") + + # Refresh the repository map if needed + if self.repo_map: + self.get_repo_map(force_refresh=True) + + # Create a new message to apply the changes + self.reflected_message = "I've identified the relevant files. Now I'll make the requested changes." + return False + + # If we already have all the files, apply the changes + edited = self.apply_updates() + + if edited: + self.io.tool_output(f"Automatically applied changes to: {', '.join(edited)}") + self.aider_edited_files.update(edited) + saved_message = self.auto_commit(edited) + + if saved_message: + self.move_back_cur_messages(saved_message) + + # Run linting if enabled + if self.auto_lint: + lint_errors = self.lint_edited(edited) + if lint_errors: + self.io.tool_output("Linting found errors. Attempting to fix...") + self.reflected_message = lint_errors + return False + + # Run tests if enabled + if self.auto_test: + test_output = self.run_tests() + if test_output: + self.io.tool_output(f"Test results: {test_output}") + + return True diff --git a/aider/coders/auto_prompts.py b/aider/coders/auto_prompts.py new file mode 100644 index 000000000..2d36896f6 --- /dev/null +++ b/aider/coders/auto_prompts.py @@ -0,0 +1,29 @@ +# flake8: noqa: E501 + +from .context_prompts import ContextPrompts + + +class AutoPrompts(ContextPrompts): + main_system = """Act as an expert code analyst and developer. +First, understand the user's question or request to determine ALL the existing source files which will need to be modified. +Then, make the necessary changes to implement the requested feature or fix the issue. + +Your task has two phases: +1. Identify all relevant files that need to be modified +2. Make the necessary changes to implement the requested feature or fix + +For phase 1: +- Return the *complete* list of files which will need to be modified based on the user's request +- Explain why each file is needed, including names of key classes/functions/methods/variables +- Be sure to include or omit the names of files already added to the chat, based on whether they are actually needed or not + +For phase 2: +- Implement the requested changes in the identified files +- Follow the codebase's style and conventions +- Ensure your changes are complete and functional +- Explain the changes you've made and why they address the user's request + +The user will use every file you mention, regardless of your commentary. +So *ONLY* mention the names of relevant files. +If a file is not relevant DO NOT mention it. +""" diff --git a/aider/commands.py b/aider/commands.py index 81fc80093..84b01d6d9 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -162,6 +162,10 @@ class Commands: "context", "Automatically identify which files will need to be edited.", ), + ( + "auto", + "Automatically identify files and make changes without confirmation.", + ), ] ) @@ -1150,6 +1154,9 @@ class Commands: def completions_context(self): raise CommandCompletionException() + def completions_auto(self): + raise CommandCompletionException() + def cmd_ask(self, args): """Ask questions about the code base without editing any files. If no prompt provided, switches to ask mode.""" # noqa return self._generic_chat_command(args, "ask") @@ -1162,6 +1169,10 @@ class Commands: """Enter architect/editor mode using 2 different models. If no prompt provided, switches to architect/editor mode.""" # noqa return self._generic_chat_command(args, "architect") + def cmd_auto(self, args): + """Enter auto mode to automatically identify files and make changes without confirmation. If no prompt provided, switches to auto mode.""" # noqa + return self._generic_chat_command(args, "auto") + def cmd_context(self, args): """Enter context mode to see surrounding code context. If no prompt provided, switches to context mode.""" # noqa return self._generic_chat_command(args, "context", placeholder=args.strip() or None) From e8b9c292ce1445cbfcc9c2274512284cbd48bb12 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sat, 3 May 2025 19:18:26 +0200 Subject: [PATCH 2/6] Enhance auto mode with improved context-finding capabilities --- aider/args.py | 15 +++++ aider/coders/auto_coder.py | 116 ++++++++++++++++++++++++++++++----- aider/coders/auto_prompts.py | 35 +++++++++-- aider/main.py | 2 + 4 files changed, 148 insertions(+), 20 deletions(-) diff --git a/aider/args.py b/aider/args.py index 6df19778b..c4060174c 100644 --- a/aider/args.py +++ b/aider/args.py @@ -248,6 +248,21 @@ def get_parser(default_config_files, git_root): help="Multiplier for map tokens when no files are specified (default: 2)", ) + ########## + group = parser.add_argument_group("Auto mode settings") + group.add_argument( + "--deep-context-search", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable enhanced context finding in auto mode (default: True)", + ) + group.add_argument( + "--min-identifier-length", + type=int, + default=3, + help="Minimum length of identifiers to consider for context finding (default: 3)", + ) + ########## group = parser.add_argument_group("History Files") default_input_history_file = ( diff --git a/aider/coders/auto_coder.py b/aider/coders/auto_coder.py index f8a763090..fbae89bb3 100644 --- a/aider/coders/auto_coder.py +++ b/aider/coders/auto_coder.py @@ -1,5 +1,7 @@ from .context_coder import ContextCoder from .auto_prompts import AutoPrompts +import re +from pathlib import Path class AutoCoder(ContextCoder): @@ -10,56 +12,142 @@ class AutoCoder(ContextCoder): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + # Set yes_to_all to bypass confirmations self.io.yes = True - + # Ensure auto_accept_architect is True self.auto_accept_architect = True - + # Enable auto-linting and auto-testing if configured self.auto_lint = kwargs.get('auto_lint', True) self.auto_test = kwargs.get('auto_test', False) + # Enhanced context finding settings + self.deep_context_search = kwargs.get('deep_context_search', True) + self.min_identifier_length = kwargs.get('min_identifier_length', 3) # Shorter than default (5) + + # Increase repo map tokens for better context + if self.repo_map: + self.repo_map.max_map_tokens *= 1.5 # Increase token allocation for repo map + self.repo_map.refresh = "always" # Always refresh the repo map + + def get_enhanced_file_mentions(self, content): + """Enhanced method to find file mentions in content with better heuristics.""" + # Get standard file mentions + standard_mentions = self.get_file_mentions(content, ignore_current=True) + + # Get identifiers that might be related to files + identifiers = self.get_ident_mentions(content) + + # Use a lower threshold for identifier length + all_fnames = {} + for fname in self.get_all_relative_files(): + if not fname or fname == ".": + continue + + try: + path = Path(fname) + + # Add the file's stem (name without extension) + base = path.stem.lower() + if len(base) >= self.min_identifier_length: + if base not in all_fnames: + all_fnames[base] = set() + all_fnames[base].add(fname) + + # Add the file's parent directory name + if path.parent.name: + parent = path.parent.name.lower() + if len(parent) >= self.min_identifier_length: + if parent not in all_fnames: + all_fnames[parent] = set() + all_fnames[parent].add(fname) + + # Add the full path components + parts = [p.lower() for p in path.parts if p and len(p) >= self.min_identifier_length] + for part in parts: + if part not in all_fnames: + all_fnames[part] = set() + all_fnames[part].add(fname) + except ValueError: + continue + + # Match identifiers to files + identifier_matches = set() + for ident in identifiers: + ident_lower = ident.lower() + if len(ident_lower) >= self.min_identifier_length and ident_lower in all_fnames: + identifier_matches.update(all_fnames[ident_lower]) + + # Look for import statements and package references + import_pattern = re.compile(r'(?:import|from|require|include)\s+([a-zA-Z0-9_.]+)') + imports = import_pattern.findall(content) + + import_matches = set() + for imp in imports: + parts = imp.split('.') + for i in range(len(parts)): + partial = '.'.join(parts[:i+1]) + partial_lower = partial.lower() + if partial_lower in all_fnames: + import_matches.update(all_fnames[partial_lower]) + + # Also check for file extensions + for ext in ['.py', '.js', '.ts', '.java', '.c', '.cpp', '.h', '.hpp']: + with_ext = partial + ext + with_ext_lower = with_ext.lower() + if with_ext_lower in all_fnames: + import_matches.update(all_fnames[with_ext_lower]) + + # Combine all matches + all_matches = standard_mentions | identifier_matches | import_matches + + return all_matches + def reply_completed(self): # First use ContextCoder's functionality to identify relevant files content = self.partial_response_content if not content or not content.strip(): return True - # Get files mentioned in the response + # Get files mentioned in the response using enhanced methods current_rel_fnames = set(self.get_inchat_relative_files()) - mentioned_rel_fnames = set(self.get_file_mentions(content, ignore_current=True)) + + if self.deep_context_search: + mentioned_rel_fnames = self.get_enhanced_file_mentions(content) + else: + mentioned_rel_fnames = set(self.get_file_mentions(content, ignore_current=True)) # If the files are different, automatically add the mentioned files if mentioned_rel_fnames != current_rel_fnames: self.abs_fnames = set() for fname in mentioned_rel_fnames: self.add_rel_fname(fname) - + # Now that we've added the files, we need to get the content again # and apply the changes automatically self.io.tool_output(f"Automatically added files: {', '.join(mentioned_rel_fnames)}") - + # Refresh the repository map if needed if self.repo_map: self.get_repo_map(force_refresh=True) - + # Create a new message to apply the changes self.reflected_message = "I've identified the relevant files. Now I'll make the requested changes." return False - + # If we already have all the files, apply the changes edited = self.apply_updates() - + if edited: self.io.tool_output(f"Automatically applied changes to: {', '.join(edited)}") self.aider_edited_files.update(edited) saved_message = self.auto_commit(edited) - + if saved_message: self.move_back_cur_messages(saved_message) - + # Run linting if enabled if self.auto_lint: lint_errors = self.lint_edited(edited) @@ -67,11 +155,11 @@ class AutoCoder(ContextCoder): self.io.tool_output("Linting found errors. Attempting to fix...") self.reflected_message = lint_errors return False - + # Run tests if enabled if self.auto_test: test_output = self.run_tests() if test_output: self.io.tool_output(f"Test results: {test_output}") - + return True diff --git a/aider/coders/auto_prompts.py b/aider/coders/auto_prompts.py index 2d36896f6..ff822f50e 100644 --- a/aider/coders/auto_prompts.py +++ b/aider/coders/auto_prompts.py @@ -4,26 +4,49 @@ from .context_prompts import ContextPrompts class AutoPrompts(ContextPrompts): - main_system = """Act as an expert code analyst and developer. -First, understand the user's question or request to determine ALL the existing source files which will need to be modified. + main_system = """Act as an expert code analyst and developer with deep understanding of software architecture. +First, thoroughly analyze the user's request to determine ALL existing source files which will need to be modified or referenced. Then, make the necessary changes to implement the requested feature or fix the issue. Your task has two phases: -1. Identify all relevant files that need to be modified +1. Identify all relevant files that need to be modified or referenced 2. Make the necessary changes to implement the requested feature or fix -For phase 1: -- Return the *complete* list of files which will need to be modified based on the user's request +For phase 1 (Context Discovery): +- Perform a comprehensive analysis to identify ALL files that might be relevant +- Consider not just files that need direct modification, but also: + * Files containing related classes, interfaces, or types + * Files with dependent functionality + * Configuration files that might affect the behavior + * Test files that will need to be updated +- Return the *complete* list of files which will need to be modified or referenced - Explain why each file is needed, including names of key classes/functions/methods/variables - Be sure to include or omit the names of files already added to the chat, based on whether they are actually needed or not +- Think about imports, inheritance hierarchies, and dependency relationships -For phase 2: +For phase 2 (Implementation): - Implement the requested changes in the identified files - Follow the codebase's style and conventions - Ensure your changes are complete and functional +- Consider edge cases and error handling +- Update any related tests - Explain the changes you've made and why they address the user's request The user will use every file you mention, regardless of your commentary. So *ONLY* mention the names of relevant files. If a file is not relevant DO NOT mention it. + +Remember to consider: +- Class hierarchies and inheritance relationships +- Interface implementations +- Import dependencies +- Configuration settings +- Related test files +""" + + system_reminder = """Remember to: +1. First identify ALL relevant files needed for the task +2. Then implement the changes +3. Only mention file names that are actually relevant +4. Consider dependencies, imports, and inheritance relationships """ diff --git a/aider/main.py b/aider/main.py index 89286e1de..694c976d5 100644 --- a/aider/main.py +++ b/aider/main.py @@ -988,6 +988,8 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F detect_urls=args.detect_urls, auto_copy_context=args.copy_paste, auto_accept_architect=args.auto_accept_architect, + deep_context_search=args.deep_context_search, + min_identifier_length=args.min_identifier_length, ) except UnknownEditFormat as err: io.tool_error(str(err)) From ea3b07215527ccbc3763a48cd22cc081ece4d81b Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sat, 3 May 2025 20:32:43 +0200 Subject: [PATCH 3/6] Fix TypeError in base_coder.py by adding deep_context_search parameter --- aider/coders/base_coder.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 675570c60..3a51aff7c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -323,6 +323,8 @@ class Coder: file_watcher=None, auto_copy_context=False, auto_accept_architect=True, + deep_context_search=True, + min_identifier_length=3, ): # Fill in a dummy Analytics if needed, but it is never .enable()'d self.analytics = analytics if analytics is not None else Analytics() @@ -337,6 +339,10 @@ class Coder: self.auto_copy_context = auto_copy_context self.auto_accept_architect = auto_accept_architect + # Auto mode settings + self.deep_context_search = deep_context_search + self.min_identifier_length = min_identifier_length + self.ignore_mentions = ignore_mentions if not self.ignore_mentions: self.ignore_mentions = set() @@ -1606,6 +1612,11 @@ class Coder: ) ] + def get_enhanced_file_mentions(self, content): + """Base implementation of enhanced file mentions - just returns standard file mentions. + This method is overridden in AutoCoder to provide more sophisticated context finding.""" + return self.get_file_mentions(content, ignore_current=True) + def get_file_mentions(self, content, ignore_current=False): words = set(word for word in content.split()) From bbad86b7334f18ca085b98552dbeb1d3263bf214 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sat, 3 May 2025 20:34:53 +0200 Subject: [PATCH 4/6] Fix UnicodeDecodeError in litellm JSON loading --- aider/exceptions.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/aider/exceptions.py b/aider/exceptions.py index a81a058e0..c06298c1d 100644 --- a/aider/exceptions.py +++ b/aider/exceptions.py @@ -58,16 +58,36 @@ class LiteLLMExceptions: self._load() def _load(self, strict=False): + # Patch litellm's JSON loading to use UTF-8 encoding + import json import litellm + from pathlib import Path - for var in dir(litellm): - if var.endswith("Error"): - if var not in self.exception_info: - raise ValueError(f"{var} is in litellm but not in aider's exceptions list") + # Monkey patch json.load in litellm.utils to handle UTF-8 encoding + original_json_load = json.load - for var in self.exception_info: - ex = getattr(litellm, var) - self.exceptions[ex] = self.exception_info[var] + def patched_json_load(fp, *args, **kwargs): + # Read the file content with UTF-8 encoding + content = Path(fp.name).read_text(encoding='utf-8') + # Parse the content as JSON + return json.loads(content, *args, **kwargs) + + # Apply the monkey patch + json.load = patched_json_load + + try: + # Now load litellm exceptions + for var in dir(litellm): + if var.endswith("Error"): + if var not in self.exception_info: + raise ValueError(f"{var} is in litellm but not in aider's exceptions list") + + for var in self.exception_info: + ex = getattr(litellm, var) + self.exceptions[ex] = self.exception_info[var] + finally: + # Restore the original json.load function + json.load = original_json_load def exceptions_tuple(self): return tuple(self.exceptions) From 2582a845dc6bd0fe4b2826b5d6e242136c333d48 Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sat, 3 May 2025 20:36:38 +0200 Subject: [PATCH 5/6] Improve fix for UnicodeDecodeError in JSON loading --- aider/exceptions.py | 36 +++++++++--------------------------- aider/llm.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/aider/exceptions.py b/aider/exceptions.py index c06298c1d..dae3449c1 100644 --- a/aider/exceptions.py +++ b/aider/exceptions.py @@ -58,36 +58,18 @@ class LiteLLMExceptions: self._load() def _load(self, strict=False): - # Patch litellm's JSON loading to use UTF-8 encoding - import json + # Import litellm - json.load is already patched in aider.llm import litellm - from pathlib import Path - # Monkey patch json.load in litellm.utils to handle UTF-8 encoding - original_json_load = json.load + # Load litellm exceptions + for var in dir(litellm): + if var.endswith("Error"): + if var not in self.exception_info: + raise ValueError(f"{var} is in litellm but not in aider's exceptions list") - def patched_json_load(fp, *args, **kwargs): - # Read the file content with UTF-8 encoding - content = Path(fp.name).read_text(encoding='utf-8') - # Parse the content as JSON - return json.loads(content, *args, **kwargs) - - # Apply the monkey patch - json.load = patched_json_load - - try: - # Now load litellm exceptions - for var in dir(litellm): - if var.endswith("Error"): - if var not in self.exception_info: - raise ValueError(f"{var} is in litellm but not in aider's exceptions list") - - for var in self.exception_info: - ex = getattr(litellm, var) - self.exceptions[ex] = self.exception_info[var] - finally: - # Restore the original json.load function - json.load = original_json_load + for var in self.exception_info: + ex = getattr(litellm, var) + self.exceptions[ex] = self.exception_info[var] def exceptions_tuple(self): return tuple(self.exceptions) diff --git a/aider/llm.py b/aider/llm.py index c57c274db..0bc1f8adb 100644 --- a/aider/llm.py +++ b/aider/llm.py @@ -1,6 +1,8 @@ import importlib +import json import os import warnings +from pathlib import Path from aider.dump import dump # noqa: F401 @@ -17,6 +19,27 @@ os.environ["LITELLM_MODE"] = "PRODUCTION" VERBOSE = False +# Patch json.load to handle UTF-8 encoding for litellm +original_json_load = json.load + +def patched_json_load(fp, *args, **kwargs): + try: + # First try the original method + return original_json_load(fp, *args, **kwargs) + except UnicodeDecodeError: + # If it fails with UnicodeDecodeError, try with UTF-8 encoding + try: + # Read the file content with UTF-8 encoding + content = Path(fp.name).read_text(encoding='utf-8') + # Parse the content as JSON + return json.loads(content, *args, **kwargs) + except Exception: + # If that also fails, re-raise the original exception + raise + +# Apply the monkey patch +json.load = patched_json_load + class LazyLiteLLM: _lazy_module = None From fbdf3ce82397da2f4cd02e973b92c751c45bb39c Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Sat, 3 May 2025 20:38:33 +0200 Subject: [PATCH 6/6] Handle binary files gracefully in base_coder.py --- aider/coders/base_coder.py | 46 +++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3a51aff7c..53ebac0f6 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -572,23 +572,34 @@ class Coder: def get_abs_fnames_content(self): for fname in list(self.abs_fnames): - content = self.io.read_text(fname) + try: + content = self.io.read_text(fname) - if content is None: + if content is None: + relative_fname = self.get_rel_fname(fname) + self.io.tool_warning(f"Dropping {relative_fname} from the chat.") + self.abs_fnames.remove(fname) + else: + yield fname, content + except UnicodeDecodeError: + # Skip binary files that can't be decoded as text relative_fname = self.get_rel_fname(fname) - self.io.tool_warning(f"Dropping {relative_fname} from the chat.") + self.io.tool_warning(f"Dropping binary file {relative_fname} from the chat.") self.abs_fnames.remove(fname) - else: - yield fname, content def choose_fence(self): all_content = "" for _fname, content in self.get_abs_fnames_content(): all_content += content + "\n" for _fname in self.abs_read_only_fnames: - content = self.io.read_text(_fname) - if content is not None: - all_content += content + "\n" + try: + content = self.io.read_text(_fname) + if content is not None: + all_content += content + "\n" + except UnicodeDecodeError: + # Skip binary files that can't be decoded as text + relative_fname = self.get_rel_fname(_fname) + self.io.tool_warning(f"Skipping binary file {relative_fname} when choosing fence.") lines = all_content.splitlines() good = False @@ -634,14 +645,19 @@ class Coder: def get_read_only_files_content(self): prompt = "" for fname in self.abs_read_only_fnames: - content = self.io.read_text(fname) - if content is not None and not is_image_file(fname): + try: + content = self.io.read_text(fname) + if content is not None and not is_image_file(fname): + relative_fname = self.get_rel_fname(fname) + prompt += "\n" + prompt += relative_fname + prompt += f"\n{self.fence[0]}\n" + prompt += content + prompt += f"{self.fence[1]}\n" + except UnicodeDecodeError: + # Skip binary files that can't be decoded as text relative_fname = self.get_rel_fname(fname) - prompt += "\n" - prompt += relative_fname - prompt += f"\n{self.fence[0]}\n" - prompt += content - prompt += f"{self.fence[1]}\n" + self.io.tool_warning(f"Skipping binary file {relative_fname} from read-only files.") return prompt def get_cur_message_text(self):