From 3a834dc7aff375d92c9bf8b508da9f779339eed1 Mon Sep 17 00:00:00 2001 From: Stephen Gruppetta Date: Thu, 15 Oct 2026 14:08:33 +0000 Subject: [PATCH 1/4] Sample code for: Build an AI Agent From Scratch in Python: The Agentic Loop --- ai-agent-from-scratch/README.md | 14 +++ ai-agent-from-scratch/ai_agent.py | 107 ++++++++++++++++++ ai-agent-from-scratch/habits/README.md | 3 + ai-agent-from-scratch/habits/habits.json | 1 + ai-agent-from-scratch/habits/habits.py | 34 ++++++ ai-agent-from-scratch/requirements.txt | 1 + .../source_code_step_1/ai_agent.py | 10 ++ .../source_code_step_2/ai_agent.py | 23 ++++ .../source_code_step_3/ai_agent.py | 26 +++++ .../source_code_step_4/ai_agent.py | 50 ++++++++ .../source_code_step_5/ai_agent.py | 86 ++++++++++++++ .../source_code_step_6/ai_agent.py | 107 ++++++++++++++++++ .../source_code_step_7/ai_agent.py | 107 ++++++++++++++++++ 13 files changed, 569 insertions(+) create mode 100644 ai-agent-from-scratch/README.md create mode 100644 ai-agent-from-scratch/ai_agent.py create mode 100644 ai-agent-from-scratch/habits/README.md create mode 100644 ai-agent-from-scratch/habits/habits.json create mode 100644 ai-agent-from-scratch/habits/habits.py create mode 100644 ai-agent-from-scratch/requirements.txt create mode 100644 ai-agent-from-scratch/source_code_step_1/ai_agent.py create mode 100644 ai-agent-from-scratch/source_code_step_2/ai_agent.py create mode 100644 ai-agent-from-scratch/source_code_step_3/ai_agent.py create mode 100644 ai-agent-from-scratch/source_code_step_4/ai_agent.py create mode 100644 ai-agent-from-scratch/source_code_step_5/ai_agent.py create mode 100644 ai-agent-from-scratch/source_code_step_6/ai_agent.py create mode 100644 ai-agent-from-scratch/source_code_step_7/ai_agent.py diff --git a/ai-agent-from-scratch/README.md b/ai-agent-from-scratch/README.md new file mode 100644 index 0000000000..7916082643 --- /dev/null +++ b/ai-agent-from-scratch/README.md @@ -0,0 +1,14 @@ +# Build an AI Agent From Scratch in Python: The Agentic Loop + +This folder contains sample code for the Real Python tutorial [Build an AI Agent From Scratch in Python: The Agentic Loop](https://realpython.com/ai-agent-from-scratch/). + +- `ai_agent.py`: the finished agent +- `source_code_step_1/` to `source_code_step_7/`: the script at the end of each step +- `habits/`: the sample project that the agent explores + +To run the code, put your OpenAI API key in a `.env` file, then: + +```console +$ export UV_ENV_FILE=.env +$ uv run --with openai==3.31.0 ai_agent.py +``` diff --git a/ai-agent-from-scratch/ai_agent.py b/ai-agent-from-scratch/ai_agent.py new file mode 100644 index 0000000000..655d7a5cad --- /dev/null +++ b/ai-agent-from-scratch/ai_agent.py @@ -0,0 +1,107 @@ +import json +from pathlib import Path + +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def list_files(folder): + return "\n".join(path.name for path in Path(folder).iterdir()) + + +def read_file(path): + return Path(path).read_text() + + +TOOLS = [ + { + "type": "function", + "function": { + "name": "list_files", + "description": "List the files in a folder.", + "parameters": { + "type": "object", + "properties": {"folder": {"type": "string"}}, + "required": ["folder"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read a text file and return its contents.", + "parameters": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + }, +] + +TOOL_FUNCTIONS = { + "list_files": list_files, + "read_file": read_file, +} + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +def ask_model(messages): + trace("harness", "model", f"sending {len(messages)} message(s)") + response = client.chat.completions.create( + model=MODEL, + tools=TOOLS, + messages=messages, + reasoning_effort="none", + ) + reply = response.choices[0].message + messages.append(reply) + for call in reply.tool_calls or []: + trace( + "model", + "harness", + f"please run {call.function.name}{call.function.arguments}", + ) + if reply.content: + trace( + "model", "harness", f"text reply ({len(reply.content)}) characters" + ) + + return reply + + +def run_tool(call): + args = json.loads(call.function.arguments) + trace("harness", "tool", f"{call.function.name}({args})") + result = TOOL_FUNCTIONS[call.function.name](**args) + trace("tool", "harness", f"{len(result)} characters") + return result + + +messages = [] + +while True: + user_input = input("you -> harness: ") + messages.append({"role": "user", "content": user_input}) + reply = ask_model(messages) + + while reply.tool_calls: + for call in reply.tool_calls: + result = run_tool(call) + messages.append( + { + "role": "tool", + "tool_call_id": call.id, + "content": result, + }, + ) + + reply = ask_model(messages) + + print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/habits/README.md b/ai-agent-from-scratch/habits/README.md new file mode 100644 index 0000000000..b5a9508e3b --- /dev/null +++ b/ai-agent-from-scratch/habits/README.md @@ -0,0 +1,3 @@ +# habits + +A tiny command-line habit tracker. diff --git a/ai-agent-from-scratch/habits/habits.json b/ai-agent-from-scratch/habits/habits.json new file mode 100644 index 0000000000..26384988ba --- /dev/null +++ b/ai-agent-from-scratch/habits/habits.json @@ -0,0 +1 @@ +{"stretch": ["2026-10-10", "2026-10-11", "2026-10-11"], "read": ["2026-10-12"]} diff --git a/ai-agent-from-scratch/habits/habits.py b/ai-agent-from-scratch/habits/habits.py new file mode 100644 index 0000000000..20fb24270c --- /dev/null +++ b/ai-agent-from-scratch/habits/habits.py @@ -0,0 +1,34 @@ +import json +import sys +from datetime import date +from pathlib import Path + +DATA_FILE = Path("habits.json") + + +def load(): + if DATA_FILE.exists(): + return json.loads(DATA_FILE.read_text()) + return {} + + +def save(data): + DATA_FILE.write_text(json.dumps(data, indent=2)) + + +def check_in(habit): + data = load() + data.setdefault(habit, []).append(date.today().isoformat()) + save(data) + + +def streak(habit): + return len(load().get(habit, [])) + + +if __name__ == "__main__": + command, habit = sys.argv[1], sys.argv[2] + if command == "done": + check_in(habit) + elif command == "streak": + print(f"{habit}: {streak(habit)} days") diff --git a/ai-agent-from-scratch/requirements.txt b/ai-agent-from-scratch/requirements.txt new file mode 100644 index 0000000000..3c24be1d6c --- /dev/null +++ b/ai-agent-from-scratch/requirements.txt @@ -0,0 +1 @@ +openai==3.31.0 diff --git a/ai-agent-from-scratch/source_code_step_1/ai_agent.py b/ai-agent-from-scratch/source_code_step_1/ai_agent.py new file mode 100644 index 0000000000..bd71722e5f --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_1/ai_agent.py @@ -0,0 +1,10 @@ +from openai import OpenAI + +client = OpenAI() + +response = client.chat.completions.create( + model="gpt-6-luna", + messages=[{"role": "user", "content": "What's in the habits folder?"}], +) + +print(response.choices[0].message.content) diff --git a/ai-agent-from-scratch/source_code_step_2/ai_agent.py b/ai-agent-from-scratch/source_code_step_2/ai_agent.py new file mode 100644 index 0000000000..a84c73f152 --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_2/ai_agent.py @@ -0,0 +1,23 @@ +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +while True: + user_input = input("you -> harness: ") + messages = [{"role": "user", "content": user_input}] + + trace("harness", "model", "sending a message") + response = client.chat.completions.create( + model=MODEL, + messages=messages, + ) + reply = response.choices[0].message + trace("model", "harness", f"text reply ({len(reply.content)}) characters") + + print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_3/ai_agent.py b/ai-agent-from-scratch/source_code_step_3/ai_agent.py new file mode 100644 index 0000000000..cfd49aa554 --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_3/ai_agent.py @@ -0,0 +1,26 @@ +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +messages = [] + +while True: + user_input = input("you -> harness: ") + messages.append({"role": "user", "content": user_input}) + + trace("harness", "model", f"sending {len(messages)} message(s)") + response = client.chat.completions.create( + model=MODEL, + messages=messages, + ) + reply = response.choices[0].message + messages.append(reply) + trace("model", "harness", f"text reply ({len(reply.content)}) characters") + + print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_4/ai_agent.py b/ai-agent-from-scratch/source_code_step_4/ai_agent.py new file mode 100644 index 0000000000..8185866fe1 --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_4/ai_agent.py @@ -0,0 +1,50 @@ +from pathlib import Path + +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def list_files(folder): + return "\n".join(path.name for path in Path(folder).iterdir()) + + +TOOLS = [ + { + "type": "function", + "function": { + "name": "list_files", + "description": "List the files in a folder.", + "parameters": { + "type": "object", + "properties": {"folder": {"type": "string"}}, + "required": ["folder"], + }, + }, + }, +] + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +messages = [] + +while True: + user_input = input("you -> harness: ") + messages.append({"role": "user", "content": user_input}) + + trace("harness", "model", f"sending {len(messages)} message(s)") + response = client.chat.completions.create( + model=MODEL, + tools=TOOLS, + messages=messages, + reasoning_effort="none", + ) + reply = response.choices[0].message + messages.append(reply) + print(reply.tool_calls) + + print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_5/ai_agent.py b/ai-agent-from-scratch/source_code_step_5/ai_agent.py new file mode 100644 index 0000000000..16b77f8c7a --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_5/ai_agent.py @@ -0,0 +1,86 @@ +import json +from pathlib import Path + +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def list_files(folder): + return "\n".join(path.name for path in Path(folder).iterdir()) + + +TOOLS = [ + { + "type": "function", + "function": { + "name": "list_files", + "description": "List the files in a folder.", + "parameters": { + "type": "object", + "properties": {"folder": {"type": "string"}}, + "required": ["folder"], + }, + }, + }, +] + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +def ask_model(messages): + trace("harness", "model", f"sending {len(messages)} message(s)") + response = client.chat.completions.create( + model=MODEL, + tools=TOOLS, + messages=messages, + reasoning_effort="none", + ) + reply = response.choices[0].message + messages.append(reply) + for call in reply.tool_calls or []: + trace( + "model", + "harness", + f"please run {call.function.name}{call.function.arguments}", + ) + if reply.content: + trace( + "model", "harness", f"text reply ({len(reply.content)}) characters" + ) + + return reply + + +def run_tool(call): + args = json.loads(call.function.arguments) + trace("harness", "tool", f"{call.function.name}({args})") + result = list_files(**args) + trace("tool", "harness", f"{len(result)} characters") + return result + + +messages = [] + +while True: + user_input = input("you -> harness: ") + messages.append({"role": "user", "content": user_input}) + reply = ask_model(messages) + + if reply.tool_calls: + for call in reply.tool_calls: + result = run_tool(call) + messages.append( + { + "role": "tool", + "tool_call_id": call.id, + "content": result, + }, + ) + + reply = ask_model(messages) + + print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_6/ai_agent.py b/ai-agent-from-scratch/source_code_step_6/ai_agent.py new file mode 100644 index 0000000000..027513ccd5 --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_6/ai_agent.py @@ -0,0 +1,107 @@ +import json +from pathlib import Path + +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def list_files(folder): + return "\n".join(path.name for path in Path(folder).iterdir()) + + +def read_file(path): + return Path(path).read_text() + + +TOOLS = [ + { + "type": "function", + "function": { + "name": "list_files", + "description": "List the files in a folder.", + "parameters": { + "type": "object", + "properties": {"folder": {"type": "string"}}, + "required": ["folder"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read a text file and return its contents.", + "parameters": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + }, +] + +TOOL_FUNCTIONS = { + "list_files": list_files, + "read_file": read_file, +} + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +def ask_model(messages): + trace("harness", "model", f"sending {len(messages)} message(s)") + response = client.chat.completions.create( + model=MODEL, + tools=TOOLS, + messages=messages, + reasoning_effort="none", + ) + reply = response.choices[0].message + messages.append(reply) + for call in reply.tool_calls or []: + trace( + "model", + "harness", + f"please run {call.function.name}{call.function.arguments}", + ) + if reply.content: + trace( + "model", "harness", f"text reply ({len(reply.content)}) characters" + ) + + return reply + + +def run_tool(call): + args = json.loads(call.function.arguments) + trace("harness", "tool", f"{call.function.name}({args})") + result = TOOL_FUNCTIONS[call.function.name](**args) + trace("tool", "harness", f"{len(result)} characters") + return result + + +messages = [] + +while True: + user_input = input("you -> harness: ") + messages.append({"role": "user", "content": user_input}) + reply = ask_model(messages) + + if reply.tool_calls: + for call in reply.tool_calls: + result = run_tool(call) + messages.append( + { + "role": "tool", + "tool_call_id": call.id, + "content": result, + }, + ) + + reply = ask_model(messages) + + print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_7/ai_agent.py b/ai-agent-from-scratch/source_code_step_7/ai_agent.py new file mode 100644 index 0000000000..655d7a5cad --- /dev/null +++ b/ai-agent-from-scratch/source_code_step_7/ai_agent.py @@ -0,0 +1,107 @@ +import json +from pathlib import Path + +from openai import OpenAI + +client = OpenAI() +MODEL = "gpt-6-luna" + + +def list_files(folder): + return "\n".join(path.name for path in Path(folder).iterdir()) + + +def read_file(path): + return Path(path).read_text() + + +TOOLS = [ + { + "type": "function", + "function": { + "name": "list_files", + "description": "List the files in a folder.", + "parameters": { + "type": "object", + "properties": {"folder": {"type": "string"}}, + "required": ["folder"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read a text file and return its contents.", + "parameters": { + "type": "object", + "properties": {"path": {"type": "string"}}, + "required": ["path"], + }, + }, + }, +] + +TOOL_FUNCTIONS = { + "list_files": list_files, + "read_file": read_file, +} + + +def trace(sender, receiver, text): + print(f" [{sender} -> {receiver}] {text}") + + +def ask_model(messages): + trace("harness", "model", f"sending {len(messages)} message(s)") + response = client.chat.completions.create( + model=MODEL, + tools=TOOLS, + messages=messages, + reasoning_effort="none", + ) + reply = response.choices[0].message + messages.append(reply) + for call in reply.tool_calls or []: + trace( + "model", + "harness", + f"please run {call.function.name}{call.function.arguments}", + ) + if reply.content: + trace( + "model", "harness", f"text reply ({len(reply.content)}) characters" + ) + + return reply + + +def run_tool(call): + args = json.loads(call.function.arguments) + trace("harness", "tool", f"{call.function.name}({args})") + result = TOOL_FUNCTIONS[call.function.name](**args) + trace("tool", "harness", f"{len(result)} characters") + return result + + +messages = [] + +while True: + user_input = input("you -> harness: ") + messages.append({"role": "user", "content": user_input}) + reply = ask_model(messages) + + while reply.tool_calls: + for call in reply.tool_calls: + result = run_tool(call) + messages.append( + { + "role": "tool", + "tool_call_id": call.id, + "content": result, + }, + ) + + reply = ask_model(messages) + + print(f"harness -> you: {reply.content}") From c361f84a5e3023a525285e133460ae356862714f Mon Sep 17 00:00:00 2001 From: Stephen Gruppetta Date: Fri, 16 Oct 2026 09:22:36 +0000 Subject: [PATCH 2/4] Fix the model-reply trace text and comment out the trace in Step 4 Co-Authored-By: Claude Opus 5.5 (1M context) --- ai-agent-from-scratch/ai_agent.py | 2 +- ai-agent-from-scratch/source_code_step_2/ai_agent.py | 2 +- ai-agent-from-scratch/source_code_step_3/ai_agent.py | 2 +- ai-agent-from-scratch/source_code_step_4/ai_agent.py | 1 + ai-agent-from-scratch/source_code_step_5/ai_agent.py | 2 +- ai-agent-from-scratch/source_code_step_6/ai_agent.py | 2 +- ai-agent-from-scratch/source_code_step_7/ai_agent.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ai-agent-from-scratch/ai_agent.py b/ai-agent-from-scratch/ai_agent.py index 655d7a5cad..151ca08482 100644 --- a/ai-agent-from-scratch/ai_agent.py +++ b/ai-agent-from-scratch/ai_agent.py @@ -70,7 +70,7 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)}) characters" + "model", "harness", f"text reply ({len(reply.content)} characters)" ) return reply diff --git a/ai-agent-from-scratch/source_code_step_2/ai_agent.py b/ai-agent-from-scratch/source_code_step_2/ai_agent.py index a84c73f152..b6b59669e8 100644 --- a/ai-agent-from-scratch/source_code_step_2/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_2/ai_agent.py @@ -18,6 +18,6 @@ def trace(sender, receiver, text): messages=messages, ) reply = response.choices[0].message - trace("model", "harness", f"text reply ({len(reply.content)}) characters") + trace("model", "harness", f"text reply ({len(reply.content)} characters)") print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_3/ai_agent.py b/ai-agent-from-scratch/source_code_step_3/ai_agent.py index cfd49aa554..17249c4847 100644 --- a/ai-agent-from-scratch/source_code_step_3/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_3/ai_agent.py @@ -21,6 +21,6 @@ def trace(sender, receiver, text): ) reply = response.choices[0].message messages.append(reply) - trace("model", "harness", f"text reply ({len(reply.content)}) characters") + trace("model", "harness", f"text reply ({len(reply.content)} characters)") print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_4/ai_agent.py b/ai-agent-from-scratch/source_code_step_4/ai_agent.py index 8185866fe1..8a85180d63 100644 --- a/ai-agent-from-scratch/source_code_step_4/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_4/ai_agent.py @@ -45,6 +45,7 @@ def trace(sender, receiver, text): ) reply = response.choices[0].message messages.append(reply) + # trace("model", "harness", f"text reply ({len(reply.content)} characters)") print(reply.tool_calls) print(f"harness -> you: {reply.content}") diff --git a/ai-agent-from-scratch/source_code_step_5/ai_agent.py b/ai-agent-from-scratch/source_code_step_5/ai_agent.py index 16b77f8c7a..13d73ba9d2 100644 --- a/ai-agent-from-scratch/source_code_step_5/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_5/ai_agent.py @@ -49,7 +49,7 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)}) characters" + "model", "harness", f"text reply ({len(reply.content)} characters)" ) return reply diff --git a/ai-agent-from-scratch/source_code_step_6/ai_agent.py b/ai-agent-from-scratch/source_code_step_6/ai_agent.py index 027513ccd5..51d398c02e 100644 --- a/ai-agent-from-scratch/source_code_step_6/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_6/ai_agent.py @@ -70,7 +70,7 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)}) characters" + "model", "harness", f"text reply ({len(reply.content)} characters)" ) return reply diff --git a/ai-agent-from-scratch/source_code_step_7/ai_agent.py b/ai-agent-from-scratch/source_code_step_7/ai_agent.py index 655d7a5cad..151ca08482 100644 --- a/ai-agent-from-scratch/source_code_step_7/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_7/ai_agent.py @@ -70,7 +70,7 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)}) characters" + "model", "harness", f"text reply ({len(reply.content)} characters)" ) return reply From b20abf8f856ec9ded9c611f42c1b08adbcfd1ffe Mon Sep 17 00:00:00 2001 From: Stephen Gruppetta Date: Fri, 16 Oct 2026 09:28:18 +0000 Subject: [PATCH 3/4] Match the Step 5 tutorial edits in ask_model() - Blank line before the tool-call loop - Split the text-reply trace with one argument per line Applied to source_code_step_5, 6, 7 and the final ai_agent.py. Co-Authored-By: Claude Opus 5.5 (1M context) --- ai-agent-from-scratch/ai_agent.py | 5 ++++- ai-agent-from-scratch/source_code_step_5/ai_agent.py | 5 ++++- ai-agent-from-scratch/source_code_step_6/ai_agent.py | 5 ++++- ai-agent-from-scratch/source_code_step_7/ai_agent.py | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ai-agent-from-scratch/ai_agent.py b/ai-agent-from-scratch/ai_agent.py index 151ca08482..8ea7da788f 100644 --- a/ai-agent-from-scratch/ai_agent.py +++ b/ai-agent-from-scratch/ai_agent.py @@ -62,6 +62,7 @@ def ask_model(messages): ) reply = response.choices[0].message messages.append(reply) + for call in reply.tool_calls or []: trace( "model", @@ -70,7 +71,9 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)} characters)" + "model", + "harness", + f"text reply ({len(reply.content)} characters)", ) return reply diff --git a/ai-agent-from-scratch/source_code_step_5/ai_agent.py b/ai-agent-from-scratch/source_code_step_5/ai_agent.py index 13d73ba9d2..912daba99c 100644 --- a/ai-agent-from-scratch/source_code_step_5/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_5/ai_agent.py @@ -41,6 +41,7 @@ def ask_model(messages): ) reply = response.choices[0].message messages.append(reply) + for call in reply.tool_calls or []: trace( "model", @@ -49,7 +50,9 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)} characters)" + "model", + "harness", + f"text reply ({len(reply.content)} characters)", ) return reply diff --git a/ai-agent-from-scratch/source_code_step_6/ai_agent.py b/ai-agent-from-scratch/source_code_step_6/ai_agent.py index 51d398c02e..049da61fad 100644 --- a/ai-agent-from-scratch/source_code_step_6/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_6/ai_agent.py @@ -62,6 +62,7 @@ def ask_model(messages): ) reply = response.choices[0].message messages.append(reply) + for call in reply.tool_calls or []: trace( "model", @@ -70,7 +71,9 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)} characters)" + "model", + "harness", + f"text reply ({len(reply.content)} characters)", ) return reply diff --git a/ai-agent-from-scratch/source_code_step_7/ai_agent.py b/ai-agent-from-scratch/source_code_step_7/ai_agent.py index 151ca08482..8ea7da788f 100644 --- a/ai-agent-from-scratch/source_code_step_7/ai_agent.py +++ b/ai-agent-from-scratch/source_code_step_7/ai_agent.py @@ -62,6 +62,7 @@ def ask_model(messages): ) reply = response.choices[0].message messages.append(reply) + for call in reply.tool_calls or []: trace( "model", @@ -70,7 +71,9 @@ def ask_model(messages): ) if reply.content: trace( - "model", "harness", f"text reply ({len(reply.content)} characters)" + "model", + "harness", + f"text reply ({len(reply.content)} characters)", ) return reply From 8a73c6a56bec594b0f76d11b8ce1683e04a2dfa4 Mon Sep 17 00:00:00 2001 From: Stephen Gruppetta Date: Fri, 16 Oct 2026 10:43:17 +0000 Subject: [PATCH 4/4] Note Python 3.15 in the README Co-Authored-By: Claude Opus 5.5 (1M context) --- ai-agent-from-scratch/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ai-agent-from-scratch/README.md b/ai-agent-from-scratch/README.md index 7916082643..59283c103b 100644 --- a/ai-agent-from-scratch/README.md +++ b/ai-agent-from-scratch/README.md @@ -6,9 +6,9 @@ This folder contains sample code for the Real Python tutorial [Build an AI Agent - `source_code_step_1/` to `source_code_step_7/`: the script at the end of each step - `habits/`: the sample project that the agent explores -To run the code, put your OpenAI API key in a `.env` file, then: +The code uses Python 3.15. To run it, put your OpenAI API key in a `.env` file, then: ```console $ export UV_ENV_FILE=.env -$ uv run --with openai==3.31.0 ai_agent.py +$ uv run --python 3.15 --with openai==3.31.0 ai_agent.py ```