← Back to DevBytes

Building a Documentation Generator with AutoGen: Complete Guide

Introduction to Building a Documentation Generator with AutoGen

Documentation is often the most neglected part of the software development lifecycle. Developers write code, ship features, and move on — leaving behind stale, incomplete, or missing docs. Microsoft's AutoGen framework, a multi-agent conversation system, offers a powerful way to automate this process by orchestrating specialized AI agents that read, analyze, and produce high-quality documentation from your codebase.

In this guide, you'll learn what an AutoGen-based documentation generator is, why it matters, how to build one from scratch, and the best practices to follow when deploying it in real-world projects.

What Is AutoGen?

AutoGen is an open-source framework by Microsoft Research that enables the creation of multi-agent applications where Large Language Models (LLMs) converse, collaborate, and solve complex tasks. Instead of relying on a single prompt-response cycle, AutoGen lets you define multiple agents — each with a specific role, persona, and skill set — that exchange messages until a task is complete.

A documentation generator built on AutoGen typically involves several agents working together:

Why Use AutoGen for Documentation Generation?

Traditional documentation tools like JSDoc, Sphinx, or Javadoc extract comments and signatures mechanically. They produce reference docs, but they don't explain the code. AutoGen-based generators add a layer of semantic understanding:

This matters because poor documentation slows onboarding, increases support burden, and hides technical debt. Automating high-quality docs reduces these costs dramatically.

Prerequisites and Setup

Before building the generator, ensure you have the following:

Install AutoGen and supporting libraries:

pip install "pyautogen>=0.2" python-dotenv pathlib

Create a .env file in your project root:

OPENAI_API_KEY=sk-your-key-here
MODEL_NAME=gpt-4o-mini

Project Structure

Organize your project as follows:

docgen/
├── .env
├── main.py
├── agents.py
├── file_utils.py
├── config.py
└── sample_code/
    └── calculator.py

Step 1: Configuration

Start by centralizing your LLM configuration. This keeps agent definitions clean and makes it easy to swap models later.

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

LLM_CONFIG = {
    "config_list": [
        {
            "model": os.getenv("MODEL_NAME", "gpt-4o-mini"),
            "api_key": os.getenv("OPENAI_API_KEY"),
        }
    ],
    "temperature": 0.3,
    "timeout": 120,
}

A low temperature (0.3) is ideal for documentation tasks because you want factual, deterministic output rather than creative variation.

Step 2: File Utilities

You need helpers to read source files and write the generated documentation. Keep these separate from agent logic for testability.

# file_utils.py
from pathlib import Path

SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".java", ".go", ".rs"}

def collect_source_files(root_dir: str) -> list[Path]:
    """Walk a directory and return supported source files."""
    root = Path(root_dir)
    files = []
    for path in root.rglob("*"):
        if path.is_file() and path.suffix in SUPPORTED_EXTENSIONS:
            files.append(path)
    return files

def read_file(path: Path) -> str:
    """Read file contents with UTF-8 encoding."""
    return path.read_text(encoding="utf-8")

def write_docs(output_dir: str, filename: str, content: str) -> Path:
    """Write generated documentation to disk."""
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)
    target = out / f"{filename}.md"
    target.write_text(content, encoding="utf-8")
    return target

Step 3: Defining the Agents

This is the heart of the system. You'll define four agents and wire them into a group chat. Each agent has a system prompt that constrains its behavior.

# agents.py
import autogen
from config import LLM_CONFIG

