Let's build an AI agent that chains multiple API calls together to make autonomous decisions. We'll create a meal planning agent that takes your requirements (budget, time, dietary needs), picks a meal on its own, and generates a complete recipe.
This might seem simple, but it's the exact pattern used in AutoGPT, research assistants, and autonomous coding tools. Once you understand this, you'll see how all modern AI agents work under the hood.
Explore the full code and examples on GitHub: agents-for-dummies
If you are new to the topic, you can explore the AI for Dummies series for broader background before diving in.
Prerequisites
Quick Setup (5 minutes):
Python setup:
# 1. Create virtual environment
python3.12 -m venv .venv
# 2. Activate it
source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
# 3. Install packages
pip install openai python-dotenv jupyter
# 4. Open VS Code
code .
Get OpenAI API Key: platform.openai.com → Create account → Billing → API keys → Create new key → Copy it
Store Key: Create .env file → Add: OPENAI_API_KEY=sk-proj-your-key-here
Never commit the .env file because it contains sensitive secrets and should remain local only.
Need Help? See attached SETUP_GUIDE.md for detailed step-by-step instructions, troubleshooting, and explanations.
Ready? Open agent.ipynb and let's start coding!
Part 1: Setting Up the Connection
Cell 1: Import the Environment Variable Loader
# The dotenv library lets us store API keys securely in a .env file instead of hardcoding them
# If you get an ImportError, make sure you've installed python-dotenv (pip install python-dotenv)
from dotenv import load_dotenv
First things first, we need to load our API key without hardcoding it into the source code. The dotenv library reads a .env file and injects those values into environment variables. This is standard practice in production systems because it keeps secrets out of version control and makes configuration changes trivial.
If you see ModuleNotFoundError, you skipped the pip install. Run pip install python-dotenv and come back.
Run this cell with Shift + Enter.
Cell 2: Load Environment Variables
# override=True ensures we get the latest values even if we run this cell multiple times
# This should return True if the .env file was found and loaded successfully
load_dotenv(override=True)
Now we execute the load. The override=True parameter is useful in notebooks—if you run this cell multiple times (say, after updating your .env file), it'll reload the latest values instead of keeping stale ones in memory.
This should return True. If it returns False, your .env file either doesn't exist or is in the wrong directory. Double-check that you have a file literally named .env (not env.txt or .env.example) in your project root, and it contains a line like OPENAI_API_KEY=sk-proj-... with no quotes around the value.
Run the cell.
Cell 3: Verify Your API Key
# This is a crucial security check before making any API calls
# Note: If using other providers (Anthropic, Google, etc.), check their respective key names
import os
openai_api_key = os.getenv('OPENAI_API_KEY')
if openai_api_key:
print(f"Success! Your OpenAI API key is loaded")
print(f"Key starts with: {openai_api_key[:8]}...")
print(f"Key length: {len(openai_api_key)} characters")
else:
print("ERROR: OpenAI API key not found!")
print("Please check your .env file contains: OPENAI_API_KEY=your-key-here")
print("See SETUP_GUIDE.md for detailed instructions")
Always verify your environment variables loaded correctly before making API calls. Nothing worse than debugging authentication errors for 20 minutes when the issue is just a missing API key.
We use os.getenv() to pull the environment variable. The slice [:8] shows the first 8 characters (enough to verify it's real without exposing the full key in logs). OpenAI keys are usually 51 characters, if yours is drastically different, something's wrong.
Expected output:
Success! Your OpenAI API key is loaded
Key starts with: sk-proj-...
Key length: 51 characters
If you see the error message, go back and check your .env file. No spaces around the equals sign, make sure the file is actually named .env (not visible in Finder if you're on Mac, use ls -la to confirm).
Run it.
Cell 4: Import the OpenAI Client
# This provides the interface to communicate with OpenAI's API
# Note: Many AI providers (Anthropic Claude, Google Gemini, etc.) have similar client libraries
# If you get an ImportError, run: pip install openai
from openai import OpenAI
The openai package is OpenAI's official Python client. It handles all the HTTP requests, authentication headers, retries, and response parsing. You could build this yourself with requests and manually craft the API calls, but why reinvent the wheel?
Note: Make sure you have version 1.0.0 or later. The older versions (pre-1.0) had a completely different API. If you're following outdated tutorials and seeing different code, that's probably why.
No output from this cell means it worked. If you see ModuleNotFoundError, run pip install openai in your terminal.
Cell 5: Create the OpenAI Client Instance
# This client will handle all our API requests
# The API key is automatically read from the OPENAI_API_KEY environment variable
openai = OpenAI()
Instantiate the client. The OpenAI() constructor looks for the OPENAI_API_KEY environment variable by default, so we don't need to pass it explicitly. If you wanted to use a different key or override it, you'd do OpenAI(api_key="sk-..."), but that defeats the purpose of using environment variables.
I'm naming the instance openai here to keep the code simple, but in a larger codebase, you might call it client or openai_client to be more explicit.
No output means success. If you hit AuthenticationError in later cells, the problem is here—go back and verify your API key loaded correctly in Cell 3.
Part 2: Testing the Connection
Cell 6: Create Your First Message
# ========================================
# STEP 1: Test Basic API Connection
# ========================================
# Create a simple message to verify everything is working correctly
# Messages are always a list of dictionaries with two keys:
# - "role": who is speaking ("user", "assistant", or "system")
# - "content": the actual message text
messages = [{"role": "user", "content": "What is 2+2?"}]
OpenAI's chat API expects messages in a specific format: a list of dictionaries, where each dictionary has a role and content. There are three valid roles:
user - That's you, the human asking questions
assistant - The AI's responses
system - High-level instructions that guide the AI's behavior
The API uses this history to understand context. Right now we're just testing, so a single user message is fine.
Run the cell. This just stores the data, no API call yet.
Cell 7: Make Your First API Call
# Send our first request to the OpenAI API
# Model: gpt-4o-mini - fast, affordable, and highly capable for most tasks
# The response object contains the AI's reply plus metadata (tokens used, timing, etc.)
# We extract just the text content from response.choices[0].message.content
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
print("AI Response:")
print(response.choices[0].message.content)
Here's where we actually hit the API. The chat.completions.create() method sends your messages to OpenAI's servers and returns a response object.
Breaking down the parameters:
model="gpt-4o-mini": GPT-4o-mini is fast and cheap ($0.15 per 1M input tokens). For comparison, gpt-4o is more capable but costs more. For most applications, mini is plenty.
messages=messages : Our conversation history
The response object contains a lot of metadata (token usage, finish reason, etc.), but we usually just want the text. That's buried in response.choices[0].message.content. The choices[0] is because you can request multiple completions per call (set n=3 to get 3 different responses), but we're only getting one.
Expected output:
AI Response:
2 + 2 equals 4.
The exact wording will vary, the model has some randomness. This call costs about $0.0001 (one-hundredth of a penny).
Run it and make sure you get a response.
Understanding the response structure:
response = {
"choices": [ # List of responses (usually just 1)
{
"message": {
"role": "assistant",
"content": "2+2 equals 4" # This is what we want!
}
}
],
"usage": { ... }, # Token counts
"model": "gpt-4o-mini",
...
}
🎉 Congratulations! You just made your first AI API call!
Part 3: Building the Agent Pattern
Now we get to the interesting part. An agent isn't just calling an API, it's chaining multiple calls where the output of one becomes the input to the next.
In our case: the AI decides what meal to make, then we automatically ask it for a recipe for that specific meal.
Cell 8: First Agent Step - Get a Decision from AI
# ========================================
# STEP 2: Build a Simple Agent Pattern
# ========================================
# What is an "agent"? It's when AI output becomes input for another AI call
# Like a two-step process:
# 1. AI makes a decision (e.g., "what should I cook?")
# 2. AI executes based on that decision (e.g., "here's the recipe")
# This is powerful because the AI can autonomously break down complex tasks!
# First API Call: Ask AI to suggest a meal based on our constraints
# We're being very specific in our instructions to get a clean, usable output
prompt = """You're a nutritionist. Suggest ONE specific healthy dinner meal
that takes less than 30 minutes to prepare and costs under $15 for 2 people.
Respond with ONLY the meal name, like 'Lemon Garlic Shrimp Pasta' - nothing else."""
messages = [{"role": "user", "content": prompt}]
Here's the first step of our agent: get the AI to make a decision. Notice how specific the prompt is. We're asking for "ONE specific meal" and "ONLY the meal name." That specificity matters.
If we're vague ("suggest a meal"), we'll get chatty responses like "How about trying something healthy?" That's useless for programmatic chaining. We need a clean output we can feed into the next step.
The constraint about format ("like 'Lemon Garlic Shrimp Pasta'") gives the model an example. Few-shot prompting like this helps constrain the output format.
Traditional approach:
You → AI: "Give me a recipe"
AI → You: [Recipe]
You manually decide what to do next
Agent approach:
You → AI: "Suggest a meal"
AI → You: "Stir-Fry Chicken"
You → AI: "Give recipe for Stir-Fry Chicken" ← Uses AI's output!
AI → You: [Recipe]
The difference: the AI's output automatically becomes the next input. That's the core pattern.
Run the cell. No API call yet, we're just preparing the message.
Cell 9: Execute the First Agent Step
# Execute the first API call to get a meal suggestion
# The AI will analyze our constraints (time, budget, health) and choose an appropriate meal
# This response becomes the "bridge" to our next step - that's the agent pattern!
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
meal_suggestion = response.choices[0].message.content
print("=" * 50)
print("AI's Meal Suggestion:")
print("=" * 50)
print(meal_suggestion)
print("=" * 50)
Now we execute the first call and store the result. The critical line is:
meal_suggestion = response.choices[0].message.content
This variable holds the AI's autonomous decision. We didn't hardcode "Chicken Stir-Fry"—the AI picked it based on our constraints. Every time you run this, you'll probably get a different meal. That's the autonomous part.
The "=" * 50 just creates visual separators (==================================================) to make the output readable. Small quality-of-life thing when working in notebooks.
Expected output (yours will differ):
==================================================
AI's Meal Suggestion:
==================================================
Chicken Stir-Fry with Vegetables
==================================================
Cost: ~$0.0001
Run it. Note what meal the AI suggests, we're about to use it.
Cell 10: Create the Second Agent Step - Use AI's Output as Input
# ========================================
# STEP 3: Use AI's Output as New Input (Agent Chain)
# ========================================
# This is THE KEY CONCEPT of agent patterns!
# We're taking the meal name the AI just created and using it to ask for more details
# The AI doesn't know we're "chaining" - it just sees a question about a meal
# But WE know the meal came from AI, making this an autonomous agent workflow
# Create a new message asking for a complete recipe for the AI-suggested meal
messages = [{"role": "user", "content": f"Provide a detailed recipe for {meal_suggestion}, including ingredients list and step-by-step cooking instructions."}]
Here's where the agent pattern actually happens. We're using an f-string to inject the meal_suggestion variable into a new prompt:
f"Provide a detailed recipe for {meal_suggestion}, including..."
If the AI suggested "Chicken Stir-Fry with Vegetables", this becomes:
"Provide a detailed recipe for Chicken Stir-Fry with Vegetables, including..."
The second API call has no idea the meal name came from another AI. From its perspective, it's just answering a question about a specific dish. But from our orchestration perspective, we've created an autonomous flow: the AI decided what to make, and we're automatically following through.
Note that we're overwriting the messages variable. We don't need the previous conversation history here—each API call is independent. If we wanted to maintain context across calls, we'd append to the list instead of recreating it.
Run the cell to prepare the second API call.
Cell 11: Execute the Second Agent Step
# Execute the second API call to get the full recipe
# This completes the agent loop:
# Input → AI Decision (meal choice) → AI Execution (recipe creation) → Output
#
# This is exactly how complex AI agents work - they chain multiple AI calls together!
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
recipe = response.choices[0].message.content
print("\n" + "=" * 50)
print("Complete Recipe:")
print("=" * 50)
print(recipe)
Second API call executes. Simple as that—same pattern as before.
Let's trace the full flow:
Your initial constraints:
"Healthy, under 30 min, under $15"
↓
[API Call #1]
↓
AI's autonomous decision:
"Chicken Stir-Fry with Vegetables"
↓
Your code automatically chains:
"Give me recipe for Chicken Stir-Fry with Vegetables"
↓
[API Call #2]
↓
AI's output:
[Complete recipe with ingredients and instructions]
You only specified high-level constraints. The AI made the tactical decision (which meal) and executed on it (recipe details). That's an agent.
The "\n" at the start of the print adds a blank line for spacing. Small detail, makes the output more readable.
Expected output: A complete recipe with ingredients list, cooking steps, timing, serving suggestions.
Cost: ~$0.001 (slightly more because the response is longer)
Run it.
Cell 12: Display with Beautiful Formatting
# Display the recipe with beautiful Markdown formatting
# Markdown rendering makes text more readable with headers, lists, bold text, etc.
# Much better than plain text for structured content like recipes!
from IPython.display import Markdown, display
print("\n" + "=" * 50)
print("FORMATTED RECIPE")
print("=" * 50 + "\n")
display(Markdown(recipe))
# Congratulations - you just built an agent!
#
# What happened:
# 1. AI made an autonomous decision (which meal to suggest)
# 2. AI executed a task based on that decision (creating the recipe)
# 3. You didn't hardcode the meal name - the AI chose it dynamically
#
# This is the foundation of modern AI agents. Scale this pattern up:
#
# Research Agents:
# Find relevant topics → Write detailed summaries → Generate insights
#
# Code Generation Agents:
# Plan software architecture → Write code modules → Generate tests
#
# Content Creation Agents:
# Outline article structure → Write each section → Edit and polish
#
# Problem-Solving Agents:
# Break down complex problems → Solve each part → Synthesize solution
#
# Autonomous Agents (like AutoGPT):
# Chain dozens of AI calls together to complete complex multi-step tasks
#
# Try modifying the prompts to create different agent behaviors
Jupyter can render Markdown, which makes the recipe much more readable. The AI naturally outputs Markdown (headers, bullet lists, bold text), so we might as well display it properly instead of raw text.
Import Markdown and display from IPython.display, then call display(Markdown(recipe)). That's it.
Here's what you just accomplished: built an agent that autonomously chooses a meal and generates a recipe for it. You gave high-level constraints, the AI made decisions, and your code automatically chained those decisions into actions.
This exact scaled up pattern is how tools like AutoGPT work. They chain dozens or hundreds of API calls, each using outputs from previous calls to make decisions about the next step. Research agents do the same: find a topic, read sources, synthesize findings, write summaries. Code generators: plan architecture, scaffold files, write implementations, generate tests.
The fundamental insight: AI output can become AI input. That creates autonomous loops.
Run the cell and admire your nicely formatted recipe.
🎉 You Did It!
What You've Learned:
Environment Setup - Secure API key management with .env
OpenAI API Basics - Authentication, messages format, API calls
Agent Pattern - AI output → AI input chaining
Practical Application - Real-world meal planning agent
The Agent Pattern in One Diagram:
┌─────────────────────────────────────────┐
│ You: "Find a meal under $15, 30min" │
└──────────────┬──────────────────────────┘
↓
┌──────────────┐
│ API Call #1 │
└──────┬───────┘
↓
┌──────────────────────────────────────────┐
│ AI Decision: "Chicken Stir-Fry" │ ← Autonomous!
└──────────────┬───────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ Automatic: "Recipe for Chicken..." │ ← Your code chains!
└──────────────┬───────────────────────────┘
↓
┌──────────────┐
│ API Call #2 │
└──────┬───────┘
↓
┌──────────────────────────────────────────┐
│ AI Output: [Complete Recipe] │
└──────────────────────────────────────────┘
Cost Breakdown
Cell 7 (test): ~$0.0001
Cell 9 (meal suggestion): ~$0.0001
Cell 11 (recipe): ~$0.001
Total: Less than $0.002 (two-tenths of a penny)
Experiments to Try
Once you've got the basic agent working, here are some ideas to extend it:
Simple modifications:
Change the constraints: "vegetarian meal under $10" or "dessert under 20 minutes"
Add nutrition info: modify Cell 10 to request "...and nutritional information per serving"
Try different cuisines: "Suggest ONE Italian meal..." or "...ONE Japanese meal..."
Three-step agent: Extend the chain:
AI suggests a meal
AI creates a shopping list
AI writes cooking instructions
Each step uses the output from the previous one.
Conversation memory: Instead of overwriting messages, append to it. This lets the AI refer to previous exchanges:
messages.append({"role": "assistant", "content": meal_suggestion})
messages.append({"role": "user", "content": "Now give me the recipe"})
Error handling: Wrap API calls in try/except:
try:
response = openai.chat.completions.create(...)
except Exception as e:
print(f"API error: {e}")
Advanced patterns:
Function calling: Let the AI decide when to use external tools (search, calculator, database lookups). Check OpenAI's function calling docs.
Multi-agent systems: Have different AI instances with different roles (planner, critic, executor) that interact.
Iterative refinement: AI generates output → AI critiques it → AI improves based on critique.
Common Issues
ModuleNotFoundError: No module named 'openai'
Your virtual environment isn't activated, or you didn't install the package.
source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
pip install openai
AuthenticationError: Invalid API key
Your API key didn't load properly. Check:
.env file exists and contains OPENAI_API_KEY=sk-proj-...
No spaces around the = sign
You ran Cell 2 to load it
Run Cell 3 to verify it loaded
RateLimitError: You exceeded your quota
Your OpenAI account needs credits. Go to platform.openai.com → Billing → Add payment method.
Response is cut off or incomplete
Rare, but sometimes responses get truncated. Add max_tokens to your API call:
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
max_tokens=2000
)
AI suggests the same meal every time
The model has some determinism. Increase randomness with temperature:
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=1.0 # 0.0 = deterministic, 2.0 = chaotic
)
API Parameters Reference
Key parameters you can tweak:
response = openai.chat.completions.create(
model="gpt-4o-mini", # Which model
messages=messages, # Conversation history
temperature=0.7, # Randomness (0-2, default 1)
max_tokens=1000, # Max response length
top_p=1.0, # Nucleus sampling (0-1)
frequency_penalty=0.0, # Reduce repetition (-2 to 2)
presence_penalty=0.0, # Encourage new topics (-2 to 2)
)
temperature: Controls randomness.
max_tokens: Limits response length. Roughly 4 characters = 1 token.
top_p: Nucleus sampling. Alternative to temperature. Usually leave at 1.0.
frequency_penalty / presence_penalty: Discourage repetition or encourage topic diversity. Rarely needed for simple tasks.
Additional Resources
Documentation
Learning More
Prompt Engineering: OpenAI Prompt Engineering Guide
LangChain: Framework for building more complex agents
AutoGPT: Study the codebase to see advanced agent patterns
Community
Final Thoughts
You've just built your first AI agent! This simple 2-step pattern is the foundation of complex systems that:
Research topics autonomously
Write and debug code
Plan and execute multi-step tasks
Make decisions and take actions
The key insight: AI output can become AI input. This creates autonomous loops where AI systems can break down and solve complex problems without human intervention at every step.
Keep experimenting, and remember: every advanced AI agent is just combinations of these simple patterns!
Happy coding!
Appendix: Complete Code Reference
Here's the entire notebook in one place for reference:
# Cell 1: Import environment loader
from dotenv import load_dotenv
# Cell 2: Load environment variables
load_dotenv(override=True)
# Cell 3: Verify API key
import os
openai_api_key = os.getenv('OPENAI_API_KEY')
if openai_api_key:
print(f"Success! Your OpenAI API key is loaded")
print(f"Key starts with: {openai_api_key[:8]}...")
print(f"Key length: {len(openai_api_key)} characters")
else:
print("ERROR: OpenAI API key not found!")
# Cell 4: Import OpenAI client
from openai import OpenAI
# Cell 5: Create client instance
openai = OpenAI()
# Cell 6: Test message
messages = [{"role": "user", "content": "What is 2+2?"}]
# Cell 7: Test API call
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
print("AI Response:")
print(response.choices[0].message.content)
# Cell 8: Agent step 1 - decision prompt
prompt = """You're a nutritionist. Suggest ONE specific healthy dinner meal
that takes less than 30 minutes to prepare and costs under $15 for 2 people.
Respond with ONLY the meal name, like 'Lemon Garlic Shrimp Pasta' - nothing else."""
messages = [{"role": "user", "content": prompt}]
# Cell 9: Execute agent step 1
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
meal_suggestion = response.choices[0].message.content
print("=" * 50)
print("AI's Meal Suggestion:")
print("=" * 50)
print(meal_suggestion)
print("=" * 50)
# Cell 10: Agent step 2 - execution prompt
messages = [{"role": "user", "content": f"Provide a detailed recipe for {meal_suggestion}, including ingredients list and step-by-step cooking instructions."}]
# Cell 11: Execute agent step 2
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
recipe = response.choices[0].message.content
print("\n" + "=" * 50)
print("Complete Recipe:")
print("=" * 50)
print(recipe)
# Cell 12: Display formatted result
from IPython.display import Markdown, display
print("\n" + "=" * 50)
print("📖 FORMATTED RECIPE")
print("=" * 50 + "\n")
display(Markdown(recipe))
Copy this to create a working agent in seconds!
Explore the full code and examples on GitHub: agents-for-dummies