← Table of Contents · Page 820 of 868

APPENDIX H: RESUME PORTFOLIO BUILDING CAPSTONE PROJECTS

Part 10 of 56

MICRO-STEPS Part A: Prepare the folders and the invented data. 1. Create a new folder named project1_tool_agent anywhere you like. 2. Open a terminal inside that folder. 3. Type python --version and press Enter. You should see a version number that begins with 3.10 or higher, such as 3.12. 4. Inside project1_tool_agent, create a folder named workspace. 5. Inside project1_tool_agent, create a folder named output and leave it empty. 6. Inside workspace, create a text file named note_a.txt containing the single sentence Tomatoes need six hours of sun each day. 7. Inside workspace, create a text file named note_b.txt containing the single sentence Water the seedlings every morning. 8. Inside workspace, create a text file named note_c.txt containing the single sentence Compost feeds the soil. 9. Directly inside project1_tool_agent, not inside workspace, create a file named secret.txt containing the words pretend secret. You should see: three notes inside workspace, nothing inside output, and secret.txt sitting outside both folders, like a locked drawer that the assistant must never open. Part B: Build the permission gate. 10. Create a file named gate.py in project1_tool_agent. 11. Type or paste the following code into gate.py and save it. File: gate.py [START OF PYTHON CODE] # Import the os module to work with file paths import os # Create a text variable called ROOT holding the folder that contains this file ROOT = os.path.dirname(os.path.abspath(__file__)) # Create a text variable called WORKSPACE pointing to the workspace subfolder, the only folder that may be read WORKSPACE = os.path.join(ROOT, "workspace") # Create a text variable called OUTPUT pointing to the output subfolder, the only folder that may be written to OUTPUT = os.path.join(ROOT, "output") # Define a custom error type raised whenever the gate refuses a request class Denied(Exception): pass # Define a function that resolves a path into its full, real, final form def _real(path): return os.path.normcase(os.path.realpath(os.path.join(ROOT, path))) # Define a function that checks whether a resolved path lies inside a given folder def _inside(full, folder): folder = os.path.normcase(os.path.realpath(folder)) try: return os.path.commonpath([full, folder]) == folder except ValueError: return False # Define a function that checks whether the agent may read the given path, raising an error if not def check_read(path): full = _real(path) if not _inside(full, WORKSPACE): raise Denied("read blocked: " + str(path)) return full # Define a function that checks whether the agent may write to the given path, raising an error if not def check_write(path): full = _real(path) if not _inside(full, OUTPUT): raise Denied("write blocked: " + str(path)) return full [END OF PYTHON CODE] Read the code from the top. ROOT is the folder that holds the project, found from the location of the file itself. WORKSPACE and OUTPUT are the only two folders the agent may touch, one for reading and one for writing. Denied is our own kind of error, raised whenever the gate says no. The function _real turns any path into its full, final form, removing the climbing dots and following any shortcuts, so that a crafted path cannot sneak around the guard. The function _inside answers one question: is this finished path really inside that folder? The functions check_read and check_write are the two doors, one for reading and one for writing. 12. In the terminal, type python -c "import gate; print(gate.check_read('workspace/note_a.txt'))" and press Enter. You should see a long path that ends with workspace and then note_a.txt. 13. Type python -c "import gate; gate.check_read('workspace/../secret.txt')" and press Enter. You should see an error message that includes the words Denied: read blocked, which proves that the gate stops the climbing-dots trick. Part C: Build the tools. 14. Create a file named tools.py in project1_tool_agent. 15. Type or paste the following code into tools.py and save it. File: tools.py [START OF PYTHON CODE] # Import the os module to work with file paths and folders import os # Import the read and write permission checks from the gate from gate import check_read, check_write # Define a function that lists the files in an allowed folder def list_files(args): full = check_read(args.get("path", "workspace")) return "\n".join(sorted(os.listdir(full))) # Define a function that reads and returns the contents of one allowed file, capped at 3000 characters def read_note(args): full = check_read(args.get("path", "")) with open(full, "r", encoding="utf-8") as f: return f.read()[:3000] # Define a function that writes a text summary into the allowed output folder def write_summary(args): full = check_write(args.get("path", "")) text = args.get("text", "") os.makedirs(os.path.dirname(full), exist_ok=True) with open(full, "w", encoding="utf-8") as f: f.write(text) return "wrote " + str(len(text)) + " characters" # Create a dictionary variable called TOOLS mapping each tool name to its function TOOLS = {"list_files": list_files, "read_note": read_note, "write_summary": write_summary} # Create a dictionary variable called DESCRIPTIONS mapping each tool name to its plain-language description DESCRIPTIONS = { "list_files": "List the files in the workspace folder. Argument: path.", "read_note": "Read one text file from the workspace folder. Argument: path.", "write_summary": "Write a text file into the output folder. Arguments: path, text.", } [END OF PYTHON CODE] A tool is a named action the harness can run for the model. Each tool receives one dictionary of arguments and passes every path through the gate before touching a file. The dictionary named TOOLS is the tool registry, which is the menu of everything the agent is allowed to ask for. If a tool is not on the menu, the model cannot use it, no matter how politely it asks. DESCRIPTIONS holds a plain sentence for each tool, which a real model would read to learn what the tools do. 16. In the terminal, type python -c "import tools; print(tools.list_files({'path': 'workspace'}))" and press Enter. You should see the three note names, each on its own line. Part D: Build the model adapter and the mock model. 17. Create a file named model.py in project1_tool_agent. 18. Type or paste the following code into model.py and save it. File: model.py [START OF PYTHON CODE] # Import the os module to read environment variables import os # Import the json module to parse structured data import json # Define a function that builds a tool-call reply dictionary for a given tool and arguments def call(tool, args): return {"action": "tool", "tool": tool, "args": args} # Define a function that scripts a stand-in model reply by following a fixed, predictable plan def mock_model(messages): # Read out the original task from the first message task = messages[0]["content"] # Collect every tool result received so far results = [m for m in messages if m["role"] == "tool"] # Collect just the names of the tools whose results have come back names = [m["name"] for m in results] # If the workspace has not been listed yet, ask to list it first if "list_files" not in names: return call("list_files", {"path": "workspace"}) # Collect the text file names out of the list_files result files = [x for x in results[names.index("list_files")]["content"].split("\n") if x.endswith(".txt")] # If this is the sneaky test task and the gate has not blocked an escape attempt yet, try to escape the workspace if "sneaky" in task and not any(m["content"].startswith("BLOCKED") for m in results): return call("read_note", {"path": "workspace/../secret.txt"}) # Collect the successful (non-blocked) note reads so far reads = [m for m in results if m["name"] == "read_note" and not m["content"].startswith("BLOCKED")] # If there are still unread files, ask to read the next one if len(reads) < len(files): return call("read_note", {"path": "workspace/" + files[len(reads)]}) # If the summary has not been written yet, build it from the first line of each note and ask to write it if "write_summary" not in names: lines = [] for i in range(len(reads)): first = reads[i]["content"].splitlines()[0] if reads[i]["content"] else "" lines.append(files[i] + ": " + first) return call("write_summary", {"path": "output/summary.txt", "text": "\n".join(lines)}) # Once the summary has been written, the task is finished return {"action": "finish", "text": "Summary written to output/summary.txt"} # Define a function that parses a model's raw reply text into a predictable action dictionary def parse_reply(text): # Attempt to parse the text as JSON and check it has a recognized action try: data = json.loads(text) if data.get("action") in ("tool", "finish"): return data # If parsing failed, fall through to the default finish response below except ValueError: pass # If the reply was not usable, treat the raw text itself as a finishing answer return {"action": "finish", "text": text} # Define a placeholder function where a real hosted model's calling code should be filled in def ask_hosted(messages): raise NotImplementedError("Fill in this function with your provider's code. It must return the same kind of dictionary the mock returns.") # Define a function that asks either the hosted model or the mock model, based on an environment setting def ask_model(messages): if os.environ.get("HARNESS_MODEL") == "hosted": return ask_hosted(messages) return mock_model(messages) [END OF PYTHON CODE] The function mock_model looks at the messages so far and follows a fixed recipe: list the files, read each note, write the summary, and then finish. If the task contains the word sneaky, the mock first tries to read a path that climbs out of workspace, so that you can watch the gate refuse it. The function ask_model is the single door through which the harness talks to any model, and it chooses the mock unless HARNESS_MODEL is set to hosted. The function ask_hosted is deliberately empty, because you will fill it in with your provider's code only if you decide to use a real model. Part E: Build the loop. 19. Create a file named harness.py in project1_tool_agent. 20. Type or paste the following code into harness.py and save it. File: harness.py [START OF PYTHON CODE] # Import the os module to work with file paths import os # Import the json module to read and write structured data import json # Import the time module to measure and record when events happen import time # Import the sys module to read command-line arguments import sys # Import the custom error type and the shared root folder from the gate from gate import Denied, ROOT # Import the tool registry from tools import TOOLS # Import the function that asks either the hosted or mock model from model import ask_model # Create a text variable called LOG_PATH pointing to the run log file LOG_PATH = os.path.join(ROOT, "run_log.jsonl") # Define a function that appends one structured event to the log file def log(event, data): record = {"time": round(time.time(), 2), "event": event, "data": data} with open(LOG_PATH, "a", encoding="utf-8") as f: f.write(json.dumps(record) + "\n") # Define a function that runs the full think/act/observe loop for one task def run(task, max_steps=10): # Start the conversation with this task as the first user message messages = [{"role": "user", "content": task}] # Record that the task has started log("start", {"task": task}) # Repeat the think/act/observe cycle up to the maximum number of steps for step in range(1, max_steps + 1): # Ask the model what to do next reply = ask_model(messages) # Record the model's reply in the log log("model_reply", {"step": step, "reply": reply}) # If the model says the task is finished, record that and return the final text if reply.get("action") == "finish": log("finish", {"step": step}) return reply.get("text", "") # Otherwise, read out the requested tool name and arguments name = reply.get("tool") args = reply.get("args", {}) # Add the model's tool-call reply to the conversation messages.append({"role": "assistant", "content": json.dumps(reply)}) # If the requested tool is not on the registry's menu, block the request if name not in TOOLS: result = "BLOCKED: unknown tool " + str(name) log("blocked", {"step": step, "why": "unknown tool"}) else: # Attempt to run the requested tool with the requested arguments try: result = TOOLS[name](args) log("tool_ok", {"step": step, "tool": name, "args": args}) # If the permission gate refused the request, record that as the result except Denied as problem: result = "BLOCKED: " + str(problem) log("blocked", {"step": step, "why": str(problem)}) # If anything else went wrong, record that as the result except Exception as problem: result = "ERROR: " + str(problem) log("tool_error", {"step": step, "why": str(problem)}) # Add the tool result to the conversation so the model can see it next step messages.append({"role": "tool", "name": str(name), "content": result}) # If the step limit was reached without finishing, record that and return a stopped message log("step_limit", {"max_steps": max_steps}) return "STOPPED: step limit reached" # If this file is run directly (not imported), run the task given on the command line, or a default task if __name__ == "__main__": task = " ".join(sys.argv[1:]) or "Summarize every note in the workspace folder." print(run(task)) [END OF PYTHON CODE] Notice the shape of the loop. For each step, the harness asks the model what to do, writes the reply to the log, and finishes if the model says finish. Otherwise it looks the tool up on the menu, runs it inside a try block so that a refusal from the gate becomes a message instead of a crash, and adds the result to the conversation so the model can see it on the next turn. If the loop uses up all its steps, the harness writes a step_limit event and returns a stopped message, like a taxi meter that cuts off the ride. Part F: Run it and look inside. 21. In the terminal, type python harness.py and press Enter. You should see the line Summary written to output/summary.txt. 22. Open the file output/summary.txt in your editor. You should see three lines, one for each note, each beginning with the note's file name. 23. Open the file run_log.jsonl in your editor. You should see one record per line, beginning with a start event, then alternating model_reply and tool_ok events, and ending with a finish event. 24. Type python harness.py "Please be sneaky and summarize every note." and press Enter. You should see the same Summary written line, because the agent recovered after the refusal. 25. Open run_log.jsonl again and search for the word blocked. You should see a blocked event whose why field mentions the path that tried to climb out of workspace. 26. Open secret.txt and confirm that it is unchanged and was never copied into output/summary.txt. Part G: Prove it with tests. 27. Create a file named test_harness.py in project1_tool_agent. 28. Type or paste the following code into test_harness.py and save it. File: test_harness.py [START OF PYTHON CODE] # Import the os module to work with file paths import os # Import the json module to read structured data import json # Import the harness module being tested import harness # Import the shared root folder from the gate from gate import ROOT # Define a function that deletes the log and summary files so each test starts clean def reset(): for name in ("run_log.jsonl", os.path.join("output", "summary.txt")): path = os.path.join(ROOT, name) if os.path.exists(path): os.remove(path) # Define a function that reads the log file and returns just the list of event names, in order def events(): with open(harness.LOG_PATH, "r", encoding="utf-8") as f: return [json.loads(line)["event"] for line in f] # Define a test that checks a normal run finishes successfully and writes the summary, with no blocks def test_normal_run(): reset() answer = harness.run("Summarize every note in the workspace folder.") assert answer.startswith("Summary written") assert os.path.exists(os.path.join(ROOT, "output", "summary.txt")) assert "blocked" not in events() print("PASS normal run") # Define a test that checks the gate blocks an attempt to escape the workspace folder def test_gate_blocks_escape(): reset() harness.run("Please be sneaky and summarize every note.") assert "blocked" in events() print("PASS gate blocks escape") # Define a test that checks the loop stops cleanly once it reaches its step limit def test_step_limit(): reset() answer = harness.run("Summarize every note in the workspace folder.", max_steps=2) assert answer.startswith("STOPPED") assert "step_limit" in events() print("PASS step limit") # If this file is run directly (not imported), run all three tests in order if __name__ == "__main__": os.makedirs(os.path.join(ROOT, "workspace"), exist_ok=True) test_normal_run() test_gate_blocks_escape() test_step_limit() print("ALL TESTS PASSED") [END OF PYTHON CODE] 29. In the terminal, type python test_harness.py and press Enter. You should see PASS normal run, PASS gate blocks escape, PASS step limit, and then ALL TESTS PASSED. 30. Open harness.py and change the number 10 in the definition of run to 3. 31. Run python harness.py again. You should see a message beginning with STOPPED, because the three-step limit ran out before the work was done. 32. Change the 3 back to 10 and run the tests once more to confirm that everything passes again. Part H: Save your work. 33. Create a new empty folder on a code hosting site of your choice, or keep the folder safely on your computer. 34. If you use a code hosting site, add the project files but do not add run_log.jsonl or secret.txt, because logs and secrets do not belong in a public portfolio.
Advertisement
Read Aloud:
0:00 / 0:00