"""Per-cluster conflict critics with hard evidence and image caps.""" from typing import Dict, List from backend import config from backend.agents.base import AgentResult, AgentScope, AgentUsage, failure from backend.llm import call_json from backend.pipeline.conflict_checker import _evidence_block, _valid_conflict from backend.prompts import CONFLICT_SYSTEM_PROMPT, CONFLICT_USER_INSTRUCTION def _as_finding(conflict: Dict, scope_id: str) -> Dict: return { "issue_id": conflict.get("conflict_id") or "", "source_stage": "conflict", "category": conflict.get("category") or "uncategorized", "severity": conflict.get("severity") or "medium", "confidence": conflict.get("confidence") or "medium", "location": conflict.get("location") or "", "disciplines": conflict.get("disciplines") or [], "sheets": conflict.get("sheets") or [], "description": conflict.get("description") or "", "evidence": conflict.get("evidence") or [], "recommended_resolution": conflict.get("recommended_resolution") or "", "code_reference": None, "agent": "conflict_critic", "scope_id": scope_id, } class ConflictCriticAgent: name = "conflict_critic" def __init__(self, usage: AgentUsage) -> None: self.usage = usage def run(self, scope: AgentScope) -> AgentResult: try: cluster = dict(scope.payload["cluster"]) cluster["assertions"] = ( cluster.get("assertions") or [] )[:config.AGENT_CLUSTER_MAX_ASSERTIONS] page_to_b64: Dict[int, str] = scope.payload.get("page_to_b64") or {} images: List[str] = [] for page_number in ( cluster.get("page_numbers") or [] )[:config.AGENT_CONFLICT_MAX_IMAGES]: if page_to_b64.get(page_number): images.append(page_to_b64[page_number]) instruction = ( CONFLICT_USER_INSTRUCTION .replace("{location}", cluster.get("location") or "") .replace("{evidence}", _evidence_block(cluster)) ) parsed = call_json( system_prompt=CONFLICT_SYSTEM_PROMPT, user_text=instruction, images_b64=images, max_tokens=config.REASON_MAX_TOKENS, model=config.AGENT_CONFLICT_MODEL, usage_tracker=self.usage, usage_stage="agent.conflict", reasoning_effort=config.EXTRACT_REASONING_EFFORT or None, reasoning_max_tokens=config.EXTRACT_REASONING_MAX_TOKENS or None, ) candidates = parsed if isinstance(parsed, list) else ( parsed.get("conflicts") if isinstance(parsed, dict) else [] ) findings = [] for candidate in candidates or []: conflict = _valid_conflict(candidate, cluster) if conflict: findings.append(_as_finding(conflict, scope.scope_id)) return AgentResult(scope_id=scope.scope_id, artifacts=findings) except Exception as exc: return failure(scope, exc)