| """
|
| Agent 3 — Verifier
|
| Arabic News Analyzer · Graduation Project
|
|
|
| Receives a claim + evidence list from Agent 2.
|
| Calls Groq API (llama-3.3-70b-versatile) to reason over the evidence
|
| and produce a structured verdict.
|
|
|
| Output schema:
|
| {
|
| "verdict": "SUPPORTED" | "REFUTED" | "PARTIALLY_TRUE" | "UNVERIFIABLE",
|
| "confidence": 0.0 - 1.0,
|
| "reasoning": "..." # Arabic or English explanation of the verdict
|
| }
|
|
|
| Rules:
|
| - One Groq call per claim
|
| - Retry once on JSON parse failure
|
| - Default to UNVERIFIABLE on any error or empty evidence
|
| - Uses AsyncGroq — non-blocking, compatible with FastAPI + LangGraph async
|
|
|
| Required setup:
|
| pip install groq python-dotenv
|
| Add to .env: GROQ_API_KEY1=gsk_...
|
| Get free key at: https://console.groq.com
|
| """
|
|
|
| import asyncio
|
| import json
|
| import os
|
| import re
|
|
|
| from groq import AsyncGroq
|
| from dotenv import load_dotenv
|
|
|
| load_dotenv()
|
|
|
|
|
| GROQ_MODEL = "llama-3.3-70b-versatile"
|
| MAX_EVIDENCE_SNIPPETS = 8
|
| MAX_SNIPPET_LENGTH = 300
|
| MAX_TOKENS = 1024
|
| TEMPERATURE = 0.1
|
|
|
|
|
| UNVERIFIABLE_DEFAULT = {
|
| "verdict": "UNVERIFIABLE",
|
| "confidence": 0.0,
|
| "reasoning": "لم يتم العثور على أدلة كافية للتحقق من هذا الادعاء.",
|
| }
|
|
|
|
|
| SYSTEM_PROMPT = """أنت نظام تحقق آلي متخصص في تدقيق الأخبار العربية.
|
|
|
| مهمتك: تحليل الادعاء المقدم بناءً على الأدلة المتاحة، ثم إصدار حكم دقيق ومبرر.
|
|
|
| ━━━ تعريف الأحكام ━━━
|
| - SUPPORTED — أدلة موثوقة تؤكد الادعاء بشكل واضح ومباشر.
|
| - REFUTED — أدلة موثوقة تنفي الادعاء أو تتناقض معه صراحةً.
|
| - PARTIALLY_TRUE — جزء من الادعاء صحيح وجزء خاطئ أو مبالغ فيه، أو الأدلة متضاربة.
|
| - UNVERIFIABLE — الأدلة غائبة تماماً أو لا تتعلق بالادعاء بما يكفي للحكم.
|
|
|
| ━━━ قواعد الوزن والتقييم ━━━
|
| 1. رتّب المصادر حسب الموثوقية: google_factcheck > wikidata > tavily.
|
| 2. إذا وجد تقييم من google_factcheck، فهو دليل أقوى ويجب أن يؤثر بشكل أكبر على الحكم.
|
| 3. عند تعارض المصادر، رجّح الأكثر موثوقية ووضّح التعارض في reasoning.
|
| 4. confidence يعكس مدى وضوح الأدلة، لا مدى صحة الادعاء:
|
| — 0.9-1.0: أدلة متعددة متسقة وموثوقة
|
| — 0.6-0.8: أدلة كافية لكن غير مكتملة أو من مصدر واحد
|
| — 0.3-0.5: أدلة متضاربة أو ضعيفة
|
| — 0.0-0.2: لا أدلة ذات صلة
|
|
|
| ━━━ قواعد الإخراج ━━━
|
| 1. أجب فقط بـ JSON صالح — لا نص قبله أو بعده، لا علامات markdown.
|
| 2. استخدم فقط هذه القيم للحكم: SUPPORTED | REFUTED | PARTIALLY_TRUE | UNVERIFIABLE
|
| 3. reasoning: جملتان إلى ثلاث جمل، تذكر المصادر المستخدمة وكيف أثّرت في الحكم.
|
| 4. لا تخترع أدلة أو وقائع غير موجودة في النص المقدم.
|
|
|
| شكل الإجابة (JSON فقط):
|
| {
|
| "verdict": "SUPPORTED | REFUTED | PARTIALLY_TRUE | UNVERIFIABLE",
|
| "confidence": 0.0,
|
| "reasoning": "تفسير مبني على الأدلة مع ذكر المصادر..."
|
| }"""
|
|
|
|
|
| class Verifier:
|
| """
|
| Agent 3 — verifies a single claim against evidence from Agent 2.
|
|
|
| Usage:
|
| verifier = Verifier()
|
| result = await verifier.run(claim, evidence)
|
|
|
| Input:
|
| claim: clean claim string (no [سياق:] tag — strip it first)
|
| evidence: list of evidence dicts from Agent 2
|
|
|
| Output:
|
| dict with verdict, confidence, reasoning
|
| """
|
|
|
| def __init__(self):
|
| self.client = AsyncGroq(api_key=os.environ.get("GROQ_API_KEY1"))
|
|
|
|
|
|
|
| async def run(self, claim: str, evidence: list[dict]) -> dict:
|
| """
|
| Main entry point.
|
|
|
| Steps:
|
| 1. If no evidence → return UNVERIFIABLE immediately (no Groq call)
|
| 2. Build prompt from claim + evidence snippets
|
| 3. Call Groq API
|
| 4. Parse JSON response
|
| 5. On parse failure → retry once
|
| 6. On second failure → return UNVERIFIABLE default
|
| """
|
|
|
| claim = self._strip_context_tag(claim)
|
|
|
| if not evidence:
|
| return {
|
| **UNVERIFIABLE_DEFAULT,
|
| "reasoning": "لم يتم العثور على أي أدلة لهذا الادعاء.",
|
| }
|
|
|
| user_prompt = self._build_prompt(claim, evidence)
|
|
|
|
|
| result = await self._call_groq(user_prompt)
|
| if result is not None:
|
| return result
|
|
|
|
|
| print(
|
| f"[Verifier] JSON parse failed — retrying once for claim: {claim[:60]}")
|
| result = await self._call_groq(user_prompt)
|
| if result is not None:
|
| return result
|
|
|
|
|
| print(f"[Verifier] Both attempts failed — defaulting to UNVERIFIABLE")
|
| return UNVERIFIABLE_DEFAULT
|
|
|
|
|
| def _build_prompt(self, claim: str, evidence: list[dict]) -> str:
|
| capped = evidence[:MAX_EVIDENCE_SNIPPETS]
|
|
|
| evidence_lines = []
|
| for i, e in enumerate(capped, 1):
|
| source = e.get("source", "unknown")
|
| snippet = e.get("snippet", "").strip()[:MAX_SNIPPET_LENGTH]
|
| rating = e.get("rating")
|
|
|
|
|
| source_label = {
|
| "google_factcheck": "تحقق من الحقائق (Google)",
|
| "wikidata": "ويكيبيديا/بيانات منظمة",
|
| "tavily": "بحث ويب",
|
| }.get(source, source)
|
|
|
| line = f"[{i}] المصدر: {source_label}"
|
| if rating:
|
| line += f"\n ⚑ تقييم رسمي: {rating}"
|
| line += f"\n {snippet}"
|
| evidence_lines.append(line)
|
|
|
| evidence_block = "\n\n".join(
|
| evidence_lines) if evidence_lines else "لا توجد أدلة متاحة."
|
|
|
| return f"""الادعاء المراد التحقق منه:
|
| «{claim}»
|
|
|
| الأدلة المتاحة ({len(capped)} مصدر):
|
| {evidence_block}
|
|
|
| بناءً على الأدلة أعلاه، حدّد الحكم المناسب وبرره بصيغة JSON."""
|
|
|
|
|
|
|
| async def _call_groq(self, user_prompt: str) -> dict | None:
|
| """
|
| Single Groq API call.
|
| Returns parsed dict on success, None on any failure.
|
| """
|
| try:
|
| response = await self.client.chat.completions.create(
|
| model=GROQ_MODEL,
|
| messages=[
|
| {"role": "system", "content": SYSTEM_PROMPT},
|
| {"role": "user", "content": user_prompt},
|
| ],
|
| max_tokens=MAX_TOKENS,
|
| temperature=TEMPERATURE,
|
| )
|
| except Exception as e:
|
| print(f"[Verifier] Groq API error: {type(e).__name__}: {e!r}")
|
| return None
|
|
|
| raw = response.choices[0].message.content or ""
|
| return self._parse_verdict(raw)
|
|
|
|
|
|
|
| def _parse_verdict(self, raw: str) -> dict | None:
|
| """
|
| Parse Groq's response into a structured verdict dict.
|
|
|
| Handles:
|
| - Clean JSON response
|
| - JSON wrapped in ```json ... ``` markdown fences
|
| - JSON embedded inside prose text
|
|
|
| Returns None if parsing fails completely.
|
| """
|
|
|
| clean = re.sub(r"```json\s*", "", raw)
|
| clean = re.sub(r"```\s*", "", clean)
|
| clean = clean.strip()
|
|
|
|
|
| try:
|
| parsed = json.loads(clean)
|
| return self._validate_verdict(parsed)
|
| except json.JSONDecodeError:
|
| pass
|
|
|
|
|
| match = re.search(r"\{.*?\}", clean, re.DOTALL)
|
| if match:
|
| try:
|
| parsed = json.loads(match.group())
|
| return self._validate_verdict(parsed)
|
| except json.JSONDecodeError:
|
| pass
|
|
|
| print(f"[Verifier] Could not parse JSON from response: {raw[:200]}")
|
| return None
|
|
|
| def _validate_verdict(self, parsed: dict) -> dict:
|
| """
|
| Validate and normalize the parsed verdict dict.
|
| Ensures all required fields exist with correct types.
|
| """
|
| valid_verdicts = {"SUPPORTED", "REFUTED",
|
| "PARTIALLY_TRUE", "UNVERIFIABLE"}
|
|
|
| verdict = parsed.get("verdict", "UNVERIFIABLE").upper().strip()
|
| if verdict not in valid_verdicts:
|
| verdict = "UNVERIFIABLE"
|
|
|
| try:
|
| confidence = float(parsed.get("confidence", 0.0))
|
| confidence = max(0.0, min(1.0, confidence))
|
| except (TypeError, ValueError):
|
| confidence = 0.0
|
|
|
| reasoning = str(parsed.get("reasoning", "")).strip()
|
| if not reasoning:
|
| reasoning = UNVERIFIABLE_DEFAULT["reasoning"]
|
|
|
| return {
|
| "verdict": verdict,
|
| "confidence": confidence,
|
| "reasoning": reasoning,
|
| }
|
|
|
|
|
|
|
| def _strip_context_tag(self, claim: str) -> str:
|
| """Remove [سياق: ...] prefix if present."""
|
| if claim.startswith("[سياق:"):
|
| match = re.match(r"\[سياق:\s*.+?\]\s*(.+)", claim, re.DOTALL)
|
| if match:
|
| return match.group(1).strip()
|
| return claim.strip()
|
|
|