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)