SwarmAudit / app /agents /graph.py
Pranoy Mukherjee
Update SwarmAudit Space demo
9237011
Raw
History Blame Contribute Delete
8.51 kB
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."]}