Building Your First AI Agent with Python
Build a read-only lookup agent with the Responses API, validation, timeouts, and a bounded loop.
Overview
For learners who want to complete this practical exercise with inspectable sample data and explicit checks.
- Declare a tool with JSON Schema and execute it in the application.
- Return tool output with the correct call_id and preserve response history.
- Stop at a final answer or when the budget is exhausted.
You will build: agent.py answers from fictional inventory, handles unknown SKUs, and stops after at most four model turns.
Prerequisites
- Python 3.10+, a virtual environment, and basic dict/function knowledge.
- Locally configured OPENAI_API_KEY and OPENAI_MODEL; choose a model supporting Responses and function calling.
The main example makes billed API calls when run live. A ChatGPT subscription does not by itself establish API credits. Testing execute with fixtures requires no key.
Step-by-step tutorial
Step 1. Install the SDK in a separate environment
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install openaiThe activation command above is for macOS/Linux; PowerShell uses .venv\Scripts\Activate.ps1. Set the key through your local environment-management mechanism, not in source or the browser. Set OPENAI_MODEL to an actual accessible model supporting tool calling; do not leave a placeholder ID.
Step 2. Write the complete agent loop
Save this as agent.py. The SDK is imported only when the file is run directly, so execute can be tested without installing it. The history preserves response.output items needed by later turns, and each tool result carries the matching call_id.
import json
import os
TOOLS = [{"type": "function", "name": "lookup_stock",
"description": "Read fictional training inventory, never live stock.",
"strict": True, "parameters": {"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"], "additionalProperties": False}}]
def execute(name, raw):
if name != "lookup_stock":
raise ValueError("unknown tool")
args = json.loads(raw)
if not isinstance(args, dict) or set(args) != {"sku"}:
raise ValueError("invalid arguments")
if not isinstance(args["sku"], str) or not 1 <= len(args["sku"]) <= 40:
raise ValueError("invalid sku")
stock = {"DEMO-PEN": 7, "DEMO-BOOK": 0}
sku = args["sku"]
return {"sku": sku, "units": stock.get(sku), "found": sku in stock}
def run(client, model, question):
history = [{"role": "user", "content": question}]
for turn in range(4):
response = client.responses.create(model=model, input=history,
tools=TOOLS, max_output_tokens=500,
instructions="Use lookup_stock before stating quantities. "
"Label all inventory as fictional. Unknown means unknown.")
history.extend(response.output)
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
if not response.output_text.strip():
raise RuntimeError("no final answer")
return response.output_text
if len(calls) > 3:
raise RuntimeError("too many tool calls")
for call in calls:
try:
result = execute(call.name, call.arguments)
except (ValueError, TypeError) as error:
result = {"error": str(error)}
history.append({"type": "function_call_output",
"call_id": call.call_id, "output": json.dumps(result)})
raise RuntimeError("turn limit reached")
if __name__ == "__main__":
from openai import OpenAI
client = OpenAI(timeout=20, max_retries=1)
print(run(client, os.environ["OPENAI_MODEL"],
"How many DEMO-PEN units are in the training inventory?"))Step 3. Test the executor offline first
from agent import execute
assert execute("lookup_stock", '{"sku":"DEMO-PEN"}')["units"] == 7
assert execute("lookup_stock", '{"sku":"MISSING"}')["found"] is False
try:
execute("delete_all", "{}")
except ValueError:
pass
else:
raise AssertionError("unknown tool accepted")
print("executor checks passed")Save the checks as check_agent.py beside agent.py and run python3 check_agent.py. This deterministic executor test does not establish that a model will always choose the tool or answer correctly.
Step 4. Run a live trial and check the answer
python3 agent.pyAfter configuring API access, run once with fictional data and inspect account usage. The answer should report 7 units in the training inventory; wording will vary. Try DEMO-BOOK and MISSING next. A tool schema does not replace execute validation, and instructions cannot guarantee factual grounding.
Step 5. Exercise limits and network failures
For loop tests, replace the client with a fake that always returns tool calls; it must reach turn limit reached. In live use, observe authentication failures and timeouts rather than retrying indefinitely. Log tool names, call IDs, and status, never keys.
Verification
- The executor rejects unknown tools and preserves found=false for missing SKUs.
- Live output is compared with the fixture, and the four-turn limit remains effective.
Troubleshooting
401 or model not found
Cause: The key, access permission, or OPENAI_MODEL is incorrect.
Fix: Check account configuration and official model IDs without logging the key.
The model keeps calling the tool
Cause: There is no final answer or evidence is insufficient.
Fix: Retain the turn limit and report incompletion rather than increasing it indefinitely.
Exercises & extensions
- Add a two-turn fake client to verify call_id handling.
- Continue to tool calling to design another tool and validate its arguments.
Do not connect a real inventory. Offline checks do not verify account billing, model access, or answer quality; live API validation needs separate credentials.
