Verifier-Service / agents /claim_extractor.py
ArabicNewsAnalyzer's picture
Upload 17 files
51d2734 verified
Raw
History Blame Contribute Delete
6.07 kB
import asyncio
import json
import logging
import os
import re
from groq import AsyncGroq
from dotenv import load_dotenv
load_dotenv()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
GROQ_MODEL = "llama-3.3-70b-versatile"
MAX_TOKENS = 1024
TEMPERATURE = 0.0
MAX_RETRIES = 2
SYSTEM_PROMPT = """أنت نظام متخصص في استخراج الادعاءات الإخبارية القابلة للتحقق من النصوص العربية.
━━━ تعريف الادعاء القابل للتحقق ━━━
الادعاء القابل للتحقق هو جملة تحتوي على:
- حدث أو فعل موصوف (اجتماع، إعلان، تسجيل أرقام، فوز، وفاة، توقيع، إلخ)
- جهة فاعلة محددة (شخص، منظمة، دولة، وزارة، إلخ) أو رقم قابل للقياس
━━━ ما يجب استخراجه ━━━
- الادعاءات الواقعية: أحداث، أرقام، نتائج، تصريحات منسوبة لجهة محددة
- الادعاءات الرقمية: "سُجّلت 300 إصابة"، "ارتفع المؤشر بنسبة 5%"
- نتائج الأحداث: فوز، خسارة، توقيع، إلغاء، إطلاق سراح
━━━ ما يجب تجاهله ━━━
- الآراء والتحليلات: "يرى المحللون أن..."، "يُتوقع أن..."
- التعليقات المبهمة: "الوضع صعب ومعقد"
- الخلفية الوصفية التي لا تحتوي فعلاً محدداً
- الجمل التمهيدية والختامية الإخبارية
━━━ قاعدة الاكتفاء الذاتي (مهمة جداً) ━━━
كل ادعاء يجب أن يكون مفهوماً تماماً بمعزل عن النص الأصلي.
إذا كانت الجملة تستخدم ضميراً أو فعلاً مضمراً (وأضاف، وأضافت، وتابع، وأكد، وأشار...),
استبدل الضمير باسم الجهة الفاعلة الصريح المذكور في السياق.
مثال:
النص: "أعلنت إدارة النادي توقيع مبابي، وأضافت أن التقديم سيكون في برنابيو"
❌ خطأ: "وأضافت أن التقديم سيكون في برنابيو"
✓ صحيح: "أعلنت إدارة النادي أن التقديم الرسمي لمبابي سيكون في ملعب سانتياغو برنابيو"
━━━ قواعد الإخراج ━━━
1. أجب فقط بـ JSON صالح — لا نص قبله أو بعده، لا علامات markdown
2. كل ادعاء جملة عربية كاملة ومستقلة مع فاعل صريح دائماً
3. يمكنك إعادة صياغة الجملة فقط لاستبدال الضمير بالاسم الصريح — لا تضف معلومات جديدة
4. إذا لم يوجد أي ادعاء قابل للتحقق، أعد قائمة فارغة
شكل الإجابة (JSON فقط):
{
"claims": [
"الادعاء الأول مع فاعل صريح",
"الادعاء الثاني مع فاعل صريح",
"الادعاء الثالث مع فاعل صريح"
]
}"""
class ClaimExtractor:
def __init__(self):
api_key = os.environ.get("GROQ_API_KEY2")
if not api_key:
logger.warning(
"GROQ_API_KEY2 not set — ClaimExtractor will fail at runtime.")
self.client = AsyncGroq(api_key=api_key)
async def run(self, text: str) -> list[str]:
if not text or not text.strip():
return []
result = await self._call_groq(text)
if result is not None:
return result
# Retry once
logger.warning(
"[ClaimExtractor] First attempt failed — retrying once.")
result = await self._call_groq(text)
if result is not None:
return result
logger.error(
"[ClaimExtractor] Both attempts failed — returning empty list.")
return []
async def _call_groq(self, text: str) -> list[str] | None:
try:
response = await self.client.chat.completions.create(
model=GROQ_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"استخرج الادعاءات القابلة للتحقق من النص التالي:\n\n{text.strip()}"},
],
max_tokens=MAX_TOKENS,
temperature=TEMPERATURE,
)
except Exception as e:
logger.error(f"[ClaimExtractor] Groq API error: {e}")
return None
raw = response.choices[0].message.content or ""
return self._parse_response(raw)
def _parse_response(self, raw: str) -> list[str] | None:
clean = re.sub(r"```json\s*", "", raw)
clean = re.sub(r"```\s*", "", clean)
clean = clean.strip()
try:
parsed = json.loads(clean)
return self._validate_claims(parsed)
except json.JSONDecodeError:
pass
match = re.search(r"\{.*\}", clean, re.DOTALL)
if match:
try:
parsed = json.loads(match.group())
return self._validate_claims(parsed)
except json.JSONDecodeError:
pass
logger.warning(f"[ClaimExtractor] Could not parse JSON: {raw[:200]}")
return None
def _validate_claims(self, parsed: dict) -> list[str]:
claims = parsed.get("claims", [])
if not isinstance(claims, list):
return []
validated = []
for claim in claims:
if isinstance(claim, str):
claim = claim.strip()
if claim:
validated.append(claim)
return validated