Ctrl K
ComunidadGuías…

soc_ai/agent/proposal_validation.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Validate a chat verdict proposal against the evidence the chat actually pulled.

A proposal is only applyable when its verdict is a terminal TP/FP AND at least one
citation is grounded in the chat session's own tool activity — the tool was invoked,
or the cited token appears in a tool's result. This is the chat-side analogue of the
orchestrator's evidence-backed gate; it deliberately does not trust a proposal that
merely re-states the alert (a self-referential `alert.*` citation is not evidence).
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

from soc_ai.agent.gates import _CITATION_STOP_WORDS, _FUZZY_TOKEN_RE

_TERMINAL = {"true_positive", "false_positive"}
# Tokens worth matching against tool results: ids, community ids, dotted paths,
# long alphanumerics — strip the (path ...)/(id ...) wrappers first.
_WRAP = re.compile(r"^\((?:path|id|tool)\s+(.*)\)$", re.IGNORECASE)
_ALERT_SELF = re.compile(r"^alert\.", re.IGNORECASE)


@dataclass
class Proposal:
    verdict: str
    confidence: float
    rationale: str
    citations: list[str] = field(default_factory=list)
    recommended_actions: list[dict[str, object]] = field(default_factory=list)


@dataclass
class ProposalValidation:
    ok: bool
    objection: str | None = None


def _unwrap(citation: str) -> str:
    m = _WRAP.match(citation.strip())
    return (m.group(1) if m else citation).strip()


def _citation_grounded_in_results(citation: str, results_blob: str) -> bool:
    """True iff a DISTINCTIVE token of ``citation`` appears in ``results_blob``.

    Same discipline as GATE C (:mod:`soc_ai.agent.gates`) and the hunt-finding
    gate: a stop-word or a short generic fragment never grounds a proposal on its
    own; a long token (>= 8 chars — an id/hash/full IP) grounds on a substring, a
    medium token (>= 5 chars — a domain label, a hyphenated host) grounds only on
    a word boundary. Replaces the old ``len(low) >= 4 and low in results_blob``
    fallback that let a bare 'false' pass just because the word appeared
    incidentally in a tool result.
    """
    if not results_blob:
        return False
    for tok in _FUZZY_TOKEN_RE.findall(citation):
        low = tok.lower()
        if low in _CITATION_STOP_WORDS:
            continue
        if len(tok) >= 8 and low in results_blob:
            return True
        if len(tok) >= 5 and re.search(rf"\b{re.escape(low)}\b", results_blob):
            return True
    return False


def validate_proposal(
    proposal: Proposal,
    *,
    tool_evidence: list[dict[str, object]],
) -> ProposalValidation:
    """``tool_evidence`` is a list of ``{"tool": name, "result": text}`` from the chat run."""
    if proposal.verdict not in _TERMINAL:
        return ProposalValidation(
            ok=False,
            objection=f"verdict {proposal.verdict!r} is not a terminal decision (TP/FP).",
        )
    if not proposal.citations:
        return ProposalValidation(ok=False, objection="proposal cites no evidence.")

    tool_names = {str(e.get("tool", "")).lower() for e in tool_evidence}
    results_blob = "\n".join(str(e.get("result", "")) for e in tool_evidence).lower()

    for raw in proposal.citations:
        token = _unwrap(raw)
        if not token or _ALERT_SELF.match(token):
            continue  # empty or self-referential — not evidence the chat fetched
        low = token.lower()
        # grounded if it names an invoked tool (exact, or the tool name appears
        # inside a prose citation), or the cited token is in a tool result. Note
        # we deliberately do NOT match a citation that is merely a *substring* of
        # a tool name — that let junk like "in" pass the gate.
        if low in tool_names or any(tn in low for tn in tool_names if tn):
            return ProposalValidation(ok=True)
        if _citation_grounded_in_results(token, results_blob):
            return ProposalValidation(ok=True)

    return ProposalValidation(
        ok=False,
        objection="no citation is grounded in evidence the chat actually fetched.",
    )