Wire specialist waves, Brain consolidation, and Classic-compatible reports so Agent mode can run end-to-end via OpenRouter without changing the default Classic path. Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.9 KiB
Python
130 lines
4.9 KiB
Python
"""Central merge, judge, and prioritization agent."""
|
|
|
|
import json
|
|
import re
|
|
from typing import Dict, List, Tuple
|
|
|
|
from backend import config
|
|
from backend.agents.base import AgentUsage
|
|
from backend.agents.prompts import BRAIN_SYSTEM_PROMPT, BRAIN_USER_PROMPT
|
|
from backend.llm import call_json
|
|
from backend.pipeline._stage import collect_list, validate_issue
|
|
|
|
|
|
def _finding_ref(finding: Dict, index: int) -> str:
|
|
return (
|
|
finding.get("issue_id")
|
|
or f"{finding.get('agent', 'agent')}:{finding.get('scope_id', '?')}:{index + 1}"
|
|
)
|
|
|
|
|
|
def _signature(finding: Dict) -> Tuple[str, str, str]:
|
|
norm = lambda value: re.sub(r"[^a-z0-9]+", " ", str(value).lower()).strip()
|
|
description = " ".join(norm(finding.get("description")).split()[:12])
|
|
return (
|
|
norm(finding.get("category")),
|
|
norm(finding.get("location")),
|
|
description,
|
|
)
|
|
|
|
|
|
def _fallback(findings: List[Dict]) -> Tuple[List[Dict], List[Dict]]:
|
|
"""Conservative local consolidation when the Brain call fails."""
|
|
kept: Dict[Tuple[str, str, str], Dict] = {}
|
|
refs: Dict[Tuple[str, str, str], List[str]] = {}
|
|
decisions: List[Dict] = []
|
|
severity_rank = {"critical": 4, "high": 3, "medium": 2, "low": 1}
|
|
for index, finding in enumerate(findings):
|
|
ref = _finding_ref(finding, index)
|
|
supported = bool(finding.get("evidence")) or finding.get("agent") == "completeness"
|
|
if not supported or not finding.get("description"):
|
|
decisions.append({
|
|
"finding_refs": [ref],
|
|
"action": "dropped",
|
|
"reason": "missing actionable support",
|
|
"kept_issue_id": None,
|
|
})
|
|
continue
|
|
signature = _signature(finding)
|
|
if signature not in kept:
|
|
kept[signature] = dict(finding)
|
|
refs[signature] = [ref]
|
|
else:
|
|
refs[signature].append(ref)
|
|
existing = kept[signature]
|
|
if severity_rank.get(finding.get("severity"), 2) > severity_rank.get(
|
|
existing.get("severity"), 2
|
|
):
|
|
existing["severity"] = finding.get("severity")
|
|
existing["evidence"] = (
|
|
existing.get("evidence") or []
|
|
) + (finding.get("evidence") or [])
|
|
|
|
issues = list(kept.values())
|
|
for index, (signature, issue) in enumerate(kept.items()):
|
|
issue["issue_id"] = issue.get("issue_id") or f"AGENT-{index + 1:04d}"
|
|
issue["risk_score"] = {
|
|
"critical": 95, "high": 75, "medium": 50, "low": 25
|
|
}.get(issue.get("severity"), 50)
|
|
issue["recommended_priority"] = {
|
|
"critical": "immediate",
|
|
"high": "before_bid",
|
|
"medium": "before_construction",
|
|
"low": "track_only",
|
|
}.get(issue.get("severity"), "before_construction")
|
|
decisions.append({
|
|
"finding_refs": refs[signature],
|
|
"action": "merged" if len(refs[signature]) > 1 else "kept",
|
|
"reason": "conservative deterministic fallback",
|
|
"kept_issue_id": issue["issue_id"],
|
|
})
|
|
issues.sort(key=lambda item: -int(item.get("risk_score") or 0))
|
|
return issues, decisions
|
|
|
|
|
|
class BrainAgent:
|
|
name = "brain"
|
|
|
|
def __init__(self, usage: AgentUsage) -> None:
|
|
self.usage = usage
|
|
|
|
def run(
|
|
self,
|
|
findings: List[Dict],
|
|
sheet_index: Dict,
|
|
jurisdiction: Dict,
|
|
) -> Tuple[List[Dict], List[Dict]]:
|
|
instruction = BRAIN_USER_PROMPT
|
|
for key, value in {
|
|
"sheet_index": sheet_index,
|
|
"jurisdiction": jurisdiction,
|
|
"findings": findings,
|
|
}.items():
|
|
instruction = instruction.replace(
|
|
"{" + key + "}", json.dumps(value, ensure_ascii=True)
|
|
)
|
|
parsed = call_json(
|
|
system_prompt=BRAIN_SYSTEM_PROMPT,
|
|
user_text=instruction,
|
|
max_tokens=config.AGENT_BRAIN_MAX_TOKENS,
|
|
model=config.AGENT_BRAIN_MODEL,
|
|
usage_tracker=self.usage,
|
|
usage_stage="agent.brain",
|
|
)
|
|
issues = collect_list(
|
|
parsed, "issues", lambda item: validate_issue(item, item.get("source_stage", ""))
|
|
)
|
|
if not issues:
|
|
return _fallback(findings)
|
|
raw_issues = parsed.get("issues") if isinstance(parsed, dict) else []
|
|
for index, issue in enumerate(issues):
|
|
raw = raw_issues[index] if index < len(raw_issues) else {}
|
|
issue["issue_id"] = issue.get("issue_id") or f"AGENT-{index + 1:04d}"
|
|
issue["risk_score"] = raw.get("risk_score") or issue.get("risk_score") or 50
|
|
issue["recommended_priority"] = (
|
|
raw.get("recommended_priority") or "before_construction"
|
|
)
|
|
issues.sort(key=lambda item: -int(item.get("risk_score") or 0))
|
|
decisions = parsed.get("decisions") or []
|
|
return issues, [item for item in decisions if isinstance(item, dict)]
|