"""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, }