NLPGenius commited on
Commit
a97117b
·
1 Parent(s): 1dd0906

Reliability: enforce JSON output via response_format, robust JSON fallback, dedupe retrieved articles, lightweight health

Browse files
cve_factchecker/analyzer.py CHANGED
@@ -29,13 +29,23 @@ class ClaimAnalyzer:
29
  self.cfg = cfg
30
  self.client = build_openrouter_client(cfg)
31
  def analyze(self, claim: str, articles: List[Dict[str, Any]]) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
32
  if not self.client:
33
  # Heuristic fallback: simple keyword overlap scoring.
34
  claim_lc = claim.lower()
35
  keywords = {w for w in claim_lc.split() if len(w) > 4}
36
  supporting: List[str] = []
37
  score = 0
38
- for a in articles:
39
  text = (a.get('content','') or '').lower()
40
  overlap = sum(1 for k in keywords if k in text)
41
  if overlap:
@@ -51,12 +61,24 @@ class ClaimAnalyzer:
51
  "contradicting_evidence": [],
52
  "context_quality": "medium" if supporting else "low",
53
  }
54
- context = "\n\n".join([f"Article {i+1}:\nTitle: {a.get('title','Unknown')}\nSource: {a.get('source','Unknown')}\nURL: {a.get('url','')}\nContent: {a.get('content','')[:500]}..." for i,a in enumerate(articles)])
55
- prompt = ("You are an expert Pakistani fact-checker. Analyze the claim against the retrieved context and return JSON only.\n\n"
56
- f"NEWS CLAIM: {claim}\n\nRETRIEVED CONTEXT:\n{context}\n\n"
57
- "Return strictly valid JSON with keys: verdict, confidence, reasoning, supporting_evidence, contradicting_evidence, context_quality.")
 
 
 
 
58
  try:
59
- content = chat_complete(self.client, self.cfg.model, prompt, temperature=self.cfg.temperature, max_tokens=self.cfg.max_tokens).strip()
 
 
 
 
 
 
 
 
60
  if content.startswith("```"):
61
  content = content.strip("`")
62
  if "\n" in content:
@@ -66,5 +88,26 @@ class ClaimAnalyzer:
66
  content = m.group(0)
67
  data = json.loads(content)
68
  except Exception as e:
69
- return {"verdict": "ERROR", "confidence": 0.0, "reasoning": f"Analysis failed: {e}", "supporting_evidence": [], "contradicting_evidence": [], "context_quality": "low"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  return normalize_result(data)
 
29
  self.cfg = cfg
30
  self.client = build_openrouter_client(cfg)
31
  def analyze(self, claim: str, articles: List[Dict[str, Any]]) -> Dict[str, Any]:
32
+ # Deduplicate articles by URL to reduce duplicates and noise
33
+ deduped: List[Dict[str, Any]] = []
34
+ seen = set()
35
+ for a in articles:
36
+ u = (a.get('url') or '').strip()
37
+ if u and u in seen:
38
+ continue
39
+ seen.add(u)
40
+ deduped.append(a)
41
+
42
  if not self.client:
43
  # Heuristic fallback: simple keyword overlap scoring.
44
  claim_lc = claim.lower()
45
  keywords = {w for w in claim_lc.split() if len(w) > 4}
46
  supporting: List[str] = []
47
  score = 0
48
+ for a in deduped:
49
  text = (a.get('content','') or '').lower()
50
  overlap = sum(1 for k in keywords if k in text)
51
  if overlap:
 
61
  "contradicting_evidence": [],
62
  "context_quality": "medium" if supporting else "low",
63
  }
64
+ context = "\n\n".join([f"Article {i+1}:\nTitle: {a.get('title','Unknown')}\nSource: {a.get('source','Unknown')}\nURL: {a.get('url','')}\nContent: {a.get('content','')[:500]}..." for i,a in enumerate(deduped)])
65
+ prompt = (
66
+ "You are an expert Pakistani fact-checker. Analyze the claim against the retrieved context.\n"
67
+ "Return JSON ONLY. No prose. Use this exact schema keys: \n"
68
+ "{verdict: string, confidence: number between 0 and 1, reasoning: string, supporting_evidence: string[], contradicting_evidence: string[], context_quality: string}.\n"
69
+ "Do not include code fences. Do not include comments."
70
+ f"\n\nNEWS CLAIM: {claim}\n\nRETRIEVED CONTEXT:\n{context}\n"
71
+ )
72
  try:
