Part 4: File tools

The coding agent can only change a project if we give it tools for reading and writing files. In this step we copy the Django template into a new project folder, then define the Python methods the model calls.

Copy the template

The notebook starts with an interactive helper:

import os
import shutil

def start():
    project_name = input("Enter the new Django project name: ").strip()
    if not project_name:
        print("Project name cannot be empty.")
        return

    if os.path.exists(project_name):
        print(f"Directory '{project_name}' already exists. Please choose a different name or remove the existing directory.")
        return

    shutil.copytree("django_template", project_name)
    print(f"Django template copied to '{project_name}' directory.")

    return project_name

Use it in the notebook:

project_name = start()

Some notebooks use a non-interactive version so repeated experiments are easier:

def start(project_name):
    if not project_name:
        print("Project name cannot be empty.")
        return False

    if os.path.exists(project_name):
        print(f"Directory '{project_name}' already exists. Please choose a different name or remove the existing directory.")
        return False

    shutil.copytree("django_template", project_name)
    print(f"Django template copied to '{project_name}' directory.")

    return True

Call it with a folder name:

start("todo-two")

The important part is that every generated app gets its own copy. If the agent makes a mess, the original django_template remains clean.

Tool class

The workshop code puts the tools in tools.py. The class takes the project directory once, then every method works with paths relative to that directory.

from pathlib import Path
import os
import subprocess

class AgentTools:
    SKIP_DIRS = {
        ".venv",
        "__pycache__",
        ".git",
        ".pytest_cache",
        ".mypy_cache",
        ".coverage",
        "node_modules",
        ".DS_Store",
    }

    def __init__(self, project_dir: Path):
        self.project_dir = project_dir

Skipping generated and dependency folders keeps the file tree and searches small. Otherwise the model wastes context on .venv, cache folders, and irrelevant files.

Read and write files

The agent needs to read files before changing them:

    def read_file(self, filepath: str) -> str:
        """
        Read and return the contents of a file at the given relative filepath.

        Parameters:
            filepath (str): Path to the file, relative to the project directory.
        Returns:
            str: Contents of the file.
        """
        abs_path = self.project_dir / filepath
        try:
            with open(abs_path, "r", encoding="utf-8") as f:
                return f.read()
        except FileNotFoundError:
            return f"Error: file '{filepath}' not found."

It also needs to create and replace files:

    def write_file(self, filepath: str, content: str) -> None:
        """
        Write the given content to a file at the given relative filepath,
        creating directories as needed.

        Parameters:
            filepath (str): Path to the file, relative to the project directory.
            content (str): Content to write to the file.
        Returns:
            None
        """
        abs_path = self.project_dir / filepath
        abs_path.parent.mkdir(parents=True, exist_ok=True)
        with open(abs_path, "w", encoding="utf-8") as f:
            f.write(content)

These two methods let the coding agent edit code. The model chooses what to read, decides what to change, and calls write_file with new content.

File tree

The model needs orientation before it can edit. see_file_tree gives it a compact list of paths.

    def see_file_tree(self, root_dir: str = ".") -> list[str]:
        """
        Return a list of all files and directories under the given root directory,
        relative to the project directory.
        """
        abs_root = self.project_dir / root_dir
        tree = []

        for dirpath, dirnames, filenames in os.walk(abs_root):
            for skip_dir in list(dirnames):
                if skip_dir in self.SKIP_DIRS:
                    dirnames.remove(skip_dir)

            for name in dirnames + filenames:
                full_path = os.path.join(dirpath, name)
                rel_path = os.path.relpath(full_path, self.project_dir)
                tree.append(rel_path)

        return tree

This is the equivalent of letting the agent look around the project before it starts editing. In the live run, the model used the tree plus file reads to discover the existing Django structure.

Bash command tool

The agent can run short validation commands, but it must not start the Django development server from the notebook. A server process keeps running and blocks the tool call.

    def execute_bash_command(
        self, command: str, cwd: str = None
    ) -> tuple[str, str, int]:
        """
        Execute a bash command in the shell and return its output, error,
        and exit code. Blocks running the Django development server.
        """
        if "runserver" in command:
            return (
                "",
                "Error: Running the Django development server (runserver) is not allowed through this tool.",
                1,
            )

After that guard, run the command with a timeout:

        abs_cwd = (self.project_dir / cwd) if cwd else self.project_dir

        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            cwd=abs_cwd,
            timeout=15,
            encoding="utf-8",
            errors="replace",
        )

        return result.stdout, result.stderr, result.returncode

The timeout is part of the same safety idea. The tool should be useful for checks like python manage.py makemigrations, not for long-running interactive processes.

Search in files

The last tool is a simple grep. It returns file path, line number, and the matching line.

    def search_in_files(
        self, pattern: str, root_dir: str = "."
    ) -> list[tuple[str, int, str]]:
        """
        Search for a pattern in all files under the given root directory.
        """
        abs_root = self.project_dir / root_dir
        matches = []

        for dirpath, dirnames, filenames in os.walk(abs_root):
            for skip_dir in list(dirnames):
                if skip_dir in self.SKIP_DIRS:
                    dirnames.remove(skip_dir)

The method scans text files and skips files it cannot decode:

            for filename in filenames:
                filepath = os.path.join(dirpath, filename)
                try:
                    with open(filepath, "r", encoding="utf-8") as f:
                        for i, line in enumerate(f, 1):
                            if pattern in line:
                                rel_path = os.path.relpath(filepath, self.project_dir)
                                matches.append((rel_path, i, line.strip()))
                except Exception:
                    continue

        return matches

This is enough for the workshop agent. A production coding agent would also need patch-based edits, better command isolation, streaming logs, and stronger safety rules.

Try the tools

Import the class and point it at the copied project:

from pathlib import Path
import tools

project_path = Path(project_name)
agent_tools = tools.AgentTools(project_path)

List files:

agent_tools.see_file_tree()

Read a file:

agent_tools.read_file("myproject/urls.py")

If these calls work, the agent can use the same methods through tool calling.

Continue with Part 5: Developer prompt.

Questions & Answers (0)

Sign in to ask questions