ai_shell
Safe, token-aware filesystem tools for LLM agents.
ai_shell provides familiar shell-like tools (cat, ls, grep, find, head/tail,
cut, sed, git, ...) reimplemented in pure Python, jailed to a root folder and
tuned to return useful, token-bounded output. They are provider-agnostic: wire
them into any agent via the generated JSON Schemas and the neutral dispatch
table (ToolKit).
ai_shell
Safe, token-aware filesystem tools for LLM agents.
ai_shell is a library of familiar shell-like tools — cat, ls, grep,
find, head/tail, cut, sed, git, unified-diff patching, and a few
more — reimplemented in pure Python, jailed to a root folder, and tuned to
return useful, token-bounded output (with optional markdown variants). They
are provider-agnostic: wire them into any agent via the generated JSON Schemas
and a neutral dispatch table. Bring your own agent loop.
History: this started in 2023 as an OpenAI-Assistant shell, before Claude Code / aider / open-interpreter existed. That bot runtime has been removed; what remains is the part that was actually worth keeping — the safe tools.
Install
pip install ai-shell
Optional linters/formatters/test-runners used by the goal-checker helpers:
pip install "ai-shell[checkers]"
Use as a library
Each tool is a small class scoped to a root folder. Tools refuse to read or write outside that folder.
import ai_shell
config = ai_shell.Config()
cat = ai_shell.CatTool(".", config)
print(cat.cat_markdown(["pyproject.toml"]))
ls = ai_shell.LsTool(".", config)
print(ls.ls_markdown(path="."))
Use with any tool-calling model
ai_shell exposes JSON Schemas for the tools and a neutral dispatcher. Register
the schemas with your model, then route each tool call through
ToolKit.dispatch(name, arguments):
import ai_shell
from ai_shell.tools_registry import ALL_TOOLS, initialize_all_tools
# Pick the tools you want to expose.
tool_names = ["ls", "cat_markdown", "grep", "apply_git_patch"]
initialize_all_tools(keeps=tool_names)
toolkit = ai_shell.ToolKit(
root_folder=".", token_model="gpt-4o", global_max_lines=500,
permitted_tools=tool_names, config=ai_shell.Config(),
)
# `ALL_TOOLS` holds the JSON Schemas to hand to your model.
# When the model asks for a tool, dispatch it:
result_json = toolkit.dispatch("ls", {"path": "."})
dispatch enforces the per-session tool allowlist, tracks usage stats, applies
optional media-type conversion, and converts errors to RFC7807 JSON so a model can
read and recover from them.
CLI (sanity harness)
A generated CLI mirrors the tools — handy for checking a tool behaves before giving it to a model:
ais cat_markdown --file-paths pyproject.toml
ais grep --regex "def " --glob-pattern "ai_shell/*.py"
Tools
- Read:
ls,find,cat,grep,head/tail,cut,pycat(python-aware),count_tokens, and read-onlygit(status/diff/log/show/branch). - Edit:
apply_git_patch(unified diff — the primary edit path), plusreplace,insert,rewrite_file/write_new_filefor non-diff edits. - Tasking: a small TODO store (
ai_shell.todo) for splitting work into verifiable items — see ai_shell/todo/README.md.
Every file is read and written as UTF-8.
Project links
1""" 2Safe, token-aware filesystem tools for LLM agents. 3 4`ai_shell` provides familiar shell-like tools (cat, ls, grep, find, head/tail, 5cut, sed, git, ...) reimplemented in pure Python, jailed to a root folder and 6tuned to return useful, token-bounded output. They are provider-agnostic: wire 7them into any agent via the generated JSON Schemas and the neutral dispatch 8table (`ToolKit`). 9 10.. include:: ../README.md 11""" 12 13from ai_shell.ai_logs.logging_utils import configure_logging 14from ai_shell.answer_tool import AnswerCollectorTool 15from ai_shell.cat_tool import CatTool 16from ai_shell.cut_tool import CutTool 17from ai_shell.externals import pytest_call 18from ai_shell.externals.black_call import invoke_black 19from ai_shell.externals.pygount_call import count_lines_of_code 20from ai_shell.externals.pylint_call import invoke_pylint 21from ai_shell.find_tool import FindTool 22from ai_shell.git_tool import GitTool 23from ai_shell.grep_tool import GrepTool 24from ai_shell.head_tail_tool import HeadTailTool 25from ai_shell.insert_tool import InsertTool 26from ai_shell.ls_tool import LsTool 27from ai_shell.patch_tool import PatchTool 28from ai_shell.pycat_tool import PyCatTool 29from ai_shell.pytest_tool import PytestTool 30from ai_shell.replace_tool import ReplaceTool 31from ai_shell.rewrite_tool import RewriteTool 32from ai_shell.sed_tool import SedTool 33from ai_shell.todo_tool import TodoTool 34from ai_shell.token_tool import TokenCounterTool 35from ai_shell.toolkit import ToolKit 36from ai_shell.tools_registry import ALL_TOOLS, initialize_all_tools, initialize_recommended_tools 37from ai_shell.utils.config_manager import Config 38from ai_shell.utils.cwd_utils import change_directory 39 40__all__ = [ 41 # tools 42 "CatTool", 43 "CutTool", 44 "FindTool", 45 "GrepTool", 46 "HeadTailTool", 47 "LsTool", 48 "GitTool", 49 "TokenCounterTool", 50 "PatchTool", 51 "RewriteTool", 52 "PyCatTool", 53 "SedTool", 54 "ReplaceTool", 55 "InsertTool", 56 "TodoTool", 57 "AnswerCollectorTool", 58 "PytestTool", 59 # registry / dispatch 60 "ToolKit", 61 "ALL_TOOLS", 62 "initialize_all_tools", 63 "initialize_recommended_tools", 64 "Config", 65 # logging 66 "configure_logging", 67 # goal-checker helpers (optional) 68 "invoke_pylint", 69 "pytest_call", 70 "invoke_black", 71 "count_lines_of_code", 72 # misc 73 "change_directory", 74]
21class CatTool: 22 """ 23 Simulates `cat` cli tool. 24 """ 25 26 def __init__(self, root_folder: str, config: Config) -> None: 27 """ 28 Initialize the CatTool class. 29 30 Args: 31 root_folder (str): The root folder path for file operations. 32 config (Config): The developer input that bot shouldn't set. 33 """ 34 self.root_folder = root_folder 35 self.config = config 36 37 @log() 38 def cat_markdown( 39 self, 40 file_paths: list[str], 41 number_lines: bool = True, 42 squeeze_blank: bool = False, 43 ) -> str: 44 """ 45 Concatenates the content of given file paths and formats them as markdown. 46 47 Args: 48 file_paths (list[str]): List of file paths to concatenate. 49 number_lines (bool, optional): If True, number all output lines. 50 squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one. 51 52 Returns: 53 str: The concatenated and formatted content as a string. 54 """ 55 output = StringIO() 56 for line in self.cat(file_paths, number_lines, squeeze_blank): 57 output.write(line) 58 # output.write("\n") 59 output.seek(0) 60 return output.read() 61 62 @log() 63 def cat( 64 self, 65 file_paths: list[str], 66 number_lines: bool = True, 67 squeeze_blank: bool = False, 68 ) -> Generator[str, None, None]: 69 """ 70 Mimics the basic functionalities of the 'cat' command in Unix. 71 72 Args: 73 file_paths (list[str]): A list of file paths to concatenate. 74 number_lines (bool, optional): If True, number all output lines. 75 squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one. 76 77 Returns: 78 Generator[str, None, None] 79 80 Yields: 81 str: Each line of the concatenated files. 82 """ 83 file_paths = convert_to_list(file_paths) 84 for location, file_path in enumerate(file_paths): 85 if file_path.startswith("./"): 86 file_paths[location] = file_path[2:] 87 88 logger.info(f"cat --file_paths {file_paths} " f"--number_lines {number_lines} --squeeze_blank {squeeze_blank}") 89 for file_path in file_paths: 90 if not is_file_in_root_folder(file_path, self.root_folder): 91 raise TypeError("No parent folder traversals allowed") 92 93 line_number = 1 94 for glob_pattern in file_paths: 95 for file_path in safe_glob(glob_pattern, self.root_folder): 96 if not os.path.isabs(file_path): 97 file_path = self.root_folder + "/" + file_path 98 try: 99 with open(file_path, "rb") as file: 100 for line in self._process_cat_file(file, line_number, number_lines, squeeze_blank): 101 yield line 102 line_number += 1 103 except PermissionError: 104 logger.warning(f"Permission denied: {file_path}, suppressing from output.") 105 106 def _process_cat_file( 107 self, 108 file: IO[bytes], 109 line_number: int, 110 number_lines: bool, 111 squeeze_blank: bool, 112 ) -> Generator[str, None, None]: 113 """ 114 Processes a file for concatenation, applying the specified formatting. 115 116 Args: 117 file: The file object to process. 118 line_number (int): Current line number for numbering lines. 119 number_lines (bool): If True, number all output lines. 120 squeeze_blank (bool): If True, consecutive blank lines are squeezed to one. 121 122 Returns: 123 Generator[str, None, None]: A generator of processed lines. 124 125 Yields: 126 str: Each processed line of the file. 127 """ 128 was_blank = False 129 for byte_lines in file: 130 # if isinstance(byte_lines, bytes): 131 line = byte_lines.decode("utf-8") # Decode bytes to string 132 133 # Use StringIO for memory-efficient line processing 134 with StringIO() as line_buffer: 135 # Normalize line endings to \n 136 line = line.replace("\r\n", "\n") 137 line_buffer.write(line) 138 139 if squeeze_blank and was_blank and line.strip() == "": 140 continue # Skip consecutive blank lines 141 142 was_blank = line.strip() == "" 143 144 if number_lines: 145 line_buffer.seek(0) 146 line = f"{line_number}\t{line_buffer.read()}" 147 line_number += 1 148 else: 149 line = line_buffer.getvalue() 150 151 yield line
Simulates cat cli tool.
26 def __init__(self, root_folder: str, config: Config) -> None: 27 """ 28 Initialize the CatTool class. 29 30 Args: 31 root_folder (str): The root folder path for file operations. 32 config (Config): The developer input that bot shouldn't set. 33 """ 34 self.root_folder = root_folder 35 self.config = config
Initialize the CatTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
37 @log() 38 def cat_markdown( 39 self, 40 file_paths: list[str], 41 number_lines: bool = True, 42 squeeze_blank: bool = False, 43 ) -> str: 44 """ 45 Concatenates the content of given file paths and formats them as markdown. 46 47 Args: 48 file_paths (list[str]): List of file paths to concatenate. 49 number_lines (bool, optional): If True, number all output lines. 50 squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one. 51 52 Returns: 53 str: The concatenated and formatted content as a string. 54 """ 55 output = StringIO() 56 for line in self.cat(file_paths, number_lines, squeeze_blank): 57 output.write(line) 58 # output.write("\n") 59 output.seek(0) 60 return output.read()
Concatenates the content of given file paths and formats them as markdown.
Args: file_paths (list[str]): List of file paths to concatenate. number_lines (bool, optional): If True, number all output lines. squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.
Returns: str: The concatenated and formatted content as a string.
62 @log() 63 def cat( 64 self, 65 file_paths: list[str], 66 number_lines: bool = True, 67 squeeze_blank: bool = False, 68 ) -> Generator[str, None, None]: 69 """ 70 Mimics the basic functionalities of the 'cat' command in Unix. 71 72 Args: 73 file_paths (list[str]): A list of file paths to concatenate. 74 number_lines (bool, optional): If True, number all output lines. 75 squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one. 76 77 Returns: 78 Generator[str, None, None] 79 80 Yields: 81 str: Each line of the concatenated files. 82 """ 83 file_paths = convert_to_list(file_paths) 84 for location, file_path in enumerate(file_paths): 85 if file_path.startswith("./"): 86 file_paths[location] = file_path[2:] 87 88 logger.info(f"cat --file_paths {file_paths} " f"--number_lines {number_lines} --squeeze_blank {squeeze_blank}") 89 for file_path in file_paths: 90 if not is_file_in_root_folder(file_path, self.root_folder): 91 raise TypeError("No parent folder traversals allowed") 92 93 line_number = 1 94 for glob_pattern in file_paths: 95 for file_path in safe_glob(glob_pattern, self.root_folder): 96 if not os.path.isabs(file_path): 97 file_path = self.root_folder + "/" + file_path 98 try: 99 with open(file_path, "rb") as file: 100 for line in self._process_cat_file(file, line_number, number_lines, squeeze_blank): 101 yield line 102 line_number += 1 103 except PermissionError: 104 logger.warning(f"Permission denied: {file_path}, suppressing from output.")
Mimics the basic functionalities of the 'cat' command in Unix.
Args: file_paths (list[str]): A list of file paths to concatenate. number_lines (bool, optional): If True, number all output lines. squeeze_blank (bool, optional): If True, consecutive blank lines are squeezed to one.
Returns: Generator[str, None, None]
Yields: str: Each line of the concatenated files.
67@dataclasses.dataclass 68class CutTool: 69 """ 70 Simulates `cut` cli tool. 71 """ 72 73 def __init__(self, root_folder: str, config: Config) -> None: 74 """ 75 Initialize the CatTool class. 76 77 Args: 78 root_folder (str): The root folder path for file operations. 79 config (Config): The developer input that bot shouldn't set. 80 """ 81 self.root_folder = root_folder 82 self.config = config 83 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 84 85 @log() 86 def cut_characters(self, file_path: str, character_ranges: str) -> str: 87 """Reads a file and extracts characters based on specified ranges. 88 89 Args: 90 file_path: The name of the file to process. 91 character_ranges: A string representing character ranges, e.g., "1-5,10". 92 93 Returns: 94 A string containing the selected characters from the file. 95 """ 96 if not is_file_in_root_folder(file_path, self.root_folder): 97 raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.") 98 ranges = parse_ranges(character_ranges) 99 output = io.StringIO() 100 101 try: 102 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 103 for line in file: 104 for i, char in enumerate(line, start=1): 105 if is_in_ranges(i, ranges): 106 output.write(char) 107 108 # Optionally add a newline character after each line 109 output.write("\n") 110 except FileNotFoundError: 111 tree_text = tree(Path(os.getcwd())) 112 markdown_content = f"# File {file_path} not found. Here are all the files you can see\n\n{tree_text}" 113 return markdown_content 114 115 return output.getvalue() 116 117 @log() 118 def cut_fields(self, filename: str, field_ranges: str, delimiter: str = ",") -> str: 119 """Reads a file and extracts fields based on specified ranges using the given delimiter. 120 121 Args: 122 filename: The name of the file to process. 123 field_ranges: A string representing field ranges, e.g., "1-3,5". 124 delimiter: A single character used as the field delimiter. 125 126 Returns: 127 A string containing the selected fields from the file. 128 """ 129 if not is_file_in_root_folder(filename, self.root_folder): 130 raise ValueError(f"File {filename} is not in root folder {self.root_folder}.") 131 ranges = parse_ranges(field_ranges) 132 output = io.StringIO() 133 try: 134 with open(filename, encoding="utf-8", errors=self.utf8_errors) as file: 135 reader = csv.reader(file, delimiter=delimiter) 136 137 for row in reader: 138 selected_fields = [field for i, field in enumerate(row, start=1) if is_in_ranges(i, ranges)] 139 output.write(delimiter.join(selected_fields) + "\n") 140 except FileNotFoundError: 141 # Host app should always have cwd == root dir. 142 tree_text = tree(Path(os.getcwd())) 143 markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}" 144 return markdown_content 145 146 return output.getvalue() 147 148 @log() 149 def cut_fields_by_name(self, filename: str, field_names: list[str], delimiter: str = ",") -> str: 150 """Reads a file and extracts fields based on specified field names using the given delimiter. 151 152 Args: 153 filename(str): The name of the file to process. 154 field_names(list[str]): A list of field names to extract. 155 delimiter(str): A single character used as the field delimiter. 156 157 Returns: 158 A string containing the selected fields from the file. 159 """ 160 if not is_file_in_root_folder(filename, self.root_folder): 161 raise ValueError(f"File {filename} is not in root folder {self.root_folder}.") 162 output = io.StringIO() 163 164 try: 165 with open(filename, encoding="utf-8", errors=self.utf8_errors) as file: 166 reader = csv.DictReader(file, delimiter=delimiter) 167 # field_indices = {field: i for i, field in enumerate(reader.fieldnames)} 168 169 for row in reader: 170 selected_fields = [row[field] for field in field_names if field in row] 171 output.write(delimiter.join(selected_fields) + "\n") 172 except FileNotFoundError: 173 tree_text = tree(Path(os.getcwd())) 174 markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}" 175 return markdown_content 176 177 return output.getvalue()
Simulates cut cli tool.
73 def __init__(self, root_folder: str, config: Config) -> None: 74 """ 75 Initialize the CatTool class. 76 77 Args: 78 root_folder (str): The root folder path for file operations. 79 config (Config): The developer input that bot shouldn't set. 80 """ 81 self.root_folder = root_folder 82 self.config = config 83 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the CatTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
85 @log() 86 def cut_characters(self, file_path: str, character_ranges: str) -> str: 87 """Reads a file and extracts characters based on specified ranges. 88 89 Args: 90 file_path: The name of the file to process. 91 character_ranges: A string representing character ranges, e.g., "1-5,10". 92 93 Returns: 94 A string containing the selected characters from the file. 95 """ 96 if not is_file_in_root_folder(file_path, self.root_folder): 97 raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.") 98 ranges = parse_ranges(character_ranges) 99 output = io.StringIO() 100 101 try: 102 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 103 for line in file: 104 for i, char in enumerate(line, start=1): 105 if is_in_ranges(i, ranges): 106 output.write(char) 107 108 # Optionally add a newline character after each line 109 output.write("\n") 110 except FileNotFoundError: 111 tree_text = tree(Path(os.getcwd())) 112 markdown_content = f"# File {file_path} not found. Here are all the files you can see\n\n{tree_text}" 113 return markdown_content 114 115 return output.getvalue()
Reads a file and extracts characters based on specified ranges.
Args: file_path: The name of the file to process. character_ranges: A string representing character ranges, e.g., "1-5,10".
Returns: A string containing the selected characters from the file.
117 @log() 118 def cut_fields(self, filename: str, field_ranges: str, delimiter: str = ",") -> str: 119 """Reads a file and extracts fields based on specified ranges using the given delimiter. 120 121 Args: 122 filename: The name of the file to process. 123 field_ranges: A string representing field ranges, e.g., "1-3,5". 124 delimiter: A single character used as the field delimiter. 125 126 Returns: 127 A string containing the selected fields from the file. 128 """ 129 if not is_file_in_root_folder(filename, self.root_folder): 130 raise ValueError(f"File {filename} is not in root folder {self.root_folder}.") 131 ranges = parse_ranges(field_ranges) 132 output = io.StringIO() 133 try: 134 with open(filename, encoding="utf-8", errors=self.utf8_errors) as file: 135 reader = csv.reader(file, delimiter=delimiter) 136 137 for row in reader: 138 selected_fields = [field for i, field in enumerate(row, start=1) if is_in_ranges(i, ranges)] 139 output.write(delimiter.join(selected_fields) + "\n") 140 except FileNotFoundError: 141 # Host app should always have cwd == root dir. 142 tree_text = tree(Path(os.getcwd())) 143 markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}" 144 return markdown_content 145 146 return output.getvalue()
Reads a file and extracts fields based on specified ranges using the given delimiter.
Args: filename: The name of the file to process. field_ranges: A string representing field ranges, e.g., "1-3,5". delimiter: A single character used as the field delimiter.
Returns: A string containing the selected fields from the file.
148 @log() 149 def cut_fields_by_name(self, filename: str, field_names: list[str], delimiter: str = ",") -> str: 150 """Reads a file and extracts fields based on specified field names using the given delimiter. 151 152 Args: 153 filename(str): The name of the file to process. 154 field_names(list[str]): A list of field names to extract. 155 delimiter(str): A single character used as the field delimiter. 156 157 Returns: 158 A string containing the selected fields from the file. 159 """ 160 if not is_file_in_root_folder(filename, self.root_folder): 161 raise ValueError(f"File {filename} is not in root folder {self.root_folder}.") 162 output = io.StringIO() 163 164 try: 165 with open(filename, encoding="utf-8", errors=self.utf8_errors) as file: 166 reader = csv.DictReader(file, delimiter=delimiter) 167 # field_indices = {field: i for i, field in enumerate(reader.fieldnames)} 168 169 for row in reader: 170 selected_fields = [row[field] for field in field_names if field in row] 171 output.write(delimiter.join(selected_fields) + "\n") 172 except FileNotFoundError: 173 tree_text = tree(Path(os.getcwd())) 174 markdown_content = f"# File {filename} not found. Here are all the files you can see\n\n{tree_text}" 175 return markdown_content 176 177 return output.getvalue()
Reads a file and extracts fields based on specified field names using the given delimiter.
Args: filename(str): The name of the file to process. field_names(list[str]): A list of field names to extract. delimiter(str): A single character used as the field delimiter.
Returns: A string containing the selected fields from the file.
22class FindTool: 23 def __init__(self, root_folder: str, config: Config) -> None: 24 """ 25 Initialize the FindTool class. 26 27 Args: 28 root_folder (str): The root folder path for file operations. 29 config (Config): The developer input that bot shouldn't set. 30 """ 31 self.root_folder = root_folder 32 self.config = config 33 self.auto_cat = config.get_flag("auto_cat", True) 34 35 @log() 36 def find_files( 37 self, 38 name: str | None = None, 39 regex: str | None = None, 40 file_type: str | None = None, 41 size: str | None = None, 42 ) -> list[str]: 43 """ 44 Recursively search for files or directories matching given criteria in a directory and its subdirectories. 45 46 Args: 47 name (str | None, optional): The exact name to match filenames against. 48 regex (str | None, optional): The regex pattern to match filenames against. 49 file_type (str | None, optional): The type to filter ('file' or 'directory'). 50 size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes. 51 52 Returns: 53 list[str]: A list of paths to files or directories that match the criteria. 54 """ 55 logger.info(f"find --name {name} --regex {regex} --type {file_type} --size {size}") 56 matching_files = [] 57 for root, dirs, files in os.walk(os.getcwd()): 58 # Combine files and directories for type filtering 59 combined = files 60 if file_type == "directory": 61 combined += dirs 62 63 for entry in combined: 64 full_path = os.path.join(root, entry) 65 # TODO: handle this differently 66 if "__pycache__" not in full_path: 67 # TODO: handle differently. The bot 68 # is put into the root_folder as cwd, so as long as there isn't .. in path we should be good. 69 # if is_file_in_root_folder(full_path, self.root_folder): 70 short_path = remove_root_folder(full_path, self.root_folder) 71 # Check for name, regex, and size match 72 if (name and fnmatch.fnmatch(entry, name)) or name is None: 73 if self._match_type_and_size(full_path, file_type, size): 74 matching_files.append(short_path) 75 elif regex and re.search(regex, entry): 76 if self._match_type_and_size(full_path, file_type, size): 77 matching_files.append(short_path) 78 79 # Not the best way to remove hidden. 80 return list(sorted(_ for _ in matching_files if not _.startswith("."))) 81 82 def _match_type_and_size(self, path: str, file_type: str | None, size: str | None) -> bool: 83 """ 84 Check if a file/directory matches the specified type and size criteria. 85 86 Args: 87 path (str): The path to the file/directory. 88 file_type (Optional[str]): The type to filter ('file' or 'directory'). 89 size (Optional[str]): The size to filter files by. 90 91 Returns: 92 bool: True if the file/directory matches the criteria, False otherwise. 93 """ 94 if file_type: 95 if file_type == "file" and not os.path.isfile(path): 96 return False 97 if file_type == "directory" and not os.path.isdir(path): 98 return False 99 100 if size: 101 size_prefix = size[0] 102 size_value = int(size[1:]) 103 file_size = os.path.getsize(path) 104 105 if size_prefix == "+" and file_size <= size_value: 106 return False 107 if size_prefix == "-" and file_size >= size_value: 108 return False 109 return True 110 111 @log() 112 def find_files_markdown( 113 self, 114 name: str | None = None, 115 regex: str | None = None, 116 file_type: str | None = None, 117 size: str | None = None, 118 ) -> str: 119 """ 120 Recursively search for files or directories matching given criteria in a directory and its subdirectories. 121 122 Args: 123 name (str | None, optional): The exact name to match filenames against. 124 regex (str | None, optional): The regex pattern to match filenames against. 125 file_type (str | None, optional): The type to filter ('file' or 'directory'). 126 size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes. 127 128 Returns: 129 str: Markdown of paths to files or directories that match the criteria. 130 """ 131 output = StringIO() 132 results = self.find_files(name, regex, file_type, size) 133 for item in results: 134 output.write(item) 135 output.write("\n") 136 output.seek(0) 137 return output.read()
23 def __init__(self, root_folder: str, config: Config) -> None: 24 """ 25 Initialize the FindTool class. 26 27 Args: 28 root_folder (str): The root folder path for file operations. 29 config (Config): The developer input that bot shouldn't set. 30 """ 31 self.root_folder = root_folder 32 self.config = config 33 self.auto_cat = config.get_flag("auto_cat", True)
Initialize the FindTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
35 @log() 36 def find_files( 37 self, 38 name: str | None = None, 39 regex: str | None = None, 40 file_type: str | None = None, 41 size: str | None = None, 42 ) -> list[str]: 43 """ 44 Recursively search for files or directories matching given criteria in a directory and its subdirectories. 45 46 Args: 47 name (str | None, optional): The exact name to match filenames against. 48 regex (str | None, optional): The regex pattern to match filenames against. 49 file_type (str | None, optional): The type to filter ('file' or 'directory'). 50 size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes. 51 52 Returns: 53 list[str]: A list of paths to files or directories that match the criteria. 54 """ 55 logger.info(f"find --name {name} --regex {regex} --type {file_type} --size {size}") 56 matching_files = [] 57 for root, dirs, files in os.walk(os.getcwd()): 58 # Combine files and directories for type filtering 59 combined = files 60 if file_type == "directory": 61 combined += dirs 62 63 for entry in combined: 64 full_path = os.path.join(root, entry) 65 # TODO: handle this differently 66 if "__pycache__" not in full_path: 67 # TODO: handle differently. The bot 68 # is put into the root_folder as cwd, so as long as there isn't .. in path we should be good. 69 # if is_file_in_root_folder(full_path, self.root_folder): 70 short_path = remove_root_folder(full_path, self.root_folder) 71 # Check for name, regex, and size match 72 if (name and fnmatch.fnmatch(entry, name)) or name is None: 73 if self._match_type_and_size(full_path, file_type, size): 74 matching_files.append(short_path) 75 elif regex and re.search(regex, entry): 76 if self._match_type_and_size(full_path, file_type, size): 77 matching_files.append(short_path) 78 79 # Not the best way to remove hidden. 80 return list(sorted(_ for _ in matching_files if not _.startswith(".")))
Recursively search for files or directories matching given criteria in a directory and its subdirectories.
Args: name (str | None, optional): The exact name to match filenames against. regex (str | None, optional): The regex pattern to match filenames against. file_type (str | None, optional): The type to filter ('file' or 'directory'). size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.
Returns: list[str]: A list of paths to files or directories that match the criteria.
111 @log() 112 def find_files_markdown( 113 self, 114 name: str | None = None, 115 regex: str | None = None, 116 file_type: str | None = None, 117 size: str | None = None, 118 ) -> str: 119 """ 120 Recursively search for files or directories matching given criteria in a directory and its subdirectories. 121 122 Args: 123 name (str | None, optional): The exact name to match filenames against. 124 regex (str | None, optional): The regex pattern to match filenames against. 125 file_type (str | None, optional): The type to filter ('file' or 'directory'). 126 size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes. 127 128 Returns: 129 str: Markdown of paths to files or directories that match the criteria. 130 """ 131 output = StringIO() 132 results = self.find_files(name, regex, file_type, size) 133 for item in results: 134 output.write(item) 135 output.write("\n") 136 output.seek(0) 137 return output.read()
Recursively search for files or directories matching given criteria in a directory and its subdirectories.
Args: name (str | None, optional): The exact name to match filenames against. regex (str | None, optional): The regex pattern to match filenames against. file_type (str | None, optional): The type to filter ('file' or 'directory'). size (str | None, optional): The size to filter files by, e.g., '+100' for files larger than 100 bytes.
Returns: str: Markdown of paths to files or directories that match the criteria.
46class GrepTool: 47 """A tool for searching files using regular expressions.""" 48 49 def __init__(self, root_folder: str, config: Config) -> None: 50 """ 51 Initialize the GrepTool with a root folder. 52 53 Args: 54 root_folder (str): The root folder to search within. 55 config (Config): The developer input that bot shouldn't set. 56 """ 57 self.root_folder: str = root_folder 58 self.config = config 59 self.auto_cat = config.get_flag("auto_cat", True) 60 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 61 62 @log() 63 def grep_markdown( 64 self, regex: str, glob_pattern: str, skip_first_matches: int = -1, maximum_matches: int = -1 65 ) -> str: 66 """ 67 Search for lines matching a regular expression in files and returns markdown formatted results. 68 69 Args: 70 regex (str): A regular expression string to search for. 71 glob_pattern (str): A glob pattern string to specify files. 72 skip_first_matches (int, optional): Number of initial matches to skip. 73 maximum_matches (int, optional): Maximum number of matches to return. 74 75 Returns: 76 str: Markdown formatted string of grep results. 77 """ 78 results = self.grep(regex, glob_pattern, skip_first_matches, maximum_matches) 79 matches_found = results.matches_found 80 81 output = StringIO() 82 for file_match in results.data: 83 output.write(file_match.filename + "\n") 84 for match in file_match.found: 85 output.write(f"line {match.line_number}: {match.line}\n") 86 output.write( 87 f"{matches_found} matches found and {min(matches_found, maximum_matches) if maximum_matches != -1 else matches_found} displayed. " 88 f"Skipped {skip_first_matches}\n" 89 ) 90 output.seek(0) 91 return output.read() 92 93 @log() 94 def grep( 95 self, 96 regex: str, 97 glob_pattern: str, 98 skip_first_matches: int = -1, 99 maximum_matches_per_file: int = -1, 100 maximum_matches_total: int = -1, 101 ) -> GrepResults: 102 """ 103 Search for lines matching a regular expression in files specified by a glob pattern. 104 105 Args: 106 regex (str): A regular expression string to search for. 107 glob_pattern (str): A glob pattern string to specify files. 108 skip_first_matches (int, optional): Number of initial matches to skip. 109 maximum_matches_per_file (int, optional): Maximum number of matches to return for one file. 110 maximum_matches_total (int, optional): Maximum number of matches to return total. 111 112 Returns: 113 GrepResults: The results of the grep operation. 114 """ 115 logger.info( 116 f"grep --regex {regex} --glob_pattern {glob_pattern} " 117 f"--skip_first_matches {skip_first_matches} " 118 f"--maximum_matches_total {maximum_matches_total} " 119 f"--maximum_matches_per_file {maximum_matches_per_file}" 120 ) 121 pattern = re.compile(regex) 122 matches_total = 0 123 skip_count = 0 if skip_first_matches < 0 else skip_first_matches 124 125 results = GrepResults(matches_found=-1) 126 127 for filename in glob.glob(glob_pattern, root_dir=self.root_folder, recursive=True): 128 matches_per_file = 0 129 if os.path.isdir(filename): 130 logging.warning(f"Skipping directory {filename}, because it isn't a file.") 131 continue 132 if not os.path.exists(filename): 133 # What a hack 134 open_path = self.root_folder + "/" + filename 135 else: 136 open_path = filename 137 with open(open_path, encoding="utf-8", errors=self.utf8_errors) as file: 138 if not is_file_in_root_folder(filename, self.root_folder): 139 logging.warning(f"Skipping file {filename}, because it isn't in the root folder.") 140 continue 141 line_number = 0 142 for line in file: 143 below_maximum = matches_per_file < maximum_matches_per_file 144 maximum_not_set = maximum_matches_per_file == -1 145 if below_maximum or maximum_not_set: 146 line_number += 1 147 if pattern.search(line): 148 matches_total += 1 149 matches_per_file += 1 150 151 if matches_total <= (matches_total + skip_count) or matches_total == -1: 152 if (0 < skip_first_matches < matches_total) or skip_first_matches == -1: 153 # This creates names like \..\..\..\ etc. 154 minimal_filename = remove_root_folder(filename, self.root_folder) 155 # avoid double count 156 found = next((fm for fm in results.data if fm.filename == minimal_filename), None) 157 if not found: 158 found = FileMatches(filename=minimal_filename) 159 results.data.append(found) 160 161 found.found.append(Match(line_number=line_number, line=line.strip())) 162 results.data = list(sorted(results.data, key=lambda x: x.filename)) 163 results.matches_found = matches_total 164 return results
A tool for searching files using regular expressions.
49 def __init__(self, root_folder: str, config: Config) -> None: 50 """ 51 Initialize the GrepTool with a root folder. 52 53 Args: 54 root_folder (str): The root folder to search within. 55 config (Config): The developer input that bot shouldn't set. 56 """ 57 self.root_folder: str = root_folder 58 self.config = config 59 self.auto_cat = config.get_flag("auto_cat", True) 60 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the GrepTool with a root folder.
Args: root_folder (str): The root folder to search within. config (Config): The developer input that bot shouldn't set.
62 @log() 63 def grep_markdown( 64 self, regex: str, glob_pattern: str, skip_first_matches: int = -1, maximum_matches: int = -1 65 ) -> str: 66 """ 67 Search for lines matching a regular expression in files and returns markdown formatted results. 68 69 Args: 70 regex (str): A regular expression string to search for. 71 glob_pattern (str): A glob pattern string to specify files. 72 skip_first_matches (int, optional): Number of initial matches to skip. 73 maximum_matches (int, optional): Maximum number of matches to return. 74 75 Returns: 76 str: Markdown formatted string of grep results. 77 """ 78 results = self.grep(regex, glob_pattern, skip_first_matches, maximum_matches) 79 matches_found = results.matches_found 80 81 output = StringIO() 82 for file_match in results.data: 83 output.write(file_match.filename + "\n") 84 for match in file_match.found: 85 output.write(f"line {match.line_number}: {match.line}\n") 86 output.write( 87 f"{matches_found} matches found and {min(matches_found, maximum_matches) if maximum_matches != -1 else matches_found} displayed. " 88 f"Skipped {skip_first_matches}\n" 89 ) 90 output.seek(0) 91 return output.read()
Search for lines matching a regular expression in files and returns markdown formatted results.
Args: regex (str): A regular expression string to search for. glob_pattern (str): A glob pattern string to specify files. skip_first_matches (int, optional): Number of initial matches to skip. maximum_matches (int, optional): Maximum number of matches to return.
Returns: str: Markdown formatted string of grep results.
93 @log() 94 def grep( 95 self, 96 regex: str, 97 glob_pattern: str, 98 skip_first_matches: int = -1, 99 maximum_matches_per_file: int = -1, 100 maximum_matches_total: int = -1, 101 ) -> GrepResults: 102 """ 103 Search for lines matching a regular expression in files specified by a glob pattern. 104 105 Args: 106 regex (str): A regular expression string to search for. 107 glob_pattern (str): A glob pattern string to specify files. 108 skip_first_matches (int, optional): Number of initial matches to skip. 109 maximum_matches_per_file (int, optional): Maximum number of matches to return for one file. 110 maximum_matches_total (int, optional): Maximum number of matches to return total. 111 112 Returns: 113 GrepResults: The results of the grep operation. 114 """ 115 logger.info( 116 f"grep --regex {regex} --glob_pattern {glob_pattern} " 117 f"--skip_first_matches {skip_first_matches} " 118 f"--maximum_matches_total {maximum_matches_total} " 119 f"--maximum_matches_per_file {maximum_matches_per_file}" 120 ) 121 pattern = re.compile(regex) 122 matches_total = 0 123 skip_count = 0 if skip_first_matches < 0 else skip_first_matches 124 125 results = GrepResults(matches_found=-1) 126 127 for filename in glob.glob(glob_pattern, root_dir=self.root_folder, recursive=True): 128 matches_per_file = 0 129 if os.path.isdir(filename): 130 logging.warning(f"Skipping directory {filename}, because it isn't a file.") 131 continue 132 if not os.path.exists(filename): 133 # What a hack 134 open_path = self.root_folder + "/" + filename 135 else: 136 open_path = filename 137 with open(open_path, encoding="utf-8", errors=self.utf8_errors) as file: 138 if not is_file_in_root_folder(filename, self.root_folder): 139 logging.warning(f"Skipping file {filename}, because it isn't in the root folder.") 140 continue 141 line_number = 0 142 for line in file: 143 below_maximum = matches_per_file < maximum_matches_per_file 144 maximum_not_set = maximum_matches_per_file == -1 145 if below_maximum or maximum_not_set: 146 line_number += 1 147 if pattern.search(line): 148 matches_total += 1 149 matches_per_file += 1 150 151 if matches_total <= (matches_total + skip_count) or matches_total == -1: 152 if (0 < skip_first_matches < matches_total) or skip_first_matches == -1: 153 # This creates names like \..\..\..\ etc. 154 minimal_filename = remove_root_folder(filename, self.root_folder) 155 # avoid double count 156 found = next((fm for fm in results.data if fm.filename == minimal_filename), None) 157 if not found: 158 found = FileMatches(filename=minimal_filename) 159 results.data.append(found) 160 161 found.found.append(Match(line_number=line_number, line=line.strip())) 162 results.data = list(sorted(results.data, key=lambda x: x.filename)) 163 results.matches_found = matches_total 164 return results
Search for lines matching a regular expression in files specified by a glob pattern.
Args: regex (str): A regular expression string to search for. glob_pattern (str): A glob pattern string to specify files. skip_first_matches (int, optional): Number of initial matches to skip. maximum_matches_per_file (int, optional): Maximum number of matches to return for one file. maximum_matches_total (int, optional): Maximum number of matches to return total.
Returns: GrepResults: The results of the grep operation.
16class HeadTailTool: 17 def __init__(self, root_folder: str, config: Config) -> None: 18 """Initialize the HeadTailTool with a root folder. 19 20 Args: 21 root_folder (str): The root folder where files will be checked. 22 config (Config): The developer input that bot shouldn't set. 23 """ 24 self.root_folder = root_folder 25 self.config = config 26 self.auto_cat = config.get_flag("auto_cat", True) 27 28 @log() 29 def head_markdown(self, file_path: str, lines: int = 10) -> str: 30 """Return the first 'lines' lines of a file formatted as markdown. 31 32 Args: 33 file_path (str): Path to the file. 34 lines (int, optional): Number of lines to return. Defaults to 10. 35 36 Returns: 37 str: String containing the first 'lines' lines of the file. 38 """ 39 return "\n".join(self.head(file_path, lines)) 40 41 @log() 42 def head(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]: 43 """Return the first 'lines' or 'byte_count' from a file. 44 45 Args: 46 file_path (str): Path to the file. 47 lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. 48 byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines. 49 50 Returns: 51 list[str]: Lines or byte_count of bytes from the start of the file. 52 """ 53 return self.head_tail(file_path, lines, "head", byte_count) 54 55 @log() 56 def tail_markdown(self, file_path: str, lines: int = 10) -> str: 57 """Return the last 'lines' lines of a file formatted as markdown. 58 59 Args: 60 file_path (str): Path to the file. 61 lines (int, optional): Number of lines to return. Defaults to 10. 62 63 Returns: 64 str: String containing the last 'lines' lines of the file. 65 """ 66 return "\n".join(self.tail(file_path, lines)) 67 68 @log() 69 def tail(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]: 70 """Return the last 'lines' or 'bytes' from a file. 71 72 Args: 73 file_path (str): Path to the file. 74 lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. 75 byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines. 76 77 Returns: 78 list[str]: Lines or bytes from the end of the file. 79 """ 80 return self.head_tail(file_path, lines, "tail", byte_count) 81 82 def head_tail( 83 self, file_path: str, lines: int = 10, mode: str = "head", byte_count: int | None = None 84 ) -> list[str]: 85 """Read lines or bytes from the start ('head') or end ('tail') of a file. 86 87 Args: 88 file_path (str): Path to the file. 89 lines (int): Number of lines to read. Ignored if byte_count is specified. Defaults to 10. 90 mode (str): Operation mode, either 'head' or 'tail'. Defaults to 'head'. 91 byte_count (int | None, optional): Number of bytes to read. If specified, overrides lines. 92 93 Returns: 94 list[str]: Requested lines or bytes from the file. 95 96 Raises: 97 ValueError: If mode is not 'head' or 'tail'. 98 FileNotFoundError: If the file is not found in the root folder. 99 """ 100 if mode == "head": 101 logger.info(f"head --file_path {file_path} --lines {lines}") 102 else: 103 logger.info(f"tail --file_path {file_path} --lines {lines}") 104 if mode not in ["head", "tail"]: 105 raise ValueError("Mode must be 'head' or 'tail'") 106 107 if not is_file_in_root_folder(file_path, self.root_folder): 108 raise FileNotFoundError(f"File {file_path} not found in root folder {self.root_folder}") 109 110 with open(file_path, "rb") as file: 111 if byte_count is not None: 112 if mode == "head": 113 return [file.read(byte_count).decode()] 114 # mode == 'tail' 115 file.seek(-byte_count, 2) # Seek from end of file 116 return [file.read(byte_count).decode()] 117 118 # Read by lines if byte_count is not specified 119 if mode == "head": 120 head_lines = [] 121 for _ in range(lines): 122 try: 123 line = next(file).decode("utf-8") 124 head_lines.append(line.rstrip("\r\n")) 125 except StopIteration: 126 break 127 return head_lines 128 # return [next(file).decode("utf-8").rstrip("\r\n") for _ in range(lines)] 129 # mode == 'tail' 130 return [line.decode("utf-8").rstrip("\r\n") for line in list(file)[-lines:]]
17 def __init__(self, root_folder: str, config: Config) -> None: 18 """Initialize the HeadTailTool with a root folder. 19 20 Args: 21 root_folder (str): The root folder where files will be checked. 22 config (Config): The developer input that bot shouldn't set. 23 """ 24 self.root_folder = root_folder 25 self.config = config 26 self.auto_cat = config.get_flag("auto_cat", True)
Initialize the HeadTailTool with a root folder.
Args: root_folder (str): The root folder where files will be checked. config (Config): The developer input that bot shouldn't set.
28 @log() 29 def head_markdown(self, file_path: str, lines: int = 10) -> str: 30 """Return the first 'lines' lines of a file formatted as markdown. 31 32 Args: 33 file_path (str): Path to the file. 34 lines (int, optional): Number of lines to return. Defaults to 10. 35 36 Returns: 37 str: String containing the first 'lines' lines of the file. 38 """ 39 return "\n".join(self.head(file_path, lines))
Return the first 'lines' lines of a file formatted as markdown.
Args: file_path (str): Path to the file. lines (int, optional): Number of lines to return. Defaults to 10.
Returns: str: String containing the first 'lines' lines of the file.
41 @log() 42 def head(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]: 43 """Return the first 'lines' or 'byte_count' from a file. 44 45 Args: 46 file_path (str): Path to the file. 47 lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. 48 byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines. 49 50 Returns: 51 list[str]: Lines or byte_count of bytes from the start of the file. 52 """ 53 return self.head_tail(file_path, lines, "head", byte_count)
Return the first 'lines' or 'byte_count' from a file.
Args: file_path (str): Path to the file. lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.
Returns: list[str]: Lines or byte_count of bytes from the start of the file.
55 @log() 56 def tail_markdown(self, file_path: str, lines: int = 10) -> str: 57 """Return the last 'lines' lines of a file formatted as markdown. 58 59 Args: 60 file_path (str): Path to the file. 61 lines (int, optional): Number of lines to return. Defaults to 10. 62 63 Returns: 64 str: String containing the last 'lines' lines of the file. 65 """ 66 return "\n".join(self.tail(file_path, lines))
Return the last 'lines' lines of a file formatted as markdown.
Args: file_path (str): Path to the file. lines (int, optional): Number of lines to return. Defaults to 10.
Returns: str: String containing the last 'lines' lines of the file.
68 @log() 69 def tail(self, file_path: str, lines: int = 10, byte_count: int | None = None) -> list[str]: 70 """Return the last 'lines' or 'bytes' from a file. 71 72 Args: 73 file_path (str): Path to the file. 74 lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. 75 byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines. 76 77 Returns: 78 list[str]: Lines or bytes from the end of the file. 79 """ 80 return self.head_tail(file_path, lines, "tail", byte_count)
Return the last 'lines' or 'bytes' from a file.
Args: file_path (str): Path to the file. lines (int): Number of lines to return. Ignored if byte_count is specified. Defaults to 10. byte_count (int | None, optional): Number of bytes to return. If specified, overrides lines.
Returns: list[str]: Lines or bytes from the end of the file.
82 def head_tail( 83 self, file_path: str, lines: int = 10, mode: str = "head", byte_count: int | None = None 84 ) -> list[str]: 85 """Read lines or bytes from the start ('head') or end ('tail') of a file. 86 87 Args: 88 file_path (str): Path to the file. 89 lines (int): Number of lines to read. Ignored if byte_count is specified. Defaults to 10. 90 mode (str): Operation mode, either 'head' or 'tail'. Defaults to 'head'. 91 byte_count (int | None, optional): Number of bytes to read. If specified, overrides lines. 92 93 Returns: 94 list[str]: Requested lines or bytes from the file. 95 96 Raises: 97 ValueError: If mode is not 'head' or 'tail'. 98 FileNotFoundError: If the file is not found in the root folder. 99 """ 100 if mode == "head": 101 logger.info(f"head --file_path {file_path} --lines {lines}") 102 else: 103 logger.info(f"tail --file_path {file_path} --lines {lines}") 104 if mode not in ["head", "tail"]: 105 raise ValueError("Mode must be 'head' or 'tail'") 106 107 if not is_file_in_root_folder(file_path, self.root_folder): 108 raise FileNotFoundError(f"File {file_path} not found in root folder {self.root_folder}") 109 110 with open(file_path, "rb") as file: 111 if byte_count is not None: 112 if mode == "head": 113 return [file.read(byte_count).decode()] 114 # mode == 'tail' 115 file.seek(-byte_count, 2) # Seek from end of file 116 return [file.read(byte_count).decode()] 117 118 # Read by lines if byte_count is not specified 119 if mode == "head": 120 head_lines = [] 121 for _ in range(lines): 122 try: 123 line = next(file).decode("utf-8") 124 head_lines.append(line.rstrip("\r\n")) 125 except StopIteration: 126 break 127 return head_lines 128 # return [next(file).decode("utf-8").rstrip("\r\n") for _ in range(lines)] 129 # mode == 'tail' 130 return [line.decode("utf-8").rstrip("\r\n") for line in list(file)[-lines:]]
Read lines or bytes from the start ('head') or end ('tail') of a file.
Args: file_path (str): Path to the file. lines (int): Number of lines to read. Ignored if byte_count is specified. Defaults to 10. mode (str): Operation mode, either 'head' or 'tail'. Defaults to 'head'. byte_count (int | None, optional): Number of bytes to read. If specified, overrides lines.
Returns: list[str]: Requested lines or bytes from the file.
Raises: ValueError: If mode is not 'head' or 'tail'. FileNotFoundError: If the file is not found in the root folder.
21class LsTool: 22 def __init__(self, root_folder: str, config: Config) -> None: 23 """ 24 Initialize the FindTool class. 25 26 Args: 27 root_folder (str): The root folder path for file operations. 28 config (Config): The developer input that bot shouldn't set. 29 """ 30 self.root_folder = root_folder 31 self.config = config 32 self.auto_cat = config.get_flag("auto_cat", True) 33 34 @log() 35 def ls_markdown(self, path: str | None = ".", all_files: bool = False, long: bool = False) -> str: 36 """List directory contents, with options to include all files and detailed view. 37 38 Args: 39 path (str | None, optional): The directory path to list. Defaults to the current directory '.'. 40 all_files (bool, optional): If True, include hidden files. Defaults to False. 41 long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False. 42 43 Returns: 44 str: The markdown representation of the ls command output. 45 """ 46 try: 47 entries_info = self.ls(path, all_files, long) 48 except (FileNotFoundError, NotADirectoryError): 49 tree_text = tree(Path(os.getcwd())) 50 markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}" 51 return markdown_content 52 53 output = StringIO() 54 55 is_first = True 56 for line in entries_info: 57 if not is_first: 58 output.write("\n") 59 is_first = False 60 output.write(line) 61 62 output.seek(0) 63 return output.read() 64 65 @log() 66 def ls(self, path: str | None = None, all_files: bool = False, long: bool = False) -> Union[list[str], str]: 67 """ 68 List directory contents, with options to include all files and detailed view. 69 70 Args: 71 path (str | None, optional): The directory path to list. Defaults to the current directory '.'. 72 all_files (bool, optional): If True, include hidden files. Defaults to False. 73 long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False. 74 75 Returns: 76 list[str] | str: List of files and directories, optionally with details. 77 """ 78 logger.info(f"ls --path {path} --all_files {all_files} --long {long}") 79 80 if path is None: 81 path = "" 82 83 if path is not None and ("?" in path or "*" in path or "[" in path or "]" in path): 84 # Globs behave very different from non-globs. :( 85 # or "{" in path or "}" <-- is this a glob pattern? 86 entries = safe_glob(path, self.root_folder) 87 else: 88 try: 89 # enumerate list to check if the path exists 90 # os.listdir order is OS-dependent (arbitrary on Linux), so sort 91 # for deterministic output that matches real `ls` behavior. 92 entries = sorted( 93 (_ for _ in os.listdir(path)) 94 if all_files 95 else (entry for entry in os.listdir(path) if not entry.startswith(".")) 96 ) 97 except (FileNotFoundError, NotADirectoryError): 98 # if not, just tell the bot everything. 99 tree_text = tree(Path(os.getcwd())) 100 markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}" 101 return markdown_content 102 entries_info = [] 103 104 for entry in entries: 105 # is this None-safety here correct? 106 full_path = entry if path is None else os.path.join(path, entry) 107 if not is_file_in_root_folder(full_path, self.root_folder): 108 continue 109 if os.path.isdir(full_path) and entry.endswith("__pycache__"): 110 continue 111 if long: 112 stats = os.stat(full_path) 113 # Always human readable, too many tokens for byte count. 114 size = human_readable_size(stats.st_size) 115 mod_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(stats.st_mtime)) 116 entries_info.append(f"{size:} {mod_time} {entry}") 117 else: 118 entries_info.append(entry) 119 if logger.level == logging.DEBUG: 120 for line in entries_info: 121 logger.debug(line) 122 return entries_info
22 def __init__(self, root_folder: str, config: Config) -> None: 23 """ 24 Initialize the FindTool class. 25 26 Args: 27 root_folder (str): The root folder path for file operations. 28 config (Config): The developer input that bot shouldn't set. 29 """ 30 self.root_folder = root_folder 31 self.config = config 32 self.auto_cat = config.get_flag("auto_cat", True)
Initialize the FindTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
34 @log() 35 def ls_markdown(self, path: str | None = ".", all_files: bool = False, long: bool = False) -> str: 36 """List directory contents, with options to include all files and detailed view. 37 38 Args: 39 path (str | None, optional): The directory path to list. Defaults to the current directory '.'. 40 all_files (bool, optional): If True, include hidden files. Defaults to False. 41 long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False. 42 43 Returns: 44 str: The markdown representation of the ls command output. 45 """ 46 try: 47 entries_info = self.ls(path, all_files, long) 48 except (FileNotFoundError, NotADirectoryError): 49 tree_text = tree(Path(os.getcwd())) 50 markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}" 51 return markdown_content 52 53 output = StringIO() 54 55 is_first = True 56 for line in entries_info: 57 if not is_first: 58 output.write("\n") 59 is_first = False 60 output.write(line) 61 62 output.seek(0) 63 return output.read()
List directory contents, with options to include all files and detailed view.
Args: path (str | None, optional): The directory path to list. Defaults to the current directory '.'. all_files (bool, optional): If True, include hidden files. Defaults to False. long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.
Returns: str: The markdown representation of the ls command output.
65 @log() 66 def ls(self, path: str | None = None, all_files: bool = False, long: bool = False) -> Union[list[str], str]: 67 """ 68 List directory contents, with options to include all files and detailed view. 69 70 Args: 71 path (str | None, optional): The directory path to list. Defaults to the current directory '.'. 72 all_files (bool, optional): If True, include hidden files. Defaults to False. 73 long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False. 74 75 Returns: 76 list[str] | str: List of files and directories, optionally with details. 77 """ 78 logger.info(f"ls --path {path} --all_files {all_files} --long {long}") 79 80 if path is None: 81 path = "" 82 83 if path is not None and ("?" in path or "*" in path or "[" in path or "]" in path): 84 # Globs behave very different from non-globs. :( 85 # or "{" in path or "}" <-- is this a glob pattern? 86 entries = safe_glob(path, self.root_folder) 87 else: 88 try: 89 # enumerate list to check if the path exists 90 # os.listdir order is OS-dependent (arbitrary on Linux), so sort 91 # for deterministic output that matches real `ls` behavior. 92 entries = sorted( 93 (_ for _ in os.listdir(path)) 94 if all_files 95 else (entry for entry in os.listdir(path) if not entry.startswith(".")) 96 ) 97 except (FileNotFoundError, NotADirectoryError): 98 # if not, just tell the bot everything. 99 tree_text = tree(Path(os.getcwd())) 100 markdown_content = f"# Bad `ls` command. Here are all the files you can see\n\n{tree_text}" 101 return markdown_content 102 entries_info = [] 103 104 for entry in entries: 105 # is this None-safety here correct? 106 full_path = entry if path is None else os.path.join(path, entry) 107 if not is_file_in_root_folder(full_path, self.root_folder): 108 continue 109 if os.path.isdir(full_path) and entry.endswith("__pycache__"): 110 continue 111 if long: 112 stats = os.stat(full_path) 113 # Always human readable, too many tokens for byte count. 114 size = human_readable_size(stats.st_size) 115 mod_time = time.strftime("%Y-%m-%d %H:%M", time.localtime(stats.st_mtime)) 116 entries_info.append(f"{size:} {mod_time} {entry}") 117 else: 118 entries_info.append(entry) 119 if logger.level == logging.DEBUG: 120 for line in entries_info: 121 logger.debug(line) 122 return entries_info
List directory contents, with options to include all files and detailed view.
Args: path (str | None, optional): The directory path to list. Defaults to the current directory '.'. all_files (bool, optional): If True, include hidden files. Defaults to False. long (bool, optional): If True, include details like permissions, owner, size, and modification date. Defaults to False.
Returns: list[str] | str: List of files and directories, optionally with details.
23class GitTool: 24 def __init__(self, root_folder: str, config: Config) -> None: 25 """ 26 Initialize the GitTool class. 27 28 Args: 29 root_folder (str): The root folder path for repo operations. 30 config (Config): The developer input that bot shouldn't set. 31 """ 32 self.repo_path = root_folder 33 self.config = config 34 self.auto_cat = config.get_flag("auto_cat", True) 35 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 36 37 def _git(self, args: str) -> CommandResult: 38 """Run a git command against the repo and return its result. 39 40 Args: 41 args (str): The git subcommand and arguments (already shell-safe). 42 43 Returns: 44 CommandResult: stdout/stderr/return_code of the command. 45 """ 46 repo = shlex.quote(os.path.abspath(self.repo_path)) 47 result = safe_subprocess("git", f"-C {repo} {args}") 48 if result.return_code != 0: 49 logger.warning("git %s failed: %s", args, result.stderr) 50 return result 51 52 def is_ignored_by_gitignore(self, file_path: str, gitignore_path: str = ".gitignore") -> bool: 53 """ 54 Check if a file is ignored by .gitignore. 55 56 Args: 57 file_path (str): The path of the file to check. 58 gitignore_path (str): The path to the .gitignore file. Defaults to '.gitignore' in the current directory. 59 60 Returns: 61 bool: True if the file is ignored, False otherwise. 62 63 Raises: 64 FileNotFoundError: If the .gitignore file is not found. 65 """ 66 full_gitignore_path = os.path.join(self.repo_path, gitignore_path) 67 68 if not os.path.isfile(full_gitignore_path): 69 raise FileNotFoundError(f"No .gitignore file found at {full_gitignore_path}") 70 71 file_path = os.path.abspath(file_path) 72 73 with open(full_gitignore_path, encoding="utf-8", errors=self.utf8_errors) as gitignore: 74 for line in gitignore: 75 line = line.strip() 76 if not line or line.startswith("#"): 77 continue 78 gitignore_pattern = os.path.join(os.path.dirname(gitignore_path), line) 79 if fnmatch.fnmatch(file_path, gitignore_pattern): 80 return True 81 return False 82 83 @log() 84 def git_status(self) -> dict[str, Any]: 85 """Returns the status of the repository. 86 87 Returns: 88 dict[str, Any]: Structured `git status` response 89 """ 90 result = self._git("status --porcelain") 91 changed_files: list[str] = [] 92 untracked_files: list[str] = [] 93 for line in result.stdout.splitlines(): 94 if not line: 95 continue 96 # porcelain format: 2-char status code, a space, then the path 97 path = line[3:].strip() 98 if line.startswith("??"): 99 untracked_files.append(path) 100 else: 101 changed_files.append(path) 102 return {"changed_files": changed_files, "untracked_files": untracked_files} 103 104 @log() 105 def get_current_branch(self) -> str: 106 """ 107 Retrieves the current branch name of the repository. 108 109 Returns: 110 str: The current branch name. 111 """ 112 return self._git("branch --show-current").stdout.strip() 113 114 @log() 115 def get_recent_commits(self, n: int = 10, short_hash: bool = False) -> list[dict[str, Any]]: 116 """ 117 Retrieves the most recent commit hashes from the current branch. 118 119 Args: 120 n (int, optional): The number of recent commits to retrieve. Defaults to 10. 121 short_hash (bool, optional): If True, also return short hashes. Defaults to False. 122 123 Returns: 124 list[dict[str, Any]]: One dict per commit with a 'full_hash' (and 125 'short_hash' when requested). 126 """ 127 result = self._git(f"log --pretty=format:%H -n {int(n)}") 128 hashes = [h for h in result.stdout.splitlines() if h] 129 if short_hash: 130 return [{"short_hash": h[:7], "full_hash": h} for h in hashes] 131 return [{"full_hash": h} for h in hashes] 132 133 @log() 134 def git_diff(self) -> list[dict[str, Any]]: 135 """Returns the differences in the working directory. 136 137 Returns: 138 list[dict[str, Any]]: Structured `git diff` response 139 """ 140 diffs = self._git("diff HEAD --name-only").stdout.splitlines() 141 return [{"file": diff} for diff in diffs if diff] 142 143 @log() 144 def git_log_file(self, filename: str) -> list[dict[str, Any]]: 145 """Returns the commit history for a specific file. 146 147 Args: 148 filename (str): The path to the file. 149 150 Returns: 151 list[dict[str, Any]]: Structured `git log` response 152 """ 153 result = self._git(f"log --pretty=format:%H - %an, %ar : %s -- {shlex.quote(filename)}") 154 commits = [c for c in result.stdout.splitlines() if c] 155 return [{"commit": commit} for commit in commits] 156 157 @log() 158 def git_log_search(self, search_string: str) -> list[dict[str, Any]]: 159 """Returns the commit history that matches the search string. 160 161 Args: 162 search_string (str): The search string. 163 164 Returns: 165 list of dict: Structured `git log` response 166 """ 167 result = self._git(f"log -S {shlex.quote(search_string)} --pretty=format:%H - %an, %ar : %s") 168 commits = [c for c in result.stdout.splitlines() if c] 169 return [{"commit": commit} for commit in commits] 170 171 @log() 172 def git_show(self) -> list[dict[str, Any]]: 173 """Shows various types of objects (commits, tags, etc.). 174 175 Returns: 176 list[dict[str, Any]]: Structured `git show` response 177 """ 178 result = self._git("show --no-patch --pretty=format:%H - %an, %ar : %s") 179 show_data = [d for d in result.stdout.splitlines() if d] 180 return [{"data": data} for data in show_data] 181 182 @log() 183 def git_diff_commit(self, commit1: str, commit2: str) -> list[dict[str, Any]]: 184 """Shows changes between two commits. 185 186 Args: 187 commit1 (str): First commit 188 commit2 (str): Second commit 189 190 Returns: 191 list[dict[str, Any]]: Structured `git diff` response 192 """ 193 result = self._git(f"diff {shlex.quote(commit1)} {shlex.quote(commit2)} --name-only") 194 diffs = [d for d in result.stdout.splitlines() if d] 195 return [{"file": diff} for diff in diffs]
24 def __init__(self, root_folder: str, config: Config) -> None: 25 """ 26 Initialize the GitTool class. 27 28 Args: 29 root_folder (str): The root folder path for repo operations. 30 config (Config): The developer input that bot shouldn't set. 31 """ 32 self.repo_path = root_folder 33 self.config = config 34 self.auto_cat = config.get_flag("auto_cat", True) 35 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the GitTool class.
Args: root_folder (str): The root folder path for repo operations. config (Config): The developer input that bot shouldn't set.
52 def is_ignored_by_gitignore(self, file_path: str, gitignore_path: str = ".gitignore") -> bool: 53 """ 54 Check if a file is ignored by .gitignore. 55 56 Args: 57 file_path (str): The path of the file to check. 58 gitignore_path (str): The path to the .gitignore file. Defaults to '.gitignore' in the current directory. 59 60 Returns: 61 bool: True if the file is ignored, False otherwise. 62 63 Raises: 64 FileNotFoundError: If the .gitignore file is not found. 65 """ 66 full_gitignore_path = os.path.join(self.repo_path, gitignore_path) 67 68 if not os.path.isfile(full_gitignore_path): 69 raise FileNotFoundError(f"No .gitignore file found at {full_gitignore_path}") 70 71 file_path = os.path.abspath(file_path) 72 73 with open(full_gitignore_path, encoding="utf-8", errors=self.utf8_errors) as gitignore: 74 for line in gitignore: 75 line = line.strip() 76 if not line or line.startswith("#"): 77 continue 78 gitignore_pattern = os.path.join(os.path.dirname(gitignore_path), line) 79 if fnmatch.fnmatch(file_path, gitignore_pattern): 80 return True 81 return False
Check if a file is ignored by .gitignore.
Args: file_path (str): The path of the file to check. gitignore_path (str): The path to the .gitignore file. Defaults to '.gitignore' in the current directory.
Returns: bool: True if the file is ignored, False otherwise.
Raises: FileNotFoundError: If the .gitignore file is not found.
83 @log() 84 def git_status(self) -> dict[str, Any]: 85 """Returns the status of the repository. 86 87 Returns: 88 dict[str, Any]: Structured `git status` response 89 """ 90 result = self._git("status --porcelain") 91 changed_files: list[str] = [] 92 untracked_files: list[str] = [] 93 for line in result.stdout.splitlines(): 94 if not line: 95 continue 96 # porcelain format: 2-char status code, a space, then the path 97 path = line[3:].strip() 98 if line.startswith("??"): 99 untracked_files.append(path) 100 else: 101 changed_files.append(path) 102 return {"changed_files": changed_files, "untracked_files": untracked_files}
Returns the status of the repository.
Returns:
dict[str, Any]: Structured git status response
104 @log() 105 def get_current_branch(self) -> str: 106 """ 107 Retrieves the current branch name of the repository. 108 109 Returns: 110 str: The current branch name. 111 """ 112 return self._git("branch --show-current").stdout.strip()
Retrieves the current branch name of the repository.
Returns: str: The current branch name.
114 @log() 115 def get_recent_commits(self, n: int = 10, short_hash: bool = False) -> list[dict[str, Any]]: 116 """ 117 Retrieves the most recent commit hashes from the current branch. 118 119 Args: 120 n (int, optional): The number of recent commits to retrieve. Defaults to 10. 121 short_hash (bool, optional): If True, also return short hashes. Defaults to False. 122 123 Returns: 124 list[dict[str, Any]]: One dict per commit with a 'full_hash' (and 125 'short_hash' when requested). 126 """ 127 result = self._git(f"log --pretty=format:%H -n {int(n)}") 128 hashes = [h for h in result.stdout.splitlines() if h] 129 if short_hash: 130 return [{"short_hash": h[:7], "full_hash": h} for h in hashes] 131 return [{"full_hash": h} for h in hashes]
Retrieves the most recent commit hashes from the current branch.
Args: n (int, optional): The number of recent commits to retrieve. Defaults to 10. short_hash (bool, optional): If True, also return short hashes. Defaults to False.
Returns: list[dict[str, Any]]: One dict per commit with a 'full_hash' (and 'short_hash' when requested).
133 @log() 134 def git_diff(self) -> list[dict[str, Any]]: 135 """Returns the differences in the working directory. 136 137 Returns: 138 list[dict[str, Any]]: Structured `git diff` response 139 """ 140 diffs = self._git("diff HEAD --name-only").stdout.splitlines() 141 return [{"file": diff} for diff in diffs if diff]
Returns the differences in the working directory.
Returns:
list[dict[str, Any]]: Structured git diff response
143 @log() 144 def git_log_file(self, filename: str) -> list[dict[str, Any]]: 145 """Returns the commit history for a specific file. 146 147 Args: 148 filename (str): The path to the file. 149 150 Returns: 151 list[dict[str, Any]]: Structured `git log` response 152 """ 153 result = self._git(f"log --pretty=format:%H - %an, %ar : %s -- {shlex.quote(filename)}") 154 commits = [c for c in result.stdout.splitlines() if c] 155 return [{"commit": commit} for commit in commits]
Returns the commit history for a specific file.
Args: filename (str): The path to the file.
Returns:
list[dict[str, Any]]: Structured git log response
157 @log() 158 def git_log_search(self, search_string: str) -> list[dict[str, Any]]: 159 """Returns the commit history that matches the search string. 160 161 Args: 162 search_string (str): The search string. 163 164 Returns: 165 list of dict: Structured `git log` response 166 """ 167 result = self._git(f"log -S {shlex.quote(search_string)} --pretty=format:%H - %an, %ar : %s") 168 commits = [c for c in result.stdout.splitlines() if c] 169 return [{"commit": commit} for commit in commits]
Returns the commit history that matches the search string.
Args: search_string (str): The search string.
Returns:
list of dict: Structured git log response
171 @log() 172 def git_show(self) -> list[dict[str, Any]]: 173 """Shows various types of objects (commits, tags, etc.). 174 175 Returns: 176 list[dict[str, Any]]: Structured `git show` response 177 """ 178 result = self._git("show --no-patch --pretty=format:%H - %an, %ar : %s") 179 show_data = [d for d in result.stdout.splitlines() if d] 180 return [{"data": data} for data in show_data]
Shows various types of objects (commits, tags, etc.).
Returns:
list[dict[str, Any]]: Structured git show response
182 @log() 183 def git_diff_commit(self, commit1: str, commit2: str) -> list[dict[str, Any]]: 184 """Shows changes between two commits. 185 186 Args: 187 commit1 (str): First commit 188 commit2 (str): Second commit 189 190 Returns: 191 list[dict[str, Any]]: Structured `git diff` response 192 """ 193 result = self._git(f"diff {shlex.quote(commit1)} {shlex.quote(commit2)} --name-only") 194 diffs = [d for d in result.stdout.splitlines() if d] 195 return [{"file": diff} for diff in diffs]
Shows changes between two commits.
Args: commit1 (str): First commit commit2 (str): Second commit
Returns:
list[dict[str, Any]]: Structured git diff response
13class TokenCounterTool: 14 """Count the number of tokens in a string.""" 15 16 def __init__(self, root_folder: str, config: Config) -> None: 17 """ 18 Initialize the FindTool class. 19 20 Args: 21 root_folder (str): The root folder path for file operations. 22 config (Config): The developer input that bot shouldn't set. 23 """ 24 self.root_folder = root_folder 25 self.config = config 26 model = config.get_value("token_model") 27 if not model: 28 raise ValueError("token_model must be set in the config") 29 self.token_model = model 30 31 def count_tokens(self, text: str) -> int: 32 """Count the number of tokens in a string. 33 34 Args: 35 text (str): The text to count the tokens in. 36 37 Returns: 38 int: The number of tokens. 39 """ 40 if not text: 41 return 0 42 # gpt3 turbo - cl100k_base 43 # gpt2 (or r50k_base) Most GPT-3 models 44 # p50k_base Code models, text-davinci-002, text-davinci-003 45 # cl100k_base text-embedding-ada-002 46 # enc = tiktoken.get_encoding("cl100k_base") 47 48 encoding = tiktoken.encoding_for_model(self.token_model) 49 tokens = encoding.encode(text) 50 token_count = len(tokens) 51 return token_count
Count the number of tokens in a string.
16 def __init__(self, root_folder: str, config: Config) -> None: 17 """ 18 Initialize the FindTool class. 19 20 Args: 21 root_folder (str): The root folder path for file operations. 22 config (Config): The developer input that bot shouldn't set. 23 """ 24 self.root_folder = root_folder 25 self.config = config 26 model = config.get_value("token_model") 27 if not model: 28 raise ValueError("token_model must be set in the config") 29 self.token_model = model
Initialize the FindTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
31 def count_tokens(self, text: str) -> int: 32 """Count the number of tokens in a string. 33 34 Args: 35 text (str): The text to count the tokens in. 36 37 Returns: 38 int: The number of tokens. 39 """ 40 if not text: 41 return 0 42 # gpt3 turbo - cl100k_base 43 # gpt2 (or r50k_base) Most GPT-3 models 44 # p50k_base Code models, text-davinci-002, text-davinci-003 45 # cl100k_base text-embedding-ada-002 46 # enc = tiktoken.get_encoding("cl100k_base") 47 48 encoding = tiktoken.encoding_for_model(self.token_model) 49 tokens = encoding.encode(text) 50 token_count = len(tokens) 51 return token_count
Count the number of tokens in a string.
Args: text (str): The text to count the tokens in.
Returns: int: The number of tokens.
24class PatchTool: 25 """Edit files by applying a unified (git) diff.""" 26 27 def __init__(self, root_folder: str, config: Config) -> None: 28 """ 29 Initialize the PatchTool with a root folder. 30 31 Args: 32 root_folder (str): The root folder for valid patchable files. 33 config (Config): The developer input that bot shouldn't set. 34 """ 35 self.root_folder: str = root_folder 36 self.config = config 37 self.auto_cat = config.get_flag("auto_cat", True) 38 39 @log() 40 def apply_git_patch(self, patch_content: str) -> str: 41 """ 42 Apply a unified (git) diff to the files in the root folder. 43 44 Args: 45 patch_content (str): The content of the unified/git diff. 46 47 Returns: 48 str: A message indicating the patch applied, plus the patched file 49 contents when auto_cat is enabled. 50 51 Raises: 52 ValueError: If the patch targets a file outside the root folder. 53 RuntimeError: If the patch application fails. 54 """ 55 target_files = self._extract_files_from_patch(patch_content) 56 if not target_files: 57 raise ValueError("No target files found in patch. Is this a valid unified diff?") 58 for file_name in target_files: 59 if not is_file_in_root_folder(file_name, self.root_folder): 60 raise ValueError(f"Patch targets '{file_name}' outside the root folder, which is not allowed.") 61 62 # Write the patch to a temp file and let `git apply` do the work; it is 63 # more forgiving of context offsets than an in-process strict apply. 64 with tempfile.NamedTemporaryFile(suffix=".patch", delete=False) as tmp_patch: 65 tmp_patch_name = tmp_patch.name 66 tmp_patch.write(patch_content.encode("utf-8")) 67 tmp_patch.flush() 68 69 cmd = ["git", "apply", tmp_patch_name, "--reject", "--verbose"] 70 try: 71 result = subprocess.run(cmd, capture_output=True, text=True, check=True, shell=False) # nosec 72 logger.info("STDOUT:\n%s", result.stdout) 73 logger.info("STDERR:\n%s", result.stderr) 74 if result.returncode != 0: 75 raise RuntimeError(f"Failed to apply patch: {result.stderr}") 76 except subprocess.CalledProcessError as cpe: 77 raise RuntimeError(f"Failed to apply patch: {cpe.stderr or cpe.stdout}") from cpe 78 finally: 79 try: 80 os.remove(tmp_patch_name) 81 except OSError: 82 pass 83 84 if self.auto_cat: 85 existing = [f for f in sorted(target_files) if os.path.exists(f)] 86 if existing: 87 contents = CatTool(self.root_folder, self.config).cat_markdown(existing) 88 return ( 89 "Patch applied without exception, please verify the contents below are what you intended.\n\n" 90 f"{contents}" 91 ) 92 return "Patch applied without exception, please verify by other means to see if it was successful." 93 94 def _extract_files_from_patch(self, patch_content: str) -> set[str]: 95 """ 96 Extract target file names from the patch content. 97 98 Args: 99 patch_content (str): The content of the unified/git diff. 100 101 Returns: 102 set[str]: The set of file names the patch touches. 103 """ 104 file_names = set() 105 for line in patch_content.split("\n"): 106 if line.startswith("diff --git "): 107 # `diff --git a/path b/path` 108 for token in line.split()[2:]: 109 file_names.add(self._strip_ab_prefix(token)) 110 elif line.startswith("--- ") or line.startswith("+++ "): 111 parts = line.split() 112 if len(parts) > 1 and parts[1] != "/dev/null": 113 file_names.add(self._strip_ab_prefix(parts[1])) 114 return file_names 115 116 @staticmethod 117 def _strip_ab_prefix(file_name: str) -> str: 118 """Strip a leading ``a/`` or ``b/`` from a diff path.""" 119 if file_name.startswith(("a/", "b/")): 120 return file_name[2:] 121 return file_name
Edit files by applying a unified (git) diff.
27 def __init__(self, root_folder: str, config: Config) -> None: 28 """ 29 Initialize the PatchTool with a root folder. 30 31 Args: 32 root_folder (str): The root folder for valid patchable files. 33 config (Config): The developer input that bot shouldn't set. 34 """ 35 self.root_folder: str = root_folder 36 self.config = config 37 self.auto_cat = config.get_flag("auto_cat", True)
Initialize the PatchTool with a root folder.
Args: root_folder (str): The root folder for valid patchable files. config (Config): The developer input that bot shouldn't set.
39 @log() 40 def apply_git_patch(self, patch_content: str) -> str: 41 """ 42 Apply a unified (git) diff to the files in the root folder. 43 44 Args: 45 patch_content (str): The content of the unified/git diff. 46 47 Returns: 48 str: A message indicating the patch applied, plus the patched file 49 contents when auto_cat is enabled. 50 51 Raises: 52 ValueError: If the patch targets a file outside the root folder. 53 RuntimeError: If the patch application fails. 54 """ 55 target_files = self._extract_files_from_patch(patch_content) 56 if not target_files: 57 raise ValueError("No target files found in patch. Is this a valid unified diff?") 58 for file_name in target_files: 59 if not is_file_in_root_folder(file_name, self.root_folder): 60 raise ValueError(f"Patch targets '{file_name}' outside the root folder, which is not allowed.") 61 62 # Write the patch to a temp file and let `git apply` do the work; it is 63 # more forgiving of context offsets than an in-process strict apply. 64 with tempfile.NamedTemporaryFile(suffix=".patch", delete=False) as tmp_patch: 65 tmp_patch_name = tmp_patch.name 66 tmp_patch.write(patch_content.encode("utf-8")) 67 tmp_patch.flush() 68 69 cmd = ["git", "apply", tmp_patch_name, "--reject", "--verbose"] 70 try: 71 result = subprocess.run(cmd, capture_output=True, text=True, check=True, shell=False) # nosec 72 logger.info("STDOUT:\n%s", result.stdout) 73 logger.info("STDERR:\n%s", result.stderr) 74 if result.returncode != 0: 75 raise RuntimeError(f"Failed to apply patch: {result.stderr}") 76 except subprocess.CalledProcessError as cpe: 77 raise RuntimeError(f"Failed to apply patch: {cpe.stderr or cpe.stdout}") from cpe 78 finally: 79 try: 80 os.remove(tmp_patch_name) 81 except OSError: 82 pass 83 84 if self.auto_cat: 85 existing = [f for f in sorted(target_files) if os.path.exists(f)] 86 if existing: 87 contents = CatTool(self.root_folder, self.config).cat_markdown(existing) 88 return ( 89 "Patch applied without exception, please verify the contents below are what you intended.\n\n" 90 f"{contents}" 91 ) 92 return "Patch applied without exception, please verify by other means to see if it was successful."
Apply a unified (git) diff to the files in the root folder.
Args: patch_content (str): The content of the unified/git diff.
Returns: str: A message indicating the patch applied, plus the patched file contents when auto_cat is enabled.
Raises: ValueError: If the patch targets a file outside the root folder. RuntimeError: If the patch application fails.
68class RewriteTool: 69 def __init__(self, root_folder: str, config: Config) -> None: 70 """ 71 Initialize the RewriteTool class. 72 73 Args: 74 root_folder (str): The root folder path for file operations. 75 config (Config): The developer input that bot shouldn't set. 76 """ 77 self.root_folder = root_folder 78 self.config = config 79 self.auto_cat = config.get_flag("auto_cat", True) 80 self.python_module = config.get_value("python_module") 81 self.only_add_text = config.get_flag("only_add_text", False) 82 83 @log() 84 def write_new_file(self, file_path: str, text: str) -> str: 85 """ 86 Write a new file at file_path within the root_folder. 87 88 Args: 89 file_path (str): The relative path to the file to be written. 90 text (str): The content to write into the file. 91 92 Returns: 93 str: A success message with the file path. 94 95 Raises: 96 ValueError: If the file already exists or if the file_path is outside the root_folder. 97 """ 98 file_path = sanitize_path(file_path) 99 # Don't prepend root folder, we will have already cd'd to it. 100 full_path = file_path 101 if not is_file_in_root_folder(full_path, self.root_folder): 102 raise ValueError("File path must be within the root folder.") 103 104 try: 105 if os.path.exists(full_path): 106 raise FileExistsError("File already exists.") 107 108 with open(full_path, "w", encoding="utf-8") as file: 109 file.write(text) 110 111 validation = self._validate_code(full_path) 112 113 if validation: 114 os.remove(full_path) 115 return f"File not written because of problems.\n{validation.message}" 116 117 return f"File written to {full_path}" 118 except FileExistsError as e: 119 tree_text = tree(Path(os.getcwd())) 120 markdown_content = f"# File {full_path} already exists. Here are all the files you can see\n\n{tree_text}" 121 raise ValueError( 122 str(e) + f" {markdown_content}\n Consider using rewrite_file method if you want to overwrite." 123 ) from e 124 125 @log() 126 def rewrite_file(self, file_path: str, text: str) -> str: 127 """ 128 Backup and rewrite an existing file at file_path within the root_folder. 129 This will completely replace the contents of the file with the new text. 130 131 Args: 132 file_path (str): The relative path to the file to be rewritten. 133 text (str): The new content to write into the file. 134 135 Returns: 136 str: A success message with the file path. 137 138 Raises: 139 ValueError: If the file does not exist or if the file_path is outside the root_folder. 140 """ 141 if not text: 142 raise TypeError("This would delete everything in the file. This is probably not what you want.") 143 144 file_path = sanitize_path(file_path) 145 146 # Don't prepend root folder, we will have already cd'd to it. 147 full_path = file_path 148 if not is_file_in_root_folder(full_path, self.root_folder): 149 raise ValueError("File path must be within the root folder.") 150 151 if not os.path.exists(full_path): 152 raise FileNotFoundError("File does not exist, use ls tool to see what files there are.") 153 154 _unchanged_proportion, initial, _unchanged, _added, removed = file_similarity(full_path, text.split("\n")) 155 if self.only_add_text and removed > 0: 156 raise TypeError("This would delete lines. Only add lines, do not remove them.") 157 if self.only_add_text and len(text.split("\n")) < initial: 158 raise TypeError("Line count decreased. Only add text, do not remove it.") 159 # if 5 < initial <= removed: 160 # # concern is taking a large file, and deleting everything (ie. confusing full rewrite for an insert or edit) 161 # raise TypeError( 162 # "Removed lines is equal initial number of lines. " 163 # "When rewriting files, you have to re-write the previous lines, too." 164 # ) 165 # if unchanged > 0 and initial > 0 and added == 0 and removed == 0: 166 # raise TypeError( 167 # "Nothing changed, nothing was added or removed. " 168 # "When rewriting files, you have to re-write the whole file " 169 # "with lines changed, added or removed." 170 # ) 171 172 try: 173 BackupRestore.backup_file(full_path) 174 175 with open(full_path, "w", encoding="utf-8") as file: 176 file.write(text) 177 178 validation = self._validate_code(full_path) 179 180 if validation: 181 BackupRestore.revert_to_latest_backup(full_path) 182 return f"File not rewritten because of problems.\n{validation.message}" 183 184 feedback = f"File rewritten to {full_path}" 185 if self.auto_cat: 186 feedback = "Changes made without exception, please verify by other means.\n" 187 contents = CatTool(self.root_folder, self.config).cat_markdown([file_path]) 188 return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}" 189 return feedback + ", please view to verify contents." 190 except FileNotFoundError as e: 191 raise FileNotFoundError( 192 str(e) + " Consider using write_new_file method if you want to create a new file." 193 ) from e 194 195 def _validate_code(self, full_path: str) -> ValidationMessageForBot | None: 196 """ 197 Validate python 198 199 Args: 200 full_path (str): The path to the file to validate. 201 202 Returns: 203 Optional[ValidationMessageForBot]: A validation message if the file is invalid, otherwise None. 204 """ 205 if not is_python_file(full_path): 206 return None 207 if not self.python_module: 208 logger.warning("No python module set, skipping validation.") 209 return None 210 validator = ValidateModule(self.python_module) 211 results = validator.validate() 212 explanation = validator.explain_to_bot(results) 213 if explanation.is_valid: 214 return None 215 return explanation
69 def __init__(self, root_folder: str, config: Config) -> None: 70 """ 71 Initialize the RewriteTool class. 72 73 Args: 74 root_folder (str): The root folder path for file operations. 75 config (Config): The developer input that bot shouldn't set. 76 """ 77 self.root_folder = root_folder 78 self.config = config 79 self.auto_cat = config.get_flag("auto_cat", True) 80 self.python_module = config.get_value("python_module") 81 self.only_add_text = config.get_flag("only_add_text", False)
Initialize the RewriteTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
83 @log() 84 def write_new_file(self, file_path: str, text: str) -> str: 85 """ 86 Write a new file at file_path within the root_folder. 87 88 Args: 89 file_path (str): The relative path to the file to be written. 90 text (str): The content to write into the file. 91 92 Returns: 93 str: A success message with the file path. 94 95 Raises: 96 ValueError: If the file already exists or if the file_path is outside the root_folder. 97 """ 98 file_path = sanitize_path(file_path) 99 # Don't prepend root folder, we will have already cd'd to it. 100 full_path = file_path 101 if not is_file_in_root_folder(full_path, self.root_folder): 102 raise ValueError("File path must be within the root folder.") 103 104 try: 105 if os.path.exists(full_path): 106 raise FileExistsError("File already exists.") 107 108 with open(full_path, "w", encoding="utf-8") as file: 109 file.write(text) 110 111 validation = self._validate_code(full_path) 112 113 if validation: 114 os.remove(full_path) 115 return f"File not written because of problems.\n{validation.message}" 116 117 return f"File written to {full_path}" 118 except FileExistsError as e: 119 tree_text = tree(Path(os.getcwd())) 120 markdown_content = f"# File {full_path} already exists. Here are all the files you can see\n\n{tree_text}" 121 raise ValueError( 122 str(e) + f" {markdown_content}\n Consider using rewrite_file method if you want to overwrite." 123 ) from e
Write a new file at file_path within the root_folder.
Args: file_path (str): The relative path to the file to be written. text (str): The content to write into the file.
Returns: str: A success message with the file path.
Raises: ValueError: If the file already exists or if the file_path is outside the root_folder.
125 @log() 126 def rewrite_file(self, file_path: str, text: str) -> str: 127 """ 128 Backup and rewrite an existing file at file_path within the root_folder. 129 This will completely replace the contents of the file with the new text. 130 131 Args: 132 file_path (str): The relative path to the file to be rewritten. 133 text (str): The new content to write into the file. 134 135 Returns: 136 str: A success message with the file path. 137 138 Raises: 139 ValueError: If the file does not exist or if the file_path is outside the root_folder. 140 """ 141 if not text: 142 raise TypeError("This would delete everything in the file. This is probably not what you want.") 143 144 file_path = sanitize_path(file_path) 145 146 # Don't prepend root folder, we will have already cd'd to it. 147 full_path = file_path 148 if not is_file_in_root_folder(full_path, self.root_folder): 149 raise ValueError("File path must be within the root folder.") 150 151 if not os.path.exists(full_path): 152 raise FileNotFoundError("File does not exist, use ls tool to see what files there are.") 153 154 _unchanged_proportion, initial, _unchanged, _added, removed = file_similarity(full_path, text.split("\n")) 155 if self.only_add_text and removed > 0: 156 raise TypeError("This would delete lines. Only add lines, do not remove them.") 157 if self.only_add_text and len(text.split("\n")) < initial: 158 raise TypeError("Line count decreased. Only add text, do not remove it.") 159 # if 5 < initial <= removed: 160 # # concern is taking a large file, and deleting everything (ie. confusing full rewrite for an insert or edit) 161 # raise TypeError( 162 # "Removed lines is equal initial number of lines. " 163 # "When rewriting files, you have to re-write the previous lines, too." 164 # ) 165 # if unchanged > 0 and initial > 0 and added == 0 and removed == 0: 166 # raise TypeError( 167 # "Nothing changed, nothing was added or removed. " 168 # "When rewriting files, you have to re-write the whole file " 169 # "with lines changed, added or removed." 170 # ) 171 172 try: 173 BackupRestore.backup_file(full_path) 174 175 with open(full_path, "w", encoding="utf-8") as file: 176 file.write(text) 177 178 validation = self._validate_code(full_path) 179 180 if validation: 181 BackupRestore.revert_to_latest_backup(full_path) 182 return f"File not rewritten because of problems.\n{validation.message}" 183 184 feedback = f"File rewritten to {full_path}" 185 if self.auto_cat: 186 feedback = "Changes made without exception, please verify by other means.\n" 187 contents = CatTool(self.root_folder, self.config).cat_markdown([file_path]) 188 return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}" 189 return feedback + ", please view to verify contents." 190 except FileNotFoundError as e: 191 raise FileNotFoundError( 192 str(e) + " Consider using write_new_file method if you want to create a new file." 193 ) from e
Backup and rewrite an existing file at file_path within the root_folder. This will completely replace the contents of the file with the new text.
Args: file_path (str): The relative path to the file to be rewritten. text (str): The new content to write into the file.
Returns: str: A success message with the file path.
Raises: ValueError: If the file does not exist or if the file_path is outside the root_folder.
22class PyCatTool: 23 def __init__(self, root_folder: str, config: Config) -> None: 24 """ 25 Initialize the PyCatTool with a root folder. 26 27 Args: 28 root_folder (str): The root folder path to start the file traversal from. 29 config (Config): The developer input that bot shouldn't set. 30 """ 31 self.root_folder = root_folder 32 self.config = config 33 self.auto_cat = config.get_flag("auto_cat", True) 34 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 35 36 @log() 37 def format_code_as_markdown( 38 self, 39 base_path: str, 40 header: str, 41 no_docs: bool = False, # pylint: disable=unused-argument 42 no_comments: bool = False, # pylint: disable=unused-argument 43 ) -> str: 44 """ 45 Combine all Python files in a directory into a single Markdown file. 46 47 This method traverses the directory starting from base_path, and for each Python file found, 48 its contents are formatted and appended to the Markdown file specified by output_file. 49 50 Args: 51 base_path (str): The base path of the directory to start traversing. 52 header (str): A header string to be included at the beginning of the Markdown file. 53 no_docs (bool): Whether to exclude docstrings from the output. Defaults to False. 54 no_comments (bool): Whether to exclude comments from the output. Defaults to False. 55 56 Returns: 57 str: The Markdown file contents. 58 """ 59 output_file = StringIO() 60 if header == "tree": 61 tree_text = tree(Path(base_path)) 62 markdown_content = f"# Source Code Filesystem Tree\n\n{tree_text}" 63 output_file.write(markdown_content) 64 65 markdown_content = f"# {header} Source Code\n\n" 66 67 for root, _dirs, files in os.walk(base_path): 68 for file in files: 69 if not is_file_in_root_folder(file, self.root_folder): 70 continue 71 if is_python_file(file): 72 full_path = os.path.join(root, file) 73 relative_path = os.path.relpath(full_path, base_path) 74 markdown_content += format_path_as_header(relative_path) 75 markdown_content += "```python\n" 76 with open(full_path, encoding="utf-8", errors=self.utf8_errors) as handle: 77 text = handle.read() 78 markdown_content += text 79 markdown_content += "\n```\n\n" 80 output_file.write(markdown_content) 81 return output_file.getvalue()
23 def __init__(self, root_folder: str, config: Config) -> None: 24 """ 25 Initialize the PyCatTool with a root folder. 26 27 Args: 28 root_folder (str): The root folder path to start the file traversal from. 29 config (Config): The developer input that bot shouldn't set. 30 """ 31 self.root_folder = root_folder 32 self.config = config 33 self.auto_cat = config.get_flag("auto_cat", True) 34 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the PyCatTool with a root folder.
Args: root_folder (str): The root folder path to start the file traversal from. config (Config): The developer input that bot shouldn't set.
36 @log() 37 def format_code_as_markdown( 38 self, 39 base_path: str, 40 header: str, 41 no_docs: bool = False, # pylint: disable=unused-argument 42 no_comments: bool = False, # pylint: disable=unused-argument 43 ) -> str: 44 """ 45 Combine all Python files in a directory into a single Markdown file. 46 47 This method traverses the directory starting from base_path, and for each Python file found, 48 its contents are formatted and appended to the Markdown file specified by output_file. 49 50 Args: 51 base_path (str): The base path of the directory to start traversing. 52 header (str): A header string to be included at the beginning of the Markdown file. 53 no_docs (bool): Whether to exclude docstrings from the output. Defaults to False. 54 no_comments (bool): Whether to exclude comments from the output. Defaults to False. 55 56 Returns: 57 str: The Markdown file contents. 58 """ 59 output_file = StringIO() 60 if header == "tree": 61 tree_text = tree(Path(base_path)) 62 markdown_content = f"# Source Code Filesystem Tree\n\n{tree_text}" 63 output_file.write(markdown_content) 64 65 markdown_content = f"# {header} Source Code\n\n" 66 67 for root, _dirs, files in os.walk(base_path): 68 for file in files: 69 if not is_file_in_root_folder(file, self.root_folder): 70 continue 71 if is_python_file(file): 72 full_path = os.path.join(root, file) 73 relative_path = os.path.relpath(full_path, base_path) 74 markdown_content += format_path_as_header(relative_path) 75 markdown_content += "```python\n" 76 with open(full_path, encoding="utf-8", errors=self.utf8_errors) as handle: 77 text = handle.read() 78 markdown_content += text 79 markdown_content += "\n```\n\n" 80 output_file.write(markdown_content) 81 return output_file.getvalue()
Combine all Python files in a directory into a single Markdown file.
This method traverses the directory starting from base_path, and for each Python file found, its contents are formatted and appended to the Markdown file specified by output_file.
Args: base_path (str): The base path of the directory to start traversing. header (str): A header string to be included at the beginning of the Markdown file. no_docs (bool): Whether to exclude docstrings from the output. Defaults to False. no_comments (bool): Whether to exclude comments from the output. Defaults to False.
Returns: str: The Markdown file contents.
17class SedTool: 18 def __init__(self, root_folder: str, config: Config) -> None: 19 """ 20 Initialize the SedTool class. 21 22 Args: 23 root_folder (str): The root folder path for file operations. 24 config (Config): The developer input that bot shouldn't set. 25 """ 26 self.root_folder = root_folder 27 self.config = config 28 self.auto_cat = config.get_flag("auto_cat", True) 29 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 30 31 @log() 32 def sed(self, file_path: str, commands: list[str]) -> str: 33 r""" 34 Transform the contents of a file located at file_path as per the provided sed-like commands. 35 36 Args: 37 file_path (str): The path of the file to be transformed. 38 commands (list[str]): A list of sed-like commands for text transformation. 39 40 Returns: 41 str: The transformed text from the file. 42 43 Supported command syntax: 44 - s/regex/replacement/flags: Regex substitution. 45 - p: Print the current line. 46 - a\text: Append text after the current line. 47 - i\text: Insert text before the current line. 48 - [number]c\text: Change the text of a specific line number. 49 - [number]d: Delete a specific line number. 50 51 Note: This function reads from a file and returns the transformed text. It does not modify the file in-place. 52 """ 53 if not is_file_in_root_folder(file_path, self.root_folder): 54 raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.") 55 56 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 57 input_text = file.read() 58 output_text = SedTool._process_sed(input_text, commands) 59 if is_python_file(file_path): 60 is_valid, error = is_valid_python_source(output_text) 61 if not is_valid and error is not None: 62 return f"Invalid Python source code. No changes made. {error.lineno} {error.msg} {error.text}" 63 64 if input_text != output_text: 65 with open(file_path, "w", encoding="utf-8") as output_file: 66 output_file.write(output_text) 67 68 if self.auto_cat: 69 feedback = "Changes without exception, please verify by other means.\n" 70 contents = CatTool(self.root_folder, self.config).cat_markdown([file_path]) 71 return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}" 72 return "Changes without exception, please verify by other means." 73 return "No changes made." 74 75 @classmethod 76 def _process_sed(cls, input_text: str, commands: list[str]) -> str: 77 r""" 78 Transform input_text as per the provided sed-like commands. 79 80 Args: 81 input_text (str): The input text to be transformed. 82 commands (list[str]): A list of sed-like commands for text transformation. 83 84 Returns: 85 str: The transformed text. 86 87 Supported command syntax: 88 - s/regex/replacement/flags: Regex substitution. 89 - a\text: Append text after the current line. 90 - i\text: Insert text before the current line. 91 - [number]c\text: Change the text of a specific line number. 92 - [number]d: Delete a specific line number. 93 94 Example: 95 >>> SedTool._process_sed("Hello World\\nThis is a test", ["s/World/Universe/", "a\\Appended text"]) 96 'Hello Universe\\nThis is a test\nAppended text' 97 >>> SedTool._process_sed("First Line\\nSecond Line", ["2d", "i\\Inserted at Start"]) 98 'Inserted at Start\nFirst Line\\nSecond Line' 99 """ 100 if isinstance(commands, str): 101 commands = [commands] 102 103 # don't know how to fix the covariant/invariant typing issue here 104 lines: list[str] = input_text.split("\n") 105 106 for i, _ in enumerate(lines): 107 for command in commands: 108 if command.startswith("s/") and re.match(r"s/.+/.*/", command): 109 # Regex substitution: s/regex/replacement/flags 110 parts = command[2:].rsplit("/", 2) 111 regex, replacement, flags = parts[0], parts[1], parts[2] if len(parts) > 2 else "" 112 count = 1 if "g" not in flags else 0 # replace all if 'g' is present 113 lines[i] = re.sub(regex, replacement, lines[i], count=count) 114 elif command.startswith("a\\"): 115 # Append: a\text 116 append_text = command[2:] 117 lines[i] += "\n" + append_text 118 elif re.match(r"\d+a\\", command): 119 # insert after the specified line. a for after. 120 target_line, change_text = command.split("a\\") 121 if i + 1 == int(target_line): 122 lines[i] = change_text 123 elif command.startswith("i\\") and i == 0: 124 # Insert: i\text (only at the beginning of the text) 125 insert_text = command[2:] 126 lines[i] = insert_text + "\n" + lines[i] 127 elif re.match(r"\d+c\\", command): 128 # Change specific line: [number]c\text 129 target_line, change_text = command.split("c\\") 130 if i + 1 == int(target_line): 131 lines[i] = change_text 132 elif re.match(r"\d+d", command): 133 # Delete specific line: [number]d 134 delete_line = int(command[:-1]) 135 if i + 1 == delete_line: 136 # None was a better deletion marker, but messes with mypy. 137 lines[i] = "None # Mark for deletion" 138 elif command == "p": 139 # print? No action? 140 pass 141 else: 142 raise TypeError( 143 "Unknown command, expected prefix of s/ or a\\ or digit + c or digit + d for replace, append, change, or delete respectively" 144 ) 145 146 # Rebuild the output from modified lines, excluding deleted ones 147 output = [line for line in lines if line is not None] 148 149 return "\n".join(output) 150 151 # # Rerun the regex substitution test with the corrected function 152 # test_regex_substitution_corrected = lambda: simulate_sed_corrected(input_text, commands) == expected_output 153 # test_regex_substitution_corrected()
18 def __init__(self, root_folder: str, config: Config) -> None: 19 """ 20 Initialize the SedTool class. 21 22 Args: 23 root_folder (str): The root folder path for file operations. 24 config (Config): The developer input that bot shouldn't set. 25 """ 26 self.root_folder = root_folder 27 self.config = config 28 self.auto_cat = config.get_flag("auto_cat", True) 29 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the SedTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
31 @log() 32 def sed(self, file_path: str, commands: list[str]) -> str: 33 r""" 34 Transform the contents of a file located at file_path as per the provided sed-like commands. 35 36 Args: 37 file_path (str): The path of the file to be transformed. 38 commands (list[str]): A list of sed-like commands for text transformation. 39 40 Returns: 41 str: The transformed text from the file. 42 43 Supported command syntax: 44 - s/regex/replacement/flags: Regex substitution. 45 - p: Print the current line. 46 - a\text: Append text after the current line. 47 - i\text: Insert text before the current line. 48 - [number]c\text: Change the text of a specific line number. 49 - [number]d: Delete a specific line number. 50 51 Note: This function reads from a file and returns the transformed text. It does not modify the file in-place. 52 """ 53 if not is_file_in_root_folder(file_path, self.root_folder): 54 raise ValueError(f"File {file_path} is not in root folder {self.root_folder}.") 55 56 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 57 input_text = file.read() 58 output_text = SedTool._process_sed(input_text, commands) 59 if is_python_file(file_path): 60 is_valid, error = is_valid_python_source(output_text) 61 if not is_valid and error is not None: 62 return f"Invalid Python source code. No changes made. {error.lineno} {error.msg} {error.text}" 63 64 if input_text != output_text: 65 with open(file_path, "w", encoding="utf-8") as output_file: 66 output_file.write(output_text) 67 68 if self.auto_cat: 69 feedback = "Changes without exception, please verify by other means.\n" 70 contents = CatTool(self.root_folder, self.config).cat_markdown([file_path]) 71 return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}" 72 return "Changes without exception, please verify by other means." 73 return "No changes made."
Transform the contents of a file located at file_path as per the provided sed-like commands.
Args: file_path (str): The path of the file to be transformed. commands (list[str]): A list of sed-like commands for text transformation.
Returns: str: The transformed text from the file.
Supported command syntax: - s/regex/replacement/flags: Regex substitution. - p: Print the current line. - a\text: Append text after the current line. - i\text: Insert text before the current line. - [number]c\text: Change the text of a specific line number. - [number]d: Delete a specific line number.
Note: This function reads from a file and returns the transformed text. It does not modify the file in-place.
20class ReplaceTool: 21 def __init__(self, root_folder: str, config: Config) -> None: 22 """ 23 Initialize the SedTool class. 24 25 Args: 26 root_folder (str): The root folder path for file operations. 27 config (Config): The developer input that bot shouldn't set. 28 """ 29 self.root_folder = root_folder 30 self.config = config 31 self.auto_cat = config.get_flag("auto_cat", True) 32 self.python_module = config.get_value("python_module") 33 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 34 35 @log() 36 def replace_line_by_line( 37 self, file_path: str, old_text: str, new_text: str, line_start: int = 0, line_end: int = -1 38 ) -> str: 39 """Replaces occurrences of a specified text with new text in a range of lines in a file. 40 41 Opens the file and replaces occurrences of 'old_text' with 'new_text' within the specified 42 line range. If 'line_end' is -1, it defaults to the end of the file. Returns a message 43 indicating whether changes were successfully applied or not. 44 45 Args: 46 file_path (str): The path to the file. 47 old_text (str): The text to be replaced. 48 new_text (str): The new text to replace the old text. 49 line_start (int, optional): The starting line number (0-indexed) for the replacement. 50 Defaults to 0. 51 line_end (int, optional): The ending line number (0-indexed) for the replacement. 52 If -1, it goes to the end of the file. Defaults to -1. 53 54 Returns: 55 str: A message indicating the success of the operation. 56 57 Raises: 58 TypeError: If file_path or old_text is None, or if no lines are left after replacement. 59 """ 60 if not file_path: 61 raise TypeError("No file_path, please provide file_path for each request.") 62 if not old_text: 63 raise TypeError("No old_text, please context so I can find the text to replace.") 64 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 65 input_text = file.read() 66 lines = [] 67 input_lines = input_text.splitlines() 68 if line_end == -1: 69 line_end = len(input_lines) 70 for line_no, line in enumerate(input_lines): 71 if line_start <= line_no < line_end and old_text in line: 72 line = line.replace(old_text, new_text) 73 lines.append(line) 74 if not lines: 75 raise TypeError("Nothing left after replace, something went wrong, cancelling.") 76 final = "\n".join(lines) 77 return self._save_if_changed(file_path, final, input_text) 78 79 @log() 80 def replace_all(self, file_path: str, old_text: str, new_text: str) -> str: 81 """Replaces all occurrences of a specified text with new text in a file. 82 83 Opens the file and replaces all occurrences of 'old_text' with 'new_text'. Returns a 84 message indicating whether changes were successfully applied or not. 85 86 Args: 87 file_path (str): The path to the file. 88 old_text (str): The text to be replaced. 89 new_text (str): The new text to replace the old text. 90 91 Returns: 92 str: A message indicating the success of the operation. 93 94 Raises: 95 TypeError: If file_path or old_text is None. 96 """ 97 if new_text is None: 98 new_text = "" 99 if not file_path: 100 raise TypeError("No file_path, please provide file_path for each request.") 101 if not old_text: 102 raise TypeError("No old_text, please context so I can find the text to replace.") 103 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 104 input_text = file.read() 105 final = input_text.replace(old_text, new_text) 106 return self._save_if_changed(file_path, final, input_text) 107 108 @log() 109 def replace_with_regex(self, file_path: str, regex_match_expression: str, replacement: str) -> str: 110 """Replaces text in a file based on a regular expression match. 111 112 Opens the file and replaces text that matches the regular expression 'regex_match_expression' 113 with the 'replacement' text. Returns a message indicating whether changes were successfully 114 applied or not. 115 116 Args: 117 file_path (str): The path to the file. 118 regex_match_expression (str): The regular expression pattern to match. 119 replacement (str): The text to replace the matched pattern. 120 121 Returns: 122 str: A message indicating the success of the operation. 123 124 Raises: 125 TypeError: If file_path or regex_match_expression is None. 126 """ 127 if not file_path: 128 raise TypeError("No file_path, please provide file_path for each request.") 129 if not regex_match_expression: 130 raise TypeError("No regex_match_expression, please context so I can find the text to replace.") 131 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 132 input_text = file.read() 133 final = re.sub(regex_match_expression, replacement, input_text) 134 return self._save_if_changed(file_path, final, input_text) 135 136 def _save_if_changed(self, file_path: str, final: str, input_text: str) -> str: 137 """Saves the modified text to the file if changes have been made. 138 139 Compares the original text with the modified text and writes the modified text 140 to the file if there are changes. Returns a message indicating whether any changes 141 were made. 142 143 Args: 144 file_path (str): The path to the file. 145 final (str): The modified text. 146 input_text (str): The original text. 147 148 Returns: 149 str: A message indicating whether changes were made or not. 150 151 Raises: 152 TypeError: If file_path is None. 153 """ 154 if not final: 155 raise TypeError("Something went wrong in replace and all text disappeared. Cancelling.") 156 157 if input_text != final: 158 BackupRestore.backup_file(file_path) 159 with open(file_path, "w", encoding="utf-8", errors=self.utf8_errors) as output_file: 160 output_file.write(final) 161 162 validation = self._validate_code(file_path) 163 164 if validation: 165 BackupRestore.revert_to_latest_backup(file_path) 166 return f"File not written because of problems.\n{validation.message}" 167 168 if self.auto_cat: 169 feedback = "Changes applied without exception, please verify by other means.\n" 170 contents = CatTool(self.root_folder, self.config).cat_markdown([file_path]) 171 return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}" 172 return "Changes applied without exception, please verify by other means." 173 return ( 174 "No changes made, this means the old file contents are the same as the new. This has nothing " 175 "to do with file permissions. Try again with a different match pattern." 176 ) 177 178 def _validate_code(self, full_path: str) -> ValidationMessageForBot | None: 179 """ 180 Validate python 181 182 Args: 183 full_path (str): The path to the file to validate. 184 185 Returns: 186 Optional[ValidationMessageForBot]: A validation message if the file is invalid, otherwise None. 187 """ 188 if not is_python_file(full_path): 189 return None 190 if not self.python_module: 191 logger.warning("No python module set, skipping validation.") 192 return None 193 validator = ValidateModule(self.python_module) 194 results = validator.validate() 195 explanation = validator.explain_to_bot(results) 196 if explanation.is_valid: 197 return None 198 return explanation
21 def __init__(self, root_folder: str, config: Config) -> None: 22 """ 23 Initialize the SedTool class. 24 25 Args: 26 root_folder (str): The root folder path for file operations. 27 config (Config): The developer input that bot shouldn't set. 28 """ 29 self.root_folder = root_folder 30 self.config = config 31 self.auto_cat = config.get_flag("auto_cat", True) 32 self.python_module = config.get_value("python_module") 33 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the SedTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
35 @log() 36 def replace_line_by_line( 37 self, file_path: str, old_text: str, new_text: str, line_start: int = 0, line_end: int = -1 38 ) -> str: 39 """Replaces occurrences of a specified text with new text in a range of lines in a file. 40 41 Opens the file and replaces occurrences of 'old_text' with 'new_text' within the specified 42 line range. If 'line_end' is -1, it defaults to the end of the file. Returns a message 43 indicating whether changes were successfully applied or not. 44 45 Args: 46 file_path (str): The path to the file. 47 old_text (str): The text to be replaced. 48 new_text (str): The new text to replace the old text. 49 line_start (int, optional): The starting line number (0-indexed) for the replacement. 50 Defaults to 0. 51 line_end (int, optional): The ending line number (0-indexed) for the replacement. 52 If -1, it goes to the end of the file. Defaults to -1. 53 54 Returns: 55 str: A message indicating the success of the operation. 56 57 Raises: 58 TypeError: If file_path or old_text is None, or if no lines are left after replacement. 59 """ 60 if not file_path: 61 raise TypeError("No file_path, please provide file_path for each request.") 62 if not old_text: 63 raise TypeError("No old_text, please context so I can find the text to replace.") 64 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 65 input_text = file.read() 66 lines = [] 67 input_lines = input_text.splitlines() 68 if line_end == -1: 69 line_end = len(input_lines) 70 for line_no, line in enumerate(input_lines): 71 if line_start <= line_no < line_end and old_text in line: 72 line = line.replace(old_text, new_text) 73 lines.append(line) 74 if not lines: 75 raise TypeError("Nothing left after replace, something went wrong, cancelling.") 76 final = "\n".join(lines) 77 return self._save_if_changed(file_path, final, input_text)
Replaces occurrences of a specified text with new text in a range of lines in a file.
Opens the file and replaces occurrences of 'old_text' with 'new_text' within the specified line range. If 'line_end' is -1, it defaults to the end of the file. Returns a message indicating whether changes were successfully applied or not.
Args: file_path (str): The path to the file. old_text (str): The text to be replaced. new_text (str): The new text to replace the old text. line_start (int, optional): The starting line number (0-indexed) for the replacement. Defaults to 0. line_end (int, optional): The ending line number (0-indexed) for the replacement. If -1, it goes to the end of the file. Defaults to -1.
Returns: str: A message indicating the success of the operation.
Raises: TypeError: If file_path or old_text is None, or if no lines are left after replacement.
79 @log() 80 def replace_all(self, file_path: str, old_text: str, new_text: str) -> str: 81 """Replaces all occurrences of a specified text with new text in a file. 82 83 Opens the file and replaces all occurrences of 'old_text' with 'new_text'. Returns a 84 message indicating whether changes were successfully applied or not. 85 86 Args: 87 file_path (str): The path to the file. 88 old_text (str): The text to be replaced. 89 new_text (str): The new text to replace the old text. 90 91 Returns: 92 str: A message indicating the success of the operation. 93 94 Raises: 95 TypeError: If file_path or old_text is None. 96 """ 97 if new_text is None: 98 new_text = "" 99 if not file_path: 100 raise TypeError("No file_path, please provide file_path for each request.") 101 if not old_text: 102 raise TypeError("No old_text, please context so I can find the text to replace.") 103 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 104 input_text = file.read() 105 final = input_text.replace(old_text, new_text) 106 return self._save_if_changed(file_path, final, input_text)
Replaces all occurrences of a specified text with new text in a file.
Opens the file and replaces all occurrences of 'old_text' with 'new_text'. Returns a message indicating whether changes were successfully applied or not.
Args: file_path (str): The path to the file. old_text (str): The text to be replaced. new_text (str): The new text to replace the old text.
Returns: str: A message indicating the success of the operation.
Raises: TypeError: If file_path or old_text is None.
108 @log() 109 def replace_with_regex(self, file_path: str, regex_match_expression: str, replacement: str) -> str: 110 """Replaces text in a file based on a regular expression match. 111 112 Opens the file and replaces text that matches the regular expression 'regex_match_expression' 113 with the 'replacement' text. Returns a message indicating whether changes were successfully 114 applied or not. 115 116 Args: 117 file_path (str): The path to the file. 118 regex_match_expression (str): The regular expression pattern to match. 119 replacement (str): The text to replace the matched pattern. 120 121 Returns: 122 str: A message indicating the success of the operation. 123 124 Raises: 125 TypeError: If file_path or regex_match_expression is None. 126 """ 127 if not file_path: 128 raise TypeError("No file_path, please provide file_path for each request.") 129 if not regex_match_expression: 130 raise TypeError("No regex_match_expression, please context so I can find the text to replace.") 131 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 132 input_text = file.read() 133 final = re.sub(regex_match_expression, replacement, input_text) 134 return self._save_if_changed(file_path, final, input_text)
Replaces text in a file based on a regular expression match.
Opens the file and replaces text that matches the regular expression 'regex_match_expression' with the 'replacement' text. Returns a message indicating whether changes were successfully applied or not.
Args: file_path (str): The path to the file. regex_match_expression (str): The regular expression pattern to match. replacement (str): The text to replace the matched pattern.
Returns: str: A message indicating the success of the operation.
Raises: TypeError: If file_path or regex_match_expression is None.
18class InsertTool: 19 def __init__(self, root_folder: str, config: Config) -> None: 20 """ 21 Initialize the InsertTool class. 22 23 Args: 24 root_folder (str): The root folder path for file operations. 25 config (Config): The developer input that bot shouldn't set. 26 """ 27 self.root_folder = root_folder 28 self.config = config 29 self.auto_cat = config.get_flag("auto_cat", True) 30 self.python_module = config.get_value("python_module") 31 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape") 32 33 @log() 34 def insert_text_after_context(self, file_path: str, context: str, text_to_insert: str) -> str: 35 """Inserts a given text immediately after a specified context in a file. 36 37 This method opens the file, finds the line containing the specified context, 38 and inserts the provided text immediately after this line. If the context 39 matches multiple lines, it raises a ValueError due to ambiguity. 40 41 Args: 42 file_path (str): The path of the file in which the text is to be inserted. 43 context (str): The context string to search for in the file. The text is 44 inserted after the line containing this context. 45 text_to_insert (str): The text to insert into the file. 46 47 Returns: 48 str: A message for the bot with the result of the insert. 49 50 Raises: 51 ValueError: If the provided context matches multiple lines in the file. 52 """ 53 if not file_path: 54 raise TypeError("No file_path, please provide file_path for each request.") 55 if not context: 56 raise TypeError("No context, please context so I can find where to insert the text.") 57 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 58 lines = file.readlines() 59 original_lines = list(lines) 60 61 context_line_indices = [i for i, line in enumerate(lines) if context in line] 62 63 if len(context_line_indices) == 0: 64 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 65 plain_text = file.read() 66 raise ValueError( 67 f"No matches found, no changes made, context is not a substring of any row. " 68 f"For reference, here is the contents of the file:\n{plain_text}" 69 ) 70 71 # Check for ambiguity in the context match 72 if len(context_line_indices) > 1: 73 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 74 plain_text = file.read() 75 found_at = ", ".join([str(i) for i in context_line_indices]) 76 raise ValueError( 77 f"Ambiguous context: The provided context matches multiple lines, namely {found_at}. A context line the " 78 "string or substring of the line just before your desired insertion point. It must " 79 "uniquely identify a location. Either use a longer substring to match or switch to using" 80 "the insert_text_after_multiline_context tool.\n" 81 f"For reference, here is the contents of the file:\n{plain_text}" 82 ) 83 84 # Index of the line after the context line 85 insert_index = context_line_indices[0] + 1 86 87 # Insert the text 88 lines.insert(insert_index, text_to_insert + "\n") 89 90 return self._save_if_changed(file_path, original_lines, lines) 91 92 @log() 93 def insert_text_at_start_or_end(self, file_path: str, text_to_insert: str, position: str = "end") -> str: 94 """Inserts text at the start or end of a file. 95 96 Opens the file and inserts the specified text either at the beginning or the 97 end of the file, based on the 'position' argument. If the position argument 98 is neither 'start' nor 'end', it raises a ValueError. 99 100 Args: 101 file_path (str): The path of the file in which the text is to be inserted. 102 text_to_insert (str): The text to insert into the file. 103 position (str, optional): The position where the text should be inserted. 104 Should be either 'start' or 'end'. Defaults to 'end'. 105 106 Raises: 107 ValueError: If the 'position' argument is not 'start' or 'end'. 108 109 """ 110 if not file_path: 111 raise TypeError("No file_path, please provide file_path for each request.") 112 if not text_to_insert: 113 raise TypeError("No text_to_insert, please provide so I have something to insert.") 114 if position not in ("start", "end"): 115 raise ValueError("position must be start or end, so I know where to insert text.") 116 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 117 lines = file.readlines() 118 original_lines = list(lines) 119 if position == "start": 120 lines.insert(0, text_to_insert + "\n") 121 elif position == "end": 122 lines.append(text_to_insert + "\n") 123 else: 124 raise ValueError("Invalid position: choose 'start' or 'end'.") 125 126 return self._save_if_changed(file_path, original_lines, lines) 127 128 @log() 129 def insert_text_after_multiline_context(self, file_path: str, context_lines: list[str], text_to_insert: str) -> str: 130 """Inserts text immediately after a specified multiline context in a file. 131 132 Opens the file and searches for a sequence of lines (context). Once the context 133 is found, it inserts the specified text immediately after this context. If the 134 context is not found, it raises a ValueError. 135 136 Args: 137 file_path (str): The path of the file in which the text is to be inserted. 138 context_lines (list of str): A list of strings representing the multiline 139 context to search for in the file. 140 text_to_insert (str): The text to insert into the file after the context. 141 142 Raises: 143 ValueError: If the multiline context is not found in the file. 144 145 """ 146 if not file_path: 147 raise TypeError("No file_path, please provide file_path for each request.") 148 if not context_lines: 149 raise TypeError("No context_lines, please context lines so I can find where to insert the new lines.") 150 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 151 lines = file.readlines() 152 153 try: 154 ends_with_n = lines[:-1][0].endswith("\n") 155 except IndexError: 156 ends_with_n = False 157 158 # this is going to make it hard to preserve whitespace. 159 # Convert context_lines to a string for easier matching 160 context_string = "".join([line + "\n" for line in context_lines]).rstrip("\n") 161 162 # Convert file lines to a string 163 file_string = "".join(lines) 164 165 starts_at = file_string.find(context_string) 166 if starts_at == -1: 167 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 168 plain_text = file.read() 169 raise ValueError( 170 f"No matches found, no changes made, context_lines are not found in this document. " 171 f"For reference, here is the contents of the file:\n{plain_text}" 172 ) 173 # Find the index where the context ends 174 context_end_index = starts_at + len(context_string) 175 176 # Split the file_string back into lines at the context end 177 before_context = file_string[:context_end_index] 178 after_context = file_string[context_end_index:] 179 180 # Insert the new text 181 new_file_string = before_context + "\n" + text_to_insert + "\n" + after_context.strip("\n") 182 183 if ends_with_n: 184 new_file_string += "\n" 185 186 return self._save_if_changed(file_path, lines, new_file_string) 187 188 def _save_if_changed(self, file_path: str, original_lines, new_file_string: Union[str, list[str]]) -> str: 189 """ 190 Save the file if it has changed. 191 192 Args: 193 file_path: The path of the file to save. 194 original_lines: The original file contents. 195 new_file_string: The new file contents. 196 197 Returns: 198 A message for the bot with the result of the save. 199 """ 200 if not new_file_string: 201 raise TypeError("Something went wrong in insert and all text disappeared. Cancelling.") 202 203 if isinstance(new_file_string, str) and "\n".join(original_lines) == new_file_string: 204 return ( 205 "File not changed this means the old file contents are the same as the new. This has nothing " 206 "to do with file permissions." 207 ) 208 if isinstance(new_file_string, list) and original_lines == new_file_string: 209 return ( 210 "File not changed, this means the old file contents are the same as the new. This has nothing " 211 "to do with file permissions." 212 ) 213 # if is_python_file(file_path): 214 # is_valid, error = is_valid_python_source(source) 215 # if not is_valid and error: 216 # return f"Invalid Python source code. No changes made. {error.lineno} {error.msg} {error.text}" 217 # if not is_valid: 218 # return f"Invalid Python source code. No changes made. {error}." 219 220 # Write back to the file 221 BackupRestore.backup_file(file_path) 222 with open(file_path, "w", encoding="utf-8", errors=self.utf8_errors) as file: 223 if isinstance(new_file_string, str): 224 file.write(new_file_string) 225 else: 226 file.writelines(new_file_string) 227 228 validation = self._validate_code(file_path) 229 230 if validation: 231 BackupRestore.revert_to_latest_backup(file_path) 232 return f"File not rewritten because of problems.\n{validation.message}" 233 234 if self.auto_cat: 235 feedback = "Insert completed and no exceptions thrown." 236 contents = CatTool(self.root_folder, self.config).cat_markdown([file_path]) 237 return f"Tool feedback: {feedback}\n\nCurrent file contents:\n\n{contents}" 238 return "Insert completed and no exceptions thrown. Please verify by other means." 239 240 def _validate_code(self, full_path: str) -> ValidationMessageForBot | None: 241 """ 242 Validate python 243 244 Args: 245 full_path (str): The path to the file to validate. 246 247 Returns: 248 Optional[ValidationMessageForBot]: A validation message if the file is invalid, otherwise None. 249 """ 250 if not is_python_file(full_path): 251 return None 252 if not self.python_module: 253 logger.warning("No python module set, skipping validation.") 254 return None 255 validator = ValidateModule(self.python_module) 256 results = validator.validate() 257 explanation = validator.explain_to_bot(results) 258 if explanation.is_valid: 259 return None 260 return explanation
19 def __init__(self, root_folder: str, config: Config) -> None: 20 """ 21 Initialize the InsertTool class. 22 23 Args: 24 root_folder (str): The root folder path for file operations. 25 config (Config): The developer input that bot shouldn't set. 26 """ 27 self.root_folder = root_folder 28 self.config = config 29 self.auto_cat = config.get_flag("auto_cat", True) 30 self.python_module = config.get_value("python_module") 31 self.utf8_errors = config.get_value("utf8_errors", "surrogateescape")
Initialize the InsertTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
33 @log() 34 def insert_text_after_context(self, file_path: str, context: str, text_to_insert: str) -> str: 35 """Inserts a given text immediately after a specified context in a file. 36 37 This method opens the file, finds the line containing the specified context, 38 and inserts the provided text immediately after this line. If the context 39 matches multiple lines, it raises a ValueError due to ambiguity. 40 41 Args: 42 file_path (str): The path of the file in which the text is to be inserted. 43 context (str): The context string to search for in the file. The text is 44 inserted after the line containing this context. 45 text_to_insert (str): The text to insert into the file. 46 47 Returns: 48 str: A message for the bot with the result of the insert. 49 50 Raises: 51 ValueError: If the provided context matches multiple lines in the file. 52 """ 53 if not file_path: 54 raise TypeError("No file_path, please provide file_path for each request.") 55 if not context: 56 raise TypeError("No context, please context so I can find where to insert the text.") 57 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 58 lines = file.readlines() 59 original_lines = list(lines) 60 61 context_line_indices = [i for i, line in enumerate(lines) if context in line] 62 63 if len(context_line_indices) == 0: 64 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 65 plain_text = file.read() 66 raise ValueError( 67 f"No matches found, no changes made, context is not a substring of any row. " 68 f"For reference, here is the contents of the file:\n{plain_text}" 69 ) 70 71 # Check for ambiguity in the context match 72 if len(context_line_indices) > 1: 73 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 74 plain_text = file.read() 75 found_at = ", ".join([str(i) for i in context_line_indices]) 76 raise ValueError( 77 f"Ambiguous context: The provided context matches multiple lines, namely {found_at}. A context line the " 78 "string or substring of the line just before your desired insertion point. It must " 79 "uniquely identify a location. Either use a longer substring to match or switch to using" 80 "the insert_text_after_multiline_context tool.\n" 81 f"For reference, here is the contents of the file:\n{plain_text}" 82 ) 83 84 # Index of the line after the context line 85 insert_index = context_line_indices[0] + 1 86 87 # Insert the text 88 lines.insert(insert_index, text_to_insert + "\n") 89 90 return self._save_if_changed(file_path, original_lines, lines)
Inserts a given text immediately after a specified context in a file.
This method opens the file, finds the line containing the specified context, and inserts the provided text immediately after this line. If the context matches multiple lines, it raises a ValueError due to ambiguity.
Args: file_path (str): The path of the file in which the text is to be inserted. context (str): The context string to search for in the file. The text is inserted after the line containing this context. text_to_insert (str): The text to insert into the file.
Returns: str: A message for the bot with the result of the insert.
Raises: ValueError: If the provided context matches multiple lines in the file.
92 @log() 93 def insert_text_at_start_or_end(self, file_path: str, text_to_insert: str, position: str = "end") -> str: 94 """Inserts text at the start or end of a file. 95 96 Opens the file and inserts the specified text either at the beginning or the 97 end of the file, based on the 'position' argument. If the position argument 98 is neither 'start' nor 'end', it raises a ValueError. 99 100 Args: 101 file_path (str): The path of the file in which the text is to be inserted. 102 text_to_insert (str): The text to insert into the file. 103 position (str, optional): The position where the text should be inserted. 104 Should be either 'start' or 'end'. Defaults to 'end'. 105 106 Raises: 107 ValueError: If the 'position' argument is not 'start' or 'end'. 108 109 """ 110 if not file_path: 111 raise TypeError("No file_path, please provide file_path for each request.") 112 if not text_to_insert: 113 raise TypeError("No text_to_insert, please provide so I have something to insert.") 114 if position not in ("start", "end"): 115 raise ValueError("position must be start or end, so I know where to insert text.") 116 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 117 lines = file.readlines() 118 original_lines = list(lines) 119 if position == "start": 120 lines.insert(0, text_to_insert + "\n") 121 elif position == "end": 122 lines.append(text_to_insert + "\n") 123 else: 124 raise ValueError("Invalid position: choose 'start' or 'end'.") 125 126 return self._save_if_changed(file_path, original_lines, lines)
Inserts text at the start or end of a file.
Opens the file and inserts the specified text either at the beginning or the end of the file, based on the 'position' argument. If the position argument is neither 'start' nor 'end', it raises a ValueError.
Args: file_path (str): The path of the file in which the text is to be inserted. text_to_insert (str): The text to insert into the file. position (str, optional): The position where the text should be inserted. Should be either 'start' or 'end'. Defaults to 'end'.
Raises: ValueError: If the 'position' argument is not 'start' or 'end'.
128 @log() 129 def insert_text_after_multiline_context(self, file_path: str, context_lines: list[str], text_to_insert: str) -> str: 130 """Inserts text immediately after a specified multiline context in a file. 131 132 Opens the file and searches for a sequence of lines (context). Once the context 133 is found, it inserts the specified text immediately after this context. If the 134 context is not found, it raises a ValueError. 135 136 Args: 137 file_path (str): The path of the file in which the text is to be inserted. 138 context_lines (list of str): A list of strings representing the multiline 139 context to search for in the file. 140 text_to_insert (str): The text to insert into the file after the context. 141 142 Raises: 143 ValueError: If the multiline context is not found in the file. 144 145 """ 146 if not file_path: 147 raise TypeError("No file_path, please provide file_path for each request.") 148 if not context_lines: 149 raise TypeError("No context_lines, please context lines so I can find where to insert the new lines.") 150 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 151 lines = file.readlines() 152 153 try: 154 ends_with_n = lines[:-1][0].endswith("\n") 155 except IndexError: 156 ends_with_n = False 157 158 # this is going to make it hard to preserve whitespace. 159 # Convert context_lines to a string for easier matching 160 context_string = "".join([line + "\n" for line in context_lines]).rstrip("\n") 161 162 # Convert file lines to a string 163 file_string = "".join(lines) 164 165 starts_at = file_string.find(context_string) 166 if starts_at == -1: 167 with open(file_path, encoding="utf-8", errors=self.utf8_errors) as file: 168 plain_text = file.read() 169 raise ValueError( 170 f"No matches found, no changes made, context_lines are not found in this document. " 171 f"For reference, here is the contents of the file:\n{plain_text}" 172 ) 173 # Find the index where the context ends 174 context_end_index = starts_at + len(context_string) 175 176 # Split the file_string back into lines at the context end 177 before_context = file_string[:context_end_index] 178 after_context = file_string[context_end_index:] 179 180 # Insert the new text 181 new_file_string = before_context + "\n" + text_to_insert + "\n" + after_context.strip("\n") 182 183 if ends_with_n: 184 new_file_string += "\n" 185 186 return self._save_if_changed(file_path, lines, new_file_string)
Inserts text immediately after a specified multiline context in a file.
Opens the file and searches for a sequence of lines (context). Once the context is found, it inserts the specified text immediately after this context. If the context is not found, it raises a ValueError.
Args: file_path (str): The path of the file in which the text is to be inserted. context_lines (list of str): A list of strings representing the multiline context to search for in the file. text_to_insert (str): The text to insert into the file after the context.
Raises: ValueError: If the multiline context is not found in the file.
17class TodoTool: 18 """Keep track of tasks.""" 19 20 def __init__(self, root_folder: str, config: Config) -> None: 21 """ 22 Initialize the TodoTool with a root folder. 23 24 Args: 25 root_folder (str): The root folder for valid files. 26 config (Config): The developer input that bot shouldn't set. 27 """ 28 self.root_folder: str = root_folder 29 self.config = config 30 self.roles = config.get_list("todo_roles") 31 self.task_manager = TaskManager(self.root_folder, self.roles) 32 33 @log() 34 def add_todo( 35 self, 36 title: str, 37 description: str, 38 category: str, 39 source_code_ref: str, 40 assignee: str | None = None, 41 done_when: str = "", 42 ) -> str: 43 """ 44 Adds a new task to the task manager. 45 46 Args: 47 title (str): The title of the task. 48 description (str): A description of the task. 49 category (str): The category of the task (e.g., 'bug', 'feature'). 50 source_code_ref (str): Reference to the source code related to the task. 51 assignee (str | None, optional): The name of the assignee. Defaults to None. 52 done_when (str, optional): Acceptance criteria — how to know the task is 53 complete. Defaults to "". 54 55 Returns: 56 str: A confirmation message indicating successful addition of the task. 57 """ 58 self.task_manager.add_task(title, description, category, source_code_ref, assignee, done_when) 59 summary = self.task_manager.get_stats() 60 return f"Successful added task {title}\n{summary}" 61 62 @log() 63 def remove_todo(self, title: str) -> str: 64 """ 65 Marks a task as finished based on its title. 66 67 Args: 68 title (str): The title of the task to be marked as finished. 69 70 Returns: 71 str: A confirmation message indicating the task was successfully marked as finished. 72 """ 73 self.task_manager.finish_task(title) 74 summary = self.task_manager.get_stats() 75 return f"Successful removed task {title}\n{summary}" 76 77 @log() 78 def query_todos_by_regex(self, regex_pattern: str = r"[\s\S]+") -> str: 79 r""" 80 Queries tasks by a keyword in their title, using a regular expression pattern. 81 82 Args: 83 regex_pattern (str, optional): The regular expression pattern to match in task titles. 84 Defaults to "[\s\S]+", which matches any title. 85 86 Returns: 87 str: The rendered Markdown string of tasks matching the given pattern. 88 """ 89 return self.task_manager.query_by_title_keyword(regex_pattern) 90 91 @log() 92 def query_todos_by_assignee(self, assignee_name: str) -> str: 93 """ 94 Queries tasks assigned to a specific assignee. Currently, the assignee is hard-coded as 'Developer'. 95 96 Args: 97 assignee_name (str): The name of the assignee to query tasks for. 98 99 Returns: 100 str: The rendered Markdown string of tasks assigned to the specified assignee. 101 """ 102 return self.task_manager.query_by_assignee(assignee_name) 103 104 @log() 105 def list_valid_assignees( 106 self, 107 ) -> list[str]: 108 """ 109 Lists the valid assignees for tasks. 110 111 Returns: 112 list[str]: The rendered Markdown string of valid assignees. 113 """ 114 return self.task_manager.valid_assignees
Keep track of tasks.
20 def __init__(self, root_folder: str, config: Config) -> None: 21 """ 22 Initialize the TodoTool with a root folder. 23 24 Args: 25 root_folder (str): The root folder for valid files. 26 config (Config): The developer input that bot shouldn't set. 27 """ 28 self.root_folder: str = root_folder 29 self.config = config 30 self.roles = config.get_list("todo_roles") 31 self.task_manager = TaskManager(self.root_folder, self.roles)
Initialize the TodoTool with a root folder.
Args: root_folder (str): The root folder for valid files. config (Config): The developer input that bot shouldn't set.
33 @log() 34 def add_todo( 35 self, 36 title: str, 37 description: str, 38 category: str, 39 source_code_ref: str, 40 assignee: str | None = None, 41 done_when: str = "", 42 ) -> str: 43 """ 44 Adds a new task to the task manager. 45 46 Args: 47 title (str): The title of the task. 48 description (str): A description of the task. 49 category (str): The category of the task (e.g., 'bug', 'feature'). 50 source_code_ref (str): Reference to the source code related to the task. 51 assignee (str | None, optional): The name of the assignee. Defaults to None. 52 done_when (str, optional): Acceptance criteria — how to know the task is 53 complete. Defaults to "". 54 55 Returns: 56 str: A confirmation message indicating successful addition of the task. 57 """ 58 self.task_manager.add_task(title, description, category, source_code_ref, assignee, done_when) 59 summary = self.task_manager.get_stats() 60 return f"Successful added task {title}\n{summary}"
Adds a new task to the task manager.
Args: title (str): The title of the task. description (str): A description of the task. category (str): The category of the task (e.g., 'bug', 'feature'). source_code_ref (str): Reference to the source code related to the task. assignee (str | None, optional): The name of the assignee. Defaults to None. done_when (str, optional): Acceptance criteria — how to know the task is complete. Defaults to "".
Returns: str: A confirmation message indicating successful addition of the task.
62 @log() 63 def remove_todo(self, title: str) -> str: 64 """ 65 Marks a task as finished based on its title. 66 67 Args: 68 title (str): The title of the task to be marked as finished. 69 70 Returns: 71 str: A confirmation message indicating the task was successfully marked as finished. 72 """ 73 self.task_manager.finish_task(title) 74 summary = self.task_manager.get_stats() 75 return f"Successful removed task {title}\n{summary}"
Marks a task as finished based on its title.
Args: title (str): The title of the task to be marked as finished.
Returns: str: A confirmation message indicating the task was successfully marked as finished.
77 @log() 78 def query_todos_by_regex(self, regex_pattern: str = r"[\s\S]+") -> str: 79 r""" 80 Queries tasks by a keyword in their title, using a regular expression pattern. 81 82 Args: 83 regex_pattern (str, optional): The regular expression pattern to match in task titles. 84 Defaults to "[\s\S]+", which matches any title. 85 86 Returns: 87 str: The rendered Markdown string of tasks matching the given pattern. 88 """ 89 return self.task_manager.query_by_title_keyword(regex_pattern)
Queries tasks by a keyword in their title, using a regular expression pattern.
Args: regex_pattern (str, optional): The regular expression pattern to match in task titles. Defaults to "[\s\S]+", which matches any title.
Returns: str: The rendered Markdown string of tasks matching the given pattern.
91 @log() 92 def query_todos_by_assignee(self, assignee_name: str) -> str: 93 """ 94 Queries tasks assigned to a specific assignee. Currently, the assignee is hard-coded as 'Developer'. 95 96 Args: 97 assignee_name (str): The name of the assignee to query tasks for. 98 99 Returns: 100 str: The rendered Markdown string of tasks assigned to the specified assignee. 101 """ 102 return self.task_manager.query_by_assignee(assignee_name)
Queries tasks assigned to a specific assignee. Currently, the assignee is hard-coded as 'Developer'.
Args: assignee_name (str): The name of the assignee to query tasks for.
Returns: str: The rendered Markdown string of tasks assigned to the specified assignee.
104 @log() 105 def list_valid_assignees( 106 self, 107 ) -> list[str]: 108 """ 109 Lists the valid assignees for tasks. 110 111 Returns: 112 list[str]: The rendered Markdown string of valid assignees. 113 """ 114 return self.task_manager.valid_assignees
Lists the valid assignees for tasks.
Returns: list[str]: The rendered Markdown string of valid assignees.
23class AnswerCollectorTool: 24 def __init__(self, root_folder: str, config: Config) -> None: 25 """ 26 Initialize the PytestTool class. 27 28 Args: 29 root_folder (str): The root folder path for file operations. (Not used yet) 30 config (Config): The developer input that bot shouldn't set. 31 """ 32 self.root_folder = root_folder 33 self.config = config 34 self.comment: str | None = None 35 self.bool_answer: bool | None = None 36 self.json_answer: str | None = None 37 self.xml_answer: str | None = None 38 self.toml_answer: str | None = None 39 self.tuple_answer: tuple | None = None 40 self.set_answer: set | None = None 41 self.text_answer: str | None = None 42 self.list_answer: list[str] | None = None 43 self.int_answer: int | None = None 44 self.float_answer: float | None = None 45 self.dict_answer: dict[str, Any] | None = None 46 self.response_received = "Response received." 47 48 def _answered(self) -> None: 49 """Check if this tool has been used. 50 51 Raises: 52 TypeError: If the tool has been used. Recreate a new one after each usage. 53 """ 54 if any( 55 [ 56 self.comment, 57 self.bool_answer is not None, 58 self.json_answer, 59 self.xml_answer, 60 self.toml_answer, 61 self.tuple_answer, 62 self.set_answer, 63 self.text_answer, 64 self.list_answer, 65 self.int_answer, 66 self.float_answer, 67 self.dict_answer, 68 ] 69 ): 70 raise TypeError("This Answer tool has been used. Please create a new one for another answer.") 71 72 @log() 73 def report_list(self, answer: list[str], comment: str = "") -> str: 74 """Report answer in list format. 75 76 Args: 77 answer (list[str]): The answer to be reported in list format. 78 comment (str, optional): Any comments, supplemental info about the answer. 79 80 Returns: 81 str: A string indicating that the response has been received. 82 """ 83 self._answered() 84 self.list_answer = answer 85 self.comment = comment 86 return self.response_received 87 88 @log() 89 def report_int(self, answer: int, comment: str = "") -> str: 90 """Report answer in integer format 91 Args: 92 answer (int): The answer to be reported in integer format. 93 comment (str, optional): Any comments, supplemental info about the answer. 94 95 96 Returns: 97 str: A string indicating that the response has been received. 98 """ 99 self._answered() 100 self.int_answer = answer 101 self.comment = comment 102 return self.response_received 103 104 @log() 105 def report_float(self, answer: float, comment: str = "") -> str: 106 """Report answer in string format. 107 108 Args: 109 answer (float): The answer to be reported in float format. 110 comment (str, optional): Any comments, supplemental info about the answer. 111 112 Returns: 113 str: A string indicating that the response has been received. 114 """ 115 self._answered() 116 self.float_answer = answer 117 self.comment = comment 118 return self.response_received 119 120 @log() 121 def report_dict(self, answer: dict[str, Any], comment: str = "") -> str: 122 """Report answer in dict format. 123 124 Args: 125 answer (dict[str, Any]): The answer to be reported in dict format. 126 comment (str, optional): Any comments, supplemental info about the answer. 127 128 Returns: 129 str: A string indicating that the response has been received. 130 """ 131 self._answered() 132 self.dict_answer = answer 133 self.comment = comment 134 return self.response_received 135 136 @log() 137 def report_text(self, answer: str, comment: str = "") -> str: 138 """Report answer in string format. 139 140 Args: 141 answer (str): The answer to be reported in string format. 142 comment (str, optional): Any comments, supplemental info about the answer. 143 144 Returns: 145 str: A string indicating that the response has been received. 146 """ 147 self._answered() 148 self.text_answer = answer 149 self.comment = comment 150 return self.response_received 151 152 @log() 153 def report_bool(self, answer: bool, comment: str = "") -> str: 154 """Report answer in bool format. 155 156 Args: 157 answer (bool): The answer to be reported in bool format. 158 comment (str, optional): Any comments, supplemental info about the answer. 159 160 Returns: 161 str: A string indicating that the response has been received. 162 """ 163 self._answered() 164 self.bool_answer = answer 165 self.comment = comment 166 return self.response_received 167 168 @log() 169 def report_tuple(self, answer: tuple, comment: str = "") -> str: 170 """Report answer in tuple format. 171 172 Args: 173 answer (tuple): The answer to be reported in tuple format. 174 comment (str, optional): Any comments, supplemental info about the answer. 175 176 Returns: 177 str: A string indicating that the response has been received. 178 """ 179 self._answered() 180 self.tuple_answer = answer 181 self.comment = comment 182 return self.response_received 183 184 @log() 185 def report_set(self, answer: set, comment: str = "") -> str: 186 """Report answer in set format. 187 188 Args: 189 answer (set): The answer to be reported in set format. 190 comment (str, optional): Any comments, supplemental info about the answer. 191 192 Returns: 193 str: A string indicating that the response has been received. 194 """ 195 self._answered() 196 self.set_answer = answer 197 self.comment = comment 198 return self.response_received 199 200 @log() 201 def report_json(self, answer: str, comment: str = "") -> str: 202 """Report answer in json format. 203 204 Args: 205 answer (str): The answer to be reported in json format. 206 comment (str, optional): Any comments, supplemental info about the answer. 207 208 Returns: 209 str: A string indicating that the response has been received. 210 """ 211 self._answered() 212 self.json_answer = answer 213 self.comment = comment 214 return self.response_received 215 216 @log() 217 def report_xml(self, answer: str, comment: str = "") -> str: 218 """Report answer in xml format. 219 220 Args: 221 answer (str): The answer to be reported in xml format. 222 comment (str, optional): Any comments, supplemental info about the answer. 223 224 Returns: 225 str: A string indicating that the response has been received. 226 """ 227 self._answered() 228 self.xml_answer = answer 229 self.comment = comment 230 return self.response_received 231 232 @log() 233 def report_toml(self, answer: str, comment: str = "") -> str: 234 """Report answer in toml format. 235 236 Args: 237 answer (str): The answer to be reported in toml format. 238 comment (str, optional): Any comments, supplemental info about the answer. 239 240 Returns: 241 str: A string indicating that the response has been received. 242 """ 243 self._answered() 244 self.toml_answer = answer 245 self.comment = comment 246 return self.response_received
24 def __init__(self, root_folder: str, config: Config) -> None: 25 """ 26 Initialize the PytestTool class. 27 28 Args: 29 root_folder (str): The root folder path for file operations. (Not used yet) 30 config (Config): The developer input that bot shouldn't set. 31 """ 32 self.root_folder = root_folder 33 self.config = config 34 self.comment: str | None = None 35 self.bool_answer: bool | None = None 36 self.json_answer: str | None = None 37 self.xml_answer: str | None = None 38 self.toml_answer: str | None = None 39 self.tuple_answer: tuple | None = None 40 self.set_answer: set | None = None 41 self.text_answer: str | None = None 42 self.list_answer: list[str] | None = None 43 self.int_answer: int | None = None 44 self.float_answer: float | None = None 45 self.dict_answer: dict[str, Any] | None = None 46 self.response_received = "Response received."
Initialize the PytestTool class.
Args: root_folder (str): The root folder path for file operations. (Not used yet) config (Config): The developer input that bot shouldn't set.
72 @log() 73 def report_list(self, answer: list[str], comment: str = "") -> str: 74 """Report answer in list format. 75 76 Args: 77 answer (list[str]): The answer to be reported in list format. 78 comment (str, optional): Any comments, supplemental info about the answer. 79 80 Returns: 81 str: A string indicating that the response has been received. 82 """ 83 self._answered() 84 self.list_answer = answer 85 self.comment = comment 86 return self.response_received
Report answer in list format.
Args: answer (list[str]): The answer to be reported in list format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
88 @log() 89 def report_int(self, answer: int, comment: str = "") -> str: 90 """Report answer in integer format 91 Args: 92 answer (int): The answer to be reported in integer format. 93 comment (str, optional): Any comments, supplemental info about the answer. 94 95 96 Returns: 97 str: A string indicating that the response has been received. 98 """ 99 self._answered() 100 self.int_answer = answer 101 self.comment = comment 102 return self.response_received
Report answer in integer format Args: answer (int): The answer to be reported in integer format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
104 @log() 105 def report_float(self, answer: float, comment: str = "") -> str: 106 """Report answer in string format. 107 108 Args: 109 answer (float): The answer to be reported in float format. 110 comment (str, optional): Any comments, supplemental info about the answer. 111 112 Returns: 113 str: A string indicating that the response has been received. 114 """ 115 self._answered() 116 self.float_answer = answer 117 self.comment = comment 118 return self.response_received
Report answer in string format.
Args: answer (float): The answer to be reported in float format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
120 @log() 121 def report_dict(self, answer: dict[str, Any], comment: str = "") -> str: 122 """Report answer in dict format. 123 124 Args: 125 answer (dict[str, Any]): The answer to be reported in dict format. 126 comment (str, optional): Any comments, supplemental info about the answer. 127 128 Returns: 129 str: A string indicating that the response has been received. 130 """ 131 self._answered() 132 self.dict_answer = answer 133 self.comment = comment 134 return self.response_received
Report answer in dict format.
Args: answer (dict[str, Any]): The answer to be reported in dict format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
136 @log() 137 def report_text(self, answer: str, comment: str = "") -> str: 138 """Report answer in string format. 139 140 Args: 141 answer (str): The answer to be reported in string format. 142 comment (str, optional): Any comments, supplemental info about the answer. 143 144 Returns: 145 str: A string indicating that the response has been received. 146 """ 147 self._answered() 148 self.text_answer = answer 149 self.comment = comment 150 return self.response_received
Report answer in string format.
Args: answer (str): The answer to be reported in string format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
152 @log() 153 def report_bool(self, answer: bool, comment: str = "") -> str: 154 """Report answer in bool format. 155 156 Args: 157 answer (bool): The answer to be reported in bool format. 158 comment (str, optional): Any comments, supplemental info about the answer. 159 160 Returns: 161 str: A string indicating that the response has been received. 162 """ 163 self._answered() 164 self.bool_answer = answer 165 self.comment = comment 166 return self.response_received
Report answer in bool format.
Args: answer (bool): The answer to be reported in bool format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
168 @log() 169 def report_tuple(self, answer: tuple, comment: str = "") -> str: 170 """Report answer in tuple format. 171 172 Args: 173 answer (tuple): The answer to be reported in tuple format. 174 comment (str, optional): Any comments, supplemental info about the answer. 175 176 Returns: 177 str: A string indicating that the response has been received. 178 """ 179 self._answered() 180 self.tuple_answer = answer 181 self.comment = comment 182 return self.response_received
Report answer in tuple format.
Args: answer (tuple): The answer to be reported in tuple format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
184 @log() 185 def report_set(self, answer: set, comment: str = "") -> str: 186 """Report answer in set format. 187 188 Args: 189 answer (set): The answer to be reported in set format. 190 comment (str, optional): Any comments, supplemental info about the answer. 191 192 Returns: 193 str: A string indicating that the response has been received. 194 """ 195 self._answered() 196 self.set_answer = answer 197 self.comment = comment 198 return self.response_received
Report answer in set format.
Args: answer (set): The answer to be reported in set format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
200 @log() 201 def report_json(self, answer: str, comment: str = "") -> str: 202 """Report answer in json format. 203 204 Args: 205 answer (str): The answer to be reported in json format. 206 comment (str, optional): Any comments, supplemental info about the answer. 207 208 Returns: 209 str: A string indicating that the response has been received. 210 """ 211 self._answered() 212 self.json_answer = answer 213 self.comment = comment 214 return self.response_received
Report answer in json format.
Args: answer (str): The answer to be reported in json format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
216 @log() 217 def report_xml(self, answer: str, comment: str = "") -> str: 218 """Report answer in xml format. 219 220 Args: 221 answer (str): The answer to be reported in xml format. 222 comment (str, optional): Any comments, supplemental info about the answer. 223 224 Returns: 225 str: A string indicating that the response has been received. 226 """ 227 self._answered() 228 self.xml_answer = answer 229 self.comment = comment 230 return self.response_received
Report answer in xml format.
Args: answer (str): The answer to be reported in xml format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
232 @log() 233 def report_toml(self, answer: str, comment: str = "") -> str: 234 """Report answer in toml format. 235 236 Args: 237 answer (str): The answer to be reported in toml format. 238 comment (str, optional): Any comments, supplemental info about the answer. 239 240 Returns: 241 str: A string indicating that the response has been received. 242 """ 243 self._answered() 244 self.toml_answer = answer 245 self.comment = comment 246 return self.response_received
Report answer in toml format.
Args: answer (str): The answer to be reported in toml format. comment (str, optional): Any comments, supplemental info about the answer.
Returns: str: A string indicating that the response has been received.
12class PytestTool: 13 """Optimized for AI version of pytest.""" 14 15 def __init__(self, root_folder: str, config: Config) -> None: 16 """ 17 Initialize the PytestTool class. 18 19 Args: 20 root_folder (str): The root folder path for file operations. 21 config (Config): The developer input that bot shouldn't set. 22 """ 23 self.root_folder = root_folder 24 25 self.config = config 26 self.module = config.get_value("pytest_module") 27 self.tests_folder = config.get_value("pytest_folder") 28 29 self.min_coverage = float(config.get_value("pytest_min_coverage") or 0.0) 30 31 @log() 32 def pytest( 33 self, 34 ) -> str: 35 """ 36 Runs pytest on tests in tests folder.. 37 38 Returns: 39 str: Output from pytest. 40 """ 41 # Host script must set env vars, temp folder location and pwd! 42 # with change_directory(self.root_folder): 43 # What is -rA 44 if not self.module or not self.tests_folder or self.min_coverage: 45 raise FatalConfigurationError("Please set in ai_config module, test_folder and min_coverage") 46 _passed_tests, _failed_tests, _coverage, command_result = count_pytest_results( 47 self.module, self.tests_folder, self.min_coverage 48 ) 49 markdown_output = f"""## Pytest Output 50### Standard Output 51{command_result.stdout} 52### Standard Error 53{command_result.stderr} 54### Return Code 55`{command_result.return_code}`""" 56 return markdown_output
Optimized for AI version of pytest.
15 def __init__(self, root_folder: str, config: Config) -> None: 16 """ 17 Initialize the PytestTool class. 18 19 Args: 20 root_folder (str): The root folder path for file operations. 21 config (Config): The developer input that bot shouldn't set. 22 """ 23 self.root_folder = root_folder 24 25 self.config = config 26 self.module = config.get_value("pytest_module") 27 self.tests_folder = config.get_value("pytest_folder") 28 29 self.min_coverage = float(config.get_value("pytest_min_coverage") or 0.0)
Initialize the PytestTool class.
Args: root_folder (str): The root folder path for file operations. config (Config): The developer input that bot shouldn't set.
31 @log() 32 def pytest( 33 self, 34 ) -> str: 35 """ 36 Runs pytest on tests in tests folder.. 37 38 Returns: 39 str: Output from pytest. 40 """ 41 # Host script must set env vars, temp folder location and pwd! 42 # with change_directory(self.root_folder): 43 # What is -rA 44 if not self.module or not self.tests_folder or self.min_coverage: 45 raise FatalConfigurationError("Please set in ai_config module, test_folder and min_coverage") 46 _passed_tests, _failed_tests, _coverage, command_result = count_pytest_results( 47 self.module, self.tests_folder, self.min_coverage 48 ) 49 markdown_output = f"""## Pytest Output 50### Standard Output 51{command_result.stdout} 52### Standard Error 53{command_result.stderr} 54### Return Code 55`{command_result.return_code}`""" 56 return markdown_output
Runs pytest on tests in tests folder..
Returns: str: Output from pytest.
32class ToolKit(ToolKitBase): 33 """AI Shell Toolkit""" 34 35 def __init__( 36 self, root_folder: str, token_model: str, global_max_lines: int, permitted_tools: list[str], config: Config 37 ) -> None: 38 super().__init__(root_folder, token_model, global_max_lines, permitted_tools, config) 39 self._lookup: dict[str, Callable[[dict[str, Any]], Any]] = { 40 "report_bool": self.report_bool, 41 "report_dict": self.report_dict, 42 "report_float": self.report_float, 43 "report_int": self.report_int, 44 "report_json": self.report_json, 45 "report_list": self.report_list, 46 "report_set": self.report_set, 47 "report_text": self.report_text, 48 "report_toml": self.report_toml, 49 "report_tuple": self.report_tuple, 50 "report_xml": self.report_xml, 51 "cat": self.cat, 52 "cat_markdown": self.cat_markdown, 53 "cut_characters": self.cut_characters, 54 "cut_fields": self.cut_fields, 55 "cut_fields_by_name": self.cut_fields_by_name, 56 "find_files": self.find_files, 57 "find_files_markdown": self.find_files_markdown, 58 "get_current_branch": self.get_current_branch, 59 "get_recent_commits": self.get_recent_commits, 60 "git_diff": self.git_diff, 61 "git_diff_commit": self.git_diff_commit, 62 "git_log_file": self.git_log_file, 63 "git_log_search": self.git_log_search, 64 "git_show": self.git_show, 65 "git_status": self.git_status, 66 "is_ignored_by_gitignore": self.is_ignored_by_gitignore, 67 "grep": self.grep, 68 "grep_markdown": self.grep_markdown, 69 "head": self.head, 70 "head_markdown": self.head_markdown, 71 "head_tail": self.head_tail, 72 "tail": self.tail, 73 "tail_markdown": self.tail_markdown, 74 "insert_text_after_context": self.insert_text_after_context, 75 "insert_text_after_multiline_context": self.insert_text_after_multiline_context, 76 "insert_text_at_start_or_end": self.insert_text_at_start_or_end, 77 "ls": self.ls, 78 "ls_markdown": self.ls_markdown, 79 "apply_git_patch": self.apply_git_patch, 80 "format_code_as_markdown": self.format_code_as_markdown, 81 "pytest": self.pytest, 82 "replace_all": self.replace_all, 83 "replace_line_by_line": self.replace_line_by_line, 84 "replace_with_regex": self.replace_with_regex, 85 "rewrite_file": self.rewrite_file, 86 "write_new_file": self.write_new_file, 87 "sed": self.sed, 88 "add_todo": self.add_todo, 89 "list_valid_assignees": self.list_valid_assignees, 90 "query_todos_by_assignee": self.query_todos_by_assignee, 91 "query_todos_by_regex": self.query_todos_by_regex, 92 "remove_todo": self.remove_todo, 93 "count_tokens": self.count_tokens, 94 } 95 # Stateful tool support. Useless assignment to make mypy happy 96 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 97 98 def report_bool(self, arguments: dict[str, Any]) -> Any: 99 """Generated Do Not Edit 100 101 Args: 102 arguments (dict[str, Any]): The arguments for the tool. 103 104 Returns: 105 Any: The result of the tool invocation. 106 """ 107 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 108 109 answer = cast( 110 bool, 111 arguments.get( 112 "answer", 113 ), 114 ) 115 comment = cast(str, arguments.get("comment", "")) 116 return self.tool_answer_collector.report_bool(answer=answer, comment=comment) 117 118 def report_dict(self, arguments: dict[str, Any]) -> Any: 119 """Generated Do Not Edit 120 121 Args: 122 arguments (dict[str, Any]): The arguments for the tool. 123 124 Returns: 125 Any: The result of the tool invocation. 126 """ 127 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 128 129 answer = cast( 130 Any, 131 arguments.get( 132 "answer", 133 ), 134 ) 135 comment = cast(str, arguments.get("comment", "")) 136 return self.tool_answer_collector.report_dict(answer=answer, comment=comment) 137 138 def report_float(self, arguments: dict[str, Any]) -> Any: 139 """Generated Do Not Edit 140 141 Args: 142 arguments (dict[str, Any]): The arguments for the tool. 143 144 Returns: 145 Any: The result of the tool invocation. 146 """ 147 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 148 149 answer = cast( 150 float, 151 arguments.get( 152 "answer", 153 ), 154 ) 155 comment = cast(str, arguments.get("comment", "")) 156 return self.tool_answer_collector.report_float(answer=answer, comment=comment) 157 158 def report_int(self, arguments: dict[str, Any]) -> Any: 159 """Generated Do Not Edit 160 161 Args: 162 arguments (dict[str, Any]): The arguments for the tool. 163 164 Returns: 165 Any: The result of the tool invocation. 166 """ 167 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 168 169 answer = cast( 170 int, 171 arguments.get( 172 "answer", 173 ), 174 ) 175 comment = cast(str, arguments.get("comment", "")) 176 return self.tool_answer_collector.report_int(answer=answer, comment=comment) 177 178 def report_json(self, arguments: dict[str, Any]) -> Any: 179 """Generated Do Not Edit 180 181 Args: 182 arguments (dict[str, Any]): The arguments for the tool. 183 184 Returns: 185 Any: The result of the tool invocation. 186 """ 187 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 188 189 answer = cast( 190 str, 191 arguments.get( 192 "answer", 193 ), 194 ) 195 comment = cast(str, arguments.get("comment", "")) 196 return self.tool_answer_collector.report_json(answer=answer, comment=comment) 197 198 def report_list(self, arguments: dict[str, Any]) -> Any: 199 """Generated Do Not Edit 200 201 Args: 202 arguments (dict[str, Any]): The arguments for the tool. 203 204 Returns: 205 Any: The result of the tool invocation. 206 """ 207 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 208 209 answer = cast( 210 str, 211 arguments.get( 212 "answer", 213 ), 214 ) 215 comment = cast(str, arguments.get("comment", "")) 216 return self.tool_answer_collector.report_list(answer=answer, comment=comment) 217 218 def report_set(self, arguments: dict[str, Any]) -> Any: 219 """Generated Do Not Edit 220 221 Args: 222 arguments (dict[str, Any]): The arguments for the tool. 223 224 Returns: 225 Any: The result of the tool invocation. 226 """ 227 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 228 229 answer = cast( 230 list[Any], 231 arguments.get( 232 "answer", 233 ), 234 ) 235 comment = cast(str, arguments.get("comment", "")) 236 return self.tool_answer_collector.report_set(answer=answer, comment=comment) 237 238 def report_text(self, arguments: dict[str, Any]) -> Any: 239 """Generated Do Not Edit 240 241 Args: 242 arguments (dict[str, Any]): The arguments for the tool. 243 244 Returns: 245 Any: The result of the tool invocation. 246 """ 247 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 248 249 answer = cast( 250 str, 251 arguments.get( 252 "answer", 253 ), 254 ) 255 comment = cast(str, arguments.get("comment", "")) 256 return self.tool_answer_collector.report_text(answer=answer, comment=comment) 257 258 def report_toml(self, arguments: dict[str, Any]) -> Any: 259 """Generated Do Not Edit 260 261 Args: 262 arguments (dict[str, Any]): The arguments for the tool. 263 264 Returns: 265 Any: The result of the tool invocation. 266 """ 267 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 268 269 answer = cast( 270 str, 271 arguments.get( 272 "answer", 273 ), 274 ) 275 comment = cast(str, arguments.get("comment", "")) 276 return self.tool_answer_collector.report_toml(answer=answer, comment=comment) 277 278 def report_tuple(self, arguments: dict[str, Any]) -> Any: 279 """Generated Do Not Edit 280 281 Args: 282 arguments (dict[str, Any]): The arguments for the tool. 283 284 Returns: 285 Any: The result of the tool invocation. 286 """ 287 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 288 289 answer = cast( 290 list[Any], 291 arguments.get( 292 "answer", 293 ), 294 ) 295 comment = cast(str, arguments.get("comment", "")) 296 return self.tool_answer_collector.report_tuple(answer=answer, comment=comment) 297 298 def report_xml(self, arguments: dict[str, Any]) -> Any: 299 """Generated Do Not Edit 300 301 Args: 302 arguments (dict[str, Any]): The arguments for the tool. 303 304 Returns: 305 Any: The result of the tool invocation. 306 """ 307 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 308 309 answer = cast( 310 str, 311 arguments.get( 312 "answer", 313 ), 314 ) 315 comment = cast(str, arguments.get("comment", "")) 316 return self.tool_answer_collector.report_xml(answer=answer, comment=comment) 317 318 def cat(self, arguments: dict[str, Any]) -> Any: 319 """Generated Do Not Edit 320 321 Args: 322 arguments (dict[str, Any]): The arguments for the tool. 323 324 Returns: 325 Any: The result of the tool invocation. 326 """ 327 tool = CatTool(self.root_folder, self.config) 328 329 file_paths = cast( 330 str, 331 arguments.get( 332 "file_paths", 333 ), 334 ) 335 number_lines = cast(bool, arguments.get("number_lines", True)) 336 squeeze_blank = cast(bool, arguments.get("squeeze_blank", False)) 337 return tool.cat(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank) 338 339 def cat_markdown(self, arguments: dict[str, Any]) -> Any: 340 """Generated Do Not Edit 341 342 Args: 343 arguments (dict[str, Any]): The arguments for the tool. 344 345 Returns: 346 Any: The result of the tool invocation. 347 """ 348 tool = CatTool(self.root_folder, self.config) 349 350 file_paths = cast( 351 str, 352 arguments.get( 353 "file_paths", 354 ), 355 ) 356 number_lines = cast(bool, arguments.get("number_lines", True)) 357 squeeze_blank = cast(bool, arguments.get("squeeze_blank", False)) 358 return tool.cat_markdown(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank) 359 360 def cut_characters(self, arguments: dict[str, Any]) -> Any: 361 """Generated Do Not Edit 362 363 Args: 364 arguments (dict[str, Any]): The arguments for the tool. 365 366 Returns: 367 Any: The result of the tool invocation. 368 """ 369 tool = CutTool(self.root_folder, self.config) 370 371 character_ranges = cast( 372 str, 373 arguments.get( 374 "character_ranges", 375 ), 376 ) 377 file_path = cast( 378 str, 379 arguments.get( 380 "file_path", 381 ), 382 ) 383 return tool.cut_characters(character_ranges=character_ranges, file_path=file_path) 384 385 def cut_fields(self, arguments: dict[str, Any]) -> Any: 386 """Generated Do Not Edit 387 388 Args: 389 arguments (dict[str, Any]): The arguments for the tool. 390 391 Returns: 392 Any: The result of the tool invocation. 393 """ 394 tool = CutTool(self.root_folder, self.config) 395 396 delimiter = cast(str, arguments.get("delimiter", ",")) 397 field_ranges = cast( 398 str, 399 arguments.get( 400 "field_ranges", 401 ), 402 ) 403 filename = cast( 404 str, 405 arguments.get( 406 "filename", 407 ), 408 ) 409 return tool.cut_fields(delimiter=delimiter, field_ranges=field_ranges, filename=filename) 410 411 def cut_fields_by_name(self, arguments: dict[str, Any]) -> Any: 412 """Generated Do Not Edit 413 414 Args: 415 arguments (dict[str, Any]): The arguments for the tool. 416 417 Returns: 418 Any: The result of the tool invocation. 419 """ 420 tool = CutTool(self.root_folder, self.config) 421 422 delimiter = cast(str, arguments.get("delimiter", ",")) 423 field_names = cast( 424 str, 425 arguments.get( 426 "field_names", 427 ), 428 ) 429 filename = cast( 430 str, 431 arguments.get( 432 "filename", 433 ), 434 ) 435 return tool.cut_fields_by_name(delimiter=delimiter, field_names=field_names, filename=filename) 436 437 def find_files(self, arguments: dict[str, Any]) -> Any: 438 """Generated Do Not Edit 439 440 Args: 441 arguments (dict[str, Any]): The arguments for the tool. 442 443 Returns: 444 Any: The result of the tool invocation. 445 """ 446 tool = FindTool(self.root_folder, self.config) 447 448 file_type = cast( 449 str | None, 450 arguments.get( 451 "file_type", 452 ), 453 ) 454 name = cast( 455 str | None, 456 arguments.get( 457 "name", 458 ), 459 ) 460 regex = cast( 461 str | None, 462 arguments.get( 463 "regex", 464 ), 465 ) 466 size = cast( 467 str | None, 468 arguments.get( 469 "size", 470 ), 471 ) 472 return tool.find_files(file_type=file_type, name=name, regex=regex, size=size) 473 474 def find_files_markdown(self, arguments: dict[str, Any]) -> Any: 475 """Generated Do Not Edit 476 477 Args: 478 arguments (dict[str, Any]): The arguments for the tool. 479 480 Returns: 481 Any: The result of the tool invocation. 482 """ 483 tool = FindTool(self.root_folder, self.config) 484 485 file_type = cast( 486 str | None, 487 arguments.get( 488 "file_type", 489 ), 490 ) 491 name = cast( 492 str | None, 493 arguments.get( 494 "name", 495 ), 496 ) 497 regex = cast( 498 str | None, 499 arguments.get( 500 "regex", 501 ), 502 ) 503 size = cast( 504 str | None, 505 arguments.get( 506 "size", 507 ), 508 ) 509 return tool.find_files_markdown(file_type=file_type, name=name, regex=regex, size=size) 510 511 def get_current_branch(self, arguments: dict[str, Any]) -> Any: 512 """Generated Do Not Edit 513 514 Args: 515 arguments (dict[str, Any]): The arguments for the tool. 516 517 Returns: 518 Any: The result of the tool invocation. 519 """ 520 tool = GitTool(self.root_folder, self.config) 521 522 return tool.get_current_branch() 523 524 def get_recent_commits(self, arguments: dict[str, Any]) -> Any: 525 """Generated Do Not Edit 526 527 Args: 528 arguments (dict[str, Any]): The arguments for the tool. 529 530 Returns: 531 Any: The result of the tool invocation. 532 """ 533 tool = GitTool(self.root_folder, self.config) 534 535 n = cast(int, arguments.get("n", 10)) 536 short_hash = cast(bool, arguments.get("short_hash", False)) 537 return tool.get_recent_commits(n=n, short_hash=short_hash) 538 539 def git_diff(self, arguments: dict[str, Any]) -> Any: 540 """Generated Do Not Edit 541 542 Args: 543 arguments (dict[str, Any]): The arguments for the tool. 544 545 Returns: 546 Any: The result of the tool invocation. 547 """ 548 tool = GitTool(self.root_folder, self.config) 549 550 return tool.git_diff() 551 552 def git_diff_commit(self, arguments: dict[str, Any]) -> Any: 553 """Generated Do Not Edit 554 555 Args: 556 arguments (dict[str, Any]): The arguments for the tool. 557 558 Returns: 559 Any: The result of the tool invocation. 560 """ 561 tool = GitTool(self.root_folder, self.config) 562 563 commit1 = cast( 564 str, 565 arguments.get( 566 "commit1", 567 ), 568 ) 569 commit2 = cast( 570 str, 571 arguments.get( 572 "commit2", 573 ), 574 ) 575 return tool.git_diff_commit(commit1=commit1, commit2=commit2) 576 577 def git_log_file(self, arguments: dict[str, Any]) -> Any: 578 """Generated Do Not Edit 579 580 Args: 581 arguments (dict[str, Any]): The arguments for the tool. 582 583 Returns: 584 Any: The result of the tool invocation. 585 """ 586 tool = GitTool(self.root_folder, self.config) 587 588 filename = cast( 589 str, 590 arguments.get( 591 "filename", 592 ), 593 ) 594 return tool.git_log_file(filename=filename) 595 596 def git_log_search(self, arguments: dict[str, Any]) -> Any: 597 """Generated Do Not Edit 598 599 Args: 600 arguments (dict[str, Any]): The arguments for the tool. 601 602 Returns: 603 Any: The result of the tool invocation. 604 """ 605 tool = GitTool(self.root_folder, self.config) 606 607 search_string = cast( 608 str, 609 arguments.get( 610 "search_string", 611 ), 612 ) 613 return tool.git_log_search(search_string=search_string) 614 615 def git_show(self, arguments: dict[str, Any]) -> Any: 616 """Generated Do Not Edit 617 618 Args: 619 arguments (dict[str, Any]): The arguments for the tool. 620 621 Returns: 622 Any: The result of the tool invocation. 623 """ 624 tool = GitTool(self.root_folder, self.config) 625 626 return tool.git_show() 627 628 def git_status(self, arguments: dict[str, Any]) -> Any: 629 """Generated Do Not Edit 630 631 Args: 632 arguments (dict[str, Any]): The arguments for the tool. 633 634 Returns: 635 Any: The result of the tool invocation. 636 """ 637 tool = GitTool(self.root_folder, self.config) 638 639 return tool.git_status() 640 641 def is_ignored_by_gitignore(self, arguments: dict[str, Any]) -> Any: 642 """Generated Do Not Edit 643 644 Args: 645 arguments (dict[str, Any]): The arguments for the tool. 646 647 Returns: 648 Any: The result of the tool invocation. 649 """ 650 tool = GitTool(self.root_folder, self.config) 651 652 file_path = cast( 653 str, 654 arguments.get( 655 "file_path", 656 ), 657 ) 658 gitignore_path = cast(str, arguments.get("gitignore_path", ".gitignore")) 659 return tool.is_ignored_by_gitignore(file_path=file_path, gitignore_path=gitignore_path) 660 661 def grep(self, arguments: dict[str, Any]) -> Any: 662 """Generated Do Not Edit 663 664 Args: 665 arguments (dict[str, Any]): The arguments for the tool. 666 667 Returns: 668 Any: The result of the tool invocation. 669 """ 670 tool = GrepTool(self.root_folder, self.config) 671 672 glob_pattern = cast( 673 str, 674 arguments.get( 675 "glob_pattern", 676 ), 677 ) 678 maximum_matches_per_file = cast(int, arguments.get("maximum_matches_per_file", -1)) 679 maximum_matches_total = cast(int, arguments.get("maximum_matches_total", -1)) 680 regex = cast( 681 str, 682 arguments.get( 683 "regex", 684 ), 685 ) 686 skip_first_matches = cast(int, arguments.get("skip_first_matches", -1)) 687 return tool.grep( 688 glob_pattern=glob_pattern, 689 maximum_matches_per_file=maximum_matches_per_file, 690 maximum_matches_total=maximum_matches_total, 691 regex=regex, 692 skip_first_matches=skip_first_matches, 693 ) 694 695 def grep_markdown(self, arguments: dict[str, Any]) -> Any: 696 """Generated Do Not Edit 697 698 Args: 699 arguments (dict[str, Any]): The arguments for the tool. 700 701 Returns: 702 Any: The result of the tool invocation. 703 """ 704 tool = GrepTool(self.root_folder, self.config) 705 706 glob_pattern = cast( 707 str, 708 arguments.get( 709 "glob_pattern", 710 ), 711 ) 712 maximum_matches = cast(int, arguments.get("maximum_matches", -1)) 713 regex = cast( 714 str, 715 arguments.get( 716 "regex", 717 ), 718 ) 719 skip_first_matches = cast(int, arguments.get("skip_first_matches", -1)) 720 return tool.grep_markdown( 721 glob_pattern=glob_pattern, 722 maximum_matches=maximum_matches, 723 regex=regex, 724 skip_first_matches=skip_first_matches, 725 ) 726 727 def head(self, arguments: dict[str, Any]) -> Any: 728 """Generated Do Not Edit 729 730 Args: 731 arguments (dict[str, Any]): The arguments for the tool. 732 733 Returns: 734 Any: The result of the tool invocation. 735 """ 736 tool = HeadTailTool(self.root_folder, self.config) 737 738 byte_count = cast( 739 int | None, 740 arguments.get( 741 "byte_count", 742 ), 743 ) 744 file_path = cast( 745 str, 746 arguments.get( 747 "file_path", 748 ), 749 ) 750 lines = cast(int, arguments.get("lines", 10)) 751 return tool.head(byte_count=byte_count, file_path=file_path, lines=lines) 752 753 def head_markdown(self, arguments: dict[str, Any]) -> Any: 754 """Generated Do Not Edit 755 756 Args: 757 arguments (dict[str, Any]): The arguments for the tool. 758 759 Returns: 760 Any: The result of the tool invocation. 761 """ 762 tool = HeadTailTool(self.root_folder, self.config) 763 764 file_path = cast( 765 str, 766 arguments.get( 767 "file_path", 768 ), 769 ) 770 lines = cast(int, arguments.get("lines", 10)) 771 return tool.head_markdown(file_path=file_path, lines=lines) 772 773 def head_tail(self, arguments: dict[str, Any]) -> Any: 774 """Generated Do Not Edit 775 776 Args: 777 arguments (dict[str, Any]): The arguments for the tool. 778 779 Returns: 780 Any: The result of the tool invocation. 781 """ 782 tool = HeadTailTool(self.root_folder, self.config) 783 784 byte_count = cast( 785 int | None, 786 arguments.get( 787 "byte_count", 788 ), 789 ) 790 file_path = cast( 791 str, 792 arguments.get( 793 "file_path", 794 ), 795 ) 796 lines = cast(int, arguments.get("lines", 10)) 797 mode = cast(str, arguments.get("mode", "head")) 798 return tool.head_tail(byte_count=byte_count, file_path=file_path, lines=lines, mode=mode) 799 800 def tail(self, arguments: dict[str, Any]) -> Any: 801 """Generated Do Not Edit 802 803 Args: 804 arguments (dict[str, Any]): The arguments for the tool. 805 806 Returns: 807 Any: The result of the tool invocation. 808 """ 809 tool = HeadTailTool(self.root_folder, self.config) 810 811 byte_count = cast( 812 int | None, 813 arguments.get( 814 "byte_count", 815 ), 816 ) 817 file_path = cast( 818 str, 819 arguments.get( 820 "file_path", 821 ), 822 ) 823 lines = cast(int, arguments.get("lines", 10)) 824 return tool.tail(byte_count=byte_count, file_path=file_path, lines=lines) 825 826 def tail_markdown(self, arguments: dict[str, Any]) -> Any: 827 """Generated Do Not Edit 828 829 Args: 830 arguments (dict[str, Any]): The arguments for the tool. 831 832 Returns: 833 Any: The result of the tool invocation. 834 """ 835 tool = HeadTailTool(self.root_folder, self.config) 836 837 file_path = cast( 838 str, 839 arguments.get( 840 "file_path", 841 ), 842 ) 843 lines = cast(int, arguments.get("lines", 10)) 844 return tool.tail_markdown(file_path=file_path, lines=lines) 845 846 def insert_text_after_context(self, arguments: dict[str, Any]) -> Any: 847 """Generated Do Not Edit 848 849 Args: 850 arguments (dict[str, Any]): The arguments for the tool. 851 852 Returns: 853 Any: The result of the tool invocation. 854 """ 855 tool = InsertTool(self.root_folder, self.config) 856 857 context = cast( 858 str, 859 arguments.get( 860 "context", 861 ), 862 ) 863 file_path = cast( 864 str, 865 arguments.get( 866 "file_path", 867 ), 868 ) 869 text_to_insert = cast( 870 str, 871 arguments.get( 872 "text_to_insert", 873 ), 874 ) 875 return tool.insert_text_after_context(context=context, file_path=file_path, text_to_insert=text_to_insert) 876 877 def insert_text_after_multiline_context(self, arguments: dict[str, Any]) -> Any: 878 """Generated Do Not Edit 879 880 Args: 881 arguments (dict[str, Any]): The arguments for the tool. 882 883 Returns: 884 Any: The result of the tool invocation. 885 """ 886 tool = InsertTool(self.root_folder, self.config) 887 888 context_lines = cast( 889 str, 890 arguments.get( 891 "context_lines", 892 ), 893 ) 894 file_path = cast( 895 str, 896 arguments.get( 897 "file_path", 898 ), 899 ) 900 text_to_insert = cast( 901 str, 902 arguments.get( 903 "text_to_insert", 904 ), 905 ) 906 return tool.insert_text_after_multiline_context( 907 context_lines=context_lines, file_path=file_path, text_to_insert=text_to_insert 908 ) 909 910 def insert_text_at_start_or_end(self, arguments: dict[str, Any]) -> Any: 911 """Generated Do Not Edit 912 913 Args: 914 arguments (dict[str, Any]): The arguments for the tool. 915 916 Returns: 917 Any: The result of the tool invocation. 918 """ 919 tool = InsertTool(self.root_folder, self.config) 920 921 file_path = cast( 922 str, 923 arguments.get( 924 "file_path", 925 ), 926 ) 927 position = cast(str, arguments.get("position", "end")) 928 text_to_insert = cast( 929 str, 930 arguments.get( 931 "text_to_insert", 932 ), 933 ) 934 return tool.insert_text_at_start_or_end(file_path=file_path, position=position, text_to_insert=text_to_insert) 935 936 def ls(self, arguments: dict[str, Any]) -> Any: 937 """Generated Do Not Edit 938 939 Args: 940 arguments (dict[str, Any]): The arguments for the tool. 941 942 Returns: 943 Any: The result of the tool invocation. 944 """ 945 tool = LsTool(self.root_folder, self.config) 946 947 all_files = cast(bool, arguments.get("all_files", False)) 948 long = cast(bool, arguments.get("long", False)) 949 path = cast( 950 str | None, 951 arguments.get( 952 "path", 953 ), 954 ) 955 return tool.ls(all_files=all_files, long=long, path=path) 956 957 def ls_markdown(self, arguments: dict[str, Any]) -> Any: 958 """Generated Do Not Edit 959 960 Args: 961 arguments (dict[str, Any]): The arguments for the tool. 962 963 Returns: 964 Any: The result of the tool invocation. 965 """ 966 tool = LsTool(self.root_folder, self.config) 967 968 all_files = cast(bool, arguments.get("all_files", False)) 969 long = cast(bool, arguments.get("long", False)) 970 path = cast(str | None, arguments.get("path", ".")) 971 return tool.ls_markdown(all_files=all_files, long=long, path=path) 972 973 def apply_git_patch(self, arguments: dict[str, Any]) -> Any: 974 """Generated Do Not Edit 975 976 Args: 977 arguments (dict[str, Any]): The arguments for the tool. 978 979 Returns: 980 Any: The result of the tool invocation. 981 """ 982 tool = PatchTool(self.root_folder, self.config) 983 984 patch_content = cast( 985 str, 986 arguments.get( 987 "patch_content", 988 ), 989 ) 990 return tool.apply_git_patch(patch_content=patch_content) 991 992 def format_code_as_markdown(self, arguments: dict[str, Any]) -> Any: 993 """Generated Do Not Edit 994 995 Args: 996 arguments (dict[str, Any]): The arguments for the tool. 997 998 Returns: 999 Any: The result of the tool invocation. 1000 """ 1001 tool = PyCatTool(self.root_folder, self.config) 1002 1003 base_path = cast( 1004 str, 1005 arguments.get( 1006 "base_path", 1007 ), 1008 ) 1009 header = cast( 1010 str, 1011 arguments.get( 1012 "header", 1013 ), 1014 ) 1015 no_comments = cast(bool, arguments.get("no_comments", False)) 1016 no_docs = cast(bool, arguments.get("no_docs", False)) 1017 return tool.format_code_as_markdown( 1018 base_path=base_path, header=header, no_comments=no_comments, no_docs=no_docs 1019 ) 1020 1021 def pytest(self, arguments: dict[str, Any]) -> Any: 1022 """Generated Do Not Edit 1023 1024 Args: 1025 arguments (dict[str, Any]): The arguments for the tool. 1026 1027 Returns: 1028 Any: The result of the tool invocation. 1029 """ 1030 tool = PytestTool(self.root_folder, self.config) 1031 1032 return tool.pytest() 1033 1034 def replace_all(self, arguments: dict[str, Any]) -> Any: 1035 """Generated Do Not Edit 1036 1037 Args: 1038 arguments (dict[str, Any]): The arguments for the tool. 1039 1040 Returns: 1041 Any: The result of the tool invocation. 1042 """ 1043 tool = ReplaceTool(self.root_folder, self.config) 1044 1045 file_path = cast( 1046 str, 1047 arguments.get( 1048 "file_path", 1049 ), 1050 ) 1051 new_text = cast( 1052 str, 1053 arguments.get( 1054 "new_text", 1055 ), 1056 ) 1057 old_text = cast( 1058 str, 1059 arguments.get( 1060 "old_text", 1061 ), 1062 ) 1063 return tool.replace_all(file_path=file_path, new_text=new_text, old_text=old_text) 1064 1065 def replace_line_by_line(self, arguments: dict[str, Any]) -> Any: 1066 """Generated Do Not Edit 1067 1068 Args: 1069 arguments (dict[str, Any]): The arguments for the tool. 1070 1071 Returns: 1072 Any: The result of the tool invocation. 1073 """ 1074 tool = ReplaceTool(self.root_folder, self.config) 1075 1076 file_path = cast( 1077 str, 1078 arguments.get( 1079 "file_path", 1080 ), 1081 ) 1082 line_end = cast(int, arguments.get("line_end", -1)) 1083 line_start = cast(int, arguments.get("line_start", 0)) 1084 new_text = cast( 1085 str, 1086 arguments.get( 1087 "new_text", 1088 ), 1089 ) 1090 old_text = cast( 1091 str, 1092 arguments.get( 1093 "old_text", 1094 ), 1095 ) 1096 return tool.replace_line_by_line( 1097 file_path=file_path, line_end=line_end, line_start=line_start, new_text=new_text, old_text=old_text 1098 ) 1099 1100 def replace_with_regex(self, arguments: dict[str, Any]) -> Any: 1101 """Generated Do Not Edit 1102 1103 Args: 1104 arguments (dict[str, Any]): The arguments for the tool. 1105 1106 Returns: 1107 Any: The result of the tool invocation. 1108 """ 1109 tool = ReplaceTool(self.root_folder, self.config) 1110 1111 file_path = cast( 1112 str, 1113 arguments.get( 1114 "file_path", 1115 ), 1116 ) 1117 regex_match_expression = cast( 1118 str, 1119 arguments.get( 1120 "regex_match_expression", 1121 ), 1122 ) 1123 replacement = cast( 1124 str, 1125 arguments.get( 1126 "replacement", 1127 ), 1128 ) 1129 return tool.replace_with_regex( 1130 file_path=file_path, regex_match_expression=regex_match_expression, replacement=replacement 1131 ) 1132 1133 def rewrite_file(self, arguments: dict[str, Any]) -> Any: 1134 """Generated Do Not Edit 1135 1136 Args: 1137 arguments (dict[str, Any]): The arguments for the tool. 1138 1139 Returns: 1140 Any: The result of the tool invocation. 1141 """ 1142 tool = RewriteTool(self.root_folder, self.config) 1143 1144 file_path = cast( 1145 str, 1146 arguments.get( 1147 "file_path", 1148 ), 1149 ) 1150 text = cast( 1151 str, 1152 arguments.get( 1153 "text", 1154 ), 1155 ) 1156 return tool.rewrite_file(file_path=file_path, text=text) 1157 1158 def write_new_file(self, arguments: dict[str, Any]) -> Any: 1159 """Generated Do Not Edit 1160 1161 Args: 1162 arguments (dict[str, Any]): The arguments for the tool. 1163 1164 Returns: 1165 Any: The result of the tool invocation. 1166 """ 1167 tool = RewriteTool(self.root_folder, self.config) 1168 1169 file_path = cast( 1170 str, 1171 arguments.get( 1172 "file_path", 1173 ), 1174 ) 1175 text = cast( 1176 str, 1177 arguments.get( 1178 "text", 1179 ), 1180 ) 1181 return tool.write_new_file(file_path=file_path, text=text) 1182 1183 def sed(self, arguments: dict[str, Any]) -> Any: 1184 """Generated Do Not Edit 1185 1186 Args: 1187 arguments (dict[str, Any]): The arguments for the tool. 1188 1189 Returns: 1190 Any: The result of the tool invocation. 1191 """ 1192 tool = SedTool(self.root_folder, self.config) 1193 1194 commands = cast( 1195 str, 1196 arguments.get( 1197 "commands", 1198 ), 1199 ) 1200 file_path = cast( 1201 str, 1202 arguments.get( 1203 "file_path", 1204 ), 1205 ) 1206 return tool.sed(commands=commands, file_path=file_path) 1207 1208 def add_todo(self, arguments: dict[str, Any]) -> Any: 1209 """Generated Do Not Edit 1210 1211 Args: 1212 arguments (dict[str, Any]): The arguments for the tool. 1213 1214 Returns: 1215 Any: The result of the tool invocation. 1216 """ 1217 tool = TodoTool(self.root_folder, self.config) 1218 1219 assignee = cast( 1220 str | None, 1221 arguments.get( 1222 "assignee", 1223 ), 1224 ) 1225 category = cast( 1226 str, 1227 arguments.get( 1228 "category", 1229 ), 1230 ) 1231 description = cast( 1232 str, 1233 arguments.get( 1234 "description", 1235 ), 1236 ) 1237 done_when = cast(str, arguments.get("done_when", "")) 1238 source_code_ref = cast( 1239 str, 1240 arguments.get( 1241 "source_code_ref", 1242 ), 1243 ) 1244 title = cast( 1245 str, 1246 arguments.get( 1247 "title", 1248 ), 1249 ) 1250 return tool.add_todo( 1251 assignee=assignee, 1252 category=category, 1253 description=description, 1254 done_when=done_when, 1255 source_code_ref=source_code_ref, 1256 title=title, 1257 ) 1258 1259 def list_valid_assignees(self, arguments: dict[str, Any]) -> Any: 1260 """Generated Do Not Edit 1261 1262 Args: 1263 arguments (dict[str, Any]): The arguments for the tool. 1264 1265 Returns: 1266 Any: The result of the tool invocation. 1267 """ 1268 tool = TodoTool(self.root_folder, self.config) 1269 1270 return tool.list_valid_assignees() 1271 1272 def query_todos_by_assignee(self, arguments: dict[str, Any]) -> Any: 1273 """Generated Do Not Edit 1274 1275 Args: 1276 arguments (dict[str, Any]): The arguments for the tool. 1277 1278 Returns: 1279 Any: The result of the tool invocation. 1280 """ 1281 tool = TodoTool(self.root_folder, self.config) 1282 1283 assignee_name = cast( 1284 str, 1285 arguments.get( 1286 "assignee_name", 1287 ), 1288 ) 1289 return tool.query_todos_by_assignee(assignee_name=assignee_name) 1290 1291 def query_todos_by_regex(self, arguments: dict[str, Any]) -> Any: 1292 """Generated Do Not Edit 1293 1294 Args: 1295 arguments (dict[str, Any]): The arguments for the tool. 1296 1297 Returns: 1298 Any: The result of the tool invocation. 1299 """ 1300 tool = TodoTool(self.root_folder, self.config) 1301 1302 regex_pattern = cast(str, arguments.get("regex_pattern", "[\\s\\S]+")) 1303 return tool.query_todos_by_regex(regex_pattern=regex_pattern) 1304 1305 def remove_todo(self, arguments: dict[str, Any]) -> Any: 1306 """Generated Do Not Edit 1307 1308 Args: 1309 arguments (dict[str, Any]): The arguments for the tool. 1310 1311 Returns: 1312 Any: The result of the tool invocation. 1313 """ 1314 tool = TodoTool(self.root_folder, self.config) 1315 1316 title = cast( 1317 str, 1318 arguments.get( 1319 "title", 1320 ), 1321 ) 1322 return tool.remove_todo(title=title) 1323 1324 def count_tokens(self, arguments: dict[str, Any]) -> Any: 1325 """Generated Do Not Edit 1326 1327 Args: 1328 arguments (dict[str, Any]): The arguments for the tool. 1329 1330 Returns: 1331 Any: The result of the tool invocation. 1332 """ 1333 tool = TokenCounterTool(self.root_folder, self.config) 1334 1335 text = cast( 1336 str, 1337 arguments.get( 1338 "text", 1339 ), 1340 ) 1341 return tool.count_tokens(text=text)
AI Shell Toolkit
35 def __init__( 36 self, root_folder: str, token_model: str, global_max_lines: int, permitted_tools: list[str], config: Config 37 ) -> None: 38 super().__init__(root_folder, token_model, global_max_lines, permitted_tools, config) 39 self._lookup: dict[str, Callable[[dict[str, Any]], Any]] = { 40 "report_bool": self.report_bool, 41 "report_dict": self.report_dict, 42 "report_float": self.report_float, 43 "report_int": self.report_int, 44 "report_json": self.report_json, 45 "report_list": self.report_list, 46 "report_set": self.report_set, 47 "report_text": self.report_text, 48 "report_toml": self.report_toml, 49 "report_tuple": self.report_tuple, 50 "report_xml": self.report_xml, 51 "cat": self.cat, 52 "cat_markdown": self.cat_markdown, 53 "cut_characters": self.cut_characters, 54 "cut_fields": self.cut_fields, 55 "cut_fields_by_name": self.cut_fields_by_name, 56 "find_files": self.find_files, 57 "find_files_markdown": self.find_files_markdown, 58 "get_current_branch": self.get_current_branch, 59 "get_recent_commits": self.get_recent_commits, 60 "git_diff": self.git_diff, 61 "git_diff_commit": self.git_diff_commit, 62 "git_log_file": self.git_log_file, 63 "git_log_search": self.git_log_search, 64 "git_show": self.git_show, 65 "git_status": self.git_status, 66 "is_ignored_by_gitignore": self.is_ignored_by_gitignore, 67 "grep": self.grep, 68 "grep_markdown": self.grep_markdown, 69 "head": self.head, 70 "head_markdown": self.head_markdown, 71 "head_tail": self.head_tail, 72 "tail": self.tail, 73 "tail_markdown": self.tail_markdown, 74 "insert_text_after_context": self.insert_text_after_context, 75 "insert_text_after_multiline_context": self.insert_text_after_multiline_context, 76 "insert_text_at_start_or_end": self.insert_text_at_start_or_end, 77 "ls": self.ls, 78 "ls_markdown": self.ls_markdown, 79 "apply_git_patch": self.apply_git_patch, 80 "format_code_as_markdown": self.format_code_as_markdown, 81 "pytest": self.pytest, 82 "replace_all": self.replace_all, 83 "replace_line_by_line": self.replace_line_by_line, 84 "replace_with_regex": self.replace_with_regex, 85 "rewrite_file": self.rewrite_file, 86 "write_new_file": self.write_new_file, 87 "sed": self.sed, 88 "add_todo": self.add_todo, 89 "list_valid_assignees": self.list_valid_assignees, 90 "query_todos_by_assignee": self.query_todos_by_assignee, 91 "query_todos_by_regex": self.query_todos_by_regex, 92 "remove_todo": self.remove_todo, 93 "count_tokens": self.count_tokens, 94 } 95 # Stateful tool support. Useless assignment to make mypy happy 96 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config)
Args: root_folder (str): The root folder path for file operations. token_model (str): The token model to use for the toolkit. global_max_lines (int): The global max lines to use for the toolkit. permitted_tools (list[str]): The tools the caller is allowed to invoke. config (Config): Developer config the model shouldn't set.
98 def report_bool(self, arguments: dict[str, Any]) -> Any: 99 """Generated Do Not Edit 100 101 Args: 102 arguments (dict[str, Any]): The arguments for the tool. 103 104 Returns: 105 Any: The result of the tool invocation. 106 """ 107 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 108 109 answer = cast( 110 bool, 111 arguments.get( 112 "answer", 113 ), 114 ) 115 comment = cast(str, arguments.get("comment", "")) 116 return self.tool_answer_collector.report_bool(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
118 def report_dict(self, arguments: dict[str, Any]) -> Any: 119 """Generated Do Not Edit 120 121 Args: 122 arguments (dict[str, Any]): The arguments for the tool. 123 124 Returns: 125 Any: The result of the tool invocation. 126 """ 127 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 128 129 answer = cast( 130 Any, 131 arguments.get( 132 "answer", 133 ), 134 ) 135 comment = cast(str, arguments.get("comment", "")) 136 return self.tool_answer_collector.report_dict(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
138 def report_float(self, arguments: dict[str, Any]) -> Any: 139 """Generated Do Not Edit 140 141 Args: 142 arguments (dict[str, Any]): The arguments for the tool. 143 144 Returns: 145 Any: The result of the tool invocation. 146 """ 147 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 148 149 answer = cast( 150 float, 151 arguments.get( 152 "answer", 153 ), 154 ) 155 comment = cast(str, arguments.get("comment", "")) 156 return self.tool_answer_collector.report_float(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
158 def report_int(self, arguments: dict[str, Any]) -> Any: 159 """Generated Do Not Edit 160 161 Args: 162 arguments (dict[str, Any]): The arguments for the tool. 163 164 Returns: 165 Any: The result of the tool invocation. 166 """ 167 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 168 169 answer = cast( 170 int, 171 arguments.get( 172 "answer", 173 ), 174 ) 175 comment = cast(str, arguments.get("comment", "")) 176 return self.tool_answer_collector.report_int(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
178 def report_json(self, arguments: dict[str, Any]) -> Any: 179 """Generated Do Not Edit 180 181 Args: 182 arguments (dict[str, Any]): The arguments for the tool. 183 184 Returns: 185 Any: The result of the tool invocation. 186 """ 187 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 188 189 answer = cast( 190 str, 191 arguments.get( 192 "answer", 193 ), 194 ) 195 comment = cast(str, arguments.get("comment", "")) 196 return self.tool_answer_collector.report_json(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
198 def report_list(self, arguments: dict[str, Any]) -> Any: 199 """Generated Do Not Edit 200 201 Args: 202 arguments (dict[str, Any]): The arguments for the tool. 203 204 Returns: 205 Any: The result of the tool invocation. 206 """ 207 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 208 209 answer = cast( 210 str, 211 arguments.get( 212 "answer", 213 ), 214 ) 215 comment = cast(str, arguments.get("comment", "")) 216 return self.tool_answer_collector.report_list(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
218 def report_set(self, arguments: dict[str, Any]) -> Any: 219 """Generated Do Not Edit 220 221 Args: 222 arguments (dict[str, Any]): The arguments for the tool. 223 224 Returns: 225 Any: The result of the tool invocation. 226 """ 227 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 228 229 answer = cast( 230 list[Any], 231 arguments.get( 232 "answer", 233 ), 234 ) 235 comment = cast(str, arguments.get("comment", "")) 236 return self.tool_answer_collector.report_set(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
238 def report_text(self, arguments: dict[str, Any]) -> Any: 239 """Generated Do Not Edit 240 241 Args: 242 arguments (dict[str, Any]): The arguments for the tool. 243 244 Returns: 245 Any: The result of the tool invocation. 246 """ 247 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 248 249 answer = cast( 250 str, 251 arguments.get( 252 "answer", 253 ), 254 ) 255 comment = cast(str, arguments.get("comment", "")) 256 return self.tool_answer_collector.report_text(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
258 def report_toml(self, arguments: dict[str, Any]) -> Any: 259 """Generated Do Not Edit 260 261 Args: 262 arguments (dict[str, Any]): The arguments for the tool. 263 264 Returns: 265 Any: The result of the tool invocation. 266 """ 267 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 268 269 answer = cast( 270 str, 271 arguments.get( 272 "answer", 273 ), 274 ) 275 comment = cast(str, arguments.get("comment", "")) 276 return self.tool_answer_collector.report_toml(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
278 def report_tuple(self, arguments: dict[str, Any]) -> Any: 279 """Generated Do Not Edit 280 281 Args: 282 arguments (dict[str, Any]): The arguments for the tool. 283 284 Returns: 285 Any: The result of the tool invocation. 286 """ 287 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 288 289 answer = cast( 290 list[Any], 291 arguments.get( 292 "answer", 293 ), 294 ) 295 comment = cast(str, arguments.get("comment", "")) 296 return self.tool_answer_collector.report_tuple(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
298 def report_xml(self, arguments: dict[str, Any]) -> Any: 299 """Generated Do Not Edit 300 301 Args: 302 arguments (dict[str, Any]): The arguments for the tool. 303 304 Returns: 305 Any: The result of the tool invocation. 306 """ 307 self.tool_answer_collector = AnswerCollectorTool(self.root_folder, self.config) 308 309 answer = cast( 310 str, 311 arguments.get( 312 "answer", 313 ), 314 ) 315 comment = cast(str, arguments.get("comment", "")) 316 return self.tool_answer_collector.report_xml(answer=answer, comment=comment)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
318 def cat(self, arguments: dict[str, Any]) -> Any: 319 """Generated Do Not Edit 320 321 Args: 322 arguments (dict[str, Any]): The arguments for the tool. 323 324 Returns: 325 Any: The result of the tool invocation. 326 """ 327 tool = CatTool(self.root_folder, self.config) 328 329 file_paths = cast( 330 str, 331 arguments.get( 332 "file_paths", 333 ), 334 ) 335 number_lines = cast(bool, arguments.get("number_lines", True)) 336 squeeze_blank = cast(bool, arguments.get("squeeze_blank", False)) 337 return tool.cat(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
339 def cat_markdown(self, arguments: dict[str, Any]) -> Any: 340 """Generated Do Not Edit 341 342 Args: 343 arguments (dict[str, Any]): The arguments for the tool. 344 345 Returns: 346 Any: The result of the tool invocation. 347 """ 348 tool = CatTool(self.root_folder, self.config) 349 350 file_paths = cast( 351 str, 352 arguments.get( 353 "file_paths", 354 ), 355 ) 356 number_lines = cast(bool, arguments.get("number_lines", True)) 357 squeeze_blank = cast(bool, arguments.get("squeeze_blank", False)) 358 return tool.cat_markdown(file_paths=file_paths, number_lines=number_lines, squeeze_blank=squeeze_blank)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
360 def cut_characters(self, arguments: dict[str, Any]) -> Any: 361 """Generated Do Not Edit 362 363 Args: 364 arguments (dict[str, Any]): The arguments for the tool. 365 366 Returns: 367 Any: The result of the tool invocation. 368 """ 369 tool = CutTool(self.root_folder, self.config) 370 371 character_ranges = cast( 372 str, 373 arguments.get( 374 "character_ranges", 375 ), 376 ) 377 file_path = cast( 378 str, 379 arguments.get( 380 "file_path", 381 ), 382 ) 383 return tool.cut_characters(character_ranges=character_ranges, file_path=file_path)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
385 def cut_fields(self, arguments: dict[str, Any]) -> Any: 386 """Generated Do Not Edit 387 388 Args: 389 arguments (dict[str, Any]): The arguments for the tool. 390 391 Returns: 392 Any: The result of the tool invocation. 393 """ 394 tool = CutTool(self.root_folder, self.config) 395 396 delimiter = cast(str, arguments.get("delimiter", ",")) 397 field_ranges = cast( 398 str, 399 arguments.get( 400 "field_ranges", 401 ), 402 ) 403 filename = cast( 404 str, 405 arguments.get( 406 "filename", 407 ), 408 ) 409 return tool.cut_fields(delimiter=delimiter, field_ranges=field_ranges, filename=filename)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
411 def cut_fields_by_name(self, arguments: dict[str, Any]) -> Any: 412 """Generated Do Not Edit 413 414 Args: 415 arguments (dict[str, Any]): The arguments for the tool. 416 417 Returns: 418 Any: The result of the tool invocation. 419 """ 420 tool = CutTool(self.root_folder, self.config) 421 422 delimiter = cast(str, arguments.get("delimiter", ",")) 423 field_names = cast( 424 str, 425 arguments.get( 426 "field_names", 427 ), 428 ) 429 filename = cast( 430 str, 431 arguments.get( 432 "filename", 433 ), 434 ) 435 return tool.cut_fields_by_name(delimiter=delimiter, field_names=field_names, filename=filename)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
437 def find_files(self, arguments: dict[str, Any]) -> Any: 438 """Generated Do Not Edit 439 440 Args: 441 arguments (dict[str, Any]): The arguments for the tool. 442 443 Returns: 444 Any: The result of the tool invocation. 445 """ 446 tool = FindTool(self.root_folder, self.config) 447 448 file_type = cast( 449 str | None, 450 arguments.get( 451 "file_type", 452 ), 453 ) 454 name = cast( 455 str | None, 456 arguments.get( 457 "name", 458 ), 459 ) 460 regex = cast( 461 str | None, 462 arguments.get( 463 "regex", 464 ), 465 ) 466 size = cast( 467 str | None, 468 arguments.get( 469 "size", 470 ), 471 ) 472 return tool.find_files(file_type=file_type, name=name, regex=regex, size=size)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
474 def find_files_markdown(self, arguments: dict[str, Any]) -> Any: 475 """Generated Do Not Edit 476 477 Args: 478 arguments (dict[str, Any]): The arguments for the tool. 479 480 Returns: 481 Any: The result of the tool invocation. 482 """ 483 tool = FindTool(self.root_folder, self.config) 484 485 file_type = cast( 486 str | None, 487 arguments.get( 488 "file_type", 489 ), 490 ) 491 name = cast( 492 str | None, 493 arguments.get( 494 "name", 495 ), 496 ) 497 regex = cast( 498 str | None, 499 arguments.get( 500 "regex", 501 ), 502 ) 503 size = cast( 504 str | None, 505 arguments.get( 506 "size", 507 ), 508 ) 509 return tool.find_files_markdown(file_type=file_type, name=name, regex=regex, size=size)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
511 def get_current_branch(self, arguments: dict[str, Any]) -> Any: 512 """Generated Do Not Edit 513 514 Args: 515 arguments (dict[str, Any]): The arguments for the tool. 516 517 Returns: 518 Any: The result of the tool invocation. 519 """ 520 tool = GitTool(self.root_folder, self.config) 521 522 return tool.get_current_branch()
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
524 def get_recent_commits(self, arguments: dict[str, Any]) -> Any: 525 """Generated Do Not Edit 526 527 Args: 528 arguments (dict[str, Any]): The arguments for the tool. 529 530 Returns: 531 Any: The result of the tool invocation. 532 """ 533 tool = GitTool(self.root_folder, self.config) 534 535 n = cast(int, arguments.get("n", 10)) 536 short_hash = cast(bool, arguments.get("short_hash", False)) 537 return tool.get_recent_commits(n=n, short_hash=short_hash)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
539 def git_diff(self, arguments: dict[str, Any]) -> Any: 540 """Generated Do Not Edit 541 542 Args: 543 arguments (dict[str, Any]): The arguments for the tool. 544 545 Returns: 546 Any: The result of the tool invocation. 547 """ 548 tool = GitTool(self.root_folder, self.config) 549 550 return tool.git_diff()
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
552 def git_diff_commit(self, arguments: dict[str, Any]) -> Any: 553 """Generated Do Not Edit 554 555 Args: 556 arguments (dict[str, Any]): The arguments for the tool. 557 558 Returns: 559 Any: The result of the tool invocation. 560 """ 561 tool = GitTool(self.root_folder, self.config) 562 563 commit1 = cast( 564 str, 565 arguments.get( 566 "commit1", 567 ), 568 ) 569 commit2 = cast( 570 str, 571 arguments.get( 572 "commit2", 573 ), 574 ) 575 return tool.git_diff_commit(commit1=commit1, commit2=commit2)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
577 def git_log_file(self, arguments: dict[str, Any]) -> Any: 578 """Generated Do Not Edit 579 580 Args: 581 arguments (dict[str, Any]): The arguments for the tool. 582 583 Returns: 584 Any: The result of the tool invocation. 585 """ 586 tool = GitTool(self.root_folder, self.config) 587 588 filename = cast( 589 str, 590 arguments.get( 591 "filename", 592 ), 593 ) 594 return tool.git_log_file(filename=filename)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
596 def git_log_search(self, arguments: dict[str, Any]) -> Any: 597 """Generated Do Not Edit 598 599 Args: 600 arguments (dict[str, Any]): The arguments for the tool. 601 602 Returns: 603 Any: The result of the tool invocation. 604 """ 605 tool = GitTool(self.root_folder, self.config) 606 607 search_string = cast( 608 str, 609 arguments.get( 610 "search_string", 611 ), 612 ) 613 return tool.git_log_search(search_string=search_string)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
615 def git_show(self, arguments: dict[str, Any]) -> Any: 616 """Generated Do Not Edit 617 618 Args: 619 arguments (dict[str, Any]): The arguments for the tool. 620 621 Returns: 622 Any: The result of the tool invocation. 623 """ 624 tool = GitTool(self.root_folder, self.config) 625 626 return tool.git_show()
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
628 def git_status(self, arguments: dict[str, Any]) -> Any: 629 """Generated Do Not Edit 630 631 Args: 632 arguments (dict[str, Any]): The arguments for the tool. 633 634 Returns: 635 Any: The result of the tool invocation. 636 """ 637 tool = GitTool(self.root_folder, self.config) 638 639 return tool.git_status()
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
641 def is_ignored_by_gitignore(self, arguments: dict[str, Any]) -> Any: 642 """Generated Do Not Edit 643 644 Args: 645 arguments (dict[str, Any]): The arguments for the tool. 646 647 Returns: 648 Any: The result of the tool invocation. 649 """ 650 tool = GitTool(self.root_folder, self.config) 651 652 file_path = cast( 653 str, 654 arguments.get( 655 "file_path", 656 ), 657 ) 658 gitignore_path = cast(str, arguments.get("gitignore_path", ".gitignore")) 659 return tool.is_ignored_by_gitignore(file_path=file_path, gitignore_path=gitignore_path)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
661 def grep(self, arguments: dict[str, Any]) -> Any: 662 """Generated Do Not Edit 663 664 Args: 665 arguments (dict[str, Any]): The arguments for the tool. 666 667 Returns: 668 Any: The result of the tool invocation. 669 """ 670 tool = GrepTool(self.root_folder, self.config) 671 672 glob_pattern = cast( 673 str, 674 arguments.get( 675 "glob_pattern", 676 ), 677 ) 678 maximum_matches_per_file = cast(int, arguments.get("maximum_matches_per_file", -1)) 679 maximum_matches_total = cast(int, arguments.get("maximum_matches_total", -1)) 680 regex = cast( 681 str, 682 arguments.get( 683 "regex", 684 ), 685 ) 686 skip_first_matches = cast(int, arguments.get("skip_first_matches", -1)) 687 return tool.grep( 688 glob_pattern=glob_pattern, 689 maximum_matches_per_file=maximum_matches_per_file, 690 maximum_matches_total=maximum_matches_total, 691 regex=regex, 692 skip_first_matches=skip_first_matches, 693 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
695 def grep_markdown(self, arguments: dict[str, Any]) -> Any: 696 """Generated Do Not Edit 697 698 Args: 699 arguments (dict[str, Any]): The arguments for the tool. 700 701 Returns: 702 Any: The result of the tool invocation. 703 """ 704 tool = GrepTool(self.root_folder, self.config) 705 706 glob_pattern = cast( 707 str, 708 arguments.get( 709 "glob_pattern", 710 ), 711 ) 712 maximum_matches = cast(int, arguments.get("maximum_matches", -1)) 713 regex = cast( 714 str, 715 arguments.get( 716 "regex", 717 ), 718 ) 719 skip_first_matches = cast(int, arguments.get("skip_first_matches", -1)) 720 return tool.grep_markdown( 721 glob_pattern=glob_pattern, 722 maximum_matches=maximum_matches, 723 regex=regex, 724 skip_first_matches=skip_first_matches, 725 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
727 def head(self, arguments: dict[str, Any]) -> Any: 728 """Generated Do Not Edit 729 730 Args: 731 arguments (dict[str, Any]): The arguments for the tool. 732 733 Returns: 734 Any: The result of the tool invocation. 735 """ 736 tool = HeadTailTool(self.root_folder, self.config) 737 738 byte_count = cast( 739 int | None, 740 arguments.get( 741 "byte_count", 742 ), 743 ) 744 file_path = cast( 745 str, 746 arguments.get( 747 "file_path", 748 ), 749 ) 750 lines = cast(int, arguments.get("lines", 10)) 751 return tool.head(byte_count=byte_count, file_path=file_path, lines=lines)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
753 def head_markdown(self, arguments: dict[str, Any]) -> Any: 754 """Generated Do Not Edit 755 756 Args: 757 arguments (dict[str, Any]): The arguments for the tool. 758 759 Returns: 760 Any: The result of the tool invocation. 761 """ 762 tool = HeadTailTool(self.root_folder, self.config) 763 764 file_path = cast( 765 str, 766 arguments.get( 767 "file_path", 768 ), 769 ) 770 lines = cast(int, arguments.get("lines", 10)) 771 return tool.head_markdown(file_path=file_path, lines=lines)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
773 def head_tail(self, arguments: dict[str, Any]) -> Any: 774 """Generated Do Not Edit 775 776 Args: 777 arguments (dict[str, Any]): The arguments for the tool. 778 779 Returns: 780 Any: The result of the tool invocation. 781 """ 782 tool = HeadTailTool(self.root_folder, self.config) 783 784 byte_count = cast( 785 int | None, 786 arguments.get( 787 "byte_count", 788 ), 789 ) 790 file_path = cast( 791 str, 792 arguments.get( 793 "file_path", 794 ), 795 ) 796 lines = cast(int, arguments.get("lines", 10)) 797 mode = cast(str, arguments.get("mode", "head")) 798 return tool.head_tail(byte_count=byte_count, file_path=file_path, lines=lines, mode=mode)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
800 def tail(self, arguments: dict[str, Any]) -> Any: 801 """Generated Do Not Edit 802 803 Args: 804 arguments (dict[str, Any]): The arguments for the tool. 805 806 Returns: 807 Any: The result of the tool invocation. 808 """ 809 tool = HeadTailTool(self.root_folder, self.config) 810 811 byte_count = cast( 812 int | None, 813 arguments.get( 814 "byte_count", 815 ), 816 ) 817 file_path = cast( 818 str, 819 arguments.get( 820 "file_path", 821 ), 822 ) 823 lines = cast(int, arguments.get("lines", 10)) 824 return tool.tail(byte_count=byte_count, file_path=file_path, lines=lines)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
826 def tail_markdown(self, arguments: dict[str, Any]) -> Any: 827 """Generated Do Not Edit 828 829 Args: 830 arguments (dict[str, Any]): The arguments for the tool. 831 832 Returns: 833 Any: The result of the tool invocation. 834 """ 835 tool = HeadTailTool(self.root_folder, self.config) 836 837 file_path = cast( 838 str, 839 arguments.get( 840 "file_path", 841 ), 842 ) 843 lines = cast(int, arguments.get("lines", 10)) 844 return tool.tail_markdown(file_path=file_path, lines=lines)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
846 def insert_text_after_context(self, arguments: dict[str, Any]) -> Any: 847 """Generated Do Not Edit 848 849 Args: 850 arguments (dict[str, Any]): The arguments for the tool. 851 852 Returns: 853 Any: The result of the tool invocation. 854 """ 855 tool = InsertTool(self.root_folder, self.config) 856 857 context = cast( 858 str, 859 arguments.get( 860 "context", 861 ), 862 ) 863 file_path = cast( 864 str, 865 arguments.get( 866 "file_path", 867 ), 868 ) 869 text_to_insert = cast( 870 str, 871 arguments.get( 872 "text_to_insert", 873 ), 874 ) 875 return tool.insert_text_after_context(context=context, file_path=file_path, text_to_insert=text_to_insert)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
877 def insert_text_after_multiline_context(self, arguments: dict[str, Any]) -> Any: 878 """Generated Do Not Edit 879 880 Args: 881 arguments (dict[str, Any]): The arguments for the tool. 882 883 Returns: 884 Any: The result of the tool invocation. 885 """ 886 tool = InsertTool(self.root_folder, self.config) 887 888 context_lines = cast( 889 str, 890 arguments.get( 891 "context_lines", 892 ), 893 ) 894 file_path = cast( 895 str, 896 arguments.get( 897 "file_path", 898 ), 899 ) 900 text_to_insert = cast( 901 str, 902 arguments.get( 903 "text_to_insert", 904 ), 905 ) 906 return tool.insert_text_after_multiline_context( 907 context_lines=context_lines, file_path=file_path, text_to_insert=text_to_insert 908 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
910 def insert_text_at_start_or_end(self, arguments: dict[str, Any]) -> Any: 911 """Generated Do Not Edit 912 913 Args: 914 arguments (dict[str, Any]): The arguments for the tool. 915 916 Returns: 917 Any: The result of the tool invocation. 918 """ 919 tool = InsertTool(self.root_folder, self.config) 920 921 file_path = cast( 922 str, 923 arguments.get( 924 "file_path", 925 ), 926 ) 927 position = cast(str, arguments.get("position", "end")) 928 text_to_insert = cast( 929 str, 930 arguments.get( 931 "text_to_insert", 932 ), 933 ) 934 return tool.insert_text_at_start_or_end(file_path=file_path, position=position, text_to_insert=text_to_insert)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
936 def ls(self, arguments: dict[str, Any]) -> Any: 937 """Generated Do Not Edit 938 939 Args: 940 arguments (dict[str, Any]): The arguments for the tool. 941 942 Returns: 943 Any: The result of the tool invocation. 944 """ 945 tool = LsTool(self.root_folder, self.config) 946 947 all_files = cast(bool, arguments.get("all_files", False)) 948 long = cast(bool, arguments.get("long", False)) 949 path = cast( 950 str | None, 951 arguments.get( 952 "path", 953 ), 954 ) 955 return tool.ls(all_files=all_files, long=long, path=path)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
957 def ls_markdown(self, arguments: dict[str, Any]) -> Any: 958 """Generated Do Not Edit 959 960 Args: 961 arguments (dict[str, Any]): The arguments for the tool. 962 963 Returns: 964 Any: The result of the tool invocation. 965 """ 966 tool = LsTool(self.root_folder, self.config) 967 968 all_files = cast(bool, arguments.get("all_files", False)) 969 long = cast(bool, arguments.get("long", False)) 970 path = cast(str | None, arguments.get("path", ".")) 971 return tool.ls_markdown(all_files=all_files, long=long, path=path)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
973 def apply_git_patch(self, arguments: dict[str, Any]) -> Any: 974 """Generated Do Not Edit 975 976 Args: 977 arguments (dict[str, Any]): The arguments for the tool. 978 979 Returns: 980 Any: The result of the tool invocation. 981 """ 982 tool = PatchTool(self.root_folder, self.config) 983 984 patch_content = cast( 985 str, 986 arguments.get( 987 "patch_content", 988 ), 989 ) 990 return tool.apply_git_patch(patch_content=patch_content)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
992 def format_code_as_markdown(self, arguments: dict[str, Any]) -> Any: 993 """Generated Do Not Edit 994 995 Args: 996 arguments (dict[str, Any]): The arguments for the tool. 997 998 Returns: 999 Any: The result of the tool invocation. 1000 """ 1001 tool = PyCatTool(self.root_folder, self.config) 1002 1003 base_path = cast( 1004 str, 1005 arguments.get( 1006 "base_path", 1007 ), 1008 ) 1009 header = cast( 1010 str, 1011 arguments.get( 1012 "header", 1013 ), 1014 ) 1015 no_comments = cast(bool, arguments.get("no_comments", False)) 1016 no_docs = cast(bool, arguments.get("no_docs", False)) 1017 return tool.format_code_as_markdown( 1018 base_path=base_path, header=header, no_comments=no_comments, no_docs=no_docs 1019 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1021 def pytest(self, arguments: dict[str, Any]) -> Any: 1022 """Generated Do Not Edit 1023 1024 Args: 1025 arguments (dict[str, Any]): The arguments for the tool. 1026 1027 Returns: 1028 Any: The result of the tool invocation. 1029 """ 1030 tool = PytestTool(self.root_folder, self.config) 1031 1032 return tool.pytest()
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1034 def replace_all(self, arguments: dict[str, Any]) -> Any: 1035 """Generated Do Not Edit 1036 1037 Args: 1038 arguments (dict[str, Any]): The arguments for the tool. 1039 1040 Returns: 1041 Any: The result of the tool invocation. 1042 """ 1043 tool = ReplaceTool(self.root_folder, self.config) 1044 1045 file_path = cast( 1046 str, 1047 arguments.get( 1048 "file_path", 1049 ), 1050 ) 1051 new_text = cast( 1052 str, 1053 arguments.get( 1054 "new_text", 1055 ), 1056 ) 1057 old_text = cast( 1058 str, 1059 arguments.get( 1060 "old_text", 1061 ), 1062 ) 1063 return tool.replace_all(file_path=file_path, new_text=new_text, old_text=old_text)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1065 def replace_line_by_line(self, arguments: dict[str, Any]) -> Any: 1066 """Generated Do Not Edit 1067 1068 Args: 1069 arguments (dict[str, Any]): The arguments for the tool. 1070 1071 Returns: 1072 Any: The result of the tool invocation. 1073 """ 1074 tool = ReplaceTool(self.root_folder, self.config) 1075 1076 file_path = cast( 1077 str, 1078 arguments.get( 1079 "file_path", 1080 ), 1081 ) 1082 line_end = cast(int, arguments.get("line_end", -1)) 1083 line_start = cast(int, arguments.get("line_start", 0)) 1084 new_text = cast( 1085 str, 1086 arguments.get( 1087 "new_text", 1088 ), 1089 ) 1090 old_text = cast( 1091 str, 1092 arguments.get( 1093 "old_text", 1094 ), 1095 ) 1096 return tool.replace_line_by_line( 1097 file_path=file_path, line_end=line_end, line_start=line_start, new_text=new_text, old_text=old_text 1098 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1100 def replace_with_regex(self, arguments: dict[str, Any]) -> Any: 1101 """Generated Do Not Edit 1102 1103 Args: 1104 arguments (dict[str, Any]): The arguments for the tool. 1105 1106 Returns: 1107 Any: The result of the tool invocation. 1108 """ 1109 tool = ReplaceTool(self.root_folder, self.config) 1110 1111 file_path = cast( 1112 str, 1113 arguments.get( 1114 "file_path", 1115 ), 1116 ) 1117 regex_match_expression = cast( 1118 str, 1119 arguments.get( 1120 "regex_match_expression", 1121 ), 1122 ) 1123 replacement = cast( 1124 str, 1125 arguments.get( 1126 "replacement", 1127 ), 1128 ) 1129 return tool.replace_with_regex( 1130 file_path=file_path, regex_match_expression=regex_match_expression, replacement=replacement 1131 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1133 def rewrite_file(self, arguments: dict[str, Any]) -> Any: 1134 """Generated Do Not Edit 1135 1136 Args: 1137 arguments (dict[str, Any]): The arguments for the tool. 1138 1139 Returns: 1140 Any: The result of the tool invocation. 1141 """ 1142 tool = RewriteTool(self.root_folder, self.config) 1143 1144 file_path = cast( 1145 str, 1146 arguments.get( 1147 "file_path", 1148 ), 1149 ) 1150 text = cast( 1151 str, 1152 arguments.get( 1153 "text", 1154 ), 1155 ) 1156 return tool.rewrite_file(file_path=file_path, text=text)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1158 def write_new_file(self, arguments: dict[str, Any]) -> Any: 1159 """Generated Do Not Edit 1160 1161 Args: 1162 arguments (dict[str, Any]): The arguments for the tool. 1163 1164 Returns: 1165 Any: The result of the tool invocation. 1166 """ 1167 tool = RewriteTool(self.root_folder, self.config) 1168 1169 file_path = cast( 1170 str, 1171 arguments.get( 1172 "file_path", 1173 ), 1174 ) 1175 text = cast( 1176 str, 1177 arguments.get( 1178 "text", 1179 ), 1180 ) 1181 return tool.write_new_file(file_path=file_path, text=text)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1183 def sed(self, arguments: dict[str, Any]) -> Any: 1184 """Generated Do Not Edit 1185 1186 Args: 1187 arguments (dict[str, Any]): The arguments for the tool. 1188 1189 Returns: 1190 Any: The result of the tool invocation. 1191 """ 1192 tool = SedTool(self.root_folder, self.config) 1193 1194 commands = cast( 1195 str, 1196 arguments.get( 1197 "commands", 1198 ), 1199 ) 1200 file_path = cast( 1201 str, 1202 arguments.get( 1203 "file_path", 1204 ), 1205 ) 1206 return tool.sed(commands=commands, file_path=file_path)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1208 def add_todo(self, arguments: dict[str, Any]) -> Any: 1209 """Generated Do Not Edit 1210 1211 Args: 1212 arguments (dict[str, Any]): The arguments for the tool. 1213 1214 Returns: 1215 Any: The result of the tool invocation. 1216 """ 1217 tool = TodoTool(self.root_folder, self.config) 1218 1219 assignee = cast( 1220 str | None, 1221 arguments.get( 1222 "assignee", 1223 ), 1224 ) 1225 category = cast( 1226 str, 1227 arguments.get( 1228 "category", 1229 ), 1230 ) 1231 description = cast( 1232 str, 1233 arguments.get( 1234 "description", 1235 ), 1236 ) 1237 done_when = cast(str, arguments.get("done_when", "")) 1238 source_code_ref = cast( 1239 str, 1240 arguments.get( 1241 "source_code_ref", 1242 ), 1243 ) 1244 title = cast( 1245 str, 1246 arguments.get( 1247 "title", 1248 ), 1249 ) 1250 return tool.add_todo( 1251 assignee=assignee, 1252 category=category, 1253 description=description, 1254 done_when=done_when, 1255 source_code_ref=source_code_ref, 1256 title=title, 1257 )
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1259 def list_valid_assignees(self, arguments: dict[str, Any]) -> Any: 1260 """Generated Do Not Edit 1261 1262 Args: 1263 arguments (dict[str, Any]): The arguments for the tool. 1264 1265 Returns: 1266 Any: The result of the tool invocation. 1267 """ 1268 tool = TodoTool(self.root_folder, self.config) 1269 1270 return tool.list_valid_assignees()
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1272 def query_todos_by_assignee(self, arguments: dict[str, Any]) -> Any: 1273 """Generated Do Not Edit 1274 1275 Args: 1276 arguments (dict[str, Any]): The arguments for the tool. 1277 1278 Returns: 1279 Any: The result of the tool invocation. 1280 """ 1281 tool = TodoTool(self.root_folder, self.config) 1282 1283 assignee_name = cast( 1284 str, 1285 arguments.get( 1286 "assignee_name", 1287 ), 1288 ) 1289 return tool.query_todos_by_assignee(assignee_name=assignee_name)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1291 def query_todos_by_regex(self, arguments: dict[str, Any]) -> Any: 1292 """Generated Do Not Edit 1293 1294 Args: 1295 arguments (dict[str, Any]): The arguments for the tool. 1296 1297 Returns: 1298 Any: The result of the tool invocation. 1299 """ 1300 tool = TodoTool(self.root_folder, self.config) 1301 1302 regex_pattern = cast(str, arguments.get("regex_pattern", "[\\s\\S]+")) 1303 return tool.query_todos_by_regex(regex_pattern=regex_pattern)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1305 def remove_todo(self, arguments: dict[str, Any]) -> Any: 1306 """Generated Do Not Edit 1307 1308 Args: 1309 arguments (dict[str, Any]): The arguments for the tool. 1310 1311 Returns: 1312 Any: The result of the tool invocation. 1313 """ 1314 tool = TodoTool(self.root_folder, self.config) 1315 1316 title = cast( 1317 str, 1318 arguments.get( 1319 "title", 1320 ), 1321 ) 1322 return tool.remove_todo(title=title)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
1324 def count_tokens(self, arguments: dict[str, Any]) -> Any: 1325 """Generated Do Not Edit 1326 1327 Args: 1328 arguments (dict[str, Any]): The arguments for the tool. 1329 1330 Returns: 1331 Any: The result of the tool invocation. 1332 """ 1333 tool = TokenCounterTool(self.root_folder, self.config) 1334 1335 text = cast( 1336 str, 1337 arguments.get( 1338 "text", 1339 ), 1340 ) 1341 return tool.count_tokens(text=text)
Generated Do Not Edit
Args: arguments (dict[str, Any]): The arguments for the tool.
Returns: Any: The result of the tool invocation.
36def initialize_all_tools(skips: list[str] | None = None, keeps: list[str] | None = None) -> None: 37 """Initialize all tools 38 39 Args: 40 skips (Optional[list[str]], optional): Tools to skip. Defaults to None. 41 keeps (Optional[list[str]], optional): Tools to keep. Defaults to None. 42 """ 43 if keeps is not None: 44 keep = keeps 45 elif skips is None: 46 keep = just_tool_names() 47 else: 48 keep = [name for name in just_tool_names() if name not in skips] 49 50 for _ns, tools in SCHEMAS.items(): 51 for name, schema in tools.items(): 52 function_style: dict[str, Union[str, Collection[str]]] = {"name": name} 53 parameters = {"type": "object", "properties": schema["properties"], "required": schema["required"]} 54 function_style["parameters"] = parameters 55 function_style["description"] = schema["description"] 56 if name in keep: 57 ALL_TOOLS.append(function_style) 58 active_tools_string = ", ".join(tool["name"] for tool in ALL_TOOLS) 59 logger.info(f"Active tools {active_tools_string}")
Initialize all tools
Args: skips (Optional[list[str]], optional): Tools to skip. Defaults to None. keeps (Optional[list[str]], optional): Tools to keep. Defaults to None.
96def initialize_recommended_tools(root_folder: str, config: Config) -> None: 97 """Initialize recommended tools 98 99 Args: 100 root_folder (str): The root folder to recommend tools for. 101 config (Config): The developer input that bot shouldn't set. 102 """ 103 initialize_all_tools(keeps=recommendations(root_folder, config))
Initialize recommended tools
Args: root_folder (str): The root folder to recommend tools for. config (Config): The developer input that bot shouldn't set.
11class Config: 12 """A class for managing the ai_shell.toml file. 13 14 This is for globally available settings that the model shouldn't (or can't) set, 15 e.g. read-only mode, the plugin folder, and tool defaults. 16 """ 17 18 def __init__(self, config_path: str = "") -> None: 19 """Initialize the Config class.""" 20 if config_path and config_path.endswith(".toml"): 21 self.config_file = config_path 22 elif config_path: 23 self.config_file = os.path.join(config_path, "ai_shell.toml") 24 else: 25 self.config_file = os.getenv("CONFIG_PATH", "ai_shell.toml") 26 # freeze the location of the config file 27 self.config_file = os.path.abspath(self.config_file) 28 self._list_data: dict[str, list[str]] = {} 29 self._values_data: dict[str, str] = {} 30 self._flags_data: dict[str, bool] = { 31 "enable_autocat": True, 32 } 33 self.load_config() 34 35 def load_config(self) -> None: 36 """Load the config from the config file.""" 37 if os.path.isfile(self.config_file): 38 data = toml.load(self.config_file) 39 self._flags_data = data.get("flags", self._flags_data) 40 self._values_data = data.get("values", {}) 41 self._list_data = data.get("lists", {}) 42 else: 43 self.save_config() 44 45 def save_config(self) -> None: 46 """Save the config to the config file.""" 47 if not os.path.isabs(self.config_file): 48 raise ValueError("Config file path must be absolute.") 49 with open(self.config_file, "w", encoding="utf-8") as f: 50 toml.dump( 51 { 52 "flags": self._flags_data, 53 "values": self._values_data, 54 "lists": self._list_data, 55 }, 56 f, 57 ) 58 59 def set_flag(self, flag_name: str, value: bool) -> None: 60 """Set the value of the given flag. 61 62 Args: 63 flag_name (str): The name of the flag. 64 value (bool): The value of the flag. 65 """ 66 self._flags_data[flag_name] = value 67 self.save_config() 68 69 def get_flag(self, flag_name: str, default_value: bool | None = None) -> bool | None: 70 """Return the value of the given flag. 71 72 Args: 73 flag_name (str): The name of the flag. 74 default_value (Optional[bool]): The default to return if the flag is unset. 75 76 Returns: 77 Optional[bool]: The value of the flag, or the default. 78 """ 79 return self._flags_data.get(flag_name, default_value) 80 81 def get_value(self, name: str, default: str | None = None) -> str | None: 82 """Return the value of the given named value. 83 84 Args: 85 name (str): The name of the config value. 86 default (Optional[str]): The default to return if unset. 87 88 Returns: 89 Optional[str]: The value, or the default. 90 """ 91 return self._values_data.get(name, default) 92 93 def get_required_value(self, name: str) -> str: 94 """Return a required named value. 95 96 Args: 97 name (str): The name of the config value. 98 99 Returns: 100 str: The value. 101 102 Raises: 103 FatalConfigurationError: If the value does not exist. 104 """ 105 value = self._values_data.get(name, None) 106 if value is None: 107 raise FatalConfigurationError(f"Need {name} in config file") 108 return value 109 110 def set_list(self, list_name: str, value: list[str]) -> None: 111 """Set a named list. 112 113 Args: 114 list_name (str): The name of the list. 115 value (list[str]): The list contents. 116 """ 117 self._list_data[list_name] = value 118 self.save_config() 119 120 def get_list(self, list_name: str) -> list[str]: 121 """Return a named list. 122 123 Args: 124 list_name (str): The name of the list. 125 126 Returns: 127 list[str]: The list, or empty. 128 """ 129 return self._list_data.get(list_name, [])
A class for managing the ai_shell.toml file.
This is for globally available settings that the model shouldn't (or can't) set, e.g. read-only mode, the plugin folder, and tool defaults.
18 def __init__(self, config_path: str = "") -> None: 19 """Initialize the Config class.""" 20 if config_path and config_path.endswith(".toml"): 21 self.config_file = config_path 22 elif config_path: 23 self.config_file = os.path.join(config_path, "ai_shell.toml") 24 else: 25 self.config_file = os.getenv("CONFIG_PATH", "ai_shell.toml") 26 # freeze the location of the config file 27 self.config_file = os.path.abspath(self.config_file) 28 self._list_data: dict[str, list[str]] = {} 29 self._values_data: dict[str, str] = {} 30 self._flags_data: dict[str, bool] = { 31 "enable_autocat": True, 32 } 33 self.load_config()
Initialize the Config class.
35 def load_config(self) -> None: 36 """Load the config from the config file.""" 37 if os.path.isfile(self.config_file): 38 data = toml.load(self.config_file) 39 self._flags_data = data.get("flags", self._flags_data) 40 self._values_data = data.get("values", {}) 41 self._list_data = data.get("lists", {}) 42 else: 43 self.save_config()
Load the config from the config file.
45 def save_config(self) -> None: 46 """Save the config to the config file.""" 47 if not os.path.isabs(self.config_file): 48 raise ValueError("Config file path must be absolute.") 49 with open(self.config_file, "w", encoding="utf-8") as f: 50 toml.dump( 51 { 52 "flags": self._flags_data, 53 "values": self._values_data, 54 "lists": self._list_data, 55 }, 56 f, 57 )
Save the config to the config file.
59 def set_flag(self, flag_name: str, value: bool) -> None: 60 """Set the value of the given flag. 61 62 Args: 63 flag_name (str): The name of the flag. 64 value (bool): The value of the flag. 65 """ 66 self._flags_data[flag_name] = value 67 self.save_config()
Set the value of the given flag.
Args: flag_name (str): The name of the flag. value (bool): The value of the flag.
69 def get_flag(self, flag_name: str, default_value: bool | None = None) -> bool | None: 70 """Return the value of the given flag. 71 72 Args: 73 flag_name (str): The name of the flag. 74 default_value (Optional[bool]): The default to return if the flag is unset. 75 76 Returns: 77 Optional[bool]: The value of the flag, or the default. 78 """ 79 return self._flags_data.get(flag_name, default_value)
Return the value of the given flag.
Args: flag_name (str): The name of the flag. default_value (Optional[bool]): The default to return if the flag is unset.
Returns: Optional[bool]: The value of the flag, or the default.
81 def get_value(self, name: str, default: str | None = None) -> str | None: 82 """Return the value of the given named value. 83 84 Args: 85 name (str): The name of the config value. 86 default (Optional[str]): The default to return if unset. 87 88 Returns: 89 Optional[str]: The value, or the default. 90 """ 91 return self._values_data.get(name, default)
Return the value of the given named value.
Args: name (str): The name of the config value. default (Optional[str]): The default to return if unset.
Returns: Optional[str]: The value, or the default.
93 def get_required_value(self, name: str) -> str: 94 """Return a required named value. 95 96 Args: 97 name (str): The name of the config value. 98 99 Returns: 100 str: The value. 101 102 Raises: 103 FatalConfigurationError: If the value does not exist. 104 """ 105 value = self._values_data.get(name, None) 106 if value is None: 107 raise FatalConfigurationError(f"Need {name} in config file") 108 return value
Return a required named value.
Args: name (str): The name of the config value.
Returns: str: The value.
Raises: FatalConfigurationError: If the value does not exist.
110 def set_list(self, list_name: str, value: list[str]) -> None: 111 """Set a named list. 112 113 Args: 114 list_name (str): The name of the list. 115 value (list[str]): The list contents. 116 """ 117 self._list_data[list_name] = value 118 self.save_config()
Set a named list.
Args: list_name (str): The name of the list. value (list[str]): The list contents.
120 def get_list(self, list_name: str) -> list[str]: 121 """Return a named list. 122 123 Args: 124 list_name (str): The name of the list. 125 126 Returns: 127 list[str]: The list, or empty. 128 """ 129 return self._list_data.get(list_name, [])
Return a named list.
Args: list_name (str): The name of the list.
Returns: list[str]: The list, or empty.
9def configure_logging() -> dict[str, Any]: 10 """Basic style""" 11 logging_config: dict[str, Any] = { 12 "version": 1, 13 "disable_existing_loggers": True, 14 "formatters": { 15 "standard": {"format": "[%(levelname)s] %(name)s: %(message)s"}, 16 }, 17 "handlers": { 18 "default": { 19 "level": "DEBUG", 20 "formatter": "standard", 21 "class": "logging.StreamHandler", 22 "stream": "ext://sys.stdout", # Default is stderr 23 }, 24 # "bug_trail": { 25 # "level": "DEBUG", 26 # # "formatter": "standard", 27 # "class": "bug_trail_core.BugTrailHandler", 28 # "db_path": bug_trail_config.database_path, 29 # "minimum_level": logging.DEBUG, 30 # }, 31 # "json": { 32 # # "()": "json_file_handler_factory", 33 # "level": "DEBUG", 34 # "class": "ai_shell.utils.json_log_handler.JSONFileHandler", 35 # "directory": "api_logs", 36 # "module_name": "openai", 37 # }, 38 }, 39 "loggers": { 40 # root logger can capture too much 41 "": { # root logger 42 "handlers": [ 43 "default", 44 # "bug_trail" 45 ], 46 "level": "DEBUG", 47 "propagate": False, 48 }, 49 }, 50 } 51 52 debug_level_modules: list[str] = ["__main__", "ai_shell", "minimal_example"] 53 54 info_level_modules: list[str] = [] 55 warn_level_modules: list[str] = [] 56 57 # json handler 58 for name in ["openai"]: 59 logging_config["loggers"][name] = { 60 "handlers": [], # ["json"], 61 "level": "DEBUG", 62 "propagate": False, 63 } 64 65 for name in debug_level_modules: 66 logging_config["loggers"][name] = { 67 "handlers": [ 68 "default", 69 # "bug_trail" 70 ], 71 "level": "DEBUG", 72 "propagate": False, 73 } 74 75 for name in info_level_modules: 76 logging_config["loggers"][name] = { 77 "handlers": [ 78 "default", 79 # "bug_trail" 80 ], 81 "level": "INFO", 82 "propagate": False, 83 } 84 85 for name in warn_level_modules: 86 logging_config["loggers"][name] = { 87 "handlers": [ 88 "default", 89 # "bug_trail" 90 ], 91 "level": "WARNING", 92 "propagate": False, 93 } 94 return logging_config
Basic style
11def invoke_pylint(module_name: str, minimum_score: float) -> CommandResult: 12 """ 13 Runs pylint on the module. 14 15 Args: 16 module_name (str): The name of the module to run pylint on. 17 minimum_score (float): The minimum score to pass. 18 19 Returns: 20 CommandResult: The result of the command. 21 """ 22 command_name = "pylint" 23 arg_string = f"'{module_name}' --fail-under {minimum_score}" 24 25 # generic response. 26 return safe_subprocess(command_name, arg_string)
Runs pylint on the module.
Args: module_name (str): The name of the module to run pylint on. minimum_score (float): The minimum score to pass.
Returns: CommandResult: The result of the command.
9def invoke_black(file_path: str) -> CommandResult: 10 """ 11 Runs black on the file or folder. Code 128 means the file is hosed. 12 13 Args: 14 file_path (str): The name of the module to run black on. 15 16 Returns: 17 CommandResult: The result of the command. 18 """ 19 command_name = "black" 20 arg_string = f"'{file_path}' --check" 21 22 return safe_subprocess(command_name, arg_string)
Runs black on the file or folder. Code 128 means the file is hosed.
Args: file_path (str): The name of the module to run black on.
Returns: CommandResult: The result of the command.
10def count_lines_of_code(file_path: str) -> "SourceAnalysis": 11 """ 12 Check the lines of code in a file. File must exist. 13 Args: 14 file_path (str): The path to the file. 15 16 Returns: 17 SourceAnalysis: The analysis of the file, including line counts. 18 """ 19 try: 20 from pygount import SourceAnalysis # pylint: disable=import-outside-toplevel 21 except ImportError as exc: 22 raise RuntimeError( 23 "count_lines_of_code requires pygount. Install ai_shell[checkers] or the pygount package." 24 ) from exc 25 26 return SourceAnalysis.from_file(file_path, "pygount", encoding="utf-8")
Check the lines of code in a file. File must exist. Args: file_path (str): The path to the file.
Returns: SourceAnalysis: The analysis of the file, including line counts.
9@contextmanager 10def change_directory(new_path: str) -> Iterator[None]: 11 """Change the current working directory to a new path. 12 13 Args: 14 new_path (str): The new path to change to. 15 """ 16 original_directory = os.getcwd() 17 try: 18 os.chdir(new_path) 19 yield None 20 finally: 21 os.chdir(original_directory)
Change the current working directory to a new path.
Args: new_path (str): The new path to change to.