Modern QA2026Agent Architecture for Living Docs — tiles
Log inJoin
85 / 89 · 04 API & Contract Testing with AI · Living API Documentation← prev⊞ allnext →☰ Read as one page

13.2Agent Architecture for Living Docs

class APIDocumentationAgent:
    """Agent that keeps API documentation synchronized with implementation."""

    def __init__(self, llm, repo_path: str, spec_path: str):
        self.llm = llm
        self.repo_path = repo_path
        self.spec_path = spec_path

    def daily_sync(self):
        """Run daily to detect and fix documentation drift."""

        # Step 1: Extract actual API behavior from code
        routes = self.extract_routes_from_code()

        # Step 2: Parse current documentation
        documented = self.parse_openapi_spec()

        # Step 3: Compare and find drifts
        drifts = self.compare(routes, documented)

        # Step 4: For each drift, generate a fix
        fixes = []
        for drift in drifts:
            if drift.type == "UNDOCUMENTED_ENDPOINT":
                fix = self.generate_endpoint_docs(drift.endpoint)
                fixes.append(fix)
            elif drift.type == "MISSING_FIELD":
                fix = self.generate_field_docs(drift.endpoint, drift.field)
                fixes.append(fix)
            elif drift.type == "STALE_EXAMPLE":
                fix = self.regenerate_example(drift.endpoint)
                fixes.append(fix)

        # Step 5: Create a PR with the fixes
        if fixes:
            self.create_documentation_pr(fixes)

    def extract_routes_from_code(self) -> list:
        """Use AI to parse route definitions from the source code."""
        prompt = f"""
        Analyze the following source files and extract all API routes.
        For each route, identify:
        - HTTP method and path
        - Request body schema (from validation decorators or type hints)
        - Response schema (from return statements or serializer usage)
        - Authentication requirements
        - Status codes returned

        Source files:
        {self.read_route_files()}
        """
        return self.llm.generate_structured(prompt, schema=RouteList)

    def compare(self, actual_routes, documented_routes) -> list:
        """Compare actual routes against documentation."""
        drifts = []

        actual_paths = {(r.method, r.path) for r in actual_routes}
        documented_paths = {(r.method, r.path) for r in documented_routes}

        # Undocumented endpoints
        for method, path in actual_paths - documented_paths:
            drifts.append(Drift(
                type="UNDOCUMENTED_ENDPOINT",
                endpoint=f"{method} {path}",
                message=f"Endpoint exists in code but not in documentation"
            ))

        # Documented but removed endpoints
        for method, path in documented_paths - actual_paths:
            drifts.append(Drift(
                type="REMOVED_ENDPOINT",
                endpoint=f"{method} {path}",
                message=f"Endpoint in documentation but not found in code"
            ))

        # Field-level comparison for shared endpoints
        for method, path in actual_paths & documented_paths:
            actual = next(r for r in actual_routes if r.method == method and r.path == path)
            documented = next(r for r in documented_routes if r.method == method and r.path == path)
            drifts.extend(self.compare_fields(actual, documented))

        return drifts

    def create_documentation_pr(self, fixes: list):
        """Create a git branch, apply fixes, and open a PR."""
        branch_name = f"docs/api-sync-{datetime.now().strftime('%Y%m%d')}"

        # Create branch
        subprocess.run(["git", "checkout", "-b", branch_name], cwd=self.repo_path)

        # Apply each fix to the OpenAPI spec
        spec = yaml.safe_load(open(self.spec_path))
        for fix in fixes:
            self.apply_fix(spec, fix)
        yaml.dump(spec, open(self.spec_path, "w"), default_flow_style=False)

        # Commit and push
        subprocess.run(["git", "add", self.spec_path], cwd=self.repo_path)
        subprocess.run(
            ["git", "commit", "-m", f"docs: sync API documentation ({len(fixes)} fixes)"],
            cwd=self.repo_path
        )
        subprocess.run(["git", "push", "origin", branch_name], cwd=self.repo_path)

        # Create PR via GitHub CLI
        pr_body = self.generate_pr_body(fixes)
        subprocess.run([
            "gh", "pr", "create",
            "--title", f"docs: sync API documentation ({len(fixes)} fixes)",
            "--body", pr_body,
            "--base", "main",
        ], cwd=self.repo_path)