mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-08 21:55:00 +00:00
192 lines
8 KiB
Python
192 lines
8 KiB
Python
import os
|
|
import traceback
|
|
|
|
class ToolError(Exception):
|
|
"""Custom exception for tool-specific errors that should be reported to the LLM."""
|
|
pass
|
|
|
|
def resolve_paths(coder, file_path):
|
|
"""Resolves absolute and relative paths for a given file path."""
|
|
try:
|
|
abs_path = coder.abs_root_path(file_path)
|
|
rel_path = coder.get_rel_fname(abs_path)
|
|
return abs_path, rel_path
|
|
except Exception as e:
|
|
# Wrap unexpected errors during path resolution
|
|
raise ToolError(f"Error resolving path '{file_path}': {e}")
|
|
|
|
def validate_file_for_edit(coder, file_path):
|
|
"""
|
|
Validates if a file exists, is in context, and is editable.
|
|
Reads and returns original content if valid.
|
|
Raises ToolError on failure.
|
|
|
|
Returns:
|
|
tuple: (absolute_path, relative_path, original_content)
|
|
"""
|
|
abs_path, rel_path = resolve_paths(coder, file_path)
|
|
|
|
if not os.path.isfile(abs_path):
|
|
raise ToolError(f"File '{file_path}' not found")
|
|
|
|
if abs_path not in coder.abs_fnames:
|
|
if abs_path in coder.abs_read_only_fnames:
|
|
raise ToolError(f"File '{file_path}' is read-only. Use MakeEditable first.")
|
|
else:
|
|
# File exists but is not in context at all
|
|
raise ToolError(f"File '{file_path}' not in context. Use View or MakeEditable first.")
|
|
|
|
# Reread content immediately before potential modification
|
|
content = coder.io.read_text(abs_path)
|
|
if content is None:
|
|
# This indicates an issue reading a file we know exists and is in context
|
|
coder.io.tool_error(f"Internal error: Could not read file '{file_path}' which should be accessible.")
|
|
raise ToolError(f"Could not read file '{file_path}'")
|
|
|
|
return abs_path, rel_path, content
|
|
|
|
def find_pattern_indices(lines, pattern, near_context=None):
|
|
"""Finds all line indices matching a pattern, optionally filtered by context."""
|
|
indices = []
|
|
for i, line in enumerate(lines):
|
|
if pattern in line:
|
|
if near_context:
|
|
# Check if near_context is within a window around the match
|
|
context_window_start = max(0, i - 5) # Check 5 lines before/after
|
|
context_window_end = min(len(lines), i + 6)
|
|
context_block = "\n".join(lines[context_window_start:context_window_end])
|
|
if near_context in context_block:
|
|
indices.append(i)
|
|
else:
|
|
indices.append(i)
|
|
return indices
|
|
|
|
def select_occurrence_index(indices, occurrence, pattern_desc="Pattern"):
|
|
"""
|
|
Selects the target 0-based index from a list of indices based on the 1-based occurrence parameter.
|
|
Raises ToolError if the pattern wasn't found or the occurrence is invalid.
|
|
"""
|
|
num_occurrences = len(indices)
|
|
if not indices:
|
|
raise ToolError(f"{pattern_desc} not found")
|
|
|
|
try:
|
|
occurrence = int(occurrence) # Ensure occurrence is an integer
|
|
if occurrence == -1: # Last occurrence
|
|
if num_occurrences == 0:
|
|
raise ToolError(f"{pattern_desc} not found, cannot select last occurrence.")
|
|
target_idx = num_occurrences - 1
|
|
elif 1 <= occurrence <= num_occurrences:
|
|
target_idx = occurrence - 1 # Convert 1-based to 0-based
|
|
else:
|
|
raise ToolError(f"Occurrence number {occurrence} is out of range for {pattern_desc}. Found {num_occurrences} occurrences.")
|
|
except ValueError:
|
|
raise ToolError(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
|
|
|
|
return indices[target_idx]
|
|
|
|
def determine_line_range(lines, start_pattern_line_index, end_pattern=None, line_count=None, pattern_desc="Block"):
|
|
"""
|
|
Determines the end line index based on end_pattern or line_count.
|
|
Raises ToolError if end_pattern is not found or line_count is invalid.
|
|
"""
|
|
start_line = start_pattern_line_index
|
|
end_line = -1
|
|
|
|
if end_pattern and line_count:
|
|
raise ToolError("Cannot specify both end_pattern and line_count")
|
|
|
|
if end_pattern:
|
|
found_end = False
|
|
# Search from the start_line onwards for the end_pattern
|
|
for i in range(start_line, len(lines)):
|
|
if end_pattern in lines[i]:
|
|
end_line = i
|
|
found_end = True
|
|
break
|
|
if not found_end:
|
|
raise ToolError(f"End pattern '{end_pattern}' not found after start pattern on line {start_line + 1}")
|
|
elif line_count:
|
|
try:
|
|
line_count = int(line_count)
|
|
if line_count <= 0:
|
|
raise ValueError("Line count must be positive")
|
|
# Calculate end line index, ensuring it doesn't exceed file bounds
|
|
end_line = min(start_line + line_count - 1, len(lines) - 1)
|
|
except ValueError:
|
|
raise ToolError(f"Invalid line_count value: '{line_count}'. Must be a positive integer.")
|
|
else:
|
|
# If neither end_pattern nor line_count is given, the range is just the start line
|
|
end_line = start_line
|
|
|
|
return start_line, end_line
|
|
|
|
|
|
def apply_change(coder, abs_path, rel_path, original_content, new_content, change_type, metadata, change_id=None):
|
|
"""
|
|
Writes the new content, tracks the change, and updates coder state.
|
|
Returns the final change ID. Raises ToolError on tracking failure.
|
|
"""
|
|
coder.io.write_text(abs_path, new_content)
|
|
try:
|
|
final_change_id = coder.change_tracker.track_change(
|
|
file_path=rel_path,
|
|
change_type=change_type,
|
|
original_content=original_content,
|
|
new_content=new_content,
|
|
metadata=metadata,
|
|
change_id=change_id
|
|
)
|
|
except Exception as track_e:
|
|
# Log the error but also raise ToolError to inform the LLM
|
|
coder.io.tool_error(f"Error tracking change for {change_type}: {track_e}")
|
|
raise ToolError(f"Failed to track change: {track_e}")
|
|
|
|
coder.aider_edited_files.add(rel_path)
|
|
return final_change_id
|
|
|
|
|
|
def handle_tool_error(coder, tool_name, e, add_traceback=True):
|
|
"""Logs tool errors and returns a formatted error message for the LLM."""
|
|
error_message = f"Error in {tool_name}: {str(e)}"
|
|
if add_traceback:
|
|
error_message += f"\n{traceback.format_exc()}"
|
|
coder.io.tool_error(error_message)
|
|
# Return only the core error message to the LLM for brevity
|
|
return f"Error: {str(e)}"
|
|
|
|
def format_tool_result(coder, tool_name, success_message, change_id=None, diff_snippet=None, dry_run=False, dry_run_message=None):
|
|
"""Formats the result message for tool execution."""
|
|
if dry_run:
|
|
full_message = dry_run_message or f"Dry run: Would execute {tool_name}."
|
|
if diff_snippet:
|
|
full_message += f" Diff snippet:\n{diff_snippet}"
|
|
coder.io.tool_output(full_message) # Log the dry run action
|
|
return full_message
|
|
else:
|
|
# Use the provided success message, potentially adding change_id and diff
|
|
full_message = f"✅ {success_message}"
|
|
if change_id:
|
|
full_message += f" (change_id: {change_id})"
|
|
coder.io.tool_output(full_message) # Log the success action
|
|
|
|
result_for_llm = f"Successfully executed {tool_name}."
|
|
if change_id:
|
|
result_for_llm += f" Change ID: {change_id}."
|
|
if diff_snippet:
|
|
result_for_llm += f" Diff snippet:\n{diff_snippet}"
|
|
return result_for_llm
|
|
|
|
# Example usage within a hypothetical tool:
|
|
# try:
|
|
# abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
|
|
# # ... tool specific logic to determine new_content and metadata ...
|
|
# if dry_run:
|
|
# return format_tool_result(coder, "MyTool", "", dry_run=True, diff_snippet=diff)
|
|
#
|
|
# change_id = apply_change(coder, abs_path, rel_path, original_content, new_content, 'mytool', metadata)
|
|
# return format_tool_result(coder, "MyTool", f"Applied change to {file_path}", change_id=change_id, diff_snippet=diff)
|
|
# except ToolError as e:
|
|
# return handle_tool_error(coder, "MyTool", e, add_traceback=False) # Don't need traceback for ToolErrors
|
|
# except Exception as e:
|
|
# return handle_tool_error(coder, "MyTool", e)
|