def build_agents():
    # Agent 1: Code Reader
    code_reader = autogen.AssistantAgent(
        name="CodeReader",
        llm_config=LLM_CONFIG,
        system_message=(
            "You are a Code Reader. Given a source file, you extract "
            "its structure: classes, functions, signatures, imports, "
            "and notable logic. Output a concise structured summary. "
            "Do NOT write documentation yet."
        ),
    )

    # Agent 2: Analyzer
    analyzer = autogen.AssistantAgent(
        name="Analyzer",
        llm_config=LLM_CONFIG,
        system_message=(
            "You are a Code Analyzer. You receive a structured summary "
            "and explain the purpose, behavior, edge cases, and "
            "dependencies of the code. Highlight anything a maintainer "
            "should know. Be precise and technical."
        ),
    )

    # Agent 3: Writer
    writer = autogen.AssistantAgent(
        name="Writer",
        llm_config=LLM_CONFIG,
        system_message=(
            "You are a Technical Writer. You produce clean Markdown "
            "documentation based on the analysis. Include a title, "
            "overview, function/class reference tables, usage examples, "
            "and notes. Use fenced code blocks for examples. "
            "Output ONLY the Markdown document."
        ),
    )

    # Agent 4: Reviewer
    reviewer = autogen.AssistantAgent(
        name="Reviewer",
        llm_config=LLM_CONFIG,
        system_message=(
            "You are a Documentation Reviewer. You check the Markdown "
            "for accuracy, clarity, missing sections, and tone. "
            "If issues exist, list them concisely. If the doc is good, "
            "respond with exactly: APPROVED"
        ),
    )

    return code_reader, analyzer, writer, reviewer

Step 4: Orchestrating the Group Chat

AutoGen's GroupChat lets multiple agents collaborate in a shared conversation. You control the flow with a GroupChatManager and a custom speaker selection function.

# main.py (part 1)
import autogen
from config import LLM_CONFIG
from agents import build_agents
from file_utils import collect_source_files, read_file, write_docs

MAX_ROUNDS = 12

def make_group_chat():
    reader, analyzer, writer, reviewer = build_agents()

    def speaker_selection(last_speaker, groupchat):
        messages = groupchat.messages
        if not messages:
            return reader

        last_msg = messages[-1]["content"].strip().upper()

        if last_speaker is reader:
            return analyzer
        if last_speaker is analyzer:
            return writer
        if last_speaker is writer:
            return reviewer
        if last_speaker is reviewer:
            if "APPROVED" in last_msg:
                return None  # end conversation
            return writer  # revise
        return reader

    group_chat = autogen.GroupChat(
        agents=[reader, analyzer, writer, reviewer],
        messages=[],
        max_round=MAX_ROUNDS,
        speaker_selection_method=speaker_selection,
    )

    manager = autogen.GroupChatManager(
        groupchat=group_chat,
        llm_config=LLM_CONFIG,
    )

    return manager, writer

The speaker_selection function enforces a pipeline: read → analyze → write → review. If the reviewer rejects, the writer revises. If approved, the conversation terminates.

Step 5: Running the Generator

Now wire everything together. You'll use a UserProxyAgent to kick off the conversation with the file contents, then extract the final Markdown from the writer's last message.

# main.py (part 2)
import re

def extract_markdown(messages, writer_name="Writer"):
    """Pull the last Markdown block produced by the Writer agent."""
    for msg in reversed(messages):
        if msg.get("name") == writer_name:
            content = msg["content"]
            # If wrapped in code fences, strip them
            match = re.search(r"(?:markdown)?\n(.*?)", content, re.DOTALL)
            return match.group(1).strip() if match else content.strip()
    return ""

def generate_for_file(file_path, manager, user_proxy):
    source = read_file(file_path)
    prompt = (
        f"Please document the following source file ({file_path.name}).\n\n"
        f"--- BEGIN SOURCE ---\n{source}\n--- END SOURCE ---"
    )
    user_proxy.initiate_chat(manager, message=prompt, clear_history=True)
    return extract_markdown(manager.groupchat.messages)

def main():
    source_dir = "sample_code"
    output_dir = "docs_output"

    manager, _ = make_group_chat()

    user_proxy = autogen.UserProxyAgent(
        name="User",
        human_input_mode="NEVER",
        max_consecutive_auto_reply=0,
        code_execution_config=False,
    )

    files = collect_source_files(source_dir)
    print(f"Found {len(files)} source file(s).")

    for f in files:
        print(f"Generating docs for: {f.name}")
        markdown = generate_for_file(f, manager, user_proxy)
        if markdown:
            path = write_docs(output_dir, f.stem, markdown)
            print(f"  -> wrote {path}")
        else:
            print(f"  -> no documentation produced for {f.name}")

