LLM tools

Helpful tools for running cli commands and reading, modifying, and creating files in python. This is used primarily for AI’s in tool loops for automating tasks involving the filesystem.

Error handling helpers


source

explain_exc


def explain_exc(
    task:str=''
):

Convert an current exception to an LLM friendly error message.

def calc_div(a:int,b:int):
    "Divides a/b - example tool"
    try: return a/b
    except: return explain_exc("dividing")
calc_div(1,0)
'Error: division by zero'

source

ensure


def ensure(
    b:bool, msg:str=''
):

Works like assert b, msg but raise ValueError and is not disabled when run with python -O

def calc_div(a:int,b:int):
    "Divides a/b - example tool"
    try:
        ensure(b != 0, "B cannot be zero") 
        return a/b
    except: return explain_exc("dividing")
calc_div(1,0)
'Error: B cannot be zero'

source

valid_path


def valid_path(
    path:str, must_exist:bool=True, chk_perms:bool=True
)->Path:

Return expanded/resolved Path, raising FileNotFoundError if must_exist and missing

assert valid_path('.')
assert valid_path('/tmp')

test_fail(lambda: valid_path('..'), exc=PermissionError)
try: valid_path(".missing")
except: print(explain_exc())
Error: File not found: /Users/jhoward/aai-ws/fastcore/nbs/.missing

Bash Tools


source

run_cmd


def run_cmd(
    cmd:str, # The command name to run
    argstr:str='', # All args to the command, will be split with shlex
    disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command
    allow_re:str=None, # optional regex which, if not matched on argstr, will disallow the command
):

Run cmd passing split argstr, optionally checking for allowed argstr

With this little function, we can now run any cli command:

print(run_cmd('ls')[:128])
__pycache__
_parallel_win.ipynb
_quarto.yml
00_test.ipynb
000_tour.ipynb
01_basics.ipynb
02_foundation.ipynb
03_xtras

Note that, for tool safety, this is not passed through the shell, so wildcards, env vars, etc will not work:

print(run_cmd('ls', 'f*')[:128])
ls: f*: No such file or directory

Let’s create some useful functions from this that will allow for searching, reading and modifing content on the file system.


source

rg


def rg(
    argstr:str, # All args to the command, will be split with shlex
    disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command
    allow_re:str=None, # optional regex which, if not matched on argstr, will disallow the command
):

Run the rg command with the args in argstr (no need to backslash escape)

rg('fast.ai CNAME')
'1:fastcore.fast.ai\n'

Functions implemented with run_cmd like this one can be passed regexps to allow or disallow arg strs, i.e to block parent or root directories:

disallowed = r' /|\.\.'
rg('[email protected] ..', disallow_re=disallowed)
'Error: args disallowed'
rg('[email protected] /', disallow_re=disallowed)
'Error: args disallowed'
print(rg('fast.ai CNAME', disallow_re=disallowed))
1:fastcore.fast.ai

NB: These tools have special behavior around errors. Since these have been speficially designed for work with LLMs, any exceptions created from there use is returned as a string to help them debug their work.

run_cmd('asdfe')
"Error: [Errno 2] No such file or directory: 'asdfe'"

source

sed


def sed(
    argstr:str, # All args to the command, will be split with shlex
    disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command
    allow_re:str=None, # optional regex which, if not matched on argstr, will disallow the command
):

Run the sed command with the args in argstr (e.g for reading a section of a file)

print(sed('-n "1,5 p" _quarto.yml'))
project:
  type: website
  pre-render: 
    - pysym2md --output_file apilist.txt fastcore
  post-render: 
# Print line numbers too
print(sed('-n "1,5 {=;p;}" _quarto.yml'))
1
project:
2
  type: website
3
  pre-render: 
4
    - pysym2md --output_file apilist.txt fastcore
5
  post-render: 

Text Edit Tools

Python implementations of the text editor tools from Anthropic, plus more. These tools are especially useful in an AI’s tool loop. See claudette for examples.


source

view


def view(
    path:str, # Path to directory or file to view
    view_range:tuple=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF. Do NOT use unless it's known that the file is too big to keep in context—simply view the WHOLE file when possible
    nums:bool=False, # Whether to show line numbers
    skip_folders:tuple=('_proc', '__pycache__'), # Folder names to skip when listing directories
):

View directory or file contents with optional line range and numbers

You can specify line ranges and whether to have the output contain line numbers:

print(view('_quarto.yml', (1,10), nums=True))
     1 │ project:
     2 │   type: website
     3 │   pre-render: 
     4 │     - pysym2md --output_file apilist.txt fastcore
     5 │   post-render: 
     6 │     - llms_txt2ctx llms.txt --optional true --save_nbdev_fname llms-ctx-full.txt
     7 │     - llms_txt2ctx llms.txt --save_nbdev_fname llms-ctx.txt
     8 │   resources: 
     9 │     - "*.txt"
    10 │   preview:

Here’s what the output looks like when viewing a directory:

print(view('.', (1,5)).replace(os.getcwd(), '/path'))
Directory contents of /path:
/path/llms.txt (3.7k)
/path/000_tour.ipynb (18.2k)
/path/parallel_test.py (0.6k)
/path/_quarto.yml (0.8k)
/path/08_style.ipynb (12.3k)

