Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ai-agent-from-scratch/README.md
Original file line number Diff line number Diff line change
@@ -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

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 --python 3.15 --with openai==3.31.0 ai_agent.py
```
110 changes: 110 additions & 0 deletions ai-agent-from-scratch/ai_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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}")
3 changes: 3 additions & 0 deletions ai-agent-from-scratch/habits/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# habits

A tiny command-line habit tracker.
1 change: 1 addition & 0 deletions ai-agent-from-scratch/habits/habits.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"stretch": ["2026-10-10", "2026-10-11", "2026-10-11"], "read": ["2026-10-12"]}
34 changes: 34 additions & 0 deletions ai-agent-from-scratch/habits/habits.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions ai-agent-from-scratch/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
openai==3.31.0
10 changes: 10 additions & 0 deletions ai-agent-from-scratch/source_code_step_1/ai_agent.py
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions ai-agent-from-scratch/source_code_step_2/ai_agent.py
Original file line number Diff line number Diff line change
@@ -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}")
26 changes: 26 additions & 0 deletions ai-agent-from-scratch/source_code_step_3/ai_agent.py
Original file line number Diff line number Diff line change
@@ -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}")
51 changes: 51 additions & 0 deletions ai-agent-from-scratch/source_code_step_4/ai_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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)
# trace("model", "harness", f"text reply ({len(reply.content)} characters)")
print(reply.tool_calls)

print(f"harness -> you: {reply.content}")
89 changes: 89 additions & 0 deletions ai-agent-from-scratch/source_code_step_5/ai_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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}")
Loading
Loading