From 504794f9a83709ddfefb31a4efc59f9f23a243c4 Mon Sep 17 00:00:00 2001 From: moosecrap Date: Thu, 23 Jul 2026 14:38:45 -0700 Subject: [PATCH] get_text_context --- tools/__init__.py | 16 ++++++++ tools/get_text_context.py | 86 +++++++++++++++++++++++++++++++++++++++ tools/preview_image.py | 28 +------------ tools/utils.py | 25 ++++++++++++ 4 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 tools/get_text_context.py diff --git a/tools/__init__.py b/tools/__init__.py index e032bde..b438055 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -21,6 +21,7 @@ from .read_metadata import handle as read_png_metadata_handler from .contact_sheet import handle as contact_sheet_handler from .list_directory import handle as list_directory_details_handler from .preview_image import handle as preview_image_handler +from .get_text_context import handle as get_text_context_handler # Central registry of all available tools TOOL_REGISTRY = [ @@ -87,4 +88,19 @@ TOOL_REGISTRY = [ }, handler=preview_image_handler ), + Tool( + name="get_text_context", + description="Extracts specific sections of a file with absolute line numbers and surrounding context. This tool MUST be called immediately before edit_file to verify the exact line numbers and content of the block being replaced, preventing misalignment errors.", + schema={ + "type": "object", + "properties": { + "path": {"type": "string", "description": "Path to the file"}, + "pattern": {"type": "string", "description": "Regex pattern to search for"}, + "lines": {"type": "string", "description": "1-based indices or ranges (e.g., '10-20, 45')"}, + "context": {"type": "integer", "description": "Number of lines of context to show above and below", "default": 1}, + }, + "required": ["path"], + }, + handler=get_text_context_handler + ), ] diff --git a/tools/get_text_context.py b/tools/get_text_context.py new file mode 100644 index 0000000..aa35776 --- /dev/null +++ b/tools/get_text_context.py @@ -0,0 +1,86 @@ +import re +import logging +from pathlib import Path +from typing import Any, Dict, List, Set +from tools.utils import ToolError, parse_indices + +logger = logging.getLogger("MattCP") + +async def handle(args: Dict[str, Any]): + """ + Extracts specific sections of a file with absolute line numbers and surrounding context. + This tool MUST be called immediately before edit_file to verify the exact line numbers + and content of the block being replaced, preventing misalignment errors. + """ + path_str = args.get("path") + pattern = args.get("pattern") + lines_str = args.get("lines") + context = int(args.get("context", 1)) + + if not path_str: + raise ToolError("Missing path argument") + + path = Path(path_str) + if not path.is_file(): + raise ToolError(f"{path_str} is not a file.") + + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + all_lines = f.readlines() + except Exception as e: + raise ToolError(f"Error reading file {path_str}: {e}") + + total_lines = len(all_lines) + + # Case 1: No pattern and no lines provided -> Dump whole file + if not pattern and not lines_str: + output = [f"{i+1}: {line}" for i, line in enumerate(all_lines)] + return {"text": "".join(output)} + + # Determine target line numbers (1-based) + target_lines: Set[int] = set() + + if pattern: + try: + regex = re.compile(pattern) + for i, line in enumerate(all_lines): + if regex.search(line): + target_lines.add(i + 1) + except re.error as e: + raise ToolError(f"Invalid regex pattern: {e}") + + if lines_str: + try: + indices = parse_indices(lines_str) + for idx in indices: + if 1 <= idx <= total_lines: + target_lines.add(idx) + except ToolError as e: + raise e + except Exception as e: + raise ToolError(f"Error parsing line indices: {e}") + + if not target_lines: + return {"text": "No matching lines found."} + + # Expand targets with context + lines_to_show: Set[int] = set() + for line in target_lines: + for offset in range(-context, context + 1): + ln = line + offset + if 1 <= ln <= total_lines: + lines_to_show.add(ln) + + # Sort and format output + sorted_lines = sorted(list(lines_to_show)) + output = [] + last_line = None + + for ln in sorted_lines: + if last_line is not None and ln > last_line + 1: + output.append("...\n") + + output.append(f"{ln}: {all_lines[ln-1]}") + last_line = ln + + return {"text": "".join(output)} diff --git a/tools/preview_image.py b/tools/preview_image.py index 7629ccc..ac62164 100644 --- a/tools/preview_image.py +++ b/tools/preview_image.py @@ -6,36 +6,10 @@ import datetime from pathlib import Path from typing import Any, Dict, List, Tuple from PIL import Image, ImageOps -from tools.utils import format_relative_time, ToolError, get_file_info_list, sort_file_list +from tools.utils import format_relative_time, ToolError, get_file_info_list, sort_file_list, parse_indices logger = logging.getLogger("MattCP") -def parse_indices(indices_str: str) -> List[int]: - """ - Parses a string like '11, 31-33, 44' into a list of 1-based indices. - Preserves the order specified in the string. - """ - indices = [] - parts = [p.strip() for p in indices_str.split(',')] - for part in parts: - if not part: - continue - if '-' in part: - try: - start, end = map(int, part.split('-')) - # Expand range in order. Handle reverse ranges as well. - step = 1 if start <= end else -1 - for i in range(start, end + step, step): - indices.append(i) - except ValueError: - raise ToolError(f"Invalid range format: {part}") - else: - try: - indices.append(int(part)) - except ValueError: - raise ToolError(f"Invalid index format: {part}") - return indices - def calculate_patch_dimensions(orig_w: int, orig_h: int, target_tokens: int = 70): """ Calculates optimal pixel dimensions to hit a token budget of at least target_tokens. diff --git a/tools/utils.py b/tools/utils.py index 46cf9ca..024a790 100644 --- a/tools/utils.py +++ b/tools/utils.py @@ -96,3 +96,28 @@ def get_paginated_list(items: List[Any], page: int, page_size: int) -> List[Any] start_idx = (page - 1) * page_size end_idx = start_idx + page_size return items[start_idx:end_idx] + +def parse_indices(indices_str: str) -> List[int]: + """ + Parses a string like '11, 31-33, 44' into a list of 1-based indices. + Preserves the order specified in the string. + """ + indices = [] + parts = [p.strip() for p in indices_str.split(',')] + for part in parts: + if not part: + continue + if '-' in part: + try: + start, end = map(int, part.split('-')) + step = 1 if start <= end else -1 + for i in range(start, end + step, step): + indices.append(i) + except ValueError: + raise ToolError(f"Invalid range format: {part}") + else: + try: + indices.append(int(part)) + except ValueError: + raise ToolError(f"Invalid index format: {part}") + return indices