source

create


def create(
    path:str, # Path where the new file should be created
    file_text:str, # Content to write to the file
    overwrite:bool=False, # Whether to overwrite existing files
)->str:

Creates a new file with the given content at the specified path

print(create('test.txt', 'Hello, world!').replace(os.getcwd(), '/path'))
f = Path('test.txt')
test_eq(f.exists(), True)
print('Contents:\n', view(f, nums=True))
Created file /path/test.txt.
Contents:
      1 │ Hello, world!

source

insert


def insert(
    path:str, # Path to the file to modify
    insert_line:int, # Line number where to insert (0-based indexing)
    new_str:str, # Text to insert at the specified line
)->str:

Insert new_str at specified line number

insert(f, 0, 'Let\'s add a new line')
print(view(f, nums=True))
     1 │ Let's add a new line
     2 │ Hello, world!

source

str_replace


def str_replace(
    path:str, # Path to the file to modify
    old_str:str, # Text to find and replace
    new_str:str, # Text to replace with
)->str:

Replace first occurrence of old_str with new_str in file

print(str_replace(f, 'new line','new line:'))
print(view(f, nums=True))
Replaced text in /Users/jhoward/aai-ws/fastcore/nbs/test.txt
     1 │ Let's add a new line:
     2 │ Hello, world!
str_replace(f, 'missing line', '')
'Error: Text "missing l…" not found in file'

source

strs_replace


def strs_replace(
    path:str, # Path to the file to modify
    old_strs:list, # List of strings to find and replace
    new_strs:list, # List of replacement strings (must match length of old_strs)
):

Replace for each str pair in old_strs,new_strs

print(strs_replace(f, ["add a new line", "world!"], ["just say", "friends!\nNice to see you."]))
print(view(f, nums=True))
Results for each replacement:
Replaced text in /Users/jhoward/aai-ws/fastcore/nbs/test.txt; Replaced text in /Users/jhoward/aai-ws/fastcore/nbs/test.txt
     1 │ Let's just say:
     2 │ Hello, friends!
     3 │ Nice to see you.
print(strs_replace(f, ["a missing", "and shouldn't polute the context"], ["", ""]))
print(view(f, nums=True))
Results for each replacement:
Error: Text "a missing" not found in file; Error: Text "and shoul…" not found in file
     1 │ Let's just say:
     2 │ Hello, friends!
     3 │ Nice to see you.

source

replace_lines


def replace_lines(
    path:str, # Path to the file to modify
    start_line:int, # Starting line number to replace (1-based indexing)
    end_line:int, # Ending line number to replace (1-based indexing, inclusive)
    new_content:str, # New content to replace the specified lines
):

Replace lines in file using start and end line-numbers (index starting at 1)

replace_lines('test.txt', 1, 2, 'Replaced first two lines')
print(view('test.txt', nums=True))
     1 │ Replaced first two lines
     2 │ Nice to see you.
replace_lines('missing.txt', 1, 2, 'Replaced first two lines').replace(os.getcwd(), '/path')
'Error: File not found: /path/missing.txt'

source

move_lines


def move_lines(
    path:str, # Path to the file to modify
    start_line:int, # Starting line number to move (1-based)
    end_line:int, # Ending line number to move (1-based, inclusive)
    dest_line:int, # Destination line number (1-based, where lines will be inserted before)
)->str:

Move lines from start_line:end_line to before dest_line

The move_lines function relocates a range of lines within a file to a new position. It handles the tricky index adjustment when the destination is after the removed chunk.

Let’s test it by creating a simple 5-line file:

create('move_test.txt', 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5', overwrite=True)
print(view('move_test.txt', nums=True))
     1 │ Line 1
     2 │ Line 2
     3 │ Line 3
     4 │ Line 4
     5 │ Line 5

Move lines 4-5 up to before line 2:

print(move_lines('move_test.txt', 4, 5, 2))
print(view('move_test.txt', nums=True))
Moved lines 4-5 to line 2
     1 │ Line 1
     2 │ Line 4
     3 │ Line 5
     4 │ Line 2
     5 │ Line 3

Move lines down — moving lines 1-2 to the end (line 6) correctly adjusts the destination index after removal:

print(move_lines('move_test.txt', 1, 2, 6))
print(view('move_test.txt', nums=True))
Moved lines 1-2 to line 4
     1 │ Line 5
     2 │ Line 2
     3 │ Line 3
     4 │ Line 1
     5 │ Line 4

Error handling — destination within source range, invalid line ranges, and invalid destinations are all caught:

print(move_lines('move_test.txt', 2, 3, 3))  # dest within source range
print(move_lines('move_test.txt', 10, 12, 1))  # invalid range
print(move_lines('move_test.txt', 1, 2, 99))  # invalid destination
print(move_lines('mising.txt', 1, 2, 99).replace(os.getcwd(), '/path'))  # missing file
Error: Destination within source range
Error: Invalid range 10-12
Error: Invalid destination 99
Error: File not found: /path/mising.txt
Path('move_test.txt').unlink()
f.unlink()

source

get_callable


def get_callable(
    
):

Return callable objects defined in caller’s module