Add scoped Agent-mode pipeline as experimental Classic fork.
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>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""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)]
|
||||
Reference in New Issue
Block a user