if __name__ == "__main__":
    main()

Step 6: Sample Input and Expected Output

Create a sample file to test the generator:

# sample_code/calculator.py
class Calculator:
    def __init__(self):
        self.history = []

    def add(self, a, b):
        result = a + b
        self.history.append(("add", a, b, result))
        return result

    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        result = a / b
        self.history.append(("divide", a, b, result))
        return result

    def clear_history(self):
        self.history.clear()

When you run python main.py, the agents collaborate and produce a Markdown file at docs_output/calculator.md resembling:

# Calculator

## Overview
The `Calculator` class provides basic arithmetic operations while
maintaining an internal history of every computation.

## Class Reference

| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| `__init__` | none | void | Initializes an empty history list |
| `add` | `a: int/float`, `b: int/float` | `int/float` | Returns the sum and logs the operation |
| `divide` | `a: int/float`, `b: int/float` | `int/float` | Returns the quotient; raises `ValueError` if `b` is zero |
| `clear_history` | none | void | Empties the history list |

## Usage

python
calc = Calculator()
print(calc.add(2, 3))       # 5
print(calc.divide(10, 2))   # 5.0
calc.clear_history()
## Notes
- `divide` raises `ValueError` when the divisor is zero.
- History grows unbounded; call `clear_history` periodically in long-running processes.

Best Practices

1. Keep Agents Focused

Each agent should have a single responsibility. Resist the temptation to make one agent both analyze and write. Separation of concerns improves output quality and makes debugging easier.

2. Constrain Output Formats

Use explicit instructions like "Output ONLY the Markdown document" or "Respond with exactly: APPROVED". This prevents agents from adding conversational filler that breaks downstream parsing.

3. Limit Conversation Rounds

Set a reasonable max_round value (10–15). Without a cap, a reviewer-writer loop can spiral indefinitely if the reviewer keeps finding minor issues.

4. Cache Expensive Calls

LLM calls are costly. Cache the structured summary and analysis per file hash so re-running the generator on unchanged files skips redundant work. A simple approach:

import hashlib, json, os

def file_hash(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()

def load_cache(cache_path):
    if os.path.exists(cache_path):
        return json.loads(Path(cache_path).read_text())
    return {}

def save_cache(cache, cache_path):
    Path(cache_path).write_text(json.dumps(cache, indent=2))

5. Validate Generated Docs

Don't blindly trust LLM output. Add a post-processing step that checks for required sections (Overview, Reference, Usage) and verifies code examples are syntactically valid using ast.parse for Python files.

6. Handle Large Files

LLMs have context limits. For files exceeding ~8,000 tokens, split them by class or function before sending to the CodeReader agent. Merge the resulting docs afterward.

7. Use Cheap Models for Review

The reviewer's job is pattern matching, not deep reasoning. You can configure it with a cheaper model like gpt-4o-mini while reserving gpt-4o for the analyzer and writer.

8. Version Your Prompts

Treat system prompts as code. Store them in separate files or a YAML config so you can iterate on wording without touching application logic.

Extending the Generator

Once the core pipeline works, consider these enhancements:

Conclusion

Building a documentation generator with AutoGen transforms a tedious manual chore into an automated, intelligent workflow. By decomposing the task into focused agents — reader, analyzer, writer, and reviewer — you get documentation that explains intent, not just signatures. The architecture is modular, so you can swap models, add agents, or integrate with existing CI pipelines without rewriting the core. Start with the pipeline in this guide, apply the best practices around caching, validation, and round limits, and you'll have a documentation system that keeps your docs as fresh as your code.

— Ad —

Google AdSense will appear here after approval

← Back to all articles