Build an AI PowerPoint Agent
Build a Python AI agent that generates PowerPoint decks from natural language.
Introduction
30 Second Summary
You have probably spent hours building PowerPoint decks slide by slide, copying layouts, formatting bullets, and making sure your content matches your brand. What if you could just describe what you want in plain English and get a finished, themed presentation back in seconds?
In this project, you will build a command-line AI agent in Python that takes natural language prompts and generates fully-themed PowerPoint presentations using your own template files.
What You'll Build
You type something like "Create a 5-slide deck about Q3 marketing results" alongside a template path, and watch as the agent inspects your template, plans the structure, generates content, and saves a ready-to-open .pptx file to your machine.
By the end of this project, you'll have:
- A conversational AI agent that accepts text instructions and produces ready-to-open .pptx files on disk.
- An autonomous template inspection flow where the AI discovers your layouts and placeholders before building anything.
- A multi-tool orchestration loop where the AI chains multiple function calls to inspect, create slides, and save the finished presentation.
- Secret Mission: A template explorer tool that gives you a human-friendly breakdown of your template's capabilities before you generate anything.
Are there any prerequisites?
You will need an OpenAI API key saved in a .env file, and at least one .pptx template file with custom slide layouts.
You should also have VS Code and Python 3.13 installed on your machine.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up Your Project and Dependencies
Before you write any agent code, you need a working Python environment with three key packages.
openai connects your code to GPT-4o. python-pptx reads and writes PowerPoint files. python-dotenv loads your API key securely from a .env file so you never hard-code secrets into your scripts.
In this step, get ready to:
- Verify your Python packages import correctly.
- Confirm your .env file contains your OpenAI API key.
Verify your packages are installed
Your project folder is open in VS Code and you have a terminal ready. Time to confirm the three packages are actually importable by Python.
Before you run the next command, what do you expect to see if all three packages installed correctly?
- In your terminal, verify all three packages are importable by running this command:
python -c "import openai, pptx, dotenv; print('All packages installed')"
You should see All packages installed printed in the terminal. This confirms Python can find all three libraries.
Seeing a ModuleNotFoundError?
If Python cannot find one of the packages, install them by running:
pip install openai python-pptx python-dotenv
If python is not recognized, try python3 instead. Some systems use python3 as the default command for Python 3.
Still stuck?
Help me fix my Python package imports for openai, python-pptx, and python-dotenv.
Confirm your environment file
The OpenAI SDK automatically reads an environment variable called OPENAI_API_KEY when you create a client. Your .env file makes that variable available without exposing the key in your code.
- Confirm your .env file is in the project folder by running:
cat .env
You should see a line containing OPENAI_API_KEY= followed by your actual key value.
Why a .env file instead of pasting the key in code?
A .env file keeps secrets out of your source code. If you ever share or publish your project, the key stays private as long as you add .env to your .gitignore.
The python-dotenv library loads this file into your environment at runtime, and the openai SDK picks up the key automatically.
File not found or key is missing?
If you see an error, make sure the file is named exactly .env (with the leading dot) and sits in the same folder your terminal is pointing at. If your key is in a different location, copy it into this project folder.
Help me find or create my .env file with my OpenAI API key.
Your environment is locked in. Next up, you'll build the agent's core loop and get it responding to your prompts.
Build the Agent Loop with a Basic Chat
Your environment is ready with all the packages installed. Now you need the core piece that makes this an agent rather than a one-shot script: a persistent conversation loop that keeps the AI engaged across multiple turns.
Before adding any PowerPoint tools, you need the OpenAI connection and input loop working so you can see the AI respond to your prompts. This step builds that foundation.
In this step, get ready to:
- Create the agent file with imports, environment setup, and a system prompt.
- Build a conversation loop that maintains message history across turns.
- Test the agent by chatting with it in your terminal.
Create the agent file with imports and system prompt
Every AI agent needs a personality and a set of instructions. The system prompt tells GPT-4o what role it plays and how to behave when the user asks for presentations.
- Create a new file called agent.py in your pptx-agent folder.
- Add the imports, environment setup, and system prompt by pasting this code:
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
from pptx import Presentation
from pptx.util import Inches, Pt
load_dotenv()
client = OpenAI()
# Store active presentation sessions
sessions = {}
SYSTEM_PROMPT = """You are a PowerPoint presentation assistant. You help users create themed presentations using their PowerPoint template files.
When a user asks you to create a presentation:
1. First, inspect the template to discover available layouts and placeholders.
2. Then create a new presentation session from that template.
3. Add slides one by one, choosing appropriate layouts based on the content.
4. Finally, save the presentation to disk.
When choosing layouts:
- Use layout index 0 for title slides (usually "Title Slide")
- Use layout index 1 for content slides (usually "Title and Content")
- Use other layouts as appropriate based on what the template offers
Always inspect the template first so you know what layouts are available. Generate professional, concise content for each slide based on the user's request. If the user doesn't specify a filename, save as 'output.pptx'."""
What does this code do?
- load_dotenv() reads your .env file and loads OPENAI_API_KEY into the environment.
- OpenAI() automatically picks up the OPENAI_API_KEY environment variable. No need to pass it explicitly.
- sessions is an empty dictionary that will hold active presentation objects in later steps.
- SYSTEM_PROMPT gives the AI its identity and a step-by-step workflow to follow when building presentations.
Add the conversation loop with message history
An agent is more than a single API call. It keeps a running list of every message exchanged so the AI remembers what you said earlier in the conversation.
- Below the SYSTEM_PROMPT in agent.py, add the run_agent() function and entry point by pasting this code:
def run_agent():
"""Main agent loop."""
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("PowerPoint Agent ready! Type your request (or 'quit' to exit).")
print("Example: Create a 5-slide deck about AI trends using template.pptx")
print("-" * 60)
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
messages.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
assistant_message = response.choices[0].message
messages.append(assistant_message)
print(f"\nAgent: {assistant_message.content}")
if __name__ == "__main__":
run_agent()
What does this code do?
- messages starts with the system prompt and grows with every exchange. This is how the AI "remembers" what you discussed earlier.
- The while True loop keeps the conversation going until you type quit or exit.
- client.chat.completions.create sends the full conversation history to gpt-4o each turn so the model has full context.
- Both the user message and assistant response get appended to messages so the conversation accumulates naturally.
- Save agent.py.
Before you run this, what do you think will happen when you type a greeting? Will the AI respond about PowerPoint, or give a generic answer?
- Run your agent by entering this command in the terminal:
python agent.py
- When you see the You: prompt, type:
Hello, can you help me make a presentation?
You should see a text response from GPT-4o printed in the terminal. The AI will respond in the context of being a PowerPoint assistant because of the system prompt you defined.
- Type quit to exit the agent.
Not seeing a response?
- Check that your .env file is in the same folder as agent.py and contains a valid OPENAI_API_KEY.
- If you see an authentication error, your API key may be invalid or expired. Double-check the key in your OpenAI dashboard.
- If you see ModuleNotFoundError, run pip install openai python-pptx python-dotenv again to ensure all packages are installed.
Help me debug why my agent isn't responding
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
Your agent.py should have two sections in order. Scroll up and confirm each is present:
- The imports and setup block (imports, load_dotenv, client, sessions, and SYSTEM_PROMPT).
- The run_agent() function with the while True loop, followed by the if __name__ == "__main__" entry point.
Your agent can chat, but it has no idea what your PowerPoint templates actually contain. Next up, you'll give it tools to inspect template files and discover what slide layouts are available.
Add Template Inspection Tools
Your agent can hold a conversation, but it has no idea what your PowerPoint template looks like. It cannot read layouts, discover placeholders, or understand your theme.
In this step, you will give the agent a tool it can call on its own. Tool calling lets the AI request that your Python code runs a function, then use the result to answer the user. By the end, the agent will autonomously inspect any .pptx template you point it at.
In this step, get ready to:
- Write a Python function that reads a template's layouts and placeholders.
- Register the function as an OpenAI tool with a JSON schema.
- Update the agent loop to execute tool calls and feed results back to the AI.
Define the template inspection function
The agent needs a way to open a .pptx file and list what slide layouts are available inside it. Each layout contains numbered placeholders (title, body, picture) that determine where content goes on a slide.
- In agent.py, find the line that says sessions = {}.
- Add the following function below that line, with a blank line separating them:
def inspect_template(file_path):
"""Inspect a PowerPoint template and return its layouts and placeholders."""
prs = Presentation(file_path)
layouts = []
for idx, layout in enumerate(prs.slide_layouts):
placeholders = []
for ph in layout.placeholders:
placeholders.append({
"idx": ph.placeholder_format.idx,
"name": ph.name,
"type": str(ph.placeholder_format.type),
})
layouts.append({
"index": idx,
"name": layout.name,
"placeholders": placeholders,
})
return json.dumps(layouts, indent=2)
What does this code do?
This function opens a .pptx file and loops through every slide layout in the template.
- For each layout, it collects the placeholder index, name, and type (title, body, picture, etc.).
- It returns a JSON string so the AI can parse the results and reason about which layouts to use.
You will not run this yet. The next chunk registers it as a tool the AI can request.
Register the tool with OpenAI
OpenAI's API needs a JSON schema describing each tool: its name, what it does, and what parameters it accepts. The AI reads this schema to decide when to call the function and what arguments to pass.
- In agent.py, add the following code below your inspect_template function (above the SYSTEM_PROMPT definition):
tools = [
{
"type": "function",
"function": {
"name": "inspect_template",
"description": "Inspect a PowerPoint template file to discover available slide layouts and their placeholders. Call this first to understand what layouts are available before creating slides.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the .pptx template file",
}
},
"required": ["file_path"],
},
},
},
]
# Map function names to callables
tool_functions = {
"inspect_template": lambda args: inspect_template(args["file_path"]),
}
What does this code do?
- The tools list describes every function the AI is allowed to call. Each entry includes the function name, a natural-language description, and a parameter schema.
- The tool_functions dictionary maps each tool name to the actual Python callable so your code can dispatch calls at runtime.
- Save agent.py.
- Confirm the module loads cleanly by running this command:
python -c "import agent; print(len(agent.tools), 'tool(s) registered')"
You should see 1 tool(s) registered printed with no errors.
Seeing a SyntaxError or ImportError?
- Check that every opening brace, bracket, and parenthesis has a matching close. The nested dictionary structure is easy to mistype.
- Make sure inspect_template is defined above the tools list. Python reads top to bottom, so the function must exist before anything references it.
- Help me debug a SyntaxError in my agent.py tool definitions.
Wire tool calling into the agent loop
Right now, the agent loop sends messages to the API and prints the text response directly. It has no idea that the AI might want to call a tool instead of replying with text.
You need to pass the tools list to the API call, check whether the response contains tool calls, execute the requested function, and send the result back so the AI can form its final answer.
- In agent.py, scroll down to the run_agent() function.
- Find this block of code inside the while True loop (starting after messages.append({"role": "user", "content": user_input})):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
assistant_message = response.choices[0].message
messages.append(assistant_message)
print(f"\nAgent: {assistant_message.content}")
- Replace that entire block with the following code:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
)
choice = response.choices[0]
assistant_message = choice.message
messages.append(assistant_message)
# If no tool calls, print the text response
if choice.finish_reason == "stop" or not assistant_message.tool_calls:
print(f"\nAgent: {assistant_message.content}")
else:
# Execute each tool call and send results back
for tool_call in assistant_message.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
try:
result = tool_functions[function_name](arguments)
except Exception as e:
result = json.dumps({"error": str(e)})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
# Call the API again so the model can use the tool results
follow_up = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
)
final_message = follow_up.choices[0].message
messages.append(final_message)
print(f"\nAgent: {final_message.content}")
What does this code do?
- The API call now includes tools=tools so the AI knows which functions it can call. tool_choice="auto" lets the model decide whether to call a tool or just reply with text.
- After the response, the code checks finish_reason. If it is "stop", the AI responded with text. If it is "tool_calls", the AI wants to execute a function.
- For each tool call, it extracts the function name and arguments, runs the matching Python function, and appends the result as a role: "tool" message.
- A second API call sends the tool results back so the AI can incorporate them into a human-readable response.
- Save agent.py.
Before you run the agent, think about what you expect to happen. If you ask it to inspect a template, do you think it will call your inspect_template function automatically, or just reply with text?
- Run the agent by entering this command:
python agent.py
- When you see the You: prompt, type a message asking the agent to inspect your template file. For example:
Inspect the template at template.pptx
The AI calls your inspect_template function behind the scenes, receives the JSON layout data, and responds with a human-friendly summary of your template's available layouts and their placeholders.
- Type quit to exit the agent.
Seeing Agent: None or an error?
- If you see Agent: None, the old response-handling code is still in place. Make sure you fully replaced the original block (from the API call through the print statement) with the new version that checks finish_reason.
- If you see a FileNotFoundError, make sure the .pptx file you named actually exists in your project folder. Use the exact filename including the extension.
- If you see a KeyError about the function name, confirm that the name in your tool_functions dictionary matches "inspect_template" exactly.
- Help me debug why my agent isn't calling the inspect_template tool.
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py with all three additions: the inspect_template function, the tools list with tool_functions, and the updated run_agent() loop.
ⓧ I'd like to double check the full code
Here is the complete agent.py at this point. Scroll through and confirm each section is present in your file, in this order:
- Imports and client setup at the top.
- The inspect_template function.
- The tools list and tool_functions dictionary.
- The SYSTEM_PROMPT string.
- The updated run_agent() function with tool-call handling.
- The if __name__ == "__main__": block at the bottom.
Your agent can now see inside any PowerPoint template you throw at it. Next up, you will give it the power to actually build slides using those layouts.
Add Slide Creation Tools
Your agent can see inside a template now, but that is only half the job.
Right now, if you ask the agent to build a presentation, it has no way to create slides, populate them, or save the file to disk. In this step you will give the agent three new tools that let it autonomously construct a full deck from your template.
In this step, get ready to:
- Define a function that creates a blank presentation session from a template.
- Define a function that adds slides with titles and bullet content.
- Define a save function and register all three tools so the AI can call them.
Define the create_presentation function
When the AI wants to start building a deck, it needs to open your template, clear out any existing slides, and hand back a reference so later tool calls can add slides to the same file.
- In agent.py, find the closing line of your inspect_template function. Add the following function directly below it:
def create_presentation(template_path):
"""Create a new presentation from a template, removing existing slides."""
prs = Presentation(template_path)
while len(prs.slides) > 0:
rId = prs.slides._sldIdLst[0].rId
prs.part.drop_rel(rId)
del prs.slides._sldIdLst[0]
session_id = f"session_{len(sessions) + 1}"
sessions[session_id] = prs
return json.dumps({"session_id": session_id, "status": "created", "template": template_path})
What does this code do?
- Presentation(template_path) opens your .pptx template file so all the branded layouts are available.
- The while loop removes any pre-existing slides by dropping their relationship ID. This gives the AI a blank canvas while keeping your template's layouts intact.
- The function stores the presentation object in the sessions dictionary so the add_slide and save_presentation tools can find it later by session ID.
Why access internal attributes like _sldIdLst?
python-pptx does not have a built-in remove_slide() method. Accessing prs.slides._sldIdLst and calling prs.part.drop_rel(rId) is the established community workaround for clearing slides from a loaded presentation.
Define the add_slide function
This is the function the AI will call repeatedly to build each slide. It picks a layout, sets the title, and fills the body with bullet points.
- Directly below create_presentation, add the add_slide function:
def add_slide(session_id, layout_index, title, body_bullets):
"""Add a slide to the presentation with a title and bullet points."""
if session_id not in sessions:
return json.dumps({"error": f"Session '{session_id}' not found"})
prs = sessions[session_id]
layout = prs.slide_layouts[layout_index]
slide = prs.slides.add_slide(layout)
if slide.shapes.title:
slide.shapes.title.text = title
if 1 in [ph.placeholder_format.idx for ph in slide.placeholders]:
body = slide.placeholders[1]
tf = body.text_frame
tf.clear()
for i, bullet in enumerate(body_bullets):
if i == 0:
tf.text = bullet
else:
p = tf.add_paragraph()
p.text = bullet
slide_number = len(prs.slides)
return json.dumps({"status": "slide_added", "slide_number": slide_number, "title": title})
What does this code do?
- It retrieves the presentation from sessions using the session ID, then picks the layout the AI requested.
- slide.shapes.title.text sets the title placeholder (always idx 0 when present).
- The body section checks whether placeholder idx 1 exists, then fills it with bullet strings using the text_frame API. The first bullet uses tf.text and each subsequent bullet uses tf.add_paragraph().
- Save agent.py.
- Confirm both new functions have no syntax errors by running:
python -c "from agent import create_presentation, add_slide; print('Functions defined successfully')"
You should see Functions defined successfully printed in the terminal.
Seeing a SyntaxError or IndentationError?
- Check that each function is at the module level (no extra indentation) and that the f"session_{len(sessions) + 1}" string uses an f prefix.
- Confirm there is a blank line between each function definition.
- help me debug a SyntaxError in my create_presentation or add_slide function
Define save_presentation and register all tools
The last function writes the finished file to disk. After defining it, you will register all three new tools with the OpenAI API so the model knows they exist.
- Below add_slide, add the save_presentation function:
def save_presentation(session_id, output_filename):
"""Save the presentation to disk."""
if session_id not in sessions:
return json.dumps({"error": f"Session '{session_id}' not found"})
prs = sessions[session_id]
prs.save(output_filename)
del sessions[session_id]
return json.dumps({"status": "saved", "file": output_filename})
What does this code do?
It retrieves the presentation from the session, calls prs.save(output_filename) to write the .pptx to disk, then removes the session from the dictionary to free memory.
Now you need to tell the OpenAI API about these three new tools. Each tool needs a JSON schema describing its name, description, and parameters.
- Find your existing tools list. Add the create_presentation and add_slide tool definitions after the existing inspect_template entry:
{
"type": "function",
"function": {
"name": "create_presentation",
"description": "Create a new presentation session from a template file.",
"parameters": {
"type": "object",
"properties": {
"template_path": {"type": "string", "description": "Path to the .pptx template file"}
},
"required": ["template_path"],
},
},
},
{
"type": "function",
"function": {
"name": "add_slide",
"description": "Add a new slide to the active presentation.",
"parameters": {
"type": "object",
"properties": {
"session_id": {"type": "string"},
"layout_index": {"type": "integer"},
"title": {"type": "string"},
"body_bullets": {"type": "array", "items": {"type": "string"}},
},
"required": ["session_id", "layout_index", "title", "body_bullets"],
},
},
},
- Directly after the add_slide entry you just added, add the save_presentation tool definition before the closing ] of the list:
{
"type": "function",
"function": {
"name": "save_presentation",
"description": "Save the completed presentation to a .pptx file on disk.",
"parameters": {
"type": "object",
"properties": {
"session_id": {"type": "string"},
"output_filename": {"type": "string"},
},
"required": ["session_id", "output_filename"],
},
},
},
Finally, update the tool_functions dictionary so your agent loop can dispatch to all four tools.
- Find the existing tool_functions dictionary and replace it with:
tool_functions = {
"inspect_template": lambda args: inspect_template(args["file_path"]),
"create_presentation": lambda args: create_presentation(args["template_path"]),
"add_slide": lambda args: add_slide(args["session_id"], args["layout_index"], args["title"], args["body_bullets"]),
"save_presentation": lambda args: save_presentation(args["session_id"], args["output_filename"]),
}
What does this dictionary do?
When the AI returns a tool call, your agent loop looks up the function name in this dictionary and calls the matching Python function with the parsed arguments. Each lambda extracts the right keys from the arguments dictionary and passes them to the corresponding function.
- Also update the inspect_template entry in your tools list to use this shorter description (to keep it concise alongside the new entries):
"description": "Inspect a PowerPoint template file to discover available slide layouts and their placeholders.",
- In run_agent(), find and delete the line that prints the example text:
print("Example: Create a 5-slide deck about AI trends using template.pptx")
- Remove that line entirely. Also remove the docstring """Main agent loop.""" from the top of run_agent().
- Save agent.py.
Before you run the agent, what do you think will happen when you ask it to create a 3-slide presentation? Will it call one tool, or several in sequence?
- Run your agent by executing:
python agent.py
- When the prompt appears, type:
Create a 3-slide presentation about AI trends using template.pptx
You should see the agent call one or two tools (likely inspect_template and create_presentation), then respond with text. Because the current agent loop only handles one round of tool calls, it may not finish all the slides in a single prompt. That is expected. The next step adds multi-tool orchestration to fix this.
- Check your project folder for a new .pptx file by running:
ls *.pptx
If the agent managed to call save_presentation in that single round, you will see a new file like output.pptx. If not, that is fine. The important thing is that the agent attempted to use your new tools. You will see the full multi-step flow working end-to-end after the next step.
Agent says it cannot find the template?
- Make sure you have a file named template.pptx in the same folder as agent.py. Any .pptx file works as a template.
- If your template has a different name, use that exact filename in your prompt.
- help me troubleshoot why the agent cannot find my template file
✔️ Awesome, I've got everything!
Great. Double check you have saved agent.py before moving on.
ⓧ I'd like to double check the full code
Your agent.py is getting longer now. Scroll through and confirm you have these sections in order:
- Imports and client setup at the top.
- sessions = {} dictionary.
- inspect_template function.
- create_presentation function.
- add_slide function.
- save_presentation function.
- tools list with four tool definitions (inspect_template, create_presentation, add_slide, save_presentation).
- tool_functions dictionary with four lambdas.
- SYSTEM_PROMPT string.
- run_agent() function and if __name__ == "__main__": block.
Your agent now has the tools to build a complete presentation. In the next step, you will wire up multi-tool orchestration so it can chain all these calls together in a single request.
Wire Up Multi-Tool Orchestration
Your agent can now inspect templates and build slides. But there's a problem: creating a full presentation requires many tool calls in sequence (inspect, create, add slide, add slide, add slide, save), and your current code only handles one round before stopping.
In this step, you'll replace the single-pass handler with a loop that keeps processing tool calls until the AI produces a final text response. This turns your agent into a true autonomous system that chains as many operations as it needs.
In this step, get ready to:
- Replace the single-pass tool handler with a continuous while loop.
- Add a maximum iteration guard to prevent runaway loops.
- Test full multi-tool orchestration with a single natural language prompt.
Replace the single-pass handler with the orchestration loop
Right now, your run_agent() function makes one API call, handles one round of tool calls, makes one follow-up call, and prints the result. A 4-slide presentation needs at least 7 tool calls across multiple rounds. The agent needs to keep looping until the AI decides it's done.
- In agent.py, find your run_agent() function (starting at def run_agent():).
- Delete the entire function body (everything from def run_agent(): down to the last line before if __name__ == "__main__":).
- Also add a MAX_TOOL_CALLS constant just above where run_agent() was. Replace everything with this code:
MAX_TOOL_CALLS = 15
def run_agent():
"""Main agent loop."""
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
print("PowerPoint Agent ready! Type your request (or 'quit' to exit).")
print("Example: Create a 5-slide deck about AI trends using template.pptx")
print("-" * 60)
while True:
user_input = input("\nYou: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
messages.append({"role": "user", "content": user_input})
# Agent loop: keep calling until we get a text response
tool_call_count = 0
What does this code do?
MAX_TOOL_CALLS = 15 sets an upper limit on how many tool calls the agent can make per user message. This prevents infinite loops if the AI keeps requesting tools without resolving.
The tool_call_count = 0 counter resets for each new user message. The inner loop (coming next) increments it with every tool call and checks against the guard.
- Now add the inner while loop directly below the tool_call_count = 0 line (still inside the outer while True block):
while True:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
)
choice = response.choices[0]
assistant_message = choice.message
messages.append(assistant_message)
# If no tool calls, we have a final response
if choice.finish_reason == "stop" or not assistant_message.tool_calls:
print(f"\nAgent: {assistant_message.content}")
break
# Process tool calls
for tool_call in assistant_message.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
print(f" [Tool Call] {function_name}({json.dumps(arguments, indent=2)})")
try:
result = tool_functions[function_name](arguments)
except Exception as e:
result = json.dumps({"error": str(e)})
print(f" [Tool Result] {result[:200]}...")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
tool_call_count += 1
# Guard against infinite loops
if tool_call_count >= MAX_TOOL_CALLS:
print("\n[Warning] Maximum tool calls reached. Stopping.")
break
What does this code do?
- The inner while True keeps calling the API until finish_reason is "stop" (the AI has a text response and no more tools to call).
- The for tool_call in assistant_message.tool_calls loop processes ALL tool calls in a single response. The API can return multiple parallel tool calls at once.
- The try/except catches errors from any tool execution and returns the error as a JSON result. This lets the AI self-correct or inform the user instead of crashing.
- The MAX_TOOL_CALLS guard breaks out if the agent exceeds 15 tool calls, preventing runaway loops that burn through API credits.
- Make sure the if __name__ == "__main__": block is still at the bottom of your file after the function.
- Save agent.py.
Seeing an indentation error?
The inner while True loop must be indented 8 spaces (two levels in from the function definition). Each line inside it needs 12 spaces. Check that the tool_call_count = 0 line and the inner while True: are at the same indentation level.
Help me fix my indentation in agent.py
Test multi-tool orchestration end to end
The real test of your orchestration loop is giving the agent a single prompt that requires many sequential tool calls. A 4-slide presentation needs at least 7 calls: inspect, create, add slide (x4), and save.
- Run your agent by running this command:
python agent.py
Before you type your prompt, think about what you expect to see. Do you think the agent will fire one tool call and stop, or will it chain multiple calls in sequence?
- Type this prompt (replacing template.pptx with the path to your actual template file):
Make a 4-slide deck about renewable energy using template.pptx and save it as energy.pptx
You should see multiple [Tool Call] and [Tool Result] lines printing in sequence as the agent inspects your template, creates a presentation, adds 4 slides one by one, and saves the file. The final line is the AI confirming it saved energy.pptx.
- Type quit to exit the agent.
- Open energy.pptx in PowerPoint (or any app that opens .pptx files). Confirm it has 4 slides with your template's theme and AI-generated content about renewable energy.
What should I see in the terminal?
You should see something like 6-8 tool call pairs printed. The exact sequence depends on how the AI decides to structure the calls, but you should see inspect_template first, then create_presentation, then multiple add_slide calls, and finally save_presentation.
Agent stops after one or two tool calls?
- Check that your inner while True: loop is inside the outer while True: loop (8 spaces of indentation, not 4).
- Make sure the if choice.finish_reason == "stop" check only breaks the inner loop, not the outer one.
- Verify you deleted the old single-pass code (the follow_up variable and second API call should be gone).
My agent isn't chaining multiple tool calls
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py and that your generated .pptx file opens correctly.
ⓧ I'd like to double check the full code
Scroll up and confirm your agent.py has these sections in order:
- Imports, load_dotenv(), client = OpenAI(), and sessions = {} at the top.
- Four tool functions: inspect_template, create_presentation, add_slide, and save_presentation.
- The tools list with all four tool definitions.
- The tool_functions dictionary mapping names to callables.
- SYSTEM_PROMPT string.
- MAX_TOOL_CALLS = 15 constant.
- run_agent() with the inner while True loop, tool_call_count counter, and MAX_TOOL_CALLS guard.
- if __name__ == "__main__": run_agent() at the very bottom.
Your agent is now fully autonomous. Give it a single natural language request, and it chains as many tool calls as needed to produce a complete presentation. Next up: the Secret Mission, where you'll add a template explorer tool that gives human-friendly summaries of what your templates can do.
Secret mission
Add a Template Explorer Tool
Your agent can inspect templates, but the raw JSON output is hard for humans to read. Can you add a dedicated explore_template tool that produces a nicely formatted summary showing each layout with its placeholder types, dimensions, and recommended usage?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. The only cost is OpenAI API usage when you actively run the agent (roughly $0.05-0.20 per session). There are no ongoing charges when the agent is not running.
Decide whether to keep your project files for future use, or delete them entirely.
Resources you used:
- The pptx-agent project folder (containing agent.py, .env, and any generated .pptx output files).
- Installed pip packages: openai, python-pptx, python-dotenv.
Keep everything running
No action needed. Choose this if you plan to keep using or extending your PowerPoint agent.
- Your pptx-agent folder stays on your Desktop with all code intact.
- Your pip packages remain installed and available for other Python projects.
- Your .env file keeps your OpenAI API key ready for next time you run the agent.
- You will only be charged for OpenAI API usage when you actively run agent.py.
Pause - I'll come back to this later
Since this project runs locally and only costs money when you actively use it, there is nothing to pause. Your files sit idle on disk with no ongoing charges.
- Leave the pptx-agent folder as-is. It uses minimal disk space.
- When you return, open the folder in VS Code and run python agent.py to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and uninstall the packages you added.
- Delete the project folder by running this in your terminal:
macOS
cd ~/Desktop
rm -rf pptx-agent
Windows
cd ~\Desktop
rmdir /s /q pptx-agent
- Uninstall the pip packages by running:
pip uninstall openai python-pptx python-dotenv -y
- Delete any generated .pptx output files (such as output.pptx or energy.pptx) if they were saved outside the project folder.
Nice Work!
Nice Work!
You just built a fully functional AI agent that turns natural language prompts into themed PowerPoint presentations. From a single chat message, your agent autonomously inspects templates, plans slide structures, generates content, and saves finished decks to disk.
You've learned how to:
- Build a conversational AI agent with a persistent input loop that maintains chat history across multiple turns.
- Implement OpenAI tool calling with multiple registered functions that the AI invokes autonomously based on the user's request.
- Connect python-pptx to read template layouts and placeholders, then write finished presentations with themed slides and structured content.
- Orchestrate multi-step tool execution where the AI chains multiple function calls in sequence to complete a complex task from a single prompt.
- Secret Mission: Added a template explorer tool that gives human-friendly summaries of template capabilities, including placeholder types, sizes, and recommended usage.
Ready to quiz yourself?