|
58 | 58 | EvidenceChunk, |
59 | 59 | ) |
60 | 60 |
|
61 | | -ALGORITHM_VERSION = "routing-nli-sec-attribution-v4" |
| 61 | +ALGORITHM_VERSION = "routing-nli-sec-attribution-v5" |
62 | 62 |
|
63 | 63 | _TOKEN_RE = re.compile(r"[A-Za-z0-9]+") |
64 | 64 | _DIRECT_SUPPORT_STOPWORDS = { |
@@ -263,6 +263,76 @@ def evaluate( |
263 | 263 | errors=tuple(errors), |
264 | 264 | ) |
265 | 265 |
|
| 266 | + def verify_against_premise(self, answer: str, premise: str) -> Decision: |
| 267 | + """Require every submitted claim to be entailed by a canonical premise. |
| 268 | +
|
| 269 | + This is used for derived facts, such as arithmetic or comparisons, for |
| 270 | + which callers can construct a deterministic premise from attributed |
| 271 | + source facts. No source routing or lexical entity vocabulary is used. |
| 272 | + """ |
| 273 | + claims = self._decomposer.decompose(answer) |
| 274 | + if not claims: |
| 275 | + return Decision(status="unavailable", reason="no_claims_extracted") |
| 276 | + |
| 277 | + verdicts = [] |
| 278 | + errors = [] |
| 279 | + for claim in claims: |
| 280 | + claim_text = " ".join(claim.text.split()) |
| 281 | + premise_text = " ".join(premise.split()) |
| 282 | + if claim_text in premise_text: |
| 283 | + verdicts.append( |
| 284 | + ClaimVerdict( |
| 285 | + claim_id=claim.claim_id, |
| 286 | + claim_text=claim.text, |
| 287 | + raw_nli_label="entailment", |
| 288 | + raw_nli_probabilities=(("entailment", 1.0),), |
| 289 | + final_label="entailment", |
| 290 | + evidence_excerpt=premise[: self._config.evidence_excerpt_length], |
| 291 | + ) |
| 292 | + ) |
| 293 | + continue |
| 294 | + try: |
| 295 | + result = self._nli.score(premise=premise, hypothesis=claim.text) |
| 296 | + except Exception as exc: |
| 297 | + error = f"NLI error for claim {claim.claim_id}: {exc!s}" |
| 298 | + errors.append(error) |
| 299 | + verdicts.append( |
| 300 | + ClaimVerdict( |
| 301 | + claim_id=claim.claim_id, |
| 302 | + claim_text=claim.text, |
| 303 | + final_label="neutral", |
| 304 | + evidence_excerpt=premise[: self._config.evidence_excerpt_length], |
| 305 | + errors=(error,), |
| 306 | + ) |
| 307 | + ) |
| 308 | + continue |
| 309 | + verdicts.append( |
| 310 | + ClaimVerdict( |
| 311 | + claim_id=claim.claim_id, |
| 312 | + claim_text=claim.text, |
| 313 | + raw_nli_label=result.label, |
| 314 | + raw_nli_probabilities=result.probabilities, |
| 315 | + final_label=result.label, |
| 316 | + evidence_excerpt=premise[: self._config.evidence_excerpt_length], |
| 317 | + ) |
| 318 | + ) |
| 319 | + |
| 320 | + if errors: |
| 321 | + return Decision( |
| 322 | + status="unavailable", |
| 323 | + reason="semantic_verification_error", |
| 324 | + verdicts=tuple(verdicts), |
| 325 | + errors=tuple(errors), |
| 326 | + ) |
| 327 | + for label in ("contradiction", "neutral"): |
| 328 | + if any(verdict.final_label == label for verdict in verdicts): |
| 329 | + return Decision( |
| 330 | + status="block", |
| 331 | + reason=f"semantic_{label}", |
| 332 | + verdicts=tuple(verdicts), |
| 333 | + ) |
| 334 | + return Decision(status="allow", reason="semantic_entailment", verdicts=tuple(verdicts)) |
| 335 | + |
266 | 336 | def _evaluate_claim( |
267 | 337 | self, |
268 | 338 | claim: AtomicClaim, |
|
0 commit comments