from collections.abc import AsyncIterator from dataclasses import dataclass from operator import add from typing import Annotated, Protocol, TypedDict from langgraph.graph import END, StateGraph from app.agents.config_agent import ConfigAgent from app.agents.cuda_migration_agent import CudaMigrationAgent from app.agents.dependency_agent import DependencyAgent from app.agents.docs_agent import DocsAgent from app.agents.error_handling_agent import ErrorHandlingAgent from app.agents.observability_agent import ObservabilityAgent from app.agents.performance_agent import PerformanceAgent from app.agents.quality_agent import QualityAgent from app.agents.security_agent import SecurityAgent from app.agents.synthesizer_agent import SynthesizerAgent from app.config import Settings, get_settings from app.schemas import AgentOutput, AuditReport, CodeChunk, RepoScanResult from app.services.chunker import Chunker from app.services.llm_client import LLMClient from app.services.repo_crawler import RepoCrawler class AnalysisAgent(Protocol): name: str async def analyze(self, chunks: list[CodeChunk]) -> AgentOutput: ... @dataclass(frozen=True) class AnalysisAgentSpec: node_name: str state_key: str progress_label: str start_message: str agent: AnalysisAgent class AuditState(TypedDict, total=False): repo_url: str repo: RepoScanResult chunks: list[CodeChunk] security_output: AgentOutput performance_output: AgentOutput quality_output: AgentOutput docs_output: AgentOutput config_output: AgentOutput dependency_output: AgentOutput error_handling_output: AgentOutput observability_output: AgentOutput cuda_migration_output: AgentOutput report: AuditReport progress: Annotated[list[str], add] class AuditGraph: def __init__(self, settings: Settings | None = None): self.settings = settings or get_settings() self.crawler = RepoCrawler(self.settings) self.chunker = Chunker(self.settings) self.llm_client = LLMClient(self.settings) self.analysis_agents = self._build_agent_registry() self.synthesizer = SynthesizerAgent() self.graph = self._build_graph() def _build_agent_registry(self) -> list[AnalysisAgentSpec]: return [ AnalysisAgentSpec( node_name="security", state_key="security_output", progress_label="Security Agent", start_message="Security Agent: scanning for risky patterns...", agent=SecurityAgent(self.llm_client), ), AnalysisAgentSpec( node_name="performance", state_key="performance_output", progress_label="Performance Agent", start_message="Performance Agent: scanning for slow-path patterns...", agent=PerformanceAgent(self.llm_client), ), AnalysisAgentSpec( node_name="quality", state_key="quality_output", progress_label="Quality Agent", start_message="Quality Agent: scanning maintainability signals...", agent=QualityAgent(self.llm_client), ), AnalysisAgentSpec( node_name="docs", state_key="docs_output", progress_label="Docs Agent", start_message="Docs Agent: scanning README and public documentation...", agent=DocsAgent(self.llm_client), ), AnalysisAgentSpec( node_name="config", state_key="config_output", progress_label="Config Agent", start_message="Config Agent: scanning production configuration risk...", agent=ConfigAgent(), ), AnalysisAgentSpec( node_name="dependency", state_key="dependency_output", progress_label="Dependency Agent", start_message="Dependency Agent: parsing manifests and optional CVE data...", agent=DependencyAgent(self.settings), ), AnalysisAgentSpec( node_name="error_handling", state_key="error_handling_output", progress_label="Error Handling Agent", start_message="Error Handling Agent: scanning resilience and failure paths...", agent=ErrorHandlingAgent(), ), AnalysisAgentSpec( node_name="observability", state_key="observability_output", progress_label="Observability Agent", start_message="Observability Agent: scanning logs, health checks, and telemetry gaps...", agent=ObservabilityAgent(), ), AnalysisAgentSpec( node_name="cuda_migration", state_key="cuda_migration_output", progress_label="CUDA-to-ROCm Agent", start_message="CUDA-to-ROCm Agent: scanning NVIDIA-specific GPU assumptions...", agent=CudaMigrationAgent(), ), ] def _build_graph(self): graph = StateGraph(AuditState) graph.add_node("crawl", self._crawl) graph.add_node("chunk", self._chunk) for spec in self.analysis_agents: graph.add_node(spec.node_name, self._make_agent_node(spec)) graph.add_node("synthesize", self._synthesize) graph.set_entry_point("crawl") graph.add_edge("crawl", "chunk") agent_node_names = [spec.node_name for spec in self.analysis_agents] for node_name in agent_node_names: graph.add_edge("chunk", node_name) graph.add_edge(agent_node_names, "synthesize") graph.add_edge("synthesize", END) return graph.compile() def _make_agent_node(self, spec: AnalysisAgentSpec): async def run_agent(state: AuditState) -> AuditState: output = await spec.agent.analyze(state["chunks"]) return { spec.state_key: output, "progress": [f"{spec.progress_label}: found {len(output.findings)} findings."], } return run_agent async def run(self, repo_url: str) -> AuditReport: result = await self.graph.ainvoke({"repo_url": repo_url, "progress": []}) return result["report"] async def run_with_progress(self, repo_url: str) -> AsyncIterator[str | AuditReport]: repo: RepoScanResult | None = None yield "Crawler Agent: cloning and mapping repository..." repo = self.crawler.clone_and_scan(repo_url) yield f"Crawler Agent: mapped {len(repo.files)} files and skipped {repo.skipped_files}." try: yield "Chunker: filtering source files and creating chunks..." chunks = self.chunker.chunk_files(repo.files) yield f"Chunker: created {len(chunks)} code chunks." outputs: list[AgentOutput] = [] for spec in self.analysis_agents: yield spec.start_message output = await spec.agent.analyze(chunks) outputs.append(output) yield f"{spec.progress_label}: found {len(output.findings)} findings." yield "Synthesizer Agent: ranking findings and formatting report..." report = await self.synthesizer.synthesize( repo, outputs, ) yield "Synthesizer Agent: final report generated." yield report finally: self.crawler.cleanup(repo) async def _crawl(self, state: AuditState) -> AuditState: repo = self.crawler.clone_and_scan(state["repo_url"]) return {"repo": repo, "progress": [f"Crawler Agent: mapped {len(repo.files)} files."]} async def _chunk(self, state: AuditState) -> AuditState: chunks = self.chunker.chunk_files(state["repo"].files) return {"chunks": chunks, "progress": [f"Chunker: created {len(chunks)} code chunks."]} async def _synthesize(self, state: AuditState) -> AuditState: outputs = [state[spec.state_key] for spec in self.analysis_agents] report = await self.synthesizer.synthesize( state["repo"], outputs, ) self.crawler.cleanup(state["repo"]) return {"report": report, "progress": ["Synthesizer Agent: final report generated."]}