stephenwahogo commited on
Commit
bd689a7
·
verified ·
1 Parent(s): 90e18af

Upload nicto_ai\verification\verifier.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. nicto_ai//verification//verifier.py +379 -0
nicto_ai//verification//verifier.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NICTO AI - Claim Verifier
3
+ Verifies extracted claims against multiple evidence sources.
4
+
5
+ Uses the knowledge base, web search, and internal knowledge
6
+ to determine if claims are supported, refuted, or unverifiable.
7
+ """
8
+
9
+ import logging
10
+ import math
11
+ from typing import Dict, List, Optional, Tuple
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+
15
+ from .claim_extractor import Claim, ClaimType, ClaimSeverity
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class VerificationStatus(Enum):
21
+ SUPPORTED = "supported" # Evidence confirms the claim
22
+ REFUTED = "refuted" # Evidence contradicts the claim
23
+ PARTIALLY_SUPPORTED = "partially_supported" # Some evidence, some gaps
24
+ UNVERIFIABLE = "unverifiable" # Cannot find evidence either way
25
+ CONFLICTING = "conflicting" # Evidence goes both ways
26
+ OUTDATED = "outdated" # Was true, but may no longer be
27
+
28
+
29
+ @dataclass
30
+ class Evidence:
31
+ """A piece of evidence supporting or refuting a claim"""
32
+ source: str # Source identifier (URL, document name, etc.)
33
+ text: str # The evidence text
34
+ relevance: float # 0-1 how relevant to the claim
35
+ supports: bool # True = supports claim, False = refutes
36
+ confidence: float # 0-1 how confident in this evidence
37
+ timestamp: Optional[str] = None # When the evidence was published
38
+ metadata: Dict = field(default_factory=dict)
39
+
40
+
41
+ @dataclass
42
+ class VerificationResult:
43
+ """Result of verifying a single claim"""
44
+ claim: Claim
45
+ status: VerificationStatus
46
+ confidence: float # 0-1 overall confidence in verification
47
+ evidence: List[Evidence] = field(default_factory=list)
48
+ explanation: str = "" # Human-readable explanation
49
+ recommendation: str = "" # What NICTO should do (state it, qualify it, or refuse)
50
+ sources: List[str] = field(default_factory=list) # Source URLs/references
51
+
52
+ @property
53
+ def is_reliable(self) -> bool:
54
+ """Is this claim safe to state as fact?"""
55
+ return self.status in (VerificationStatus.SUPPORTED,) and self.confidence > 0.7
56
+
57
+ @property
58
+ def should_refuse(self) -> bool:
59
+ """Should NICTO refuse to state this claim?"""
60
+ return self.status in (VerificationStatus.REFUTED, VerificationStatus.UNVERIFIABLE) or self.confidence < 0.3
61
+
62
+
63
+ class ClaimVerifier:
64
+ """
65
+ Verifies claims against evidence from multiple sources.
66
+
67
+ Verification pipeline:
68
+ 1. Check internal knowledge (训练 data, learned facts)
69
+ 2. Query knowledge base for relevant documents
70
+ 3. Search the web for current information
71
+ 4. Cross-reference multiple sources
72
+ 5. Compute verification confidence
73
+ 6. Determine if claim should be stated, qualified, or refused
74
+
75
+ The verifier is conservative: when in doubt, it flags the claim
76
+ as unverifiable rather than guessing.
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ knowledge_base=None,
82
+ browser=None,
83
+ min_evidence: int = 1,
84
+ min_confidence: float = 0.5,
85
+ ):
86
+ """
87
+ Args:
88
+ knowledge_base: NICTOKnowledgeBase instance for local knowledge
89
+ browser: NICTOBrowser instance for web search
90
+ min_evidence: Minimum evidence pieces needed for verification
91
+ min_confidence: Minimum confidence to consider a claim verified
92
+ """
93
+ self.knowledge_base = knowledge_base
94
+ self.browser = browser
95
+ self.min_evidence = min_evidence
96
+ self.min_confidence = min_confidence
97
+ self._verification_cache: Dict[str, VerificationResult] = {}
98
+
99
+ def verify(self, claim: Claim) -> VerificationResult:
100
+ """
101
+ Verify a single claim against available evidence.
102
+
103
+ Args:
104
+ claim: The claim to verify
105
+
106
+ Returns:
107
+ VerificationResult with status, confidence, and evidence
108
+ """
109
+ # Check cache
110
+ cache_key = claim.text.lower().strip()
111
+ if cache_key in self._verification_cache:
112
+ return self._verification_cache[cache_key]
113
+
114
+ evidence = []
115
+
116
+ # Step 1: Check internal knowledge patterns
117
+ internal_evidence = self._check_internal_knowledge(claim)
118
+ evidence.extend(internal_evidence)
119
+
120
+ # Step 2: Query knowledge base if available
121
+ if self.knowledge_base is not None:
122
+ kb_evidence = self._query_knowledge_base(claim)
123
+ evidence.extend(kb_evidence)
124
+
125
+ # Step 3: Web search for current information
126
+ if self.browser is not None:
127
+ web_evidence = self._search_web(claim)
128
+ evidence.extend(web_evidence)
129
+
130
+ # Step 4: Cross-reference and compute verification
131
+ result = self._compute_verification(claim, evidence)
132
+
133
+ # Cache result
134
+ self._verification_cache[cache_key] = result
135
+
136
+ return result
137
+
138
+ def verify_batch(self, claims: List[Claim]) -> List[VerificationResult]:
139
+ """Verify multiple claims."""
140
+ return [self.verify(claim) for claim in claims]
141
+
142
+ def _check_internal_knowledge(self, claim: Claim) -> List[Evidence]:
143
+ """Check claim against internal knowledge patterns."""
144
+ evidence = []
145
+
146
+ # Pattern-based checks for common factual claims
147
+ text = claim.text.lower()
148
+
149
+ # Check for well-known facts
150
+ well_known = self._check_well_known_facts(text)
151
+ if well_known:
152
+ evidence.append(well_known)
153
+
154
+ # Check for logical consistency
155
+ logical = self._check_logical_consistency(claim)
156
+ if logical:
157
+ evidence.append(logical)
158
+
159
+ return evidence
160
+
161
+ def _check_well_known_facts(self, text: str) -> Optional[Evidence]:
162
+ """Check against a database of well-known facts."""
163
+ # This is a simplified version - in production, this would
164
+ # query a comprehensive fact database
165
+ well_known_facts = {
166
+ "earth": {
167
+ "orbits the sun": True,
168
+ "is round": True,
169
+ "has one moon": True,
170
+ "is the third planet": True,
171
+ },
172
+ "water": {
173
+ "boils at 100": True,
174
+ "freezes at 0": True,
175
+ "is h2o": True,
176
+ },
177
+ "python": {
178
+ "was created by guido": True,
179
+ "is a programming language": True,
180
+ "first released in 1991": True,
181
+ },
182
+ }
183
+
184
+ for entity, facts in well_known_facts.items():
185
+ if entity in text:
186
+ for fact, is_true in facts.items():
187
+ if fact in text:
188
+ return Evidence(
189
+ source="internal:well_known_facts",
190
+ text=f"Well-known fact: {fact}",
191
+ relevance=0.9,
192
+ supports=is_true,
193
+ confidence=0.95,
194
+ )
195
+ return None
196
+
197
+ def _check_logical_consistency(self, claim: Claim) -> Optional[Evidence]:
198
+ """Check if claim is logically consistent."""
199
+ text = claim.text.lower()
200
+
201
+ # Check for contradictions within the claim
202
+ contradictions = [
203
+ ("greater than", "less than"),
204
+ ("always", "never"),
205
+ ("all", "none"),
206
+ ("more", "fewer"),
207
+ ("increase", "decrease"),
208
+ ]
209
+
210
+ for word1, word2 in contradictions:
211
+ if word1 in text and word2 in text:
212
+ return Evidence(
213
+ source="internal:logic",
214
+ text=f"Claim contains potential contradiction: '{word1}' and '{word2}'",
215
+ relevance=0.8,
216
+ supports=False,
217
+ confidence=0.7,
218
+ )
219
+
220
+ return None
221
+
222
+ def _query_knowledge_base(self, claim: Claim) -> List[Evidence]:
223
+ """Query the knowledge base for relevant evidence."""
224
+ evidence = []
225
+ try:
226
+ results = self.knowledge_base.query(claim.text, top_k=5)
227
+ for result in results:
228
+ # Determine if the knowledge base entry supports or refutes
229
+ supports = self._determine_support(claim.text, result.get("text", ""))
230
+ evidence.append(Evidence(
231
+ source=f"knowledge_base:{result.get('title', 'unknown')}",
232
+ text=result.get("text", "")[:500],
233
+ relevance=result.get("score", 0.5),
234
+ supports=supports,
235
+ confidence=result.get("score", 0.5),
236
+ ))
237
+ except Exception as e:
238
+ logger.warning("Knowledge base query failed: %s", e)
239
+ return evidence
240
+
241
+ def _search_web(self, claim: Claim) -> List[Evidence]:
242
+ """Search the web for evidence about the claim."""
243
+ evidence = []
244
+ try:
245
+ search_results = self.browser.search(claim.text, max_results=3)
246
+ for result in search_results.results[:3]:
247
+ # Try to extract relevant content
248
+ content = result.get("snippet", "")
249
+ if content:
250
+ supports = self._determine_support(claim.text, content)
251
+ evidence.append(Evidence(
252
+ source=f"web:{result.get('url', 'unknown')}",
253
+ text=content[:500],
254
+ relevance=0.6,
255
+ supports=supports,
256
+ confidence=0.5,
257
+ metadata={"url": result.get("url", "")},
258
+ ))
259
+ except Exception as e:
260
+ logger.warning("Web search failed: %s", e)
261
+ return evidence
262
+
263
+ def _determine_support(self, claim_text: str, evidence_text: str) -> bool:
264
+ """Determine if evidence text supports or refutes the claim."""
265
+ claim_lower = claim_text.lower()
266
+ evidence_lower = evidence_text.lower()
267
+
268
+ # Simple heuristic: check for supporting or contradicting keywords
269
+ supporting_keywords = ["confirmed", "verified", "true", "correct", "indeed", "yes"]
270
+ contradicting_keywords = ["false", "incorrect", "refuted", "debunked", "myth", "no"]
271
+
272
+ support_score = sum(1 for k in supporting_keywords if k in evidence_lower)
273
+ contradict_score = sum(1 for k in contradicting_keywords if k in evidence_lower)
274
+
275
+ # Check for negation alignment
276
+ claim_negated = any(w in claim_lower for w in ["not", "never", "no", "neither"])
277
+ evidence_negated = any(w in evidence_lower for w in ["not", "never", "no", "neither"])
278
+
279
+ if claim_negated == evidence_negated:
280
+ support_score += 1
281
+ else:
282
+ contradict_score += 1
283
+
284
+ return support_score > contradict_score
285
+
286
+ def _compute_verification(self, claim: Claim, evidence: List[Evidence]) -> VerificationResult:
287
+ """Compute the final verification result from all evidence."""
288
+ if not evidence:
289
+ return VerificationResult(
290
+ claim=claim,
291
+ status=VerificationStatus.UNVERIFIABLE,
292
+ confidence=0.0,
293
+ explanation="No evidence found to verify this claim.",
294
+ recommendation="Do not state this claim as fact. Either qualify it or omit it.",
295
+ )
296
+
297
+ # Count supporting vs refuting evidence
298
+ supporting = [e for e in evidence if e.supports]
299
+ refuting = [e for e in evidence if not e.supports]
300
+
301
+ # Weight by relevance and confidence
302
+ support_weight = sum(e.relevance * e.confidence for e in supporting)
303
+ refute_weight = sum(e.relevance * e.confidence for e in refuting)
304
+ total_weight = support_weight + refute_weight
305
+
306
+ if total_weight == 0:
307
+ confidence = 0.0
308
+ else:
309
+ confidence = support_weight / total_weight
310
+
311
+ # Determine status
312
+ if len(supporting) > 0 and len(refuting) == 0:
313
+ status = VerificationStatus.SUPPORTED
314
+ elif len(refuting) > 0 and len(supporting) == 0:
315
+ status = VerificationStatus.REFUTED
316
+ elif support_weight > refute_weight * 2:
317
+ status = VerificationStatus.SUPPORTED
318
+ elif refute_weight > support_weight * 2:
319
+ status = VerificationStatus.REFUTED
320
+ elif support_weight > 0 and refute_weight > 0:
321
+ status = VerificationStatus.CONFLICTING
322
+ else:
323
+ status = VerificationStatus.PARTIALLY_SUPPORTED
324
+
325
+ # Generate explanation
326
+ explanation = self._generate_explanation(claim, status, supporting, refuting, confidence)
327
+
328
+ # Generate recommendation
329
+ recommendation = self._generate_recommendation(status, confidence, claim.severity)
330
+
331
+ # Collect sources
332
+ sources = [e.source for e in evidence if e.source.startswith("web:")]
333
+
334
+ return VerificationResult(
335
+ claim=claim,
336
+ status=status,
337
+ confidence=confidence,
338
+ evidence=evidence,
339
+ explanation=explanation,
340
+ recommendation=recommendation,
341
+ sources=sources,
342
+ )
343
+
344
+ def _generate_explanation(
345
+ self, claim: Claim, status: VerificationStatus,
346
+ supporting: List[Evidence], refuting: List[Evidence],
347
+ confidence: float,
348
+ ) -> str:
349
+ """Generate a human-readable explanation of the verification."""
350
+ n_support = len(supporting)
351
+ n_refute = len(refuting)
352
+
353
+ if status == VerificationStatus.SUPPORTED:
354
+ return f"Supported by {n_support} evidence source(s) with {confidence:.0%} confidence."
355
+ elif status == VerificationStatus.REFUTED:
356
+ return f"Refuted by {n_refute} evidence source(s). Confidence: {confidence:.0%}."
357
+ elif status == VerificationStatus.CONFLICTING:
358
+ return f"Conflicting evidence: {n_support} support, {n_refute} refute. Confidence: {confidence:.0%}."
359
+ elif status == VerificationStatus.PARTIALLY_SUPPORTED:
360
+ return f"Partially supported. Some evidence found but not conclusive. Confidence: {confidence:.0%}."
361
+ else:
362
+ return "Insufficient evidence to verify this claim."
363
+
364
+ def _generate_recommendation(
365
+ self, status: VerificationStatus, confidence: float, severity: ClaimSeverity,
366
+ ) -> str:
367
+ """Generate a recommendation for how NICTO should handle this claim."""
368
+ if status == VerificationStatus.SUPPORTED and confidence > 0.8:
369
+ return "SAFE_TO_STATE: Claim is well-supported. State it as fact."
370
+ elif status == VerificationStatus.SUPPORTED and confidence > 0.5:
371
+ return "QUALIFY: Claim is supported but with limited evidence. Add qualifiers like 'according to' or 'evidence suggests'."
372
+ elif status == VerificationStatus.REFUTED:
373
+ return "REFUSE: Claim is contradicted by evidence. Do not state it."
374
+ elif status == VerificationStatus.CONFLICTING:
375
+ return "QUALIFY: Evidence is mixed. Present both sides or note the uncertainty."
376
+ elif severity == ClaimSeverity.SAFETY:
377
+ return "REFUSE: Safety-critical claim with insufficient evidence. Do not state it."
378
+ else:
379
+ return "QUALIFY: Uncertain claim. Use hedging language like 'may', 'appears to', or 'some sources suggest'."