← Back to DevBytes

Building a Documentation Generator with OpenAI Agents SDK: Complete Guide

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

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles