Building a Documentation Generator with OpenAI Agents SDK: Complete Guide
Documentation is one of the most valuable yet consistently neglected parts of software development. Engineers ship code faster than they ship docs, and stale README files become a liability rather than a guide. In this tutorial, you'll learn how to build an autonomous documentation generator using the OpenAI Agents SDK — a system that reads your codebase, understands its structure, and produces accurate, maintainable documentation without manual effort.
What Is the OpenAI Agents SDK?
The OpenAI Agents SDK is a Python framework for building multi-agent workflows on top of OpenAI's language models. It provides primitives like Agent, Runner, handoffs, tools, and guardrails that let you compose autonomous systems capable of reasoning, calling functions, and coordinating with one another. Unlike a single prompt-and-response setup, agents can loop, delegate, and verify their own output — which is exactly what high-quality documentation generation requires.
A documentation generator built on this SDK doesn't just summarize files. It can inspect source code, extract public APIs, cross-reference usage examples, validate links, and produce structured output in Markdown, reStructuredText, or HTML. Because each concern is handled by a specialized agent, the system stays modular and easy to extend.
Why This Matters
- Consistency: Every module gets documented using the same structure and tone.
- Freshness: Running the generator in CI keeps docs in sync with code.
- Depth: Agents can trace function calls and explain behavior, not just signatures.
- Cost efficiency: You only regenerate documentation for files that changed.
- Onboarding: New contributors get a coherent narrative instead of scattered comments.
Prerequisites and Project Setup
Start by creating a virtual environment and installing the required packages. You'll need the Agents SDK, an OpenAI API key, and a few utilities for parsing code.
python -m venv .venv
source .venv/bin/activate
pip install openai-agents pydantic tiktoken pathlib
export OPENAI_API_KEY="sk-..."
Create the following project structure:
docgen/
├── agents/
│ ├── __init__.py
│ ├── explorer.py
│ ├── analyzer.py
│ ├── writer.py
│ └── reviewer.py
├── tools/
│ ├── __init__.py
│ └── code_tools.py
├── main.py
└── sample_project/
└── calculator.py
Defining the Tools
Agents need tools to interact with the filesystem. We'll define two tools: one to list Python files in a directory and another to read a specific file's contents. Tools are plain async functions decorated with @function_tool.
# tools/code_tools.py
from agents import function_tool
from pathlib import Path
import ast
@function_tool
def list_python_files(directory: str) -> list[str]:
"""Return all .py files under the given directory."""
root = Path(directory)
return [str(p.relative_to(root)) for p in root.rglob("*.py")]
@function_tool
def read_file(path: str) -> str:
"""Read and return the contents of a file."""
return Path(path).read_text(encoding="utf-8")
@function_tool
def extract_symbols(path: str) -> dict:
"""Parse a Python file and return its top-level classes and functions."""
tree = ast.parse(Path(path).read_text(encoding="utf-8"))
symbols = {"classes": [], "functions": []}
for node in tree.body:
if isinstance(node, ast.ClassDef):
methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)]
symbols["classes"].append({"name": node.name, "methods": methods})
elif isinstance(node, ast.FunctionDef):
symbols["functions"].append(node.name)
return symbols
Building the Explorer Agent
The Explorer agent is responsible for scanning the codebase and producing a manifest of files worth documenting. It uses the listing and symbol-extraction tools.
# agents/explorer.py
from agents import Agent, Runner
from tools.code_tools import list_python_files, extract_symbols
explorer_agent = Agent(
name="Explorer",
instructions=(
"You are a code explorer. Given a project directory, list every Python "
"file, extract its top-level symbols, and return a JSON manifest mapping "
"file paths to their classes and functions. Skip __init__.py files that "
"are empty. Be thorough and deterministic."
),
tools=[list_python_files, extract_symbols],
model="gpt-4o-mini",
)
async def run_explorer(directory: str) -> str:
result = await Runner.run(
explorer_agent,
input=f"Explore the project at: {directory}",
)
return result.final_output
Building the Analyzer Agent
The Analyzer takes a single file and produces a structured understanding of it: purpose, public API, dependencies, and edge cases. This is where the model does the heavy lifting of comprehension.
# agents/analyzer.py
from agents import Agent, Runner
from tools.code_tools import read_file
from pydantic import BaseModel, Field
class FileAnalysis(BaseModel):
purpose: str = Field(description="One-paragraph summary of the file's role")
public_api: list[str] = Field(description="Public functions and classes")
dependencies: list[str] = Field(description="Imported modules")
edge_cases: list[str] = Field(description="Notable edge cases or pitfalls")
analyzer_agent = Agent(
name="Analyzer",
instructions=(
"You analyze a single Python file and return a structured analysis. "
"Focus on what a consumer of this code needs to know. Always respond "
"using the provided output schema."
),
tools=[read_file],
model="gpt-4o",
output_type=FileAnalysis,
)
async def run_analyzer(file_path: str) -> FileAnalysis:
result = await Runner.run(
analyzer_agent,
input=f"Analyze the file at: {file_path}",
)
return result.final_output_as(FileAnalysis)
Building the Writer Agent
The Writer converts a FileAnalysis into polished Markdown. It follows a strict template so output stays consistent across files.
# agents/writer.py
from agents import Agent, Runner
from agents.analyzer import FileAnalysis
writer_agent = Agent(
name="Writer",
instructions=(
"You write clean, professional Markdown documentation from a structured "
"file analysis. Use this template:\n\n"
"## {File Name}\n\n"
"### Overview\n{purpose}\n\n"
"### Public API\n- bullet list with brief descriptions\n\n"
"### Dependencies\n- bullet list\n\n"
"### Edge Cases\n- bullet list\n\n"
"Do not invent behavior that is not in the analysis. Keep prose tight."
),
model="gpt-4o",
)
async def run_writer(file_name: str, analysis: FileAnalysis) -> str:
payload = f"File: {file_name}\nAnalysis: {analysis.model_dump_json()}"
result = await Runner.run(writer_agent, input=payload)
return result.final_output
Building the Reviewer Agent
The Reviewer checks the generated Markdown for hallucinations, broken structure, and missing sections. If it finds issues, it returns a corrected version. This feedback loop is what separates an agent pipeline from a single prompt.
# agents/reviewer.py
from agents import Agent, Runner
reviewer_agent = Agent(
name="Reviewer",
instructions=(
"You review generated Markdown documentation. Check that:\n"
"1. All four sections (Overview, Public API, Dependencies, Edge Cases) exist.\n"
"2. No claims contradict the provided analysis.\n"
"3. There are no broken code fences or stray HTML.\n"
"Return the corrected Markdown. If no changes are needed, return it unchanged."
),
model="gpt-4o-mini",
)
async def run_reviewer(markdown: str, analysis_json: str) -> str:
prompt = (
f"Analysis JSON for reference:\n{analysis_json}\n\n"
f"Markdown to review:\n{markdown}"
)
result = await Runner.run(reviewer_agent, input=prompt)
return result.final_output
Orchestrating the Pipeline
Now wire everything together in main.py. The orchestrator runs the Explorer once, then for each file runs Analyzer → Writer → Reviewer, and writes the final Markdown to disk.
# main.py
import asyncio
import json
from pathlib import Path
from agents.explorer import run_explorer
from agents.analyzer import run_analyzer
from agents.writer import run_writer
from agents.reviewer import run_reviewer
async def generate_docs(project_dir: str, output_dir: str):
Path(output_dir).mkdir(parents=True, exist_ok=True)
print("Exploring project...")
manifest_raw = await run_explorer(project_dir)
manifest = json.loads(manifest_raw)
for file_name in manifest.keys():
full_path = Path(project_dir) / file_name
print(f"Analyzing {file_name}...")
analysis = await run_analyzer(str(full_path))
markdown = await run_writer(file_name, analysis)
final = await run_reviewer(markdown, analysis.model_dump_json())
out_path = Path(output_dir) / (Path(file_name).stem + ".md")
out_path.write_text(final, encoding="utf-8")
print(f" -> wrote {out_path}")
# Build an index page
index = "# Project Documentation\n\n"
for file_name in manifest.keys():
stem = Path(file_name).stem
index += f"- [{stem}]({stem}.md)\n"
Path(output_dir, "index.md").write_text(index, encoding="utf-8")
print("Done.")
if __name__ == "__main__":
asyncio.run(generate_docs("sample_project", "docs"))
A Sample Project to Document
Drop a small module into sample_project/calculator.py so the generator has something to chew on:
# sample_project/calculator.py
"""A tiny calculator module used for testing the doc generator."""
from typing import Optional
class Calculator:
def __init__(self, precision: int = 2):
self.precision = precision
def add(self, a: float, b: float) -> float:
return round(a + b, self.precision)
def divide(self, a: float, b: float) -> Optional[float]:
if b == 0:
return None
return round(a / b, self.precision)
def format_result(value: float) -> str:
return f"Result: {value:.2f}"
Run the generator:
python main.py
You should see a docs/ directory containing calculator.md and index.md, each with the four required sections filled in based on the actual source.
Best Practices
- Use cheap models for routing, strong models for analysis. The Explorer and Reviewer can run on
gpt-4o-mini; the Analyzer benefits fromgpt-4o. - Constrain outputs with Pydantic. Structured output types prevent the model from drifting into free-form prose where you need data.
- Cache file reads. If you regenerate docs in CI, only re-analyze files whose git hash changed.
- Keep tools pure. Tools should not mutate state or depend on hidden globals; this makes agents predictable and testable.
- Add guardrails. Use the SDK's
input_guardrailsto reject requests targeting files outside the project root, preventing path traversal. - Log every agent step. Wrap
Runner.runcalls with logging so you can audit which agent produced which section. - Test the pipeline on synthetic projects. Keep a fixture codebase with known expected docs and assert against them in CI.
Extending the System
Once the core pipeline works, you can extend it in several directions. Add a Usage Examples agent that reads test files and extracts realistic usage snippets to embed in the docs. Add a Cross-Reference agent that builds a dependency graph and inserts "See also" links between modules. You can also introduce a Changelog agent that diffs the previous documentation against the new output and summarizes what changed for release notes.
For larger codebases, parallelize the per-file stage with asyncio.gather and a semaphore to respect rate limits. The Agents SDK is fully async, so concurrency is straightforward.
Conclusion
Building a documentation generator with the OpenAI Agents SDK turns a tedious chore into a repeatable, auditable pipeline. By splitting the work into Explorer, Analyzer, Writer, and Reviewer agents, you get a system where each stage has a clear contract, where structured outputs keep the data machine-readable, and where the Reviewer's feedback loop catches hallucinations before they reach your repository. Start with the minimal pipeline above, wire it into your CI, and iterate by adding specialized agents as your documentation needs grow. The result is documentation that stays as alive as the code it describes.