Files

219 lines
7.2 KiB
Python

"""DocAgent - Documentation automation for VoIdea.
This agent automatically:
- Creates README.md for new modules
- Updates documentation when code changes
- Generates docstrings
- Maintains Runbook
"""
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from app.agents.base import AgentResult, AgentStatus, AgentTrigger, BaseAgent
from app.core.config import get_settings
settings = get_settings()
class DocAgent(BaseAgent):
"""Documentation automation agent."""
name = "doc_agent"
version = "1.0.0"
description = "Automatically maintains project documentation"
triggers = [
AgentTrigger.MANUAL,
AgentTrigger.PRE_COMMIT,
AgentTrigger.PUSH,
]
def __init__(self):
super().__init__()
self.project_root = Path(__file__).parent.parent.parent
self.docs_dir = self.project_root / "docs"
self.app_dir = self.project_root / "app"
async def run(self, context: dict[str, Any] | None = None) -> AgentResult:
"""Execute documentation task.
Context can contain:
- action: str (update_readme, generate_docs, etc.)
- module: str (module path to document)
- files: list[str] (changed files)
"""
await self.set_running("documentation")
try:
action = context.get("action", "update_all") if context else "update_all"
if action == "update_readme":
module = context.get("module", "")
result = await self._update_module_readme(module)
elif action == "generate_docs":
result = await self._generate_docs()
elif action == "update_session_context":
result = await self._update_session_context(context or {})
else:
result = await self._update_all()
await self.set_idle()
return result
except Exception as e:
await self.set_error(str(e))
return AgentResult(
success=False,
message=f"DocAgent failed: {str(e)}",
errors=[str(e)],
)
async def health_check(self) -> bool:
"""Check if DocAgent is operational."""
try:
return self.docs_dir.exists() and self.app_dir.exists()
except Exception:
return False
async def _update_module_readme(self, module_path: str) -> AgentResult:
"""Update README.md for a specific module."""
module_dir = self.app_dir / module_path if module_path else self.app_dir
if not module_dir.exists():
return AgentResult(
success=False,
message=f"Module not found: {module_path}",
)
files = list(module_dir.glob("*.py"))
files = [f for f in files if f.name != "__init__.py"]
content = f"""# {module_path or 'app'} Module - VoIdea
## Overview
[Auto-generated documentation]
## Files
| File | Purpose |
|------|---------|
"""
for file in files:
purpose = self._get_file_purpose(file.name)
content += f"| `{file.name}` | {purpose} |\n"
readme_path = module_dir / "README.md"
readme_path.write_text(content, encoding="utf-8")
return AgentResult(
success=True,
message=f"Updated README for {module_path or 'app'}",
data={"module": module_path, "files_count": len(files)},
)
async def _update_all(self) -> AgentResult:
"""Update all documentation."""
updated = []
for module_dir in self.app_dir.iterdir():
if module_dir.is_dir() and (module_dir / "__init__.py").exists():
result = await self._update_module_readme(module_dir.name)
if result.success:
updated.append(module_dir.name)
session_result = await self._update_session_context({})
if session_result.success:
updated.append("SESSION_CONTEXT")
return AgentResult(
success=True,
message=f"Updated {len(updated)} documentation files",
data={"updated": updated},
)
async def _generate_docs(self) -> AgentResult:
"""Generate API documentation from docstrings."""
docs_generated = 0
endpoints = []
api_dir = self.app_dir / "api"
if api_dir.exists():
for file in api_dir.rglob("*.py"):
if file.name == "__init__.py":
continue
docs_generated += 1
endpoints.append(str(file.relative_to(self.app_dir)))
return AgentResult(
success=True,
message=f"Generated documentation for {docs_generated} files",
data={"endpoints": endpoints, "count": docs_generated},
)
async def _update_session_context(self, context: dict[str, Any]) -> AgentResult:
"""Update SESSION_CONTEXT.md with current progress."""
session_file = self.project_root / "SESSION_CONTEXT.md"
if not session_file.exists():
return AgentResult(
success=False,
message="SESSION_CONTEXT.md not found",
)
try:
content = session_file.read_text(encoding="utf-8")
if "Last Updated" not in content:
content = content.replace(
"================================================================================",
"Last Updated: {}\n================================================================================".format(
datetime.now(timezone.utc).isoformat()
),
)
session_file.write_text(content, encoding="utf-8")
return AgentResult(
success=True,
message="SESSION_CONTEXT.md updated",
data={"timestamp": datetime.now(timezone.utc).isoformat()},
)
except Exception as e:
return AgentResult(
success=False,
message=f"Failed to update SESSION_CONTEXT: {str(e)}",
errors=[str(e)],
)
def _get_file_purpose(self, filename: str) -> str:
"""Get file purpose based on naming convention."""
purposes = {
"main.py": "FastAPI application entry point",
"config.py": "Configuration management",
"models.py": "Data models",
"schemas.py": "Pydantic schemas",
"service.py": "Business logic",
"repository.py": "Data access layer",
"router.py": "API routes",
"base.py": "Base classes",
"exceptions.py": "Custom exceptions",
"security.py": "Security utilities",
"database.py": "Database setup",
"dependencies.py": "FastAPI dependencies",
}
return purposes.get(filename, "Module file")
async def get_metrics(self) -> dict[str, Any]:
"""Get DocAgent metrics."""
from app.agents.models import AgentMetric
return {
"agent_id": self.name,
"version": self.version,
"status": self.status.value,
"last_run": self.last_run.isoformat() if self.last_run else None,
}