| 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
|
|
|
|
|
| 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
|
|
|