73
+ # Request structured JSON when supported
74
+ content = chat_complete(
75
+ self.client,
76
+ self.cfg.model,
77
+ prompt,
78
+ temperature=self.cfg.temperature,
79
+ max_tokens=self.cfg.max_tokens,
80
+ response_format={"type": "json_object"}
81
+ ).strip()
82
  if content.startswith("```"):
83
  content = content.strip("`")
84
  if "\n" in content:
 
88
  content = m.group(0)
89
  data = json.loads(content)
90
  except Exception as e:
91
+ # Robust fallback instead of returning ERROR: use heuristic pathway on failure
92
+ print(f"⚠️ JSON parse failed, falling back to heuristic: {e}")
93
+ claim_lc = claim.lower()
94
+ keywords = {w for w in claim_lc.split() if len(w) > 4}
95
+ supporting: List[str] = []
96
+ score = 0
97
+ for a in deduped:
98
+ text = (a.get('content','') or '').lower()
99
+ overlap = sum(1 for k in keywords if k in text)
100
+ if overlap:
101
+ supporting.append(f"Match ({overlap}) in {a.get('url','')}")
102
+ score += overlap
103
+ confidence = min(0.6, 0.1 * score) if supporting else 0.05
104
+ verdict = "POSSIBLY TRUE" if confidence > 0.3 else "UNVERIFIED"
105
+ data = {
106
+ "verdict": verdict,
107
+ "confidence": confidence,
108
+ "reasoning": "LLM output invalid; heuristic fallback used.",
109
+ "supporting_evidence": supporting[:5],
110
+ "contradicting_evidence": [],
111
+ "context_quality": "medium" if supporting else "low",
112
+ }
113
  return normalize_result(data)
cve_factchecker/llm.py CHANGED
@@ -13,7 +13,16 @@ def build_openrouter_client(cfg: OpenRouterConfig) -> Optional[OpenAI]:
13
  print(f"⚠️ LLM initialization failed: {e}")
14
  return None
15
 
16
- def chat_complete(client: OpenAI, model: str, prompt: str, temperature: float = 0.0, max_tokens: int = 800) -> str:
17
- resp = client.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt}], temperature=temperature, max_tokens=max_tokens)
 
 
 
 
 
 
 
 
 
18
  choice = resp.choices[0]
19
  return getattr(getattr(choice, "message", None), "content", None) or getattr(choice, "text", "") or ""
 
13
  print(f"⚠️ LLM initialization failed: {e}")
14
  return None
15
 
16
+ def chat_complete(client: OpenAI, model: str, prompt: str, temperature: float = 0.0, max_tokens: int = 800, response_format: dict | None = None) -> str:
17
+ # Pass response_format when supported by the model/provider (OpenAI/OpenRouter)
18
+ kwargs = {
19
+ "model": model,
20
+ "messages": [{"role": "user", "content": prompt}],
21
+ "temperature": temperature,
22
+ "max_tokens": max_tokens,
23
+ }
24
+ if response_format:
25
+ kwargs["response_format"] = response_format
26
+ resp = client.chat.completions.create(**kwargs)
27
  choice = resp.choices[0]
28
  return getattr(getattr(choice, "message", None), "content", None) or getattr(choice, "text", "") or ""
cve_factchecker/retriever.py CHANGED
@@ -213,6 +213,7 @@ class VectorNewsRetriever:
213
  print(f"❌ Vector search failed: {e}")
214
  return []
215
  results: List[Dict[str, Any]] = []
 
216
  for d in docs:
217
  meta = getattr(d, "metadata", {}) or {}
218
  content = getattr(d, "page_content", "") or ""
@@ -220,5 +221,9 @@ class VectorNewsRetriever:
220
  if content.startswith("Title: "):
221
  line = content.splitlines()[0]
222
  title = line.replace("Title: ", "").strip() or title
223
- results.append({"title": title, "content": content, "url": meta.get("url", ""), "source": meta.get("source", "Unknown"), "metadata": meta})
 
 
 
 
224
  return results
 
213
  print(f"❌ Vector search failed: {e}")
214
  return []
215
  results: List[Dict[str, Any]] = []
216
+ seen_urls = set()
217
  for d in docs:
218
  meta = getattr(d, "metadata", {}) or {}
219
  content = getattr(d, "page_content", "") or ""
 
221
  if content.startswith("Title: "):
222
  line = content.splitlines()[0]
223
  title = line.replace("Title: ", "").strip() or title
224
+ url = meta.get("url", "")
225
+ if url and url in seen_urls:
226
+ continue
227
+ seen_urls.add(url)
228
+ results.append({"title": title, "content": content, "url": url, "source": meta.get("source", "Unknown"), "metadata": meta})
229
  return results