Spaces:
Running on Zero
feat: add modular generic retrieval and evidence synthesis pipeline
Browse filesImplemented HUDA-Net v37.0.0 modular generic architecture.
Key changes:
- Split query analysis, evidence processing, compatibility checks, consensus detection, ranking, and answer synthesis into separate modules.
- Added generic intent and constraint matching without question-specific hardcoded answers.
- Added logical evidence gates to prevent unrelated high-scoring results from building the final answer.
- Added multi-source answer synthesis with source attribution.
- Added automatic conflict detection between rulings and evidence clusters.
- Added detailed evidence inspection while keeping the final user-facing answer clear and professional.
- Added external JSON resources for semantic rules, evidence schema, ranking configuration, and answer templates.
- Added generic pipeline tests and validation reports.
- Updated Arabic dialect normalization and retrieval rules.
- Isolated conversation history from current-query retrieval.
- DEPLOY_HUDANET_V37.md +9 -0
- app.py +313 -502
- hudanet_core/README.md +28 -0
- hudanet_core/__init__.py +4 -0
- hudanet_core/compatibility.py +170 -0
- hudanet_core/consensus.py +121 -0
- hudanet_core/evidence.py +78 -0
- hudanet_core/pipeline.py +121 -0
- hudanet_core/query.py +86 -0
- hudanet_core/resources.py +68 -0
- hudanet_core/resources/answer_templates.json +51 -0
- hudanet_core/resources/evidence_schema.json +42 -0
- hudanet_core/resources/ranking_config.json +48 -0
- hudanet_core/resources/semantic_rules.json +1142 -0
- hudanet_core/synthesis.py +179 -0
- hudanet_core/tests/test_generic_pipeline.py +84 -0
- hudanet_core/text.py +143 -0
- hudanet_core/types.py +79 -0
- hudanet_retrieval_rules.json +4 -328
- hudanet_v37_generic_tests.json +62 -0
- hudanet_v37_manifest.json +124 -0
- hudanet_v37_validation_report.json +88 -0
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploy HUDA-Net v37.0.0
|
| 2 |
+
|
| 3 |
+
1. Upload `app.py`, `hudanet_dialects.json`, `hudanet_retrieval_rules.json`, and the entire `hudanet_core` directory to the Space root.
|
| 4 |
+
2. Preserve the directory structure exactly. The JSON files inside `hudanet_core/resources` are required at startup.
|
| 5 |
+
3. Keep the existing read-only `HF_TOKEN` Space secret.
|
| 6 |
+
4. Run a Factory reboot.
|
| 7 |
+
5. Confirm that the startup log reports HUDA-Net v37.0.0 and that the generic evidence self-test passes.
|
| 8 |
+
|
| 9 |
+
The old named-intent answer rules are intentionally removed. `hudanet_retrieval_rules.json` now contains an empty compatibility table; all decisions come from the generic modular library.
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
-
HUDA-Net
|
| 4 |
=============================
|
| 5 |
Deploy this file as app.py in a Hugging Face Space and add HF_TOKEN as a
|
| 6 |
read-only Space secret.
|
|
@@ -28,6 +28,8 @@ from scipy import sparse
|
|
| 28 |
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 29 |
import joblib
|
| 30 |
|
|
|
|
|
|
|
| 31 |
import json
|
| 32 |
from pathlib import Path
|
| 33 |
|
|
@@ -39,53 +41,85 @@ except Exception:
|
|
| 39 |
_DIALECTS = {"interrogative_particles": [], "clitic_exceptions": [],
|
| 40 |
"dialect_phrases": {}, "spelling_variants": {}, "en_spelling_variants": {}}
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
_CLITIC_EXCEPTIONS_SET = set(_DIALECTS.get("clitic_exceptions", []))
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
def _apply_dialect_normalization(s: str) -> str:
|
| 55 |
-
"""
|
| 56 |
-
حل جينيرك واحد يغطي:
|
| 57 |
-
1. استبدال العبارات اللهجية بالفصحى
|
| 58 |
-
2. فصل أدوات الاستفهام الملتصقة
|
| 59 |
-
3. تصحيح الأخطاء الإملائية الشائعة
|
| 60 |
-
|
| 61 |
-
يُستدعى من norm_ar_ui و norm_ar. لا يحتاج تغيير في أي مكان آخر.
|
| 62 |
-
"""
|
| 63 |
-
# ① استبدال العبارات اللهجية (الأطول أولاً لتجنب التطابق الجزئي)
|
| 64 |
-
for dialect, fusha in sorted(
|
| 65 |
-
_DIALECTS.get("dialect_phrases", {}).items(), key=lambda x: len(x[0]), reverse=True
|
| 66 |
-
):
|
| 67 |
-
s = s.replace(dialect, fusha)
|
| 68 |
-
|
| 69 |
-
# ② فصل أدوات الاستفهام الملتصقة (ماحكم → ما حكم)
|
| 70 |
-
if _CLITIC_PATTERN:
|
| 71 |
-
def _split_clitic(m):
|
| 72 |
-
particle = m.group(1)
|
| 73 |
-
# لا تفصل إذا الكلمة الكاملة استثناء (ماذا، هلال، إلخ)
|
| 74 |
-
# نشوف الكلمة الكاملة بعد الأداة
|
| 75 |
-
start = m.end()
|
| 76 |
-
rest = s[start:start + 12] # أقصى طول كلمة عربية تقريباً
|
| 77 |
-
word_end = re.match(r'[^\s]*', rest)
|
| 78 |
-
full_word = particle + (word_end.group() if word_end else "")
|
| 79 |
-
if full_word in _CLITIC_EXCEPTIONS_SET:
|
| 80 |
-
return m.group(0) # لا تفصل
|
| 81 |
-
return particle + ' '
|
| 82 |
-
s = _CLITIC_PATTERN.sub(_split_clitic, s)
|
| 83 |
-
|
| 84 |
-
# ③ تصحيح الأخطاء الإملائية
|
| 85 |
-
for wrong, correct in _DIALECTS.get("spelling_variants", {}).items():
|
| 86 |
-
s = s.replace(wrong, correct)
|
| 87 |
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
try:
|
| 91 |
from IPython.display import display
|
|
@@ -150,7 +184,7 @@ def _zerogpu_registration_only():
|
|
| 150 |
|
| 151 |
_download_hudanet_private_datasets()
|
| 152 |
|
| 153 |
-
VERSION = "
|
| 154 |
CONFIG = {
|
| 155 |
"INPUT_ROOT": "/kaggle/input",
|
| 156 |
"WORK_ROOT": "/tmp/hudanet_v27",
|
|
@@ -596,7 +630,7 @@ def norm_ar_base(v: Any) -> str:
|
|
| 596 |
def norm_en(v: Any) -> str:
|
| 597 |
s = unicodedata.normalize("NFKC", clean_display(v)).casefold()
|
| 598 |
s = re.sub(r"[^a-z0-9\s'-]", " ", s)
|
| 599 |
-
for a,b in EN_REPL.items(): s = re.sub(rf"\b{re.escape(a)}\b", b, s)
|
| 600 |
return _SPACE.sub(" ", s).strip()
|
| 601 |
|
| 602 |
def has_ar(v: Any) -> bool: return bool(_AR.search(str(v or "")))
|
|
@@ -2922,6 +2956,7 @@ def smart_all_in_one() -> Dict[str, Any]:
|
|
| 2922 |
|
| 2923 |
|
| 2924 |
HUDANET_SMART_RESULT = smart_all_in_one()
|
|
|
|
| 2925 |
|
| 2926 |
|
| 2927 |
# ======================== PROFESSIONAL BILINGUAL UI ========================
|
|
@@ -2939,7 +2974,7 @@ import pandas as pd
|
|
| 2939 |
from scipy import sparse
|
| 2940 |
import joblib
|
| 2941 |
|
| 2942 |
-
UI_VERSION = "
|
| 2943 |
UI_CONFIG = {
|
| 2944 |
"INPUT_ROOT": "/kaggle/input",
|
| 2945 |
"RUNTIME_DATASET_SLUG": "hudanet-bilingual-certified-runtime",
|
|
@@ -3218,7 +3253,7 @@ def normalize_filter_payload(
|
|
| 3218 |
"sort_by": sort_value if sort_value in FILTER_SORT_VALUES else "relevance",
|
| 3219 |
"evidence_count": count,
|
| 3220 |
"min_score": round(score, 3),
|
| 3221 |
-
"use_context":
|
| 3222 |
"compare": _filter_bool(compare, True),
|
| 3223 |
"diverse": _filter_bool(diverse, True),
|
| 3224 |
}
|
|
@@ -3246,7 +3281,7 @@ def norm_ar_ui(v: Any) -> str:
|
|
| 3246 |
def norm_en_ui(v: Any) -> str:
|
| 3247 |
s=unicodedata.normalize("NFKC",clean_ui(v)).casefold()
|
| 3248 |
s=re.sub(r"[^a-z0-9\s'-]"," ",s)
|
| 3249 |
-
for a,b in EN_REPL.items(): s=re.sub(rf"\b{re.escape(a)}\b",b,s)
|
| 3250 |
return _SPACE.sub(" ",s).strip()
|
| 3251 |
|
| 3252 |
|
|
@@ -3497,65 +3532,25 @@ def _contains_any_phrase_ui(normalized_text: str, phrases: Sequence[Any], lang:
|
|
| 3497 |
|
| 3498 |
|
| 3499 |
def extract_case_facts_ui(query:str,lang:str)->dict:
|
| 3500 |
-
|
| 3501 |
-
|
| 3502 |
-
|
| 3503 |
-
|
| 3504 |
-
|
| 3505 |
-
|
| 3506 |
-
|
| 3507 |
-
|
| 3508 |
-
|
| 3509 |
-
|
| 3510 |
-
|
| 3511 |
-
|
| 3512 |
-
|
| 3513 |
-
|
| 3514 |
-
|
| 3515 |
-
|
| 3516 |
-
|
| 3517 |
-
|
| 3518 |
-
|
| 3519 |
-
"ihram":[("No ihram",["without ihram","did not enter ihram"]),("In ihram",["while in ihram","in ihram"]),("After tahallul",["after tahallul"]),("Before tahallul",["before tahallul"])],
|
| 3520 |
-
"time":[("Before dawn",["before dawn"]),("After dawn",["after dawn"]),("Day of sacrifice",["day of sacrifice"]),("Before Arafah",["before arafah"]),("After Arafah",["after arafah"])],
|
| 3521 |
-
"place":[("Miqat",["miqat","meeqat"]),("Makkah",["makkah","haram"]),("Arafah",["arafah","arafat"]),("Mina",["mina"]),("Muzdalifah",["muzdalifah"])],
|
| 3522 |
-
"ability":[("Can return",["can return","able to return"]),("Cannot return",["cannot return","unable to return"])],
|
| 3523 |
-
}
|
| 3524 |
-
for field,choices in maps.items():
|
| 3525 |
-
for value,terms in choices:
|
| 3526 |
-
if _contains_any_phrase_ui(n, terms, lang): facts[field]=value; break
|
| 3527 |
-
action_terms=_explain_tokens(raw,lang)[:6]
|
| 3528 |
-
if action_terms: facts["action"]=" · ".join(action_terms)
|
| 3529 |
-
# Missing details are requested only when they can plausibly change a ruling.
|
| 3530 |
-
if _contains_any_phrase_ui(n, (["ميقات","المواقيت"] if lang=="ar" else ["miqat","meeqat"]), lang) and "ability" not in facts:
|
| 3531 |
-
missing.append("القدرة على الرجوع إلى الميقات" if lang=="ar" else "ability to return to the miqat")
|
| 3532 |
-
if _contains_any_phrase_ui(n, (["نسي","نسيت","ترك","فعل","جاوز","تجاوز"] if lang=="ar" else ["forgot","left","did","passed"]), lang) and "intent" not in facts:
|
| 3533 |
-
missing.append("هل وقع الفعل عمدًا أم نسيانًا أو جهلًا" if lang=="ar" else "whether it was intentional, forgotten, or due to lack of knowledge")
|
| 3534 |
-
if _contains_any_phrase_ui(n, (["شعر","ظفر","عطر","تعطر","استعمال الطيب","لبس","صيد","محظور"] if lang=="ar" else ["hair","nail","perfume","clothing","hunting","prohibition"]), lang) and "ihram" not in facts:
|
| 3535 |
-
missing.append("هل كان الشخص محرمًا وقت الفعل" if lang=="ar" else "whether the person was in ihram at the time")
|
| 3536 |
-
return {"facts":facts,"missing":list(dict.fromkeys(missing)),"labels":CASE_FACT_LABELS[lang]}
|
| 3537 |
-
|
| 3538 |
-
RULING_PATTERNS={
|
| 3539 |
-
"ar":[
|
| 3540 |
-
("pillar",r"\bركن\b"),("condition",r"\bشرط\b"),
|
| 3541 |
-
("obligatory",r"\b(?:واجب|يجب|يلزم|وجوب|لازم)\b"),
|
| 3542 |
-
("prohibited",r"\b(?:حرام|يحرم|محظور)\b|\bلا\s+يجوز\b"),
|
| 3543 |
-
("recommended",r"\b(?:مستحب|سنة|مندوب)\b"),
|
| 3544 |
-
("disliked",r"\b(?:مكروه|كراهة)\b"),
|
| 3545 |
-
("permissible",r"\b(?:جائز|يجوز|مباح)\b"),
|
| 3546 |
-
("remedy",r"\b(?:دم|فدية|كفارة)\b"),
|
| 3547 |
-
],
|
| 3548 |
-
"en":[
|
| 3549 |
-
("pillar",r"\bpillar\b"),("condition",r"\bcondition\b"),
|
| 3550 |
-
("obligatory",r"\b(?:obligatory|required|must)\b"),
|
| 3551 |
-
("prohibited",r"\b(?:prohibited|forbidden)\b|\bnot\s+permissible\b"),
|
| 3552 |
-
("recommended",r"\b(?:recommended|sunnah)\b"),
|
| 3553 |
-
("disliked",r"\b(?:disliked|makruh)\b"),
|
| 3554 |
-
("permissible",r"\b(?:permissible|allowed)\b"),
|
| 3555 |
-
("remedy",r"\b(?:fidyah|sacrifice|expiation)\b"),
|
| 3556 |
-
],
|
| 3557 |
-
}
|
| 3558 |
-
INCOMPATIBLE={frozenset(("prohibited","permissible")),frozenset(("obligatory","recommended")),frozenset(("pillar","recommended")),frozenset(("condition","recommended"))}
|
| 3559 |
|
| 3560 |
def canonical_ruling_ui(text:str,lang:str)->list[str]:
|
| 3561 |
n=norm_ar_ui(text) if lang=="ar" else norm_en_ui(text)
|
|
@@ -3571,6 +3566,8 @@ RULING_FILTER_LABELS = {
|
|
| 3571 |
"ar": {
|
| 3572 |
"pillar": "ركن",
|
| 3573 |
"condition": "شرط",
|
|
|
|
|
|
|
| 3574 |
"obligatory": "واجب أو لازم",
|
| 3575 |
"prohibited": "محرم أو غير جائز",
|
| 3576 |
"recommended": "مستحب أو سنة",
|
|
@@ -3582,6 +3579,8 @@ RULING_FILTER_LABELS = {
|
|
| 3582 |
"en": {
|
| 3583 |
"pillar": "Pillar",
|
| 3584 |
"condition": "Condition",
|
|
|
|
|
|
|
| 3585 |
"obligatory": "Obligatory or required",
|
| 3586 |
"prohibited": "Prohibited",
|
| 3587 |
"recommended": "Recommended or Sunnah",
|
|
@@ -3595,59 +3594,76 @@ RULING_FILTER_KEYS = tuple(RULING_FILTER_LABELS["en"].keys())
|
|
| 3595 |
|
| 3596 |
|
| 3597 |
|
| 3598 |
-
def analyze_direct_answer_intent_ui(query: Any, lang: str) -> dict:
|
| 3599 |
-
"""Detect the exact requested act, timing, and consequence before ranking sources."""
|
| 3600 |
-
qn = norm_ar_ui(query) if lang == "ar" else norm_en_ui(query)
|
| 3601 |
-
ar = lang == "ar"
|
| 3602 |
|
| 3603 |
-
|
| 3604 |
-
|
| 3605 |
-
|
| 3606 |
-
|
| 3607 |
-
|
| 3608 |
-
|
| 3609 |
-
|
| 3610 |
-
|
| 3611 |
-
|
| 3612 |
-
|
| 3613 |
-
|
| 3614 |
-
|
| 3615 |
-
|
| 3616 |
-
|
| 3617 |
-
|
| 3618 |
-
|
| 3619 |
-
|
| 3620 |
-
|
| 3621 |
-
|
| 3622 |
-
|
| 3623 |
-
|
| 3624 |
-
|
| 3625 |
-
|
| 3626 |
-
|
| 3627 |
-
|
| 3628 |
-
|
| 3629 |
-
|
| 3630 |
-
|
| 3631 |
-
|
| 3632 |
-
),
|
| 3633 |
-
"jamarat_before_zawal": (
|
| 3634 |
-
("رمي" in qn and ("جمرات" in qn or "الجمرات" in qn) and "زوال" in qn)
|
| 3635 |
-
) if ar else (
|
| 3636 |
-
("stoning" in qn or "jamarat" in qn) and any(x in qn for x in ("before zawal", "before midday", "before noon"))
|
| 3637 |
-
),
|
| 3638 |
-
"ihsar_prevented_hajj": (
|
| 3639 |
-
any(x in qn for x in ("احصر", "الاحصار", "منع من اتمام الحج", "منع من اكمال الحج", "تعذر عليه اتمام الحج"))
|
| 3640 |
-
) if ar else (
|
| 3641 |
-
"hajj" in qn and any(x in qn for x in ("prevented from completing", "prevented from finishing", "obstructed from completing", "ihsar", "blocked from completing"))
|
| 3642 |
-
),
|
| 3643 |
-
}
|
| 3644 |
-
intents["requires_operational_answer"] = any(
|
| 3645 |
-
value for key, value in intents.items()
|
| 3646 |
-
if key not in {"miqat_after_makkah", "first_tahallul"}
|
| 3647 |
)
|
| 3648 |
-
return intents
|
| 3649 |
|
| 3650 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3651 |
def _direct_source_text_ui(source: Mapping[str,Any], lang: str) -> str:
|
| 3652 |
normalizer = norm_ar_ui if lang == "ar" else norm_en_ui
|
| 3653 |
values = []
|
|
@@ -3658,132 +3674,26 @@ def _direct_source_text_ui(source: Mapping[str,Any], lang: str) -> str:
|
|
| 3658 |
return normalizer(" ".join(values))
|
| 3659 |
|
| 3660 |
|
| 3661 |
-
def direct_intent_diagnostics_ui(source: Mapping[str,Any], query: Any, lang: str) -> dict:
|
| 3662 |
-
"""Return exact action/timing/consequence diagnostics for one candidate source."""
|
| 3663 |
-
intents = analyze_direct_answer_intent_ui(query, lang)
|
| 3664 |
-
if not intents.get("requires_operational_answer"):
|
| 3665 |
-
return {"passed": source_has_usable_answer_ui(source), "required": [], "missing": [], "conflicts": []}
|
| 3666 |
|
| 3667 |
-
|
| 3668 |
-
|
| 3669 |
-
|
| 3670 |
-
|
| 3671 |
-
|
| 3672 |
-
|
| 3673 |
-
|
| 3674 |
-
|
| 3675 |
-
required
|
| 3676 |
-
|
| 3677 |
-
missing.append(label)
|
| 3678 |
-
return passed
|
| 3679 |
-
|
| 3680 |
-
if intents["hair_before_first_tahallul"]:
|
| 3681 |
-
hair = has(("شعر", "حلق", "تقصير", "قص الشعر") if ar else ("hair", "shav", "shorten", "cutting hair"))
|
| 3682 |
-
consequence = has(("محظور", "يحرم", "لا يجوز", "فدية", "دم", "مد", "شاة") if ar else ("prohibited", "not permissible", "fidyah", "sacrifice", "mudd", "dam", "penalty"))
|
| 3683 |
-
intercourse_only = has(("جامع", "جماع", "وطء") if ar else ("intercourse", "sexual relations")) and not hair
|
| 3684 |
-
stoning_only = has(("رمي جمرة", "سبع حصيات") if ar else ("stoning jamrat", "seven pebbles")) and not hair
|
| 3685 |
-
require("hair_act", hair)
|
| 3686 |
-
require("hair_ruling_or_consequence", consequence)
|
| 3687 |
-
if intercourse_only:
|
| 3688 |
-
conflicts.append("intercourse_evidence_for_hair_question")
|
| 3689 |
-
if stoning_only:
|
| 3690 |
-
conflicts.append("stoning_evidence_for_hair_question")
|
| 3691 |
-
|
| 3692 |
-
elif intents["intercourse_before_first_tahallul"]:
|
| 3693 |
-
intercourse = has(("جامع", "جماع", "وطء", "واقع زوجته") if ar else ("intercourse", "sexual relations", "had relations"))
|
| 3694 |
-
before_timing = has(("قبل التحلل", "قبل ان يتحلل", "قبل رمي", "قبل التحلل الاول") if ar else ("before first tahallul", "before the first tahallul", "before release", "before stoning"))
|
| 3695 |
-
consequence_groups = [
|
| 3696 |
-
has(("فسد حجه", "يفسد الحج", "بطل الحج") if ar else ("invalidates hajj", "hajj is invalid", "pilgrimage is invalid")),
|
| 3697 |
-
has(("يمضي في فاسده", "يتم مناسكه", "يمضي في الحج") if ar else ("continue the rites", "complete the rites", "continue hajj")),
|
| 3698 |
-
has(("يقضي", "يحج من قابل", "العام القادم") if ar else ("make it up", "perform hajj the following year", "next year")),
|
| 3699 |
-
has(("بدنة", "دم", "هدي", "ذبح") if ar else ("camel", "sacrifice", "hady", "slaughter")),
|
| 3700 |
-
]
|
| 3701 |
-
consequence_count = sum(bool(x) for x in consequence_groups)
|
| 3702 |
-
stoning_only = has(("ترمى بسبع", "سبع حصيات", "رمي جمرة العقبة") if ar else ("seven pebbles", "stoning jamrat al-aqabah")) and not intercourse
|
| 3703 |
-
require("intercourse_act", intercourse)
|
| 3704 |
-
require("before_first_tahallul_timing", before_timing or consequence_groups[0])
|
| 3705 |
-
require("principal_consequences", consequence_count >= 2)
|
| 3706 |
-
if stoning_only:
|
| 3707 |
-
conflicts.append("stoning_instructions_for_intercourse_question")
|
| 3708 |
-
|
| 3709 |
-
elif intents["generic_first_tahallul"]:
|
| 3710 |
-
hajj_context = has(("الحج", "يوم النحر", "ينحر", "نحر", "هدي", "حل له كل شيء إلا النساء") if ar else ("hajj", "day of sacrifice", "sacrifice", "slaughter", "hady", "everything except women"))
|
| 3711 |
-
acts = sum(1 for token in (("رمي", "حلق", "تقصير", "طواف الافاضة", "ينحر", "نحر") if ar else ("stoning", "shaving", "shortening", "tawaf al-ifadah", "sacrifice", "slaughter")) if token in text)
|
| 3712 |
-
umrah_only = has(("عمرة", "سعي") if ar else ("umrah", "sai")) and not has(("يوم النحر",) if ar else ("day of sacrifice",))
|
| 3713 |
-
require("hajj_context", hajj_context)
|
| 3714 |
-
require("first_tahallul_act", acts >= 1)
|
| 3715 |
-
if umrah_only:
|
| 3716 |
-
conflicts.append("umrah_release_not_hajj_first_tahallul")
|
| 3717 |
-
|
| 3718 |
-
if intents["missed_arafah"]:
|
| 3719 |
-
require("exit_by_umrah", has(("عمرة",) if ar else ("umrah",)))
|
| 3720 |
-
require("repeat_hajj", has(("قابل", "العام القادم", "يقضي") if ar else ("following year", "next year", "make up", "performs hajj")))
|
| 3721 |
-
require("sacrifice", has(("دم", "هدي", "ذبح") if ar else ("sacrifice", "hady", "slaughter")))
|
| 3722 |
-
|
| 3723 |
-
if intents["arafah_end_time"]:
|
| 3724 |
-
require("explicit_arafah_end_time", has(("فجر يوم النحر", "طلوع الفجر من يوم النحر", "الى الفجر", "حتى الفجر") if ar else ("dawn of the day of sacrifice", "until dawn", "fajr of the day of sacrifice")))
|
| 3725 |
-
|
| 3726 |
-
if intents["jamrat_start_time"]:
|
| 3727 |
-
act = has(("رمي جمرة العقبة", "رمي العقبة") if ar else ("stoning jamrat al-aqabah", "stone jamrat al-aqabah", "aqabah stoning"))
|
| 3728 |
-
time = has(("نصف الليل", "بعد الفجر", "طلوع الشمس", "يبدأ وقت") if ar else ("midnight", "after dawn", "sunrise", "stoning time begins", "begins at"))
|
| 3729 |
-
require("jamrat_al_aqabah_act", act)
|
| 3730 |
-
require("explicit_start_time", time)
|
| 3731 |
-
|
| 3732 |
-
if intents["miqat_remedy"]:
|
| 3733 |
-
require("miqat_remedy", has(("يرجع", "الرجوع", "يعود", "دم", "ذبح") if ar else ("return", "go back", "sacrifice", "slaughter")))
|
| 3734 |
-
|
| 3735 |
-
if intents["tawaf_wada_before_leave"]:
|
| 3736 |
-
require("tawaf_before_departure", has(("قبل الخروج", "قبل ان يخرج", "آخر عهده", "يطوف للوداع") if ar else ("before leaving", "before departure", "last act", "bids farewell")))
|
| 3737 |
-
|
| 3738 |
-
if intents["tawaf_doubt"]:
|
| 3739 |
-
require("build_on_certainty", has(("اليقين", "الاقل", "يبني على", "اقل العدد") if ar else ("certainty", "lesser number", "lower number", "builds on")))
|
| 3740 |
-
|
| 3741 |
-
if intents["menstruating_tawaf_necessity"]:
|
| 3742 |
-
require("operational_necessity_ruling", has(("تتحفظ", "التحفظ", "تطوف للضرورة", "تعذر التأخر", "تعذر الرجوع") if ar else ("take protective measures", "protect herself", "perform tawaf out of necessity", "cannot wait", "cannot return")))
|
| 3743 |
-
|
| 3744 |
-
if intents["tamattu_sacrifice"]:
|
| 3745 |
-
obligation = has(("يجب", "واجب", "يلزمه هدي", "عليه هدي") if ar else ("obligatory", "must offer", "owes a sacrifice", "required"))
|
| 3746 |
-
exception_only = has(("اهل مكة", "اهل الحرم", "لا هدي عليهم") if ar else ("people of makkah", "people of haram", "do not owe a sacrifice")) and not obligation
|
| 3747 |
-
require("general_tamattu_obligation", obligation)
|
| 3748 |
-
if exception_only:
|
| 3749 |
-
conflicts.append("exception_only_without_general_rule")
|
| 3750 |
-
|
| 3751 |
-
if intents["hajj_modes_comparison"]:
|
| 3752 |
-
tamattu = has(("تمتع",) if ar else ("tamattu", "tamatu"))
|
| 3753 |
-
qiran = has(("قران",) if ar else ("qiran",))
|
| 3754 |
-
ifrad = has(("افراد",) if ar else ("ifrad",))
|
| 3755 |
-
definition_hits = sum((
|
| 3756 |
-
has(("عمرة ثم", "يتحلل من العمرة", "عمرة مستقلة ثم حج") if ar else ("umrah then", "exits ihram after umrah", "separate umrah then hajj")),
|
| 3757 |
-
has(("الحج والعمرة باحرام واحد", "جمع الحج والعمرة", "قرن الحج بالعمرة") if ar else ("hajj and umrah in one ihram", "combines hajj and umrah", "one ihram for hajj and umrah")),
|
| 3758 |
-
has(("الحج وحده", "يحرم بالحج فقط") if ar else ("hajj alone", "hajj only")),
|
| 3759 |
-
))
|
| 3760 |
-
require("all_three_hajj_modes", tamattu and qiran and ifrad)
|
| 3761 |
-
require("mode_definitions", definition_hits >= 2)
|
| 3762 |
-
|
| 3763 |
-
if intents["jamarat_before_zawal"]:
|
| 3764 |
-
act = has(("رمي الجمرات", "جمرات التشريق") if ar else ("stoning the jamarat", "jamarat during tashriq", "stoning during the days of tashriq"))
|
| 3765 |
-
direct_rule = has(("لا يجوز", "لا يصح", "قبل الوقت", "بعد الزوال") if ar else ("not permissible", "not valid", "before its time", "after zawal", "after midday"))
|
| 3766 |
-
require("jamarat_tashriq_act", act)
|
| 3767 |
-
require("direct_before_zawal_rule", direct_rule)
|
| 3768 |
-
|
| 3769 |
-
if intents["ihsar_prevented_hajj"]:
|
| 3770 |
-
obstruction = has(("احصر", "الاحصار", "محصر", "منع من اتمام النسك", "منع من الحج") if ar else ("ihsar", "obstructed", "prevented from completing", "prevented from finishing"))
|
| 3771 |
-
remedy = has(("هدي", "ذبح", "يحلق", "يتحلل", "مكانه") if ar else ("sacrifice", "slaughter", "shave", "exit ihram", "release from ihram"))
|
| 3772 |
-
death_or_proxy = has(("مات", "عجز عنه", "يقضى عنه") if ar else ("dies", "died", "becomes unable", "proxy performance", "completed on his behalf")) and not obstruction
|
| 3773 |
-
require("ihsar_or_obstruction", obstruction)
|
| 3774 |
-
require("ihsar_remedy", remedy)
|
| 3775 |
-
if death_or_proxy:
|
| 3776 |
-
conflicts.append("death_or_proxy_record_for_ihsar_question")
|
| 3777 |
-
|
| 3778 |
-
passed = not missing and not conflicts and source_has_usable_answer_ui(source)
|
| 3779 |
return {
|
| 3780 |
-
"passed":
|
| 3781 |
-
"required":
|
| 3782 |
-
"missing":
|
| 3783 |
-
"conflicts":
|
|
|
|
|
|
|
| 3784 |
}
|
| 3785 |
|
| 3786 |
-
|
| 3787 |
def source_satisfies_direct_intent_ui(source: Mapping[str,Any], query: Any, lang: str) -> bool:
|
| 3788 |
return bool(direct_intent_diagnostics_ui(source, query, lang).get("passed"))
|
| 3789 |
|
|
@@ -3813,68 +3723,11 @@ def answer_satisfies_direct_intent_ui(answer: Any, query: Any, lang: str) -> boo
|
|
| 3813 |
|
| 3814 |
|
| 3815 |
def _direct_intent_bonus_ui(source: Mapping[str,Any], query: Any, lang: str) -> float:
|
| 3816 |
-
|
| 3817 |
-
|
| 3818 |
-
return 0.0
|
| 3819 |
-
text = _direct_source_text_ui(source, lang)
|
| 3820 |
-
ar = lang == "ar"
|
| 3821 |
-
diag = direct_intent_diagnostics_ui(source, query, lang)
|
| 3822 |
-
bonus = 1.60 if diag.get("passed") else -2.25
|
| 3823 |
-
if intents["hair_before_first_tahallul"] and any(token in text for token in (("شعر", "حلق", "تقصير", "فدية") if ar else ("hair", "shav", "shorten", "fidyah"))):
|
| 3824 |
-
bonus += 0.85
|
| 3825 |
-
if intents["intercourse_before_first_tahallul"] and any(token in text for token in (("جامع", "جماع", "فسد", "بدنة") if ar else ("intercourse", "invalidates", "camel", "following year"))):
|
| 3826 |
-
bonus += 0.90
|
| 3827 |
-
if intents["missed_arafah"] and any(token in text for token in (("عمرة", "العام القادم", "دم") if ar else ("umrah", "following year", "sacrifice"))):
|
| 3828 |
-
bonus += 0.70
|
| 3829 |
-
if intents["generic_first_tahallul"] and any(token in text for token in (("يوم النحر", "رمي", "حلق") if ar else ("day of sacrifice", "stoning", "shaving"))):
|
| 3830 |
-
bonus += 0.65
|
| 3831 |
-
if intents["miqat_remedy"] and any(token in text for token in (("يرجع", "الرجوع") if ar else ("return", "go back"))):
|
| 3832 |
-
bonus += 0.55
|
| 3833 |
-
if intents["tamattu_sacrifice"] and any(token in text for token in (("يجب", "واجب") if ar else ("obligatory", "must"))):
|
| 3834 |
-
bonus += 0.55
|
| 3835 |
-
if intents["generic_first_tahallul"] and any(token in text for token in (("التحلل الاول", "متى يحصل التحلل الاول") if ar else ("first tahallul", "when first tahallul occurs"))):
|
| 3836 |
-
bonus += 1.10
|
| 3837 |
-
if intents["hajj_modes_comparison"] and all(token in text for token in (("تمتع", "قران", "افراد") if ar else ("tamattu", "qiran", "ifrad"))):
|
| 3838 |
-
bonus += 1.20
|
| 3839 |
-
if intents["jamarat_before_zawal"] and any(token in text for token in (("بعد الزوال", "لا يجوز", "لا يصح") if ar else ("after zawal", "after midday", "not permissible", "not valid"))):
|
| 3840 |
-
bonus += 0.95
|
| 3841 |
-
if intents["ihsar_prevented_hajj"] and any(token in text for token in (("احصار", "محصر", "احصر") if ar else ("ihsar", "obstructed", "prevented from completing"))):
|
| 3842 |
-
bonus += 1.20
|
| 3843 |
-
return bonus
|
| 3844 |
-
|
| 3845 |
|
| 3846 |
def direct_query_expansion_ui(query: Any, lang: str) -> str:
|
| 3847 |
-
|
| 3848 |
-
if lang == "ar":
|
| 3849 |
-
if intents["hair_before_first_tahallul"]: return "ما حكم قص أو حلق الشعر قبل التحلل الأول من الحج وما الفدية أو الدم المترتب؟"
|
| 3850 |
-
if intents["intercourse_before_first_tahallul"]: return "ماذا يلزم من جامع قبل التحلل الأول من الحج من فساد الحج والمضي فيه والقضاء والبدنة؟"
|
| 3851 |
-
if intents["generic_first_tahallul"]: return "متى يحصل التحلل الأول من الحج؟"
|
| 3852 |
-
if intents["hajj_modes_comparison"]: return "ما الفرق بين التمتع والقران والإفراد: تعريف كل نسك وكيفية الإحرام والتحلل والهدي؟"
|
| 3853 |
-
if intents["jamarat_before_zawal"]: return "ما حكم رمي جمرات أيام التشريق قبل الزوال ومتى يبدأ وقت الرمي الصحيح؟"
|
| 3854 |
-
if intents["ihsar_prevented_hajj"]: return "ما حكم المحصر الذي منع من إتمام الحج وما الذي يفعله من الهدي والتحلل؟"
|
| 3855 |
-
if intents["missed_arafah"]: return "ماذا يلزم من فاته الوقوف بعرفة حتى طلع فجر يوم النحر؟"
|
| 3856 |
-
if intents["arafah_end_time"]: return "إلى متى يمتد وقت الوقوف بعرفة ومتى ينتهي؟"
|
| 3857 |
-
if intents["jamrat_start_time"]: return "متى يبدأ وقت رمي جمرة العقبة يوم النحر؟"
|
| 3858 |
-
if intents["tawaf_wada_before_leave"]: return "من نسي طواف الوداع ثم تذكر قبل مغادرة مكة ماذا يفعل؟"
|
| 3859 |
-
if intents["tawaf_doubt"]: return "من شك في عدد أشواط الطواف هل يبني على اليقين وهو الأقل؟"
|
| 3860 |
-
if intents["menstruating_tawaf_necessity"]: return "ما حكم الحائض في طواف الإفاضة عند تعذر الانتظار والرجوع؟"
|
| 3861 |
-
if intents["miqat_remedy"]: return "من تجاوز الميقات بلا إحرام هل يرجع إلى الميقات ومتى يلزمه الدم؟"
|
| 3862 |
-
if intents["tamattu_sacrifice"]: return "هل هدي التمتع واجب على المتمتع من غير حاضري المسجد الحرام؟"
|
| 3863 |
-
else:
|
| 3864 |
-
if intents["hair_before_first_tahallul"]: return "What is the ruling and fidyah for cutting, shaving, or shortening hair before the first Tahallul of Hajj?"
|
| 3865 |
-
if intents["intercourse_before_first_tahallul"]: return "What are the consequences of intercourse before the first Tahallul of Hajj, including invalidation, completion, make-up Hajj, and sacrifice?"
|
| 3866 |
-
if intents["generic_first_tahallul"]: return "When does the first Tahallul of Hajj occur?"
|
| 3867 |
-
if intents["hajj_modes_comparison"]: return "What are the practical differences between Tamattu, Qiran, and Ifrad, including ihram, Umrah, release, and sacrifice?"
|
| 3868 |
-
if intents["jamarat_before_zawal"]: return "Is stoning the Jamarat during the Days of Tashriq valid before zawal, and when does the valid time begin?"
|
| 3869 |
-
if intents["ihsar_prevented_hajj"]: return "What must an obstructed pilgrim prevented from completing Hajj do regarding sacrifice and release from ihram?"
|
| 3870 |
-
if intents["missed_arafah"]: return "What must a pilgrim do after missing the standing at Arafah until dawn of the Day of Sacrifice?"
|
| 3871 |
-
if intents["arafah_end_time"]: return "Until what time does the standing at Arafah remain valid, and when does its time end?"
|
| 3872 |
-
if intents["jamrat_start_time"]: return "At what time does stoning Jamrat al-Aqabah begin on the Day of Sacrifice?"
|
| 3873 |
-
if intents["tawaf_wada_before_leave"]: return "What should a pilgrim do after forgetting Tawaf al-Wada but remembering before leaving Makkah?"
|
| 3874 |
-
if intents["tawaf_doubt"]: return "If a pilgrim doubts the number of Tawaf circuits, should the pilgrim build on certainty and the lesser number?"
|
| 3875 |
-
if intents["menstruating_tawaf_necessity"]: return "What may a menstruating woman do for Tawaf al-Ifadah when she cannot wait or return?"
|
| 3876 |
-
if intents["miqat_remedy"]: return "After passing the miqat without ihram, when must the pilgrim return and when is a sacrifice due?"
|
| 3877 |
-
if intents["tamattu_sacrifice"]: return "Is the Tamattu sacrifice obligatory for a pilgrim who is not a resident of the Sacred Mosque area?"
|
| 3878 |
return ""
|
| 3879 |
|
| 3880 |
def merge_search_results_ui(primary: Mapping[str,Any], extra: Mapping[str,Any]) -> dict:
|
|
@@ -3914,41 +3767,13 @@ def merge_search_results_ui(primary: Mapping[str,Any], extra: Mapping[str,Any])
|
|
| 3914 |
|
| 3915 |
|
| 3916 |
def direct_answer_preface_ui(query: Any, lang: str, support: Sequence[Mapping[str,Any]]) -> str:
|
| 3917 |
-
|
| 3918 |
-
texts = [_direct_source_text_ui(src, lang) for src in support or []]
|
| 3919 |
-
ar = lang == "ar"
|
| 3920 |
-
if intents["tawaf_wada_before_leave"] and any(source_satisfies_direct_intent_ui(src, query, lang) for src in support or []):
|
| 3921 |
-
return ("إذا تذكرت قبل مغادرة مكة، فطف طواف الوداع قبل خروجك واجعله آخر عهدك بالبيت. ما دمت ما زلت في مكة وتستطيع أداءه، فلا تخرج قبل أن تطوفه." if ar else "If you remember before leaving Makkah, perform Tawaf al-Wada before departure and make it your last act at the House. While you are still in Makkah and able to perform it, do not leave before completing it.")
|
| 3922 |
-
if intents["miqat_remedy"]:
|
| 3923 |
-
has_return = any(any(token in text for token in (("يرجع", "الرجوع", "يعود") if ar else ("return", "go back"))) for text in texts)
|
| 3924 |
-
has_sacrifice = any(any(token in text for token in (("دم", "ذبح", "هدي") if ar else ("sacrifice", "slaughter", "hady"))) for text in texts)
|
| 3925 |
-
if has_return and has_sacrifice:
|
| 3926 |
-
return ("إن كنت تستطيع الرجوع إلى الميقات قبل إتمام النسك، فالواجب أن ترجع فتحرم منه. فإن لم ترجع وأحرمت من دونه، فيأتي حكم الدم في فرع عدم الرجوع بحسب الشواهد المسترجعة." if ar else "If you can return to the miqat before completing the rite, return and enter ihram from there. If you do not return and enter ihram from a point after it, the sacrifice ruling applies to that non-return branch according to the retrieved evidence.")
|
| 3927 |
-
if intents["hajj_modes_comparison"]:
|
| 3928 |
-
joined = " ".join(texts)
|
| 3929 |
-
enough = all(token in joined for token in (("تمتع", "قران", "افراد") if ar else ("tamattu", "qiran", "ifrad")))
|
| 3930 |
-
if enough:
|
| 3931 |
-
return ("التمتع: يعتمر أولًا ثم يتحلل من العمرة، ثم يحرم بالحج في عامه. القران: يجمع الحج والعمرة في إحرام واحد ولا يتحلل بينهما. الإفراد: يحرم بالحج وحده. ويشترك التمتع والقران في وجوب الهدي على من استوفى شروطه، بخلاف الإفراد." if ar else "Tamattu: the pilgrim performs Umrah first, exits its ihram, then enters a separate ihram for Hajj in the same year. Qiran: Hajj and Umrah are combined in one ihram without release between them. Ifrad: the pilgrim enters ihram for Hajj alone. Tamattu and Qiran carry the sacrifice requirement when its conditions apply, unlike Ifrad.")
|
| 3932 |
-
if intents["jamarat_before_zawal"]:
|
| 3933 |
-
joined = " ".join(texts)
|
| 3934 |
-
supported = any(token in joined for token in (("بعد الزوال", "قبل الوقت", "لا يجوز", "لا يصح") if ar else ("after zawal", "after midday", "before its time", "not permissible", "not valid")))
|
| 3935 |
-
if supported:
|
| 3936 |
-
return ("لا يصح رمي جمرات أيام التشريق قبل الزوال بحسب الشواهد المسترجعة؛ يبدأ وقتها بعد الزوال." if ar else "According to the retrieved evidence, stoning the Jamarat during the Days of Tashriq is not valid before zawal; its valid time begins after zawal.")
|
| 3937 |
return ""
|
| 3938 |
|
| 3939 |
-
|
| 3940 |
def filter_case_missing_for_query_ui(missing: Sequence[str], query: Any, lang: str) -> list[str]:
|
| 3941 |
-
|
| 3942 |
-
if intents["tawaf_wada_before_leave"] or intents["arafah_end_time"] or intents["jamrat_start_time"] or intents["tawaf_doubt"] or intents["generic_first_tahallul"] or intents["hajj_modes_comparison"] or intents["jamarat_before_zawal"] or intents["ihsar_prevented_hajj"]:
|
| 3943 |
-
return []
|
| 3944 |
-
# v36.4.1 general-obligation intent softening
|
| 3945 |
-
_qn = norm_ar_ui(query) if lang == "ar" else norm_en_ui(query)
|
| 3946 |
-
_general_obligation = bool(re.search(r"ما\s+الواجب\s+على\s+من\s+(?:ترك|فعل|تجاوز|نسي|فات)", _qn)) if lang == "ar" else bool(re.search(r"what\s+(?:is\s+required|must|should)\b.*\b(?:after|if|when)\b.*\b(?:missing|omitting|passing|forgetting|left|leaving)\b", _qn))
|
| 3947 |
-
if _general_obligation:
|
| 3948 |
-
missing = [m for m in (missing or []) if not any(w in m for w in ("عمد", "نسيان", "جهل", "intentional", "forgotten", "knowledge"))]
|
| 3949 |
return list(dict.fromkeys(missing or []))
|
| 3950 |
|
| 3951 |
-
|
| 3952 |
def _answer_source_relevance_ui(source: Mapping[str,Any], query: str, lang: str) -> float:
|
| 3953 |
"""Rank already-supported sources by how directly their answer resolves the request."""
|
| 3954 |
if not source_has_usable_answer_ui(source):
|
|
@@ -4006,49 +3831,54 @@ def _answer_source_relevance_ui(source: Mapping[str,Any], query: str, lang: str)
|
|
| 4006 |
|
| 4007 |
|
| 4008 |
def rank_support_for_answer_ui(support: Sequence[Mapping[str,Any]], query: str, lang: str) -> list[dict]:
|
| 4009 |
-
|
| 4010 |
-
|
| 4011 |
-
|
| 4012 |
|
| 4013 |
def compose_answer_for_filter_preferences_ui(
|
| 4014 |
engine, support: Sequence[Mapping[str,Any]], style: str, lang: str,
|
| 4015 |
compare_sources: bool, consensus: Mapping[str,Any], query: str = "",
|
| 4016 |
) -> str:
|
| 4017 |
-
"""
|
| 4018 |
-
|
| 4019 |
-
|
| 4020 |
-
|
| 4021 |
-
|
| 4022 |
-
return engine._select_answer(ranked[0], style)
|
| 4023 |
-
return compose_multi_source_answer_ui(engine, ranked, style, lang, consensus)
|
| 4024 |
-
|
| 4025 |
|
| 4026 |
-
def analyze_source_consensus_ui(
|
| 4027 |
-
|
| 4028 |
-
|
| 4029 |
-
|
| 4030 |
-
|
| 4031 |
-
|
| 4032 |
-
|
| 4033 |
-
|
| 4034 |
-
|
| 4035 |
-
|
| 4036 |
-
|
| 4037 |
-
|
| 4038 |
-
|
| 4039 |
-
|
| 4040 |
-
|
| 4041 |
-
|
| 4042 |
-
|
| 4043 |
-
|
| 4044 |
-
|
| 4045 |
-
|
| 4046 |
-
|
| 4047 |
-
|
| 4048 |
-
|
| 4049 |
-
|
| 4050 |
-
|
| 4051 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4052 |
|
| 4053 |
def render_case_facts_inline(case:Mapping[str,Any],lang:str)->str:
|
| 4054 |
if not case or not case.get("facts"): return ""
|
|
@@ -5024,7 +4854,7 @@ class ProfessionalEvidenceEngine:
|
|
| 5024 |
@staticmethod
|
| 5025 |
def _cache_key(query: str, lang: str, filters: dict) -> tuple:
|
| 5026 |
stable = json.dumps(filters or {}, ensure_ascii=False, sort_keys=True, default=str)
|
| 5027 |
-
return (clean_ui(query), str(lang), stable)
|
| 5028 |
|
| 5029 |
def _cache_get(self, key):
|
| 5030 |
value = self._search_cache.get(key)
|
|
@@ -5479,19 +5309,19 @@ class ProfessionalEvidenceEngine:
|
|
| 5479 |
return {"answer":prompt,"mode":"broad_query","security":decision,"exact":[],"related":[],"distant":[],"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":original,"case_facts":case,"consensus":{},"stats":{"latency":time.perf_counter()-started,"allowed_records":0,"allowed_books":0,"matched_books":0,"exact_count":0,"related_count":0,"distant_count":0,"neural_search_skipped":True}}
|
| 5480 |
if decision["action"] in messages[lang]:
|
| 5481 |
return {"answer":messages[lang][decision["action"]],"mode":decision["action"],"security":decision,"exact":[],"related":[],"distant":[],"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":clean_ui(query),"case_facts":case,"consensus":{},"stats":{"latency":time.perf_counter()-started,"allowed_records":0,"allowed_books":0,"matched_books":0,"exact_count":0,"related_count":0,"distant_count":0}}
|
|
|
|
|
|
|
| 5482 |
effective=original
|
| 5483 |
-
|
| 5484 |
-
n=norm_ar_ui(original) if ar else norm_en_ui(original)
|
| 5485 |
-
starters=("وماذا","طيب","ولو","واذا","إذا","ماذا عن") if ar else ("what if","and if","how about","then what","what about")
|
| 5486 |
-
if len(n.split())<=8 or any(n.startswith(norm_ar_ui(s) if ar else norm_en_ui(s)) for s in starters):
|
| 5487 |
-
effective=previous_user+"\n"+("سؤال متابعة: " if ar else "Follow-up: ")+original
|
| 5488 |
compact=compact_query_ui(original,lang); parts=split_multi_ui(compact,lang)
|
| 5489 |
if len(parts)>1:
|
| 5490 |
sub=[]
|
| 5491 |
for part in parts:
|
| 5492 |
-
result=self.search(part,lang,filters)
|
|
|
|
|
|
|
| 5493 |
if not best: continue
|
| 5494 |
-
consensus=analyze_source_consensus_ui(result["exact"]+result["related"],lang)
|
| 5495 |
display_consensus=consensus if compare_sources else {}
|
| 5496 |
sub_answer=compose_answer_for_filter_preferences_ui(self,result["exact"]+result["related"],filters.get("answer_style","detailed"),lang,compare_sources,display_consensus,part)
|
| 5497 |
if not usable_output_text_ui(sub_answer):
|
|
@@ -5536,8 +5366,9 @@ class ProfessionalEvidenceEngine:
|
|
| 5536 |
search=merge_search_results_ui(search,expanded_search)
|
| 5537 |
search.setdefault("stats",{})["expanded_query"]=expanded_query
|
| 5538 |
search=annotate_direct_intent_diagnostics_ui(search,intent_query,lang)
|
|
|
|
| 5539 |
support=search["exact"] or search["related"]
|
| 5540 |
-
consensus=analyze_source_consensus_ui(search["exact"]+search["related"],lang)
|
| 5541 |
if not support:
|
| 5542 |
if int(search.get("stats", {}).get("allowed_records", 0) or 0) == 0:
|
| 5543 |
msg=("لا يوجد سجل يطابق اجتماع الفلاتر الحالية. هذا ليس فشلًا في البحث؛ بعض الاختيارات متعارضة. أزل فلترًا واحدًا أو استخدم إعادة الضبط." if ar else "No record matches the current filter intersection. This is not a retrieval failure; some selections conflict. Remove one filter or reset them.")
|
|
@@ -5564,8 +5395,9 @@ class ProfessionalEvidenceEngine:
|
|
| 5564 |
collective_comparison=all(token in combined_text for token in (("تمتع","قران","افراد") if ar else ("tamattu","qiran","ifrad")))
|
| 5565 |
if complete_support:
|
| 5566 |
complete_operational_support=True
|
| 5567 |
-
|
| 5568 |
-
|
|
|
|
| 5569 |
elif collective_comparison:
|
| 5570 |
complete_operational_support=True
|
| 5571 |
else:
|
|
@@ -5573,11 +5405,14 @@ class ProfessionalEvidenceEngine:
|
|
| 5573 |
msg=(("لم أجد في الكتاب أو الكتب المحددة نصًا يجيب عن الفرع العملي المطلوب مباشرة. أزل فلتر الكتاب أو استخدم نطاق التغطية الواسعة." if selected_books else "وجدت شواهد قريبة من الموضوع، لكنها لا تجيب عن الفرع العملي المطلوب مباشرة، لذلك لن أعرض جوابًا ناقصًا." ) if ar else (("The selected book or books do not contain evidence that directly resolves the requested practical branch. Remove the book filter or use Wide Coverage." if selected_books else "Nearby evidence was found, but it does not directly resolve the requested practical branch, so an incomplete answer will not be presented.")))
|
| 5574 |
mode=("selected_books_insufficient" if selected_books else "direct_answer_incomplete")
|
| 5575 |
return {"answer":msg,"mode":mode,"security":decision,**search,"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":effective,"case_facts":case,"consensus":consensus if compare_sources else {},"stats":{**search.get("stats",{}),"latency":time.perf_counter()-started}}
|
| 5576 |
-
primary=ranked_support[0]; direct=float(primary.get("direct_probability",primary.get("score",0)) or 0); agreement=int(primary.get("retriever_agreement",0) or 0)
|
| 5577 |
-
second=max([float(x.get("direct_probability",x.get("score",0)) or 0) for x in ranked_support[1:]]+[0.0]); margin=direct-second
|
| 5578 |
exact_present=bool(search["exact"]); matched_books=int(search.get("stats",{}).get("matched_books",0))
|
| 5579 |
-
|
| 5580 |
-
|
|
|
|
|
|
|
|
|
|
| 5581 |
missing_critical=bool(case.get("missing")) and not exact_present and direct<0.78
|
| 5582 |
if uncertain or conflict_weak:
|
| 5583 |
missing=case.get("missing",[])
|
|
@@ -5588,7 +5423,9 @@ class ProfessionalEvidenceEngine:
|
|
| 5588 |
else:
|
| 5589 |
prompt=("النتائج القريبة متقاربة جدًا في الثقة ولا تكفي لبناء حكم آمن. وضّح الفعل والوقت وحالة الإحرام." if ar else "The closest candidates are too close in confidence to ground a safe ruling. Clarify the act, timing, and ihram status.")
|
| 5590 |
return {"answer":prompt,"mode":"needs_context","security":decision,**search,"confidence":round(direct*100,1),"language":lang,"query":clean_ui(query),"effective_query":effective,"primary":primary,"case_facts":case,"consensus":consensus if compare_sources else {},"uncertainty":{"margin":margin,"agreement":agreement,"reason":"missing_or_conflicting_context"},"stats":{**search.get("stats",{}),"latency":time.perf_counter()-started}}
|
| 5591 |
-
|
|
|
|
|
|
|
| 5592 |
if not usable_output_text_ui(answer):
|
| 5593 |
msg=("وجدت سجلات مرتبطة، لكن نصوص الإجابة المتاحة كانت قوالب ناقصة أو غير مترجمة، لذلك لن أعرضها كحكم. استخدم كتابًا آخر أو أزل الفلاتر." if ar else "Related records were found, but their available answers were incomplete templates or untranslated placeholders, so they will not be presented as a ruling. Try another book or remove the filters.")
|
| 5594 |
return {"answer":msg,"mode":"placeholder_blocked","security":decision,**search,"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":effective,"primary":primary,"case_facts":case,"consensus":consensus if compare_sources else {},"stats":{**search.get("stats",{}),"latency":time.perf_counter()-started}}
|
|
@@ -5632,85 +5469,50 @@ class ProfessionalEvidenceEngine:
|
|
| 5632 |
|
| 5633 |
|
| 5634 |
def validate_answer_quality_v36_4() -> dict:
|
| 5635 |
-
"""
|
| 5636 |
-
|
|
|
|
| 5637 |
def add(name, passed, value=""):
|
| 5638 |
checks.append({"name":name,"passed":bool(passed),"value":value})
|
| 5639 |
|
| 5640 |
-
|
| 5641 |
-
|
| 5642 |
-
|
| 5643 |
-
|
| 5644 |
-
|
| 5645 |
-
|
| 5646 |
-
|
| 5647 |
-
|
| 5648 |
-
|
| 5649 |
-
|
| 5650 |
-
|
| 5651 |
-
|
| 5652 |
-
add("
|
| 5653 |
-
add("
|
| 5654 |
-
|
| 5655 |
-
|
| 5656 |
-
|
| 5657 |
-
|
| 5658 |
-
|
| 5659 |
-
|
| 5660 |
-
|
| 5661 |
-
|
| 5662 |
-
|
| 5663 |
-
|
| 5664 |
-
|
| 5665 |
-
|
| 5666 |
-
|
| 5667 |
-
|
| 5668 |
-
|
| 5669 |
-
add("
|
| 5670 |
-
add("
|
| 5671 |
-
add("
|
| 5672 |
-
add("
|
| 5673 |
-
|
| 5674 |
-
|
| 5675 |
-
add("intercourse_exact_source_passes", source_satisfies_direct_intent_ui(intercourse_source,intercourse_query,"en"))
|
| 5676 |
-
add("stoning_source_rejected_for_intercourse", not source_satisfies_direct_intent_ui(stoning_source,intercourse_query,"en"))
|
| 5677 |
-
add("generic_tahallul_not_intercourse_answer", not source_satisfies_direct_intent_ui(generic_tahallul,intercourse_query,"en"))
|
| 5678 |
-
add("generic_first_tahallul_source_passes", source_satisfies_direct_intent_ui(generic_tahallul,"Which acts establish the first Tahallul of Hajj?","en"))
|
| 5679 |
-
|
| 5680 |
-
missed={"question":"What should someone do if he misses Arafah?","title":"Missing Arafah","chapter":"Fidyah","answer":"Missing Arafah means missing Hajj; he exits through Umrah, performs Hajj the following year, and owes a sacrifice.","evidence":"Whoever misses Arafah exits by Umrah and returns next year with a sacrifice."}
|
| 5681 |
-
add("missed_arafah_complete_source", source_satisfies_direct_intent_ui(missed,"What is requird if a pilgrim misses the standing at Arafah?","en"))
|
| 5682 |
-
|
| 5683 |
-
wrong_tahallul={"question":"Hair in Umrah","title":"Umrah haircut","chapter":"Umrah","answer":"After Tawaf and Sai in Umrah, the pilgrim cuts the hair and exits ihram.","evidence":""}
|
| 5684 |
-
add("umrah_hair_not_first_tahallul", not source_satisfies_direct_intent_ui(wrong_tahallul,hair_query,"en"))
|
| 5685 |
-
|
| 5686 |
-
follow={"question":"Tawaf al-Wada","title":"Farewell Tawaf","chapter":"Departure","answer":"The pilgrim should perform Tawaf al-Wada before departure and make it the last act at the House.","evidence":""}
|
| 5687 |
-
effective="ما حكم من نسي طواف الوداع؟\nسؤال متابعة: طيب وإذا تذكرت قبل أن أغادر مكة؟"
|
| 5688 |
-
add("followup_before_leave_detected", analyze_direct_answer_intent_ui(effective,"ar").get("tawaf_wada_before_leave"))
|
| 5689 |
-
add("followup_source_complete", source_satisfies_direct_intent_ui(follow,"What should a pilgrim who forgot Tawaf al-Wada do if remembered before leaving Makkah?","en"))
|
| 5690 |
-
|
| 5691 |
-
add("conditions_boilerplate_trimmed", usable_output_text_ui("A direct ruling. The ruling is applied with the conditions and qualifications stated in that chapter.")=="A direct ruling")
|
| 5692 |
-
add("record_extraction_template_blocked", usable_output_text_ui("This record was extracted from the Hajj section. The Arabic source excerpt is preserved in the Arabic fields. Source excerpt: text")=="")
|
| 5693 |
-
add("comparison_intent_detected", analyze_direct_answer_intent_ui("What is the difference between Tamattu, Qiran, and Ifrad?","en").get("hajj_modes_comparison"))
|
| 5694 |
-
add("before_zawal_intent_detected", analyze_direct_answer_intent_ui("ما حكم رمي الجمرات قبل الزوال في أيام التشريق؟","ar").get("jamarat_before_zawal"))
|
| 5695 |
-
add("ihsar_intent_detected", analyze_direct_answer_intent_ui("What is the ruling when a pilgrim is prevented from completing Hajj?","en").get("ihsar_prevented_hajj"))
|
| 5696 |
-
ihsar_source={"question":"Ihsar","title":"Obstructed pilgrim","chapter":"Obstruction","answer":"An obstructed pilgrim offers the required sacrifice and exits ihram.","evidence":"A pilgrim prevented from completing Hajj slaughters the sacrifice and releases from ihram."}
|
| 5697 |
-
proxy_source={"question":"Died during Hajj","title":"Proxy completion","chapter":"Ability","answer":"The remaining rites are completed on behalf of one who died or became unable.","evidence":""}
|
| 5698 |
-
add("ihsar_source_passes", source_satisfies_direct_intent_ui(ihsar_source,"What must a pilgrim prevented from completing Hajj do?","en"))
|
| 5699 |
-
add("proxy_source_rejected_for_ihsar", not source_satisfies_direct_intent_ui(proxy_source,"What must a pilgrim prevented from completing Hajj do?","en"))
|
| 5700 |
-
|
| 5701 |
-
consensus=analyze_source_consensus_ui([
|
| 5702 |
-
{"book_id":"one","book":"One","ruling":"Obligatory","answer":""},
|
| 5703 |
-
{"book_id":"one","book":"One","ruling":"Obligatory","answer":""},
|
| 5704 |
-
{"book_id":"one","book":"One","ruling":"Obligatory","answer":""},
|
| 5705 |
-
],"en")
|
| 5706 |
-
add("consensus_never_above_one", 0.0 <= float(consensus.get("agreement_ratio",0)) <= 1.0, consensus)
|
| 5707 |
-
rendered=render_consensus_inline({"state":"agreement","books":1,"agreement_ratio":2.0},"en")
|
| 5708 |
-
add("rendered_consensus_clamped", "200%" not in rendered and "100%" in rendered, rendered)
|
| 5709 |
-
|
| 5710 |
-
failed=[x for x in checks if not x["passed"]]
|
| 5711 |
if failed:
|
| 5712 |
-
raise RuntimeError("HUDA-Net
|
| 5713 |
-
print(f"✅ HUDA-Net
|
| 5714 |
return {"passed":True,"tested":len(checks),"checks":checks}
|
| 5715 |
|
| 5716 |
def validate_specificity_guard_v33(engine: ProfessionalEvidenceEngine) -> dict:
|
|
@@ -5981,6 +5783,15 @@ def tier_card(source:dict,lang:str,number:int,user_query:str="") -> str:
|
|
| 5981 |
direct_rejection=clean_ui(source.get("direct_intent_rejection",""))
|
| 5982 |
if direct_rejection:
|
| 5983 |
reason += ((" · سبب رفضه للجواب المباشر: " if ar else " · Direct-answer rejection: ")+direct_rejection)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5984 |
score=max(0.0,min(1.0,float(source.get("score",0) or 0))); score_pct=score*100
|
| 5985 |
is_quran=bool(ar and re.search(r'[﴿﷽]|قال الله|قوله تعالى',evidence))
|
| 5986 |
citation_class="quran-ayah" if is_quran else ("citation-text" if ar else "source-text-en")
|
|
@@ -9250,8 +9061,8 @@ def create_professional_app():
|
|
| 9250 |
ar_kinds=gr.CheckboxGroup(ar_opts["source_kinds"],value=[],label="مصدر السجل")
|
| 9251 |
|
| 9252 |
with gr.Accordion("خيارات متقدمة",open=False):
|
| 9253 |
-
gr.HTML(field_guide("
|
| 9254 |
-
ar_context=gr.Checkbox(value=
|
| 9255 |
gr.HTML(field_guide("مقارنة صيغ الأحكام","يعرض تنبيهًا عند اختلاف صياغة الحكم بين أقرب المصادر.","ar"))
|
| 9256 |
ar_compare=gr.Checkbox(value=True,label="قارن صيغ الأحكام بين المصادر")
|
| 9257 |
gr.HTML(field_guide("تمثيل جميع الكتب","يحتفظ المحرك بأفضل شاهد آمن من كل كتاب مسموح. هذا الضمان ثابت حتى لا يختفي أي كتاب بسبب ترتيب النتائج.","ar"))
|
|
@@ -9340,8 +9151,8 @@ def create_professional_app():
|
|
| 9340 |
en_kinds=gr.CheckboxGroup(en_opts["source_kinds"],value=[],label="Record source")
|
| 9341 |
|
| 9342 |
with gr.Accordion("Advanced options",open=False):
|
| 9343 |
-
gr.HTML(field_guide("Conversation context","
|
| 9344 |
-
en_context=gr.Checkbox(value=
|
| 9345 |
gr.HTML(field_guide("Compare ruling formulations","Shows a note when the closest sources use different ruling formulations.","en"))
|
| 9346 |
en_compare=gr.Checkbox(value=True,label="Compare ruling formulations across sources")
|
| 9347 |
gr.HTML(field_guide("Represent every book","Keeps the best safe item from every allowed book, even when its relevance is only distant.","en"))
|
|
@@ -9429,7 +9240,7 @@ def create_professional_app():
|
|
| 9429 |
|
| 9430 |
reset_outputs_ar=[ar_books,ar_authors,ar_types,ar_madhhabs,ar_categories,ar_rulings,ar_kinds,ar_style,ar_mode,ar_sort,ar_count,ar_min,ar_context,ar_compare,ar_diverse]
|
| 9431 |
reset_outputs_en=[en_books,en_authors,en_types,en_madhhabs,en_categories,en_rulings,en_kinds,en_style,en_mode,en_sort,en_count,en_min,en_context,en_compare,en_diverse]
|
| 9432 |
-
defaults=([],[],[],[],[],[],[],"detailed","balanced","relevance",1,0,
|
| 9433 |
ar_reset_event=ar_reset.click(lambda:defaults,None,reset_outputs_ar,queue=False)
|
| 9434 |
en_reset_event=en_reset.click(lambda:defaults,None,reset_outputs_en,queue=False)
|
| 9435 |
for event in (ar_reset_event,en_reset_event):
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
+
HUDA-Net Modular Generic Evidence Library v37.0.0 Academic Integrated — Gradio Stable
|
| 4 |
=============================
|
| 5 |
Deploy this file as app.py in a Hugging Face Space and add HF_TOKEN as a
|
| 6 |
read-only Space secret.
|
|
|
|
| 28 |
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 29 |
import joblib
|
| 30 |
|
| 31 |
+
from hudanet_core import GenericEvidencePipeline
|
| 32 |
+
|
| 33 |
import json
|
| 34 |
from pathlib import Path
|
| 35 |
|
|
|
|
| 41 |
_DIALECTS = {"interrogative_particles": [], "clitic_exceptions": [],
|
| 42 |
"dialect_phrases": {}, "spelling_variants": {}, "en_spelling_variants": {}}
|
| 43 |
|
| 44 |
+
# External retrieval policy. Keep scenario/ranking rules out of app.py so they can
|
| 45 |
+
# be reviewed and extended without changing the retrieval engine itself.
|
| 46 |
+
_RETRIEVAL_RULES_PATH = Path(__file__).parent / "hudanet_retrieval_rules.json"
|
| 47 |
+
if not _RETRIEVAL_RULES_PATH.is_file():
|
| 48 |
+
raise RuntimeError(
|
| 49 |
+
"hudanet_retrieval_rules.json is missing. Place it beside app.py before starting HUDA-Net."
|
| 50 |
+
)
|
| 51 |
+
try:
|
| 52 |
+
_RETRIEVAL_RULES = json.loads(_RETRIEVAL_RULES_PATH.read_text(encoding="utf-8"))
|
| 53 |
+
except json.JSONDecodeError as exc:
|
| 54 |
+
raise RuntimeError(
|
| 55 |
+
f"Invalid hudanet_retrieval_rules.json at line {exc.lineno}, column {exc.colno}: {exc.msg}"
|
| 56 |
+
) from exc
|
| 57 |
+
if not isinstance(_RETRIEVAL_RULES, dict) or not isinstance(_RETRIEVAL_RULES.get("intents"), dict):
|
| 58 |
+
raise RuntimeError("hudanet_retrieval_rules.json must contain an object named 'intents'.")
|
| 59 |
+
_RETRIEVAL_RULES_FINGERPRINT = hashlib.sha256(
|
| 60 |
+
json.dumps(_RETRIEVAL_RULES, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
| 61 |
+
).hexdigest()[:16]
|
| 62 |
+
|
| 63 |
+
# Dialect resources are normalized lazily because the canonical base normalizer is
|
| 64 |
+
# defined later in the file. Every replacement uses Arabic word boundaries. This
|
| 65 |
+
# prevents short keys such as "تو" and "وش" from corrupting valid words such as
|
| 66 |
+
# "توفرها" and "وشرعا".
|
| 67 |
+
_DIALECT_RULE_CACHE = None
|
| 68 |
+
_AR_WORD_BOUNDARY_CLASS = r"\w\u0600-\u06FF"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _compile_whole_arabic_rule(key: str):
|
| 72 |
+
return re.compile(
|
| 73 |
+
rf"(?<![{_AR_WORD_BOUNDARY_CLASS}]){re.escape(key)}(?![{_AR_WORD_BOUNDARY_CLASS}])"
|
| 74 |
+
)
|
| 75 |
|
|
|
|
| 76 |
|
| 77 |
+
def _dialect_rule_cache():
|
| 78 |
+
global _DIALECT_RULE_CACHE
|
| 79 |
+
if _DIALECT_RULE_CACHE is not None:
|
| 80 |
+
return _DIALECT_RULE_CACHE
|
| 81 |
+
|
| 82 |
+
base_normalizer = globals().get("norm_ar_base")
|
| 83 |
+
if not callable(base_normalizer):
|
| 84 |
+
# This fallback is used only during unusually early calls while the module is
|
| 85 |
+
# still being defined. Normal runtime calls use norm_ar_base below.
|
| 86 |
+
def base_normalizer(value):
|
| 87 |
+
value = unicodedata.normalize("NFKC", str(value or "")).replace("ـ", "")
|
| 88 |
+
value = re.sub(r"[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06ED]", "", value)
|
| 89 |
+
value = value.translate(str.maketrans({"أ":"ا","إ":"ا","آ":"ا","ٱ":"ا","ى":"ي","ؤ":"و","ئ":"ي","ک":"ك","ی":"ي","ۀ":"ة"}))
|
| 90 |
+
value = re.sub(r"[^\w\s\u0600-\u06FF]", " ", value.casefold())
|
| 91 |
+
return re.sub(r"\s+", " ", value).strip()
|
| 92 |
+
|
| 93 |
+
def compile_mapping(mapping):
|
| 94 |
+
compiled = []
|
| 95 |
+
normalized = {}
|
| 96 |
+
for raw_key, raw_value in (mapping or {}).items():
|
| 97 |
+
key = base_normalizer(raw_key)
|
| 98 |
+
value = base_normalizer(raw_value)
|
| 99 |
+
if not key or key == value:
|
| 100 |
+
continue
|
| 101 |
+
# Last declaration wins; then longest phrases are applied first.
|
| 102 |
+
normalized[key] = value
|
| 103 |
+
for key, value in sorted(normalized.items(), key=lambda item: (-len(item[0]), item[0])):
|
| 104 |
+
compiled.append((_compile_whole_arabic_rule(key), value))
|
| 105 |
+
return compiled
|
| 106 |
+
|
| 107 |
+
_DIALECT_RULE_CACHE = {
|
| 108 |
+
"phrases": compile_mapping(_DIALECTS.get("dialect_phrases", {})),
|
| 109 |
+
"spelling": compile_mapping(_DIALECTS.get("spelling_variants", {})),
|
| 110 |
+
}
|
| 111 |
+
return _DIALECT_RULE_CACHE
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
def _apply_dialect_normalization(s: str) -> str:
|
| 115 |
+
"""Normalize only complete dialect words/phrases, never substrings of valid words."""
|
| 116 |
+
text = str(s or "")
|
| 117 |
+
rules = _dialect_rule_cache()
|
| 118 |
+
for pattern, replacement in rules["phrases"]:
|
| 119 |
+
text = pattern.sub(replacement, text)
|
| 120 |
+
for pattern, replacement in rules["spelling"]:
|
| 121 |
+
text = pattern.sub(replacement, text)
|
| 122 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 123 |
|
| 124 |
try:
|
| 125 |
from IPython.display import display
|
|
|
|
| 184 |
|
| 185 |
_download_hudanet_private_datasets()
|
| 186 |
|
| 187 |
+
VERSION = "37.0.0"
|
| 188 |
CONFIG = {
|
| 189 |
"INPUT_ROOT": "/kaggle/input",
|
| 190 |
"WORK_ROOT": "/tmp/hudanet_v27",
|
|
|
|
| 630 |
def norm_en(v: Any) -> str:
|
| 631 |
s = unicodedata.normalize("NFKC", clean_display(v)).casefold()
|
| 632 |
s = re.sub(r"[^a-z0-9\s'-]", " ", s)
|
| 633 |
+
for a,b in {**EN_REPL, **_DIALECTS.get("en_spelling_variants", {})}.items(): s = re.sub(rf"\b{re.escape(a)}\b", b, s)
|
| 634 |
return _SPACE.sub(" ", s).strip()
|
| 635 |
|
| 636 |
def has_ar(v: Any) -> bool: return bool(_AR.search(str(v or "")))
|
|
|
|
| 2956 |
|
| 2957 |
|
| 2958 |
HUDANET_SMART_RESULT = smart_all_in_one()
|
| 2959 |
+
print(f"🔒 HUDA-Net UI v{UI_VERSION if 'UI_VERSION' in globals() else VERSION} | current-turn retrieval only | rules={_RETRIEVAL_RULES_FINGERPRINT}")
|
| 2960 |
|
| 2961 |
|
| 2962 |
# ======================== PROFESSIONAL BILINGUAL UI ========================
|
|
|
|
| 2974 |
from scipy import sparse
|
| 2975 |
import joblib
|
| 2976 |
|
| 2977 |
+
UI_VERSION = "37.0.0"
|
| 2978 |
UI_CONFIG = {
|
| 2979 |
"INPUT_ROOT": "/kaggle/input",
|
| 2980 |
"RUNTIME_DATASET_SLUG": "hudanet-bilingual-certified-runtime",
|
|
|
|
| 3253 |
"sort_by": sort_value if sort_value in FILTER_SORT_VALUES else "relevance",
|
| 3254 |
"evidence_count": count,
|
| 3255 |
"min_score": round(score, 3),
|
| 3256 |
+
"use_context": False,
|
| 3257 |
"compare": _filter_bool(compare, True),
|
| 3258 |
"diverse": _filter_bool(diverse, True),
|
| 3259 |
}
|
|
|
|
| 3281 |
def norm_en_ui(v: Any) -> str:
|
| 3282 |
s=unicodedata.normalize("NFKC",clean_ui(v)).casefold()
|
| 3283 |
s=re.sub(r"[^a-z0-9\s'-]"," ",s)
|
| 3284 |
+
for a,b in {**EN_REPL, **_DIALECTS.get("en_spelling_variants", {})}.items(): s=re.sub(rf"\b{re.escape(a)}\b",b,s)
|
| 3285 |
return _SPACE.sub(" ",s).strip()
|
| 3286 |
|
| 3287 |
|
|
|
|
| 3532 |
|
| 3533 |
|
| 3534 |
def extract_case_facts_ui(query:str,lang:str)->dict:
|
| 3535 |
+
"""Generic question frame displayed in the UI; no fiqh scenario is hard-coded."""
|
| 3536 |
+
frame=generic_evidence_pipeline_ui().query_analyzer.analyze(query,lang)
|
| 3537 |
+
request_labels={
|
| 3538 |
+
"ar":{"definition":"تعريف","conditions":"شروط","pillars":"أركان","duties":"واجبات","ruling":"حكم","validity":"صحة","remedy":"ما يترتب أو يلزم","procedure":"كيفية","timing":"توقيت","amount":"عدد أو مقدار","location":"مكان","cause":"سبب أو حكمة","comparison":"مقارنة","evidence":"دليل أو مصدر","exception":"استثناء","list":"قائمة","components":"عناصر","description":"بيان"},
|
| 3539 |
+
"en":{"definition":"Definition","conditions":"Conditions","pillars":"Pillars","duties":"Duties","ruling":"Ruling","validity":"Validity","remedy":"Remedy or consequence","procedure":"Procedure","timing":"Timing","amount":"Amount","location":"Location","cause":"Cause or wisdom","comparison":"Comparison","evidence":"Evidence or source","exception":"Exception","list":"List","components":"Components","description":"Description"},
|
| 3540 |
+
}[lang]
|
| 3541 |
+
facts={
|
| 3542 |
+
"request_type":request_labels.get(frame.primary_request_type,frame.primary_request_type),
|
| 3543 |
+
"polarity":(("واقعة منفية أو متعذرة" if frame.polarity=="negative" else "واقعة مثبتة") if lang=="ar" else ("Negative or unavailable case" if frame.polarity=="negative" else "Affirmative case")),
|
| 3544 |
+
}
|
| 3545 |
+
if frame.subject_terms:
|
| 3546 |
+
facts["action"]=" · ".join(frame.subject_terms[:8])
|
| 3547 |
+
if frame.dimensions:
|
| 3548 |
+
facts["dimensions"]=" · ".join(frame.dimensions)
|
| 3549 |
+
labels={
|
| 3550 |
+
"ar":{"request_type":"نوع المطلوب","polarity":"بنية الواقعة","action":"موضوع السؤال","dimensions":"أبعاد السؤال"},
|
| 3551 |
+
"en":{"request_type":"Request type","polarity":"Case structure","action":"Question topic","dimensions":"Question dimensions"},
|
| 3552 |
+
}[lang]
|
| 3553 |
+
return {"facts":facts,"missing":[],"labels":labels}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3554 |
|
| 3555 |
def canonical_ruling_ui(text:str,lang:str)->list[str]:
|
| 3556 |
n=norm_ar_ui(text) if lang=="ar" else norm_en_ui(text)
|
|
|
|
| 3566 |
"ar": {
|
| 3567 |
"pillar": "ركن",
|
| 3568 |
"condition": "شرط",
|
| 3569 |
+
"obligation_dropped": "يسقط الوجوب",
|
| 3570 |
+
"disputed": "فيه خلاف أو وجهان",
|
| 3571 |
"obligatory": "واجب أو لازم",
|
| 3572 |
"prohibited": "محرم أو غير جائز",
|
| 3573 |
"recommended": "مستحب أو سنة",
|
|
|
|
| 3579 |
"en": {
|
| 3580 |
"pillar": "Pillar",
|
| 3581 |
"condition": "Condition",
|
| 3582 |
+
"obligation_dropped": "Obligation dropped",
|
| 3583 |
+
"disputed": "Disputed or two views",
|
| 3584 |
"obligatory": "Obligatory or required",
|
| 3585 |
"prohibited": "Prohibited",
|
| 3586 |
"recommended": "Recommended or Sunnah",
|
|
|
|
| 3594 |
|
| 3595 |
|
| 3596 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3597 |
|
| 3598 |
+
_GENERIC_EVIDENCE_PIPELINE = None
|
| 3599 |
+
|
| 3600 |
+
|
| 3601 |
+
def generic_evidence_pipeline_ui() -> GenericEvidencePipeline:
|
| 3602 |
+
"""Load the generic evidence library lazily after UI normalizers exist."""
|
| 3603 |
+
global _GENERIC_EVIDENCE_PIPELINE
|
| 3604 |
+
if _GENERIC_EVIDENCE_PIPELINE is None:
|
| 3605 |
+
resource_root = Path(__file__).resolve().parent / "hudanet_core" / "resources"
|
| 3606 |
+
_GENERIC_EVIDENCE_PIPELINE = GenericEvidencePipeline(
|
| 3607 |
+
resource_root,
|
| 3608 |
+
normalizers={"ar": norm_ar_ui, "en": norm_en_ui},
|
| 3609 |
+
)
|
| 3610 |
+
return _GENERIC_EVIDENCE_PIPELINE
|
| 3611 |
+
|
| 3612 |
+
|
| 3613 |
+
def apply_generic_evidence_gate_ui(search: Mapping[str, Any], query: Any, lang: str) -> dict:
|
| 3614 |
+
"""Reclassify every retrieved record through the generic logic-first gate."""
|
| 3615 |
+
return generic_evidence_pipeline_ui().annotate_and_rebucket(query, search, lang)
|
| 3616 |
+
|
| 3617 |
+
|
| 3618 |
+
def generic_resolution_ui(
|
| 3619 |
+
query: Any,
|
| 3620 |
+
sources: Sequence[Mapping[str, Any]],
|
| 3621 |
+
lang: str,
|
| 3622 |
+
style: str = "detailed",
|
| 3623 |
+
compare_sources: bool = True,
|
| 3624 |
+
):
|
| 3625 |
+
return generic_evidence_pipeline_ui().resolve(
|
| 3626 |
+
query, sources, lang, style=style, compare_sources=compare_sources
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3627 |
)
|
|
|
|
| 3628 |
|
| 3629 |
|
| 3630 |
+
def _external_direct_intent_ui(query: Any, lang: str) -> dict:
|
| 3631 |
+
"""Deprecated compatibility hook. Generic semantic resources replace named intents."""
|
| 3632 |
+
return {}
|
| 3633 |
+
|
| 3634 |
+
def _rule_pattern_hit_ui(text: str, patterns: Sequence[str]) -> bool:
|
| 3635 |
+
for pattern in patterns or []:
|
| 3636 |
+
try:
|
| 3637 |
+
if re.search(str(pattern), text, re.I):
|
| 3638 |
+
return True
|
| 3639 |
+
except re.error as exc:
|
| 3640 |
+
raise RuntimeError(f"Invalid retrieval rule regex: {pattern}: {exc}") from exc
|
| 3641 |
+
return False
|
| 3642 |
+
|
| 3643 |
+
|
| 3644 |
+
def analyze_direct_answer_intent_ui(query: Any, lang: str) -> dict:
|
| 3645 |
+
"""Compatibility shim: all semantic decisions are now generic and modular."""
|
| 3646 |
+
frame = generic_evidence_pipeline_ui().query_analyzer.analyze(query, lang)
|
| 3647 |
+
legacy_keys = (
|
| 3648 |
+
"external_direct_rule", "no_hajj_deputy", "first_tahallul",
|
| 3649 |
+
"hair_before_first_tahallul", "intercourse_before_first_tahallul",
|
| 3650 |
+
"generic_first_tahallul", "missed_arafah", "arafah_end_time",
|
| 3651 |
+
"jamrat_start_time", "miqat_remedy", "miqat_after_makkah",
|
| 3652 |
+
"tawaf_wada_before_leave", "tawaf_doubt",
|
| 3653 |
+
"menstruating_tawaf_necessity", "tamattu_sacrifice",
|
| 3654 |
+
"hajj_modes_comparison", "jamarat_before_zawal", "ihsar_prevented_hajj",
|
| 3655 |
+
)
|
| 3656 |
+
result = {key: False for key in legacy_keys}
|
| 3657 |
+
result.update({
|
| 3658 |
+
"external_intent_id": "",
|
| 3659 |
+
"requires_operational_answer": False,
|
| 3660 |
+
"generic_request_type": frame.primary_request_type,
|
| 3661 |
+
"generic_subject_terms": list(frame.subject_terms),
|
| 3662 |
+
"generic_critical_terms": list(frame.critical_terms),
|
| 3663 |
+
"generic_polarity": frame.polarity,
|
| 3664 |
+
})
|
| 3665 |
+
return result
|
| 3666 |
+
|
| 3667 |
def _direct_source_text_ui(source: Mapping[str,Any], lang: str) -> str:
|
| 3668 |
normalizer = norm_ar_ui if lang == "ar" else norm_en_ui
|
| 3669 |
values = []
|
|
|
|
| 3674 |
return normalizer(" ".join(values))
|
| 3675 |
|
| 3676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3677 |
|
| 3678 |
+
def _external_source_rule_diagnostics_ui(source: Mapping[str,Any], query: Any, lang: str) -> dict:
|
| 3679 |
+
"""Deprecated compatibility hook; no named scenarios are evaluated."""
|
| 3680 |
+
return {"matched":False,"passed":True,"scenario":False,"preferred":False,"ambiguous":False}
|
| 3681 |
+
|
| 3682 |
+
def direct_intent_diagnostics_ui(source: Mapping[str,Any], query: Any, lang: str) -> dict:
|
| 3683 |
+
"""Generic source diagnostics in the legacy UI contract."""
|
| 3684 |
+
resolution=generic_resolution_ui(query,[source],lang,style="short",compare_sources=False)
|
| 3685 |
+
if not resolution.ranked:
|
| 3686 |
+
return {"passed":False,"required":[],"missing":["missing_source"],"conflicts":[]}
|
| 3687 |
+
item=resolution.ranked[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3688 |
return {
|
| 3689 |
+
"passed":bool(item.accepted),
|
| 3690 |
+
"required":["request_type","topic_or_case","answer_text"],
|
| 3691 |
+
"missing":list(item.hard_rejections),
|
| 3692 |
+
"conflicts":[],
|
| 3693 |
+
"generic_score":round(item.score,4),
|
| 3694 |
+
"metrics":{k:round(float(v),4) for k,v in item.metrics.items()},
|
| 3695 |
}
|
| 3696 |
|
|
|
|
| 3697 |
def source_satisfies_direct_intent_ui(source: Mapping[str,Any], query: Any, lang: str) -> bool:
|
| 3698 |
return bool(direct_intent_diagnostics_ui(source, query, lang).get("passed"))
|
| 3699 |
|
|
|
|
| 3723 |
|
| 3724 |
|
| 3725 |
def _direct_intent_bonus_ui(source: Mapping[str,Any], query: Any, lang: str) -> float:
|
| 3726 |
+
"""Legacy hook retained for API compatibility; generic ranking owns the score."""
|
| 3727 |
+
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3728 |
|
| 3729 |
def direct_query_expansion_ui(query: Any, lang: str) -> str:
|
| 3730 |
+
"""No question-specific expansions. Generic evidence promotion handles recall."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3731 |
return ""
|
| 3732 |
|
| 3733 |
def merge_search_results_ui(primary: Mapping[str,Any], extra: Mapping[str,Any]) -> dict:
|
|
|
|
| 3767 |
|
| 3768 |
|
| 3769 |
def direct_answer_preface_ui(query: Any, lang: str, support: Sequence[Mapping[str,Any]]) -> str:
|
| 3770 |
+
"""No hard-coded answer prefaces; synthesis is extractive and source-grounded."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3771 |
return ""
|
| 3772 |
|
|
|
|
| 3773 |
def filter_case_missing_for_query_ui(missing: Sequence[str], query: Any, lang: str) -> list[str]:
|
| 3774 |
+
"""Keep only deduplicated case details; no scenario-specific exceptions."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3775 |
return list(dict.fromkeys(missing or []))
|
| 3776 |
|
|
|
|
| 3777 |
def _answer_source_relevance_ui(source: Mapping[str,Any], query: str, lang: str) -> float:
|
| 3778 |
"""Rank already-supported sources by how directly their answer resolves the request."""
|
| 3779 |
if not source_has_usable_answer_ui(source):
|
|
|
|
| 3831 |
|
| 3832 |
|
| 3833 |
def rank_support_for_answer_ui(support: Sequence[Mapping[str,Any]], query: str, lang: str) -> list[dict]:
|
| 3834 |
+
"""Return only evidence accepted by the generic request/case/outcome gate."""
|
| 3835 |
+
resolution = generic_resolution_ui(query, support, lang, style="short", compare_sources=True)
|
| 3836 |
+
return [dict(item.source) for item in resolution.accepted]
|
| 3837 |
|
| 3838 |
def compose_answer_for_filter_preferences_ui(
|
| 3839 |
engine, support: Sequence[Mapping[str,Any]], style: str, lang: str,
|
| 3840 |
compare_sources: bool, consensus: Mapping[str,Any], query: str = "",
|
| 3841 |
) -> str:
|
| 3842 |
+
"""Build the final answer generically from accepted source text and source consensus."""
|
| 3843 |
+
resolution = generic_resolution_ui(
|
| 3844 |
+
query, support, lang, style=style, compare_sources=compare_sources
|
| 3845 |
+
)
|
| 3846 |
+
return resolution.answer
|
|
|
|
|
|
|
|
|
|
| 3847 |
|
| 3848 |
+
def analyze_source_consensus_ui(
|
| 3849 |
+
sources: Sequence[Mapping[str,Any]], lang: str, query: str = ""
|
| 3850 |
+
) -> dict:
|
| 3851 |
+
"""Expose modular consensus in the legacy UI shape."""
|
| 3852 |
+
if not sources:
|
| 3853 |
+
return {"state":"insufficient","books":0,"categories":{},"majority":"unspecified","agreement_ratio":0.0,"conflict_pairs":[],"rows":[]}
|
| 3854 |
+
probe = query or clean_ui((sources[0] or {}).get("question", "")) or clean_ui((sources[0] or {}).get("title", ""))
|
| 3855 |
+
resolution = generic_resolution_ui(probe, sources, lang, style="short", compare_sources=True)
|
| 3856 |
+
generic = resolution.consensus
|
| 3857 |
+
state_map = {"agreement":"agreement","mixed":"mixed","conflict":"conflict","single_source":"insufficient","insufficient":"insufficient"}
|
| 3858 |
+
categories = {}
|
| 3859 |
+
rows = []
|
| 3860 |
+
for item in resolution.accepted:
|
| 3861 |
+
for outcome in item.evidence.outcomes:
|
| 3862 |
+
if outcome != "unspecified":
|
| 3863 |
+
categories[outcome] = categories.get(outcome, 0) + 1
|
| 3864 |
+
rows.append({
|
| 3865 |
+
"book_id": item.evidence.book_id,
|
| 3866 |
+
"book": item.evidence.book,
|
| 3867 |
+
"ruling": clean_ui(item.source.get("ruling", "")) or item.evidence.answer_text,
|
| 3868 |
+
"categories": list(item.evidence.outcomes),
|
| 3869 |
+
})
|
| 3870 |
+
selected_weight = float((generic.clusters.get(generic.selected_cluster, {}) or {}).get("weight", 0.0) or 0.0)
|
| 3871 |
+
total_weight = sum(float((cluster or {}).get("weight", 0.0) or 0.0) for cluster in generic.clusters.values())
|
| 3872 |
+
return {
|
| 3873 |
+
"state": state_map.get(generic.state, "insufficient"),
|
| 3874 |
+
"books": generic.book_count,
|
| 3875 |
+
"categories": categories,
|
| 3876 |
+
"majority": generic.selected_cluster or "unspecified",
|
| 3877 |
+
"agreement_ratio": round(selected_weight / max(total_weight, 1e-9), 3),
|
| 3878 |
+
"conflict_pairs": [],
|
| 3879 |
+
"rows": rows[:8],
|
| 3880 |
+
"generic_explanation": generic.explanation,
|
| 3881 |
+
}
|
| 3882 |
|
| 3883 |
def render_case_facts_inline(case:Mapping[str,Any],lang:str)->str:
|
| 3884 |
if not case or not case.get("facts"): return ""
|
|
|
|
| 4854 |
@staticmethod
|
| 4855 |
def _cache_key(query: str, lang: str, filters: dict) -> tuple:
|
| 4856 |
stable = json.dumps(filters or {}, ensure_ascii=False, sort_keys=True, default=str)
|
| 4857 |
+
return (UI_VERSION, _RETRIEVAL_RULES_FINGERPRINT, clean_ui(query), str(lang), stable)
|
| 4858 |
|
| 4859 |
def _cache_get(self, key):
|
| 4860 |
value = self._search_cache.get(key)
|
|
|
|
| 5309 |
return {"answer":prompt,"mode":"broad_query","security":decision,"exact":[],"related":[],"distant":[],"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":original,"case_facts":case,"consensus":{},"stats":{"latency":time.perf_counter()-started,"allowed_records":0,"allowed_books":0,"matched_books":0,"exact_count":0,"related_count":0,"distant_count":0,"neural_search_skipped":True}}
|
| 5310 |
if decision["action"] in messages[lang]:
|
| 5311 |
return {"answer":messages[lang][decision["action"]],"mode":decision["action"],"security":decision,"exact":[],"related":[],"distant":[],"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":clean_ui(query),"case_facts":case,"consensus":{},"stats":{"latency":time.perf_counter()-started,"allowed_records":0,"allowed_books":0,"matched_books":0,"exact_count":0,"related_count":0,"distant_count":0}}
|
| 5312 |
+
# Conversation history is presentation state only. Retrieval always receives
|
| 5313 |
+
# the current message exactly as submitted, preventing topic leakage between turns.
|
| 5314 |
effective=original
|
| 5315 |
+
previous_user=""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5316 |
compact=compact_query_ui(original,lang); parts=split_multi_ui(compact,lang)
|
| 5317 |
if len(parts)>1:
|
| 5318 |
sub=[]
|
| 5319 |
for part in parts:
|
| 5320 |
+
result=self.search(part,lang,filters)
|
| 5321 |
+
result=apply_generic_evidence_gate_ui(result,part,lang)
|
| 5322 |
+
best=result["exact"] or result["related"]
|
| 5323 |
if not best: continue
|
| 5324 |
+
consensus=analyze_source_consensus_ui(result["exact"]+result["related"],lang,part)
|
| 5325 |
display_consensus=consensus if compare_sources else {}
|
| 5326 |
sub_answer=compose_answer_for_filter_preferences_ui(self,result["exact"]+result["related"],filters.get("answer_style","detailed"),lang,compare_sources,display_consensus,part)
|
| 5327 |
if not usable_output_text_ui(sub_answer):
|
|
|
|
| 5366 |
search=merge_search_results_ui(search,expanded_search)
|
| 5367 |
search.setdefault("stats",{})["expanded_query"]=expanded_query
|
| 5368 |
search=annotate_direct_intent_diagnostics_ui(search,intent_query,lang)
|
| 5369 |
+
search=apply_generic_evidence_gate_ui(search,intent_query,lang)
|
| 5370 |
support=search["exact"] or search["related"]
|
| 5371 |
+
consensus=analyze_source_consensus_ui(search["exact"]+search["related"],lang,intent_query)
|
| 5372 |
if not support:
|
| 5373 |
if int(search.get("stats", {}).get("allowed_records", 0) or 0) == 0:
|
| 5374 |
msg=("لا يوجد سجل يطابق اجتماع الفلاتر الحالية. هذا ليس فشلًا في البحث؛ بعض الاختيارات متعارضة. أزل فلترًا واحدًا أو استخدم إعادة الضبط." if ar else "No record matches the current filter intersection. This is not a retrieval failure; some selections conflict. Remove one filter or reset them.")
|
|
|
|
| 5395 |
collective_comparison=all(token in combined_text for token in (("تمتع","قران","افراد") if ar else ("tamattu","qiran","ifrad")))
|
| 5396 |
if complete_support:
|
| 5397 |
complete_operational_support=True
|
| 5398 |
+
# Sources that fail the requested action/scenario remain visible in the
|
| 5399 |
+
# evidence explorer, but they are excluded from answer composition.
|
| 5400 |
+
ranked_support=complete_support
|
| 5401 |
elif collective_comparison:
|
| 5402 |
complete_operational_support=True
|
| 5403 |
else:
|
|
|
|
| 5405 |
msg=(("لم أجد في الكتاب أو الكتب المحددة نصًا يجيب عن الفرع العملي المطلوب مباشرة. أزل فلتر الكتاب أو استخدم نطاق التغطية الواسعة." if selected_books else "وجدت شواهد قريبة من الموضوع، لكنها لا تجيب عن الفرع العملي المطلوب مباشرة، لذلك لن أعرض جوابًا ناقصًا." ) if ar else (("The selected book or books do not contain evidence that directly resolves the requested practical branch. Remove the book filter or use Wide Coverage." if selected_books else "Nearby evidence was found, but it does not directly resolve the requested practical branch, so an incomplete answer will not be presented.")))
|
| 5406 |
mode=("selected_books_insufficient" if selected_books else "direct_answer_incomplete")
|
| 5407 |
return {"answer":msg,"mode":mode,"security":decision,**search,"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":effective,"case_facts":case,"consensus":consensus if compare_sources else {},"stats":{**search.get("stats",{}),"latency":time.perf_counter()-started}}
|
| 5408 |
+
primary=ranked_support[0]; direct=float(primary.get("generic_score",primary.get("direct_probability",primary.get("score",0))) or 0); agreement=int(primary.get("retriever_agreement",0) or 0)
|
| 5409 |
+
second=max([float(x.get("generic_score",x.get("direct_probability",x.get("score",0))) or 0) for x in ranked_support[1:]]+[0.0]); margin=direct-second
|
| 5410 |
exact_present=bool(search["exact"]); matched_books=int(search.get("stats",{}).get("matched_books",0))
|
| 5411 |
+
generic_strong=bool(float(primary.get("generic_score",0.0) or 0.0)>=0.70)
|
| 5412 |
+
uncertain=(not generic_strong and not complete_operational_support and not exact_present and matched_books<int(UI_CONFIG["ABSTAIN_MIN_BOOKS"]) and (margin<float(UI_CONFIG["ABSTAIN_MARGIN"]) or agreement<2))
|
| 5413 |
+
# The modular consensus layer presents strong conflicts with their sources;
|
| 5414 |
+
# do not suppress them behind a generic clarification message.
|
| 5415 |
+
conflict_weak=False
|
| 5416 |
missing_critical=bool(case.get("missing")) and not exact_present and direct<0.78
|
| 5417 |
if uncertain or conflict_weak:
|
| 5418 |
missing=case.get("missing",[])
|
|
|
|
| 5423 |
else:
|
| 5424 |
prompt=("النتائج القريبة متقاربة جدًا في الثقة ولا تكفي لبناء حكم آمن. وضّح الفعل والوقت وحالة الإحرام." if ar else "The closest candidates are too close in confidence to ground a safe ruling. Clarify the act, timing, and ihram status.")
|
| 5425 |
return {"answer":prompt,"mode":"needs_context","security":decision,**search,"confidence":round(direct*100,1),"language":lang,"query":clean_ui(query),"effective_query":effective,"primary":primary,"case_facts":case,"consensus":consensus if compare_sources else {},"uncertainty":{"margin":margin,"agreement":agreement,"reason":"missing_or_conflicting_context"},"stats":{**search.get("stats",{}),"latency":time.perf_counter()-started}}
|
| 5426 |
+
answer_consensus=analyze_source_consensus_ui(ranked_support,lang,intent_query)
|
| 5427 |
+
answer=compose_answer_for_filter_preferences_ui(self,ranked_support,filters.get("answer_style","detailed"),lang,compare_sources,answer_consensus if compare_sources else {},intent_query)
|
| 5428 |
+
consensus=answer_consensus
|
| 5429 |
if not usable_output_text_ui(answer):
|
| 5430 |
msg=("وجدت سجلات مرتبطة، لكن نصوص الإجابة المتاحة كانت قوالب ناقصة أو غير مترجمة، لذلك لن أعرضها كحكم. استخدم كتابًا آخر أو أزل الفلاتر." if ar else "Related records were found, but their available answers were incomplete templates or untranslated placeholders, so they will not be presented as a ruling. Try another book or remove the filters.")
|
| 5431 |
return {"answer":msg,"mode":"placeholder_blocked","security":decision,**search,"confidence":0.0,"language":lang,"query":clean_ui(query),"effective_query":effective,"primary":primary,"case_facts":case,"consensus":consensus if compare_sources else {},"stats":{**search.get("stats",{}),"latency":time.perf_counter()-started}}
|
|
|
|
| 5469 |
|
| 5470 |
|
| 5471 |
def validate_answer_quality_v36_4() -> dict:
|
| 5472 |
+
"""Generic regressions: type isolation, case coverage, conflict, and synthesis."""
|
| 5473 |
+
pipeline = generic_evidence_pipeline_ui()
|
| 5474 |
+
checks = []
|
| 5475 |
def add(name, passed, value=""):
|
| 5476 |
checks.append({"name":name,"passed":bool(passed),"value":value})
|
| 5477 |
|
| 5478 |
+
definition = {
|
| 5479 |
+
"record_id":"definition", "book_id":"furu", "book":"الفروع", "title":"معنى الحج لغة وشرعا",
|
| 5480 |
+
"question":"ما معنى الحج؟", "ruling":"تعريف", "answer":"الحج لغة القصد، وشرعا قصد مكة للنسك.",
|
| 5481 |
+
"dense_score":0.99, "cross_encoder_score":0.95, "score":0.95,
|
| 5482 |
+
}
|
| 5483 |
+
conditions = {
|
| 5484 |
+
"record_id":"conditions", "book_id":"dalil", "book":"دليل الطالب", "title":"شروط وجوب الحج",
|
| 5485 |
+
"question":"ما شروط وجوب الحج؟", "ruling":"شروط",
|
| 5486 |
+
"answer":"شروط وجوب الحج: الإسلام، والعقل، والبلوغ، وكمال الحرية، والاستطاعة.",
|
| 5487 |
+
"dense_score":0.88, "cross_encoder_score":0.75, "score":0.70,
|
| 5488 |
+
}
|
| 5489 |
+
condition_result = pipeline.resolve("ما هي الشروط التي يجب توفرها لوجوب الحج؟", [definition, conditions], "ar")
|
| 5490 |
+
add("generic_type_gate_rejects_definition", any(x.evidence.record_id=="definition" and not x.accepted for x in condition_result.ranked), condition_result.details)
|
| 5491 |
+
add("generic_conditions_selected", bool(condition_result.consensus.selected and condition_result.consensus.selected[0].evidence.record_id=="conditions"), condition_result.answer)
|
| 5492 |
+
add("generic_conditions_answer_contains_list", all(term in norm_ar_ui(condition_result.answer) for term in ("اسلام","عقل","بلوغ","حري","استطاع")), condition_result.answer)
|
| 5493 |
+
|
| 5494 |
+
decisive = {
|
| 5495 |
+
"record_id":"dropped", "book_id":"rawd", "book":"الروض المربع", "title":"عجز من لا يجد نائبا",
|
| 5496 |
+
"question":"ما حكم من لم يجد نائبا؟", "ruling":"يسقط الوجوب",
|
| 5497 |
+
"answer":"إذا لم يجد الشخص نائبا للحج عنه، يسقط عنه وجوب الحج.",
|
| 5498 |
+
"dense_score":0.82, "cross_encoder_score":0.71, "score":0.65,
|
| 5499 |
+
}
|
| 5500 |
+
disputed = {
|
| 5501 |
+
"record_id":"two_views", "book_id":"furu", "book":"الفروع", "title":"من وجد المال ولم يجد نائبا",
|
| 5502 |
+
"question":"من لم يجد نائبا للحج", "ruling":"وجهان",
|
| 5503 |
+
"answer":"إن وجد مالا ولم يجد نائبا ففي وجوب الحج في ذمته وجهان.",
|
| 5504 |
+
"dense_score":0.98, "cross_encoder_score":0.90, "score":0.92,
|
| 5505 |
+
}
|
| 5506 |
+
ruling_result = pipeline.resolve("ما هو الحكم إذا لم يجد الشخص نائبا للحج عنه؟", [disputed, decisive], "ar")
|
| 5507 |
+
add("generic_decisive_result_beats_dispute", bool(ruling_result.consensus.selected and ruling_result.consensus.selected[0].evidence.record_id=="dropped"), ruling_result.answer)
|
| 5508 |
+
add("generic_answer_grounded_in_source", "يسقط" in norm_ar_ui(ruling_result.answer), ruling_result.answer)
|
| 5509 |
+
add("generic_sources_are_named", "الروض المربع" in ruling_result.answer, ruling_result.answer)
|
| 5510 |
+
add("no_question_specific_intents", not bool((_RETRIEVAL_RULES.get("intents") or {})), _RETRIEVAL_RULES)
|
| 5511 |
+
|
| 5512 |
+
failed=[item for item in checks if not item["passed"]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5513 |
if failed:
|
| 5514 |
+
raise RuntimeError("HUDA-Net v37.0.0 generic evidence self-test failed: "+json.dumps(failed,ensure_ascii=False))
|
| 5515 |
+
print(f"✅ HUDA-Net v37.0.0 generic evidence self-test passed: {len(checks)} checks")
|
| 5516 |
return {"passed":True,"tested":len(checks),"checks":checks}
|
| 5517 |
|
| 5518 |
def validate_specificity_guard_v33(engine: ProfessionalEvidenceEngine) -> dict:
|
|
|
|
| 5783 |
direct_rejection=clean_ui(source.get("direct_intent_rejection",""))
|
| 5784 |
if direct_rejection:
|
| 5785 |
reason += ((" · سبب رفضه للجواب المباشر: " if ar else " · Direct-answer rejection: ")+direct_rejection)
|
| 5786 |
+
generic_score=float(source.get("generic_score",0.0) or 0.0)
|
| 5787 |
+
generic_reasons=list(source.get("generic_reasons",[]) or [])
|
| 5788 |
+
generic_rejections=list(source.get("generic_rejections",[]) or [])
|
| 5789 |
+
if generic_reasons:
|
| 5790 |
+
reason += ((" · التحليل الجينيريك: " if ar else " · Generic analysis: ")+"؛ ".join(map(str,generic_reasons)))
|
| 5791 |
+
if generic_rejections:
|
| 5792 |
+
reason += ((" · بوابة الرفض: " if ar else " · Gate rejection: ")+", ".join(map(str,generic_rejections)))
|
| 5793 |
+
if generic_score:
|
| 5794 |
+
reason += ((" · درجة التوافق المنطقي " if ar else " · Logic compatibility ")+f"{generic_score*100:.1f}%")
|
| 5795 |
score=max(0.0,min(1.0,float(source.get("score",0) or 0))); score_pct=score*100
|
| 5796 |
is_quran=bool(ar and re.search(r'[﴿﷽]|قال الله|قوله تعالى',evidence))
|
| 5797 |
citation_class="quran-ayah" if is_quran else ("citation-text" if ar else "source-text-en")
|
|
|
|
| 9061 |
ar_kinds=gr.CheckboxGroup(ar_opts["source_kinds"],value=[],label="مصدر السجل")
|
| 9062 |
|
| 9063 |
with gr.Accordion("خيارات متقدمة",open=False):
|
| 9064 |
+
gr.HTML(field_guide("سياق المحادثة","سجل المحادثة للعرض فقط. كل سؤال يُسترجع مستقلًا لمنع تسرّب موضوع السؤال السابق.","ar"))
|
| 9065 |
+
ar_context=gr.Checkbox(value=False,label="السؤال الحالي فقط في الاسترجاع",interactive=False)
|
| 9066 |
gr.HTML(field_guide("مقارنة صيغ الأحكام","يعرض تنبيهًا عند اختلاف صياغة الحكم بين أقرب المصادر.","ar"))
|
| 9067 |
ar_compare=gr.Checkbox(value=True,label="قارن صيغ الأحكام بين المصادر")
|
| 9068 |
gr.HTML(field_guide("تمثيل جميع الكتب","يحتفظ المحرك بأفضل شاهد آمن من كل كتاب مسموح. هذا الضمان ثابت حتى لا يختفي أي كتاب بسبب ترتيب النتائج.","ar"))
|
|
|
|
| 9151 |
en_kinds=gr.CheckboxGroup(en_opts["source_kinds"],value=[],label="Record source")
|
| 9152 |
|
| 9153 |
with gr.Accordion("Advanced options",open=False):
|
| 9154 |
+
gr.HTML(field_guide("Conversation context","Conversation history is display-only. Each message is retrieved independently to prevent topic leakage.","en"))
|
| 9155 |
+
en_context=gr.Checkbox(value=False,label="Current question only for retrieval",interactive=False)
|
| 9156 |
gr.HTML(field_guide("Compare ruling formulations","Shows a note when the closest sources use different ruling formulations.","en"))
|
| 9157 |
en_compare=gr.Checkbox(value=True,label="Compare ruling formulations across sources")
|
| 9158 |
gr.HTML(field_guide("Represent every book","Keeps the best safe item from every allowed book, even when its relevance is only distant.","en"))
|
|
|
|
| 9240 |
|
| 9241 |
reset_outputs_ar=[ar_books,ar_authors,ar_types,ar_madhhabs,ar_categories,ar_rulings,ar_kinds,ar_style,ar_mode,ar_sort,ar_count,ar_min,ar_context,ar_compare,ar_diverse]
|
| 9242 |
reset_outputs_en=[en_books,en_authors,en_types,en_madhhabs,en_categories,en_rulings,en_kinds,en_style,en_mode,en_sort,en_count,en_min,en_context,en_compare,en_diverse]
|
| 9243 |
+
defaults=([],[],[],[],[],[],[],"detailed","balanced","relevance",1,0,False,True,True)
|
| 9244 |
ar_reset_event=ar_reset.click(lambda:defaults,None,reset_outputs_ar,queue=False)
|
| 9245 |
en_reset_event=en_reset.click(lambda:defaults,None,reset_outputs_en,queue=False)
|
| 9246 |
for event in (ar_reset_event,en_reset_event):
|
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HUDA-Net Generic Evidence Library v37.0.0
|
| 2 |
+
|
| 3 |
+
This package separates the answer pipeline into independent, reviewable layers. It contains no stored answer for any named fiqh question.
|
| 4 |
+
|
| 5 |
+
## Runtime flow
|
| 6 |
+
|
| 7 |
+
1. `query.py` classifies the request and extracts the subject, critical case constraints, polarity, and requested dimensions.
|
| 8 |
+
2. `evidence.py` converts every retrieved record into a normalized evidence frame.
|
| 9 |
+
3. `compatibility.py` applies hard gates before ranking. A neural score cannot rescue a request-type mismatch or a missing case constraint.
|
| 10 |
+
4. `consensus.py` groups outcomes by independent books, detects incompatible results, prefers a direct decisive result over a merely disputed formulation when justified, and preserves dissent.
|
| 11 |
+
5. `synthesis.py` builds the visible answer extractively from accepted source text, merges complementary list items, names every source used, and presents strong conflicts separately.
|
| 12 |
+
6. `pipeline.py` orchestrates ranking, promotion of compatible distant evidence, rejection/demotion of incompatible evidence, consensus, and answer synthesis.
|
| 13 |
+
|
| 14 |
+
## External resources
|
| 15 |
+
|
| 16 |
+
- `resources/semantic_rules.json`: bilingual request types, negation, dimensions, and ruling outcomes.
|
| 17 |
+
- `resources/evidence_schema.json`: generic evidence-field roles.
|
| 18 |
+
- `resources/ranking_config.json`: logic-first weights, hard thresholds, type compatibility, and limits.
|
| 19 |
+
- `resources/answer_templates.json`: presentation-only strings and extractive cleanup rules.
|
| 20 |
+
|
| 21 |
+
## Safety properties
|
| 22 |
+
|
| 23 |
+
- Current-turn retrieval only. Conversation history is display-only.
|
| 24 |
+
- No question-specific intent ID and no expected answer stored in JSON.
|
| 25 |
+
- Fail closed when no evidence passes request-type and case-constraint gates.
|
| 26 |
+
- Strong incompatible outcomes are displayed as separate sourced views.
|
| 27 |
+
- Distant neural candidates may be promoted only after passing the generic logic gate.
|
| 28 |
+
- Every evidence card receives generic compatibility metrics, acceptance reasons, or rejection reasons.
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HUDA-Net generic evidence resolution library."""
|
| 2 |
+
from .pipeline import GenericEvidencePipeline, ResolutionResult
|
| 3 |
+
|
| 4 |
+
__all__ = ["GenericEvidencePipeline", "ResolutionResult"]
|
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Dict, Iterable, List, Mapping, Sequence, Set, Tuple
|
| 4 |
+
|
| 5 |
+
from .text import TextProcessor
|
| 6 |
+
from .types import EvidenceFrame, QueryFrame, ScoredEvidence
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class CompatibilityScorer:
|
| 10 |
+
def __init__(self, text: TextProcessor, semantic: dict, ranking: dict):
|
| 11 |
+
self.text = text
|
| 12 |
+
self.semantic = semantic
|
| 13 |
+
self.config = ranking
|
| 14 |
+
self.weights = {key: float(value) for key, value in ranking.get("weights", {}).items()}
|
| 15 |
+
self.thresholds = {key: float(value) for key, value in ranking.get("thresholds", {}).items()}
|
| 16 |
+
self.compatibility = ranking.get("type_compatibility", {})
|
| 17 |
+
self.incompatible_outcomes = {
|
| 18 |
+
frozenset(pair) for pair in semantic.get("incompatible_outcomes", [])
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@staticmethod
|
| 22 |
+
def _bounded_score(value: object) -> float:
|
| 23 |
+
try:
|
| 24 |
+
number = float(value or 0.0)
|
| 25 |
+
except Exception:
|
| 26 |
+
return 0.0
|
| 27 |
+
if number > 1.0:
|
| 28 |
+
number /= 100.0
|
| 29 |
+
return max(0.0, min(1.0, number))
|
| 30 |
+
|
| 31 |
+
def _type_match(self, query: QueryFrame, evidence: EvidenceFrame) -> float:
|
| 32 |
+
if "unclassified" in evidence.answer_types:
|
| 33 |
+
return 0.48
|
| 34 |
+
values = []
|
| 35 |
+
for query_type in query.request_types:
|
| 36 |
+
allowed = self.compatibility.get(query_type, {})
|
| 37 |
+
for answer_type in evidence.answer_types:
|
| 38 |
+
values.append(float(allowed.get(answer_type, 0.0)))
|
| 39 |
+
return max(values or [0.0])
|
| 40 |
+
|
| 41 |
+
def _topic_match(self, query: QueryFrame, evidence: EvidenceFrame) -> Tuple[float, float]:
|
| 42 |
+
metadata_terms = self.text.content_terms(evidence.metadata_text, query.language)
|
| 43 |
+
answer_terms = self.text.content_terms(evidence.answer_text, query.language)
|
| 44 |
+
metadata_overlap = self.text.fuzzy_term_overlap(query.subject_terms, metadata_terms)
|
| 45 |
+
answer_overlap = self.text.fuzzy_term_overlap(query.subject_terms, answer_terms)
|
| 46 |
+
topic = max(metadata_overlap, 0.72 * metadata_overlap + 0.28 * answer_overlap, 0.52 * answer_overlap)
|
| 47 |
+
critical = self.text.fuzzy_term_overlap(query.critical_terms, tuple(metadata_terms) + tuple(answer_terms))
|
| 48 |
+
return topic, critical
|
| 49 |
+
|
| 50 |
+
def _polarity_alignment(self, query: QueryFrame, evidence: EvidenceFrame) -> float:
|
| 51 |
+
if query.polarity != "negative":
|
| 52 |
+
return 0.75
|
| 53 |
+
if not query.negated_terms:
|
| 54 |
+
return 0.62
|
| 55 |
+
answer_terms = self.text.content_terms(f"{evidence.metadata_text} {evidence.answer_text}", query.language)
|
| 56 |
+
term_coverage = self.text.fuzzy_term_overlap(query.negated_terms, answer_terms)
|
| 57 |
+
negative_patterns = self.semantic.get("negation_markers", {}).get(query.language, []) or []
|
| 58 |
+
has_negative = any(self.text.phrase_hit(f"{evidence.normalized_metadata} {evidence.normalized_answer}", pattern) for pattern in negative_patterns)
|
| 59 |
+
return min(1.0, 0.18 + 0.58 * term_coverage + (0.24 if has_negative else 0.0))
|
| 60 |
+
|
| 61 |
+
def _directness(self, query: QueryFrame, evidence: EvidenceFrame) -> float:
|
| 62 |
+
type_match = self._type_match(query, evidence)
|
| 63 |
+
outcome_directness = evidence.decisiveness
|
| 64 |
+
if query.primary_request_type in {"definition", "conditions", "comparison", "procedure", "timing", "amount", "location", "cause"}:
|
| 65 |
+
outcome_directness = max(outcome_directness, 0.78 if query.primary_request_type in evidence.answer_types else 0.35)
|
| 66 |
+
if query.asks_for_decisive_answer and "disputed" in evidence.outcomes and len(evidence.outcomes) == 1:
|
| 67 |
+
outcome_directness *= 0.42
|
| 68 |
+
return min(1.0, 0.56 * type_match + 0.44 * outcome_directness)
|
| 69 |
+
|
| 70 |
+
def _neural_prior(self, source: Mapping[str, object]) -> float:
|
| 71 |
+
values = [
|
| 72 |
+
self._bounded_score(source.get("direct_probability")),
|
| 73 |
+
self._bounded_score(source.get("score")),
|
| 74 |
+
self._bounded_score(source.get("dense_score")),
|
| 75 |
+
self._bounded_score(source.get("cross_encoder_score")),
|
| 76 |
+
self._bounded_score(source.get("bm25_score")),
|
| 77 |
+
]
|
| 78 |
+
values.sort(reverse=True)
|
| 79 |
+
return 0.55 * values[0] + 0.25 * values[1] + 0.12 * values[2] + 0.08 * values[3]
|
| 80 |
+
|
| 81 |
+
def score(self, query: QueryFrame, evidence: EvidenceFrame, source: Mapping[str, object]) -> ScoredEvidence:
|
| 82 |
+
type_match = self._type_match(query, evidence)
|
| 83 |
+
topic_match, critical_coverage = self._topic_match(query, evidence)
|
| 84 |
+
polarity = self._polarity_alignment(query, evidence)
|
| 85 |
+
directness = self._directness(query, evidence)
|
| 86 |
+
completeness = evidence.completeness
|
| 87 |
+
neural = self._neural_prior(source)
|
| 88 |
+
source_quality = 0.68
|
| 89 |
+
source_kind = str(source.get("source_kind", "")).casefold()
|
| 90 |
+
if any(token in source_kind for token in ("clean", "منظف", "certified", "معتمد")):
|
| 91 |
+
source_quality = 0.85
|
| 92 |
+
if any(token in source_kind for token in ("raw", "خام", "ocr")):
|
| 93 |
+
source_quality = min(source_quality, 0.58)
|
| 94 |
+
|
| 95 |
+
metrics = {
|
| 96 |
+
"request_type_match": type_match,
|
| 97 |
+
"topic_match": topic_match,
|
| 98 |
+
"critical_constraint_coverage": critical_coverage,
|
| 99 |
+
"polarity_alignment": polarity,
|
| 100 |
+
"answer_directness": directness,
|
| 101 |
+
"evidence_completeness": completeness,
|
| 102 |
+
"source_quality": source_quality,
|
| 103 |
+
"neural_prior": neural,
|
| 104 |
+
}
|
| 105 |
+
score = sum(self.weights.get(name, 0.0) * value for name, value in metrics.items())
|
| 106 |
+
compositional_request = query.primary_request_type in {"conditions", "list", "components", "pillars", "duties"}
|
| 107 |
+
compositional_extension = bool(
|
| 108 |
+
compositional_request
|
| 109 |
+
and type_match >= 0.85
|
| 110 |
+
and neural >= 0.60
|
| 111 |
+
and evidence.completeness >= 0.30
|
| 112 |
+
)
|
| 113 |
+
if compositional_extension:
|
| 114 |
+
# Generic component recovery: a specialized condition/component can be
|
| 115 |
+
# useful even when its short title omits the broader subject named in
|
| 116 |
+
# the user's question. Neural retrieval is only a recall witness here;
|
| 117 |
+
# the evidence still must match the requested answer type.
|
| 118 |
+
score = min(1.0, score + 0.075)
|
| 119 |
+
hard_rejections: List[str] = []
|
| 120 |
+
reasons: List[str] = []
|
| 121 |
+
|
| 122 |
+
if query.confidence >= 0.60 and type_match < self.thresholds.get("hard_type_mismatch", 0.18):
|
| 123 |
+
hard_rejections.append("request_type_mismatch")
|
| 124 |
+
if (
|
| 125 |
+
topic_match < self.thresholds.get("minimum_topic_match", 0.18)
|
| 126 |
+
and critical_coverage < self.thresholds.get("minimum_critical_coverage", 0.24)
|
| 127 |
+
and not compositional_extension
|
| 128 |
+
):
|
| 129 |
+
hard_rejections.append("insufficient_case_or_topic_overlap")
|
| 130 |
+
if query.polarity == "negative" and polarity < self.thresholds.get("minimum_negative_alignment", 0.36):
|
| 131 |
+
hard_rejections.append("negative_case_not_covered")
|
| 132 |
+
if not evidence.answer_text.strip():
|
| 133 |
+
hard_rejections.append("missing_answer_text")
|
| 134 |
+
|
| 135 |
+
if type_match >= 0.80:
|
| 136 |
+
reasons.append("نوع الجواب يطابق نوع السؤال" if query.language == "ar" else "Answer type matches the request")
|
| 137 |
+
if critical_coverage >= 0.68:
|
| 138 |
+
reasons.append("قيود الواقعة مغطاة بوضوح" if query.language == "ar" else "Case constraints are clearly covered")
|
| 139 |
+
if directness >= 0.75:
|
| 140 |
+
reasons.append("النتيجة مباشرة وقابلة للبناء عليها" if query.language == "ar" else "The outcome is direct and usable")
|
| 141 |
+
if neural >= 0.75:
|
| 142 |
+
reasons.append("الاسترجاع الهجين يدعم الصلة" if query.language == "ar" else "Hybrid retrieval supports relevance")
|
| 143 |
+
if compositional_extension:
|
| 144 |
+
reasons.append("شاهد مكمل من النوع نفسه جرى استرداده للتجميع" if query.language == "ar" else "A same-type complementary source was recovered for synthesis")
|
| 145 |
+
if "disputed" in evidence.outcomes and query.asks_for_decisive_answer:
|
| 146 |
+
reasons.append("الشاهد يعرض خلافًا لا نتيجة حاسمة" if query.language == "ar" else "The source presents disagreement rather than a decisive outcome")
|
| 147 |
+
|
| 148 |
+
accepted = not hard_rejections and score >= self.thresholds.get("accept", 0.54)
|
| 149 |
+
promoted = accepted and score >= self.thresholds.get("promote_distant", 0.68)
|
| 150 |
+
source_copy = dict(source)
|
| 151 |
+
source_copy.update({
|
| 152 |
+
"generic_score": round(float(score), 6),
|
| 153 |
+
"generic_accepted": bool(accepted),
|
| 154 |
+
"generic_promoted": bool(promoted),
|
| 155 |
+
"generic_rejections": list(hard_rejections),
|
| 156 |
+
"generic_reasons": list(reasons),
|
| 157 |
+
"generic_metrics": {key: round(float(value), 4) for key, value in metrics.items()},
|
| 158 |
+
"generic_answer_types": list(evidence.answer_types),
|
| 159 |
+
"generic_outcomes": list(evidence.outcomes),
|
| 160 |
+
})
|
| 161 |
+
return ScoredEvidence(
|
| 162 |
+
source=source_copy,
|
| 163 |
+
evidence=evidence,
|
| 164 |
+
score=float(score),
|
| 165 |
+
accepted=accepted,
|
| 166 |
+
promoted=promoted,
|
| 167 |
+
hard_rejections=hard_rejections,
|
| 168 |
+
reasons=reasons,
|
| 169 |
+
metrics=metrics,
|
| 170 |
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections import defaultdict
|
| 4 |
+
from typing import Dict, Iterable, List, Sequence, Tuple
|
| 5 |
+
|
| 6 |
+
from .text import TextProcessor
|
| 7 |
+
from .types import ConsensusResult, QueryFrame, ScoredEvidence
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ConsensusAnalyzer:
|
| 11 |
+
def __init__(self, text: TextProcessor, semantic: dict, ranking: dict):
|
| 12 |
+
self.text = text
|
| 13 |
+
self.semantic = semantic
|
| 14 |
+
self.ranking = ranking
|
| 15 |
+
self.incompatible = {frozenset(pair) for pair in semantic.get("incompatible_outcomes", [])}
|
| 16 |
+
|
| 17 |
+
def _cluster_key(self, query: QueryFrame, item: ScoredEvidence) -> str:
|
| 18 |
+
outcomes = [value for value in item.evidence.outcomes if value != "unspecified"]
|
| 19 |
+
if query.primary_request_type in {"conditions", "definition", "procedure", "comparison", "timing", "amount", "location", "cause", "evidence"}:
|
| 20 |
+
return query.primary_request_type
|
| 21 |
+
decisive = [value for value in outcomes if value != "disputed"]
|
| 22 |
+
if decisive:
|
| 23 |
+
return "+".join(sorted(decisive))
|
| 24 |
+
if outcomes:
|
| 25 |
+
return "+".join(sorted(outcomes))
|
| 26 |
+
sentence = item.evidence.sentences[0] if item.evidence.sentences else item.evidence.answer_text
|
| 27 |
+
signature = self.text.content_terms(sentence, query.language)[:7]
|
| 28 |
+
return "text:" + ":".join(signature)
|
| 29 |
+
|
| 30 |
+
def analyze(self, query: QueryFrame, ranked: Sequence[ScoredEvidence]) -> ConsensusResult:
|
| 31 |
+
accepted = [item for item in ranked if item.accepted]
|
| 32 |
+
clusters: Dict[str, Dict[str, object]] = {}
|
| 33 |
+
grouped: Dict[str, List[ScoredEvidence]] = defaultdict(list)
|
| 34 |
+
for item in accepted:
|
| 35 |
+
key = self._cluster_key(query, item)
|
| 36 |
+
item.cluster_key = key
|
| 37 |
+
grouped[key].append(item)
|
| 38 |
+
for key, items in grouped.items():
|
| 39 |
+
books = {item.evidence.book_id or item.evidence.book or item.evidence.record_id for item in items}
|
| 40 |
+
weight = sum(item.score * (0.82 + 0.18 * item.evidence.decisiveness) for item in items)
|
| 41 |
+
clusters[key] = {
|
| 42 |
+
"weight": float(weight),
|
| 43 |
+
"books": len(books),
|
| 44 |
+
"items": items,
|
| 45 |
+
"outcomes": sorted({outcome for item in items for outcome in item.evidence.outcomes}),
|
| 46 |
+
}
|
| 47 |
+
if not clusters:
|
| 48 |
+
return ConsensusResult(
|
| 49 |
+
state="insufficient", selected_cluster="", selected=[], dissent=[], clusters={},
|
| 50 |
+
confidence=0.0, source_count=0, book_count=0,
|
| 51 |
+
explanation="لا توجد شواهد اجتازت البوابة العامة." if query.language == "ar" else "No evidence passed the generic gate.",
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
ordered = sorted(clusters.items(), key=lambda pair: (float(pair[1]["weight"]), int(pair[1]["books"])), reverse=True)
|
| 55 |
+
selected_key, selected_cluster = ordered[0]
|
| 56 |
+
|
| 57 |
+
# A decisive ruling is preferred over a merely disputed formulation when both
|
| 58 |
+
# cover the same case. The disagreement remains visible as dissent.
|
| 59 |
+
if selected_key == "disputed" and query.asks_for_decisive_answer:
|
| 60 |
+
decisive_options = [pair for pair in ordered[1:] if pair[0] != "disputed"]
|
| 61 |
+
if decisive_options:
|
| 62 |
+
best_key, best_cluster = decisive_options[0]
|
| 63 |
+
if float(best_cluster["weight"]) >= 0.52 * float(selected_cluster["weight"]):
|
| 64 |
+
selected_key, selected_cluster = best_key, best_cluster
|
| 65 |
+
|
| 66 |
+
selected = sorted(selected_cluster["items"], key=lambda item: item.score, reverse=True)
|
| 67 |
+
dissent = sorted(
|
| 68 |
+
[item for key, cluster in ordered if key != selected_key for item in cluster["items"]],
|
| 69 |
+
key=lambda item: item.score,
|
| 70 |
+
reverse=True,
|
| 71 |
+
)
|
| 72 |
+
selected_weight = float(selected_cluster["weight"])
|
| 73 |
+
total_weight = sum(float(cluster["weight"]) for cluster in clusters.values())
|
| 74 |
+
share = selected_weight / max(total_weight, 1e-9)
|
| 75 |
+
|
| 76 |
+
conflict = False
|
| 77 |
+
selected_outcomes = set(selected_cluster.get("outcomes", []))
|
| 78 |
+
for key, cluster in ordered:
|
| 79 |
+
if key == selected_key:
|
| 80 |
+
continue
|
| 81 |
+
other_outcomes = set(cluster.get("outcomes", []))
|
| 82 |
+
if any(pair.issubset(selected_outcomes | other_outcomes) and pair & selected_outcomes and pair & other_outcomes for pair in self.incompatible):
|
| 83 |
+
relative = float(cluster["weight"]) / max(selected_weight, 1e-9)
|
| 84 |
+
if relative >= float(self.ranking.get("thresholds", {}).get("conflict_weight_ratio", 0.62)):
|
| 85 |
+
conflict = True
|
| 86 |
+
break
|
| 87 |
+
|
| 88 |
+
if conflict:
|
| 89 |
+
state = "conflict"
|
| 90 |
+
elif len(selected) >= 2 or int(selected_cluster["books"]) >= 2:
|
| 91 |
+
state = "agreement" if share >= 0.58 else "mixed"
|
| 92 |
+
else:
|
| 93 |
+
state = "single_source"
|
| 94 |
+
|
| 95 |
+
all_books = {item.evidence.book_id or item.evidence.book for item in accepted}
|
| 96 |
+
confidence = min(0.99, max(0.05, 0.58 * selected[0].score + 0.27 * share + 0.15 * min(1.0, len(all_books) / 4.0)))
|
| 97 |
+
explanation_map = {
|
| 98 |
+
"ar": {
|
| 99 |
+
"agreement": "تدعم عدة مصادر النتيجة نفسها.",
|
| 100 |
+
"mixed": "النتيجة الأقوى واضحة مع وجود صيغ أو تفاصيل إضافية.",
|
| 101 |
+
"conflict": "توجد نتائج قوية متعارضة، لذلك عُرضت الآراء منفصلة.",
|
| 102 |
+
"single_source": "يعتمد الجواب على أقوى شاهد مباشر متاح.",
|
| 103 |
+
},
|
| 104 |
+
"en": {
|
| 105 |
+
"agreement": "Multiple sources support the same result.",
|
| 106 |
+
"mixed": "The strongest result is clear, with additional formulations or details.",
|
| 107 |
+
"conflict": "Strong sources conflict, so the views are presented separately.",
|
| 108 |
+
"single_source": "The answer relies on the strongest direct evidence available.",
|
| 109 |
+
},
|
| 110 |
+
}
|
| 111 |
+
return ConsensusResult(
|
| 112 |
+
state=state,
|
| 113 |
+
selected_cluster=selected_key,
|
| 114 |
+
selected=selected,
|
| 115 |
+
dissent=dissent,
|
| 116 |
+
clusters=clusters,
|
| 117 |
+
confidence=float(confidence),
|
| 118 |
+
source_count=len(accepted),
|
| 119 |
+
book_count=len(all_books),
|
| 120 |
+
explanation=explanation_map[query.language][state],
|
| 121 |
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Dict, List, Mapping, Tuple
|
| 5 |
+
|
| 6 |
+
from .text import TextProcessor
|
| 7 |
+
from .types import EvidenceFrame
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class EvidenceAnalyzer:
|
| 11 |
+
def __init__(self, text: TextProcessor, semantic: dict, schema: dict):
|
| 12 |
+
self.text = text
|
| 13 |
+
self.semantic = semantic
|
| 14 |
+
self.schema = schema
|
| 15 |
+
|
| 16 |
+
@staticmethod
|
| 17 |
+
def _clean(value: Any) -> str:
|
| 18 |
+
text = str(value or "").strip()
|
| 19 |
+
if text.casefold() in {"nan", "none", "null", "n/a"}:
|
| 20 |
+
return ""
|
| 21 |
+
return re.sub(r"\s+", " ", text)
|
| 22 |
+
|
| 23 |
+
def _join_fields(self, source: Mapping[str, Any], names: List[str]) -> str:
|
| 24 |
+
return " ".join(filter(None, (self._clean(source.get(name, "")) for name in names)))
|
| 25 |
+
|
| 26 |
+
def analyze(self, source: Mapping[str, Any], lang: str) -> EvidenceFrame:
|
| 27 |
+
metadata_fields = list(self.schema.get("metadata_fields", []))
|
| 28 |
+
answer_fields = list(self.schema.get("answer_fields", []))
|
| 29 |
+
metadata_text = self._join_fields(source, metadata_fields)
|
| 30 |
+
answer_text = self._join_fields(source, answer_fields)
|
| 31 |
+
normalized_metadata = self.text.normalize(metadata_text, lang)
|
| 32 |
+
normalized_answer = self.text.normalize(answer_text, lang)
|
| 33 |
+
ruling_text = self._clean(source.get("ruling", ""))
|
| 34 |
+
normalized_outcome_text = self.text.normalize(f"{answer_text} {ruling_text}", lang)
|
| 35 |
+
all_text = f"{normalized_metadata} {normalized_answer}".strip()
|
| 36 |
+
|
| 37 |
+
answer_types: List[str] = []
|
| 38 |
+
for type_id, rule in self.semantic.get("request_types", {}).items():
|
| 39 |
+
patterns = (rule.get("evidence_patterns", {}) or {}).get(lang, []) or []
|
| 40 |
+
if any(self.text.phrase_hit(all_text, pattern) for pattern in patterns):
|
| 41 |
+
answer_types.append(type_id)
|
| 42 |
+
|
| 43 |
+
outcomes: List[str] = []
|
| 44 |
+
for outcome_id, rule in self.semantic.get("outcomes", {}).items():
|
| 45 |
+
patterns = (rule.get("patterns", {}) or {}).get(lang, []) or []
|
| 46 |
+
if any(self.text.phrase_hit(normalized_outcome_text, pattern) for pattern in patterns):
|
| 47 |
+
outcomes.append(outcome_id)
|
| 48 |
+
|
| 49 |
+
sentences = self.text.sentences(answer_text)
|
| 50 |
+
list_items = self.text.split_list_items(answer_text, lang)
|
| 51 |
+
content_terms = self.text.content_terms(f"{metadata_text} {answer_text}", lang)
|
| 52 |
+
decisive = 0.25
|
| 53 |
+
if outcomes:
|
| 54 |
+
decisive = max(float(self.semantic["outcomes"][name].get("decisiveness", 0.65)) for name in outcomes)
|
| 55 |
+
if "disputed" in outcomes and len(outcomes) == 1:
|
| 56 |
+
decisive = min(decisive, 0.38)
|
| 57 |
+
completeness = min(1.0, 0.18 + 0.06 * len(sentences) + 0.055 * len(list_items) + 0.012 * len(self.text.tokens(answer_text, lang)))
|
| 58 |
+
if len(normalized_answer) < 12:
|
| 59 |
+
completeness *= 0.45
|
| 60 |
+
|
| 61 |
+
return EvidenceFrame(
|
| 62 |
+
record_id=self._clean(source.get("record_id", "")),
|
| 63 |
+
book_id=self._clean(source.get("book_id", source.get("book", ""))),
|
| 64 |
+
book=self._clean(source.get("book", source.get("book_ar", source.get("book_en", "")))),
|
| 65 |
+
page=self._clean(source.get("page", source.get("page_number", ""))),
|
| 66 |
+
metadata_text=metadata_text,
|
| 67 |
+
answer_text=answer_text,
|
| 68 |
+
normalized_metadata=normalized_metadata,
|
| 69 |
+
normalized_answer=normalized_answer,
|
| 70 |
+
answer_types=tuple(dict.fromkeys(answer_types)) or ("unclassified",),
|
| 71 |
+
outcomes=tuple(dict.fromkeys(outcomes)) or ("unspecified",),
|
| 72 |
+
subject_terms=tuple(content_terms),
|
| 73 |
+
sentences=sentences,
|
| 74 |
+
list_items=list_items,
|
| 75 |
+
decisiveness=float(decisive),
|
| 76 |
+
completeness=float(completeness),
|
| 77 |
+
raw=source,
|
| 78 |
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Sequence
|
| 5 |
+
|
| 6 |
+
from .compatibility import CompatibilityScorer
|
| 7 |
+
from .consensus import ConsensusAnalyzer
|
| 8 |
+
from .evidence import EvidenceAnalyzer
|
| 9 |
+
from .query import QueryAnalyzer
|
| 10 |
+
from .resources import ResourceBundle
|
| 11 |
+
from .synthesis import AnswerSynthesizer
|
| 12 |
+
from .text import TextProcessor
|
| 13 |
+
from .types import ConsensusResult, ResolutionResult, ScoredEvidence
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class GenericEvidencePipeline:
|
| 17 |
+
"""Generic, evidence-first resolution pipeline with no question-specific answers."""
|
| 18 |
+
|
| 19 |
+
def __init__(self, resource_root: Path, normalizers: Optional[Dict[str, Callable[[Any], str]]] = None):
|
| 20 |
+
self.resources = ResourceBundle(Path(resource_root))
|
| 21 |
+
self.text = TextProcessor(self.resources.semantic, normalizers=normalizers)
|
| 22 |
+
self.query_analyzer = QueryAnalyzer(self.text, self.resources.semantic)
|
| 23 |
+
self.evidence_analyzer = EvidenceAnalyzer(self.text, self.resources.semantic, self.resources.schema)
|
| 24 |
+
self.scorer = CompatibilityScorer(self.text, self.resources.semantic, self.resources.ranking)
|
| 25 |
+
self.consensus_analyzer = ConsensusAnalyzer(self.text, self.resources.semantic, self.resources.ranking)
|
| 26 |
+
self.synthesizer = AnswerSynthesizer(self.text, self.resources.semantic, self.resources.templates, self.resources.ranking)
|
| 27 |
+
|
| 28 |
+
def rank(self, query: Any, sources: Sequence[Mapping[str, Any]], lang: str) -> List[ScoredEvidence]:
|
| 29 |
+
frame = self.query_analyzer.analyze(query, lang)
|
| 30 |
+
scored: List[ScoredEvidence] = []
|
| 31 |
+
for source in sources or []:
|
| 32 |
+
evidence = self.evidence_analyzer.analyze(source, lang)
|
| 33 |
+
scored.append(self.scorer.score(frame, evidence, source))
|
| 34 |
+
scored.sort(key=lambda item: item.score, reverse=True)
|
| 35 |
+
return scored
|
| 36 |
+
|
| 37 |
+
def resolve(
|
| 38 |
+
self,
|
| 39 |
+
query: Any,
|
| 40 |
+
sources: Sequence[Mapping[str, Any]],
|
| 41 |
+
lang: str,
|
| 42 |
+
*,
|
| 43 |
+
style: str = "detailed",
|
| 44 |
+
compare_sources: bool = True,
|
| 45 |
+
) -> ResolutionResult:
|
| 46 |
+
query_frame = self.query_analyzer.analyze(query, lang)
|
| 47 |
+
ranked: List[ScoredEvidence] = []
|
| 48 |
+
for source in sources or []:
|
| 49 |
+
evidence = self.evidence_analyzer.analyze(source, lang)
|
| 50 |
+
ranked.append(self.scorer.score(query_frame, evidence, source))
|
| 51 |
+
ranked.sort(key=lambda item: item.score, reverse=True)
|
| 52 |
+
accepted = [item for item in ranked if item.accepted]
|
| 53 |
+
rejected = [item for item in ranked if not item.accepted]
|
| 54 |
+
consensus = self.consensus_analyzer.analyze(query_frame, ranked)
|
| 55 |
+
answer = self.synthesizer.synthesize(
|
| 56 |
+
query_frame, consensus, style=style, compare_sources=compare_sources
|
| 57 |
+
)
|
| 58 |
+
return ResolutionResult(
|
| 59 |
+
answer=answer,
|
| 60 |
+
query=query_frame,
|
| 61 |
+
ranked=ranked,
|
| 62 |
+
accepted=accepted,
|
| 63 |
+
rejected=rejected,
|
| 64 |
+
consensus=consensus,
|
| 65 |
+
confidence=consensus.confidence,
|
| 66 |
+
details={
|
| 67 |
+
"request_type": query_frame.primary_request_type,
|
| 68 |
+
"subject_terms": list(query_frame.subject_terms),
|
| 69 |
+
"critical_terms": list(query_frame.critical_terms),
|
| 70 |
+
"polarity": query_frame.polarity,
|
| 71 |
+
"accepted": len(accepted),
|
| 72 |
+
"rejected": len(rejected),
|
| 73 |
+
"consensus_state": consensus.state,
|
| 74 |
+
"selected_cluster": consensus.selected_cluster,
|
| 75 |
+
},
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
def annotate_and_rebucket(self, query: Any, search: Mapping[str, Any], lang: str) -> Dict[str, Any]:
|
| 79 |
+
result = dict(search or {})
|
| 80 |
+
all_items: List[Dict[str, Any]] = []
|
| 81 |
+
for tier in ("exact", "related", "distant"):
|
| 82 |
+
for raw in result.get(tier, []) or []:
|
| 83 |
+
item = dict(raw)
|
| 84 |
+
item["generic_original_tier"] = tier
|
| 85 |
+
all_items.append(item)
|
| 86 |
+
resolution = self.resolve(query, all_items, lang, style="short", compare_sources=True)
|
| 87 |
+
exact: List[Dict[str, Any]] = []
|
| 88 |
+
related: List[Dict[str, Any]] = []
|
| 89 |
+
distant: List[Dict[str, Any]] = []
|
| 90 |
+
selected_ids = {
|
| 91 |
+
item.evidence.record_id or str(id(item.source)) for item in resolution.consensus.selected
|
| 92 |
+
}
|
| 93 |
+
for item in resolution.ranked:
|
| 94 |
+
source = dict(item.source)
|
| 95 |
+
source["generic_cluster"] = item.cluster_key
|
| 96 |
+
source["generic_selected_for_answer"] = (
|
| 97 |
+
item.evidence.record_id or str(id(item.source))
|
| 98 |
+
) in selected_ids
|
| 99 |
+
original = source.get("generic_original_tier", "distant")
|
| 100 |
+
if item.accepted:
|
| 101 |
+
if original == "exact" and item.score >= 0.72:
|
| 102 |
+
exact.append(source)
|
| 103 |
+
elif item.promoted or original in {"exact", "related"}:
|
| 104 |
+
related.append(source)
|
| 105 |
+
else:
|
| 106 |
+
distant.append(source)
|
| 107 |
+
else:
|
| 108 |
+
source["generic_gate_rejected"] = True
|
| 109 |
+
distant.append(source)
|
| 110 |
+
exact.sort(key=lambda source: float(source.get("generic_score", 0.0)), reverse=True)
|
| 111 |
+
related.sort(key=lambda source: float(source.get("generic_score", 0.0)), reverse=True)
|
| 112 |
+
distant.sort(key=lambda source: float(source.get("generic_score", 0.0)), reverse=True)
|
| 113 |
+
result["exact"] = exact
|
| 114 |
+
result["related"] = related
|
| 115 |
+
result["distant"] = distant
|
| 116 |
+
result.setdefault("stats", {})["generic_request_type"] = resolution.query.primary_request_type
|
| 117 |
+
result["stats"]["generic_accepted"] = len(resolution.accepted)
|
| 118 |
+
result["stats"]["generic_rejected"] = len(resolution.rejected)
|
| 119 |
+
result["stats"]["generic_consensus_state"] = resolution.consensus.state
|
| 120 |
+
result["generic_resolution"] = resolution.details
|
| 121 |
+
return result
|
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Dict, List, Tuple
|
| 5 |
+
|
| 6 |
+
from .text import TextProcessor
|
| 7 |
+
from .types import QueryFrame
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class QueryAnalyzer:
|
| 11 |
+
def __init__(self, text: TextProcessor, semantic: dict):
|
| 12 |
+
self.text = text
|
| 13 |
+
self.semantic = semantic
|
| 14 |
+
|
| 15 |
+
def analyze(self, query: Any, lang: str) -> QueryFrame:
|
| 16 |
+
raw = str(query or "").strip()
|
| 17 |
+
normalized = self.text.normalize(raw, lang)
|
| 18 |
+
request_scores: List[Tuple[str, float]] = []
|
| 19 |
+
matched_intent_tokens = set()
|
| 20 |
+
for type_id, rule in self.semantic.get("request_types", {}).items():
|
| 21 |
+
patterns = (rule.get("patterns", {}) or {}).get(lang, []) or []
|
| 22 |
+
hits = sum(1 for pattern in patterns if self.text.phrase_hit(normalized, pattern))
|
| 23 |
+
if hits:
|
| 24 |
+
priority = float(rule.get("priority", 1.0) or 1.0)
|
| 25 |
+
request_scores.append((type_id, min(1.0, 0.58 + 0.18 * hits) * priority))
|
| 26 |
+
matched_intent_tokens.update(self.text.content_terms(" ".join(rule.get("keywords", {}).get(lang, [])), lang))
|
| 27 |
+
request_scores.sort(key=lambda item: item[1], reverse=True)
|
| 28 |
+
request_types = tuple(item[0] for item in request_scores)
|
| 29 |
+
primary = request_types[0] if request_types else "ruling"
|
| 30 |
+
|
| 31 |
+
all_terms = list(self.text.content_terms(normalized, lang))
|
| 32 |
+
generic_words = set(self.text.content_terms(" ".join(self.semantic.get("generic_question_words", {}).get(lang, [])), lang))
|
| 33 |
+
subject_terms = [term for term in all_terms if term not in matched_intent_tokens and term not in generic_words]
|
| 34 |
+
if not subject_terms:
|
| 35 |
+
subject_terms = all_terms
|
| 36 |
+
|
| 37 |
+
negation_markers = self.semantic.get("negation_markers", {}).get(lang, []) or []
|
| 38 |
+
polarity = "negative" if any(self.text.phrase_hit(normalized, pattern) for pattern in negation_markers) else "affirmative"
|
| 39 |
+
negated_terms: List[str] = []
|
| 40 |
+
if polarity == "negative":
|
| 41 |
+
tokens = self.text.tokens(normalized, lang)
|
| 42 |
+
marker_words = set(self.semantic.get("negation_tokens", {}).get(lang, []))
|
| 43 |
+
for index, token in enumerate(tokens):
|
| 44 |
+
if token in marker_words:
|
| 45 |
+
for following in tokens[index + 1:index + 5]:
|
| 46 |
+
stem = self.text.light_stem(following, lang)
|
| 47 |
+
if stem and stem not in negated_terms:
|
| 48 |
+
negated_terms.append(stem)
|
| 49 |
+
|
| 50 |
+
critical_terms = list(subject_terms)
|
| 51 |
+
for term in negated_terms:
|
| 52 |
+
if term not in critical_terms:
|
| 53 |
+
critical_terms.append(term)
|
| 54 |
+
|
| 55 |
+
dimensions: List[str] = []
|
| 56 |
+
for dimension, rule in self.semantic.get("dimensions", {}).items():
|
| 57 |
+
for pattern in (rule.get("patterns", {}) or {}).get(lang, []) or []:
|
| 58 |
+
if self.text.phrase_hit(normalized, pattern):
|
| 59 |
+
dimensions.append(dimension)
|
| 60 |
+
break
|
| 61 |
+
|
| 62 |
+
decisive_types = set(self.semantic.get("decisive_request_types", []))
|
| 63 |
+
asks_for_decisive = primary in decisive_types and not any(
|
| 64 |
+
self.text.phrase_hit(normalized, pattern)
|
| 65 |
+
for pattern in self.semantic.get("asks_for_disagreement", {}).get(lang, []) or []
|
| 66 |
+
)
|
| 67 |
+
asks_for_sources = any(
|
| 68 |
+
self.text.phrase_hit(normalized, pattern)
|
| 69 |
+
for pattern in self.semantic.get("asks_for_sources", {}).get(lang, []) or []
|
| 70 |
+
)
|
| 71 |
+
confidence = request_scores[0][1] if request_scores else 0.48
|
| 72 |
+
return QueryFrame(
|
| 73 |
+
raw=raw,
|
| 74 |
+
normalized=normalized,
|
| 75 |
+
language=lang,
|
| 76 |
+
request_types=request_types or ("ruling",),
|
| 77 |
+
primary_request_type=primary,
|
| 78 |
+
subject_terms=tuple(subject_terms),
|
| 79 |
+
critical_terms=tuple(critical_terms),
|
| 80 |
+
negated_terms=tuple(negated_terms),
|
| 81 |
+
polarity=polarity,
|
| 82 |
+
dimensions=tuple(dict.fromkeys(dimensions)),
|
| 83 |
+
asks_for_decisive_answer=asks_for_decisive,
|
| 84 |
+
asks_for_sources=asks_for_sources,
|
| 85 |
+
confidence=float(min(1.0, confidence)),
|
| 86 |
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, Dict
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ResourceError(RuntimeError):
|
| 9 |
+
pass
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ResourceBundle:
|
| 13 |
+
REQUIRED = (
|
| 14 |
+
"semantic_rules.json",
|
| 15 |
+
"evidence_schema.json",
|
| 16 |
+
"ranking_config.json",
|
| 17 |
+
"answer_templates.json",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
def __init__(self, root: Path):
|
| 21 |
+
self.root = Path(root)
|
| 22 |
+
self.data: Dict[str, Dict[str, Any]] = {}
|
| 23 |
+
for name in self.REQUIRED:
|
| 24 |
+
self.data[name] = self._load(name)
|
| 25 |
+
self._validate()
|
| 26 |
+
|
| 27 |
+
def _load(self, name: str) -> Dict[str, Any]:
|
| 28 |
+
path = self.root / name
|
| 29 |
+
if not path.is_file():
|
| 30 |
+
raise ResourceError(f"Missing HUDA-Net resource: {path}")
|
| 31 |
+
try:
|
| 32 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 33 |
+
except json.JSONDecodeError as exc:
|
| 34 |
+
raise ResourceError(
|
| 35 |
+
f"Invalid JSON in {path.name}, line {exc.lineno}, column {exc.colno}: {exc.msg}"
|
| 36 |
+
) from exc
|
| 37 |
+
if not isinstance(value, dict):
|
| 38 |
+
raise ResourceError(f"{path.name} must contain a JSON object")
|
| 39 |
+
return value
|
| 40 |
+
|
| 41 |
+
def _validate(self) -> None:
|
| 42 |
+
semantic = self.semantic
|
| 43 |
+
required = ("request_types", "outcomes", "stopwords", "negation_markers")
|
| 44 |
+
missing = [key for key in required if key not in semantic]
|
| 45 |
+
if missing:
|
| 46 |
+
raise ResourceError(f"semantic_rules.json is missing: {', '.join(missing)}")
|
| 47 |
+
ranking = self.ranking
|
| 48 |
+
if "weights" not in ranking or "thresholds" not in ranking:
|
| 49 |
+
raise ResourceError("ranking_config.json must contain weights and thresholds")
|
| 50 |
+
total = sum(float(v) for v in ranking["weights"].values())
|
| 51 |
+
if not 0.98 <= total <= 1.02:
|
| 52 |
+
raise ResourceError(f"Ranking weights must sum to 1.0, got {total:.4f}")
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def semantic(self) -> Dict[str, Any]:
|
| 56 |
+
return self.data["semantic_rules.json"]
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def schema(self) -> Dict[str, Any]:
|
| 60 |
+
return self.data["evidence_schema.json"]
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def ranking(self) -> Dict[str, Any]:
|
| 64 |
+
return self.data["ranking_config.json"]
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def templates(self) -> Dict[str, Any]:
|
| 68 |
+
return self.data["answer_templates.json"]
|
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0.0",
|
| 3 |
+
"answer_prefix": {
|
| 4 |
+
"ar": "**الإجابة:** ",
|
| 5 |
+
"en": "**Answer:** "
|
| 6 |
+
},
|
| 7 |
+
"list_heading": {
|
| 8 |
+
"ar": "**الإجابة:** العناصر المستخلصة من أقوى الشواهد هي:",
|
| 9 |
+
"en": "**Answer:** The elements extracted from the strongest evidence are:"
|
| 10 |
+
},
|
| 11 |
+
"comparison_heading": {
|
| 12 |
+
"ar": "**الإجابة المقارنة:**",
|
| 13 |
+
"en": "**Comparative answer:**"
|
| 14 |
+
},
|
| 15 |
+
"sources_heading": {
|
| 16 |
+
"ar": "المصادر التي بُني عليها الجواب",
|
| 17 |
+
"en": "Sources used for the answer"
|
| 18 |
+
},
|
| 19 |
+
"dissent_heading": {
|
| 20 |
+
"ar": "صياغات أو آراء أخرى ظهرت في المصادر",
|
| 21 |
+
"en": "Other formulations or views found in the sources"
|
| 22 |
+
},
|
| 23 |
+
"conflict_intro": {
|
| 24 |
+
"ar": "**النتيجة:** توجد في المصادر المرفوعة نتائج قوية متعارضة، لذلك لا يصح دمجها في حكم واحد:",
|
| 25 |
+
"en": "**Result:** The uploaded sources contain strong conflicting outcomes, so they should not be merged into one ruling:"
|
| 26 |
+
},
|
| 27 |
+
"insufficient": {
|
| 28 |
+
"ar": "لم أجد شاهدًا يطابق نوع السؤال وقيود الواقعة بدرجة تكفي لبناء جواب موثق.",
|
| 29 |
+
"en": "No evidence matched the request type and case constraints strongly enough to build a grounded answer."
|
| 30 |
+
},
|
| 31 |
+
"preferred_answer_fields": ["answer_short", "answer_detailed", "answer", "answer_ar", "answer_en", "evidence", "ruling"],
|
| 32 |
+
"preferred_clause_markers": {
|
| 33 |
+
"ar": ["الخلاصة(?:\\s+المعتمدة)?(?:\\s+للسؤال)?\\s*[::]", "وخلاصتها\\s*[::]", "فالجواب\\s*[::]", "الإجابة\\s*[::]"],
|
| 34 |
+
"en": ["the answer is\\s*[::]", "in summary\\s*[::]", "the conclusion is\\s*[::]"]
|
| 35 |
+
},
|
| 36 |
+
"preamble_patterns": {
|
| 37 |
+
"ar": [
|
| 38 |
+
"^في\\s+كتاب\\s+.+?وردت\\s+المسالة\\s+بمعناها\\s*[::]\\s*",
|
| 39 |
+
"^ذكر\\s+المصنف\\s+.+?\\s*[::]\\s*",
|
| 40 |
+
"^بحسب\\s+النص\\s+المستخرج\\s*[::]\\s*"
|
| 41 |
+
],
|
| 42 |
+
"en": [
|
| 43 |
+
"^in\\s+the\\s+book\\s+.+?the\\s+issue\\s+is\\s+stated\\s+as\\s*[::]\\s*",
|
| 44 |
+
"^according\\s+to\\s+the\\s+extracted\\s+text\\s*[::]\\s*"
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
"drop_item_patterns": {
|
| 48 |
+
"ar": ["^(?:ذكر|قال|ورد|في كتاب|الخلاصة|المسالة|السؤال|الجواب)$", "^كتاب\\s+", "^المؤلف\\s+"],
|
| 49 |
+
"en": ["^(?:the book|the author|the question|the answer|in summary)$"]
|
| 50 |
+
}
|
| 51 |
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0.0",
|
| 3 |
+
"description": "Generic field roles for HUDA-Net evidence records.",
|
| 4 |
+
"metadata_fields": [
|
| 5 |
+
"question",
|
| 6 |
+
"title",
|
| 7 |
+
"chapter",
|
| 8 |
+
"category",
|
| 9 |
+
"ruling",
|
| 10 |
+
"book",
|
| 11 |
+
"book_ar",
|
| 12 |
+
"book_en",
|
| 13 |
+
"madhhab",
|
| 14 |
+
"source_type"
|
| 15 |
+
],
|
| 16 |
+
"answer_fields": [
|
| 17 |
+
"answer_short",
|
| 18 |
+
"answer_detailed",
|
| 19 |
+
"answer",
|
| 20 |
+
"answer_ar",
|
| 21 |
+
"answer_en",
|
| 22 |
+
"evidence",
|
| 23 |
+
"answer_evidence",
|
| 24 |
+
"answer_evidence_en"
|
| 25 |
+
],
|
| 26 |
+
"identity_fields": [
|
| 27 |
+
"record_id",
|
| 28 |
+
"book_id",
|
| 29 |
+
"source_file",
|
| 30 |
+
"original_row"
|
| 31 |
+
],
|
| 32 |
+
"source_fields": [
|
| 33 |
+
"book",
|
| 34 |
+
"author",
|
| 35 |
+
"chapter",
|
| 36 |
+
"page",
|
| 37 |
+
"page_number",
|
| 38 |
+
"source_type",
|
| 39 |
+
"madhhab",
|
| 40 |
+
"source_kind"
|
| 41 |
+
]
|
| 42 |
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0.0",
|
| 3 |
+
"description": "Logic-first generic evidence ranking. Neural scores are deliberately a small prior.",
|
| 4 |
+
"weights": {
|
| 5 |
+
"request_type_match": 0.23,
|
| 6 |
+
"topic_match": 0.19,
|
| 7 |
+
"critical_constraint_coverage": 0.18,
|
| 8 |
+
"polarity_alignment": 0.10,
|
| 9 |
+
"answer_directness": 0.14,
|
| 10 |
+
"evidence_completeness": 0.07,
|
| 11 |
+
"source_quality": 0.05,
|
| 12 |
+
"neural_prior": 0.04
|
| 13 |
+
},
|
| 14 |
+
"thresholds": {
|
| 15 |
+
"accept": 0.53,
|
| 16 |
+
"promote_distant": 0.66,
|
| 17 |
+
"hard_type_mismatch": 0.18,
|
| 18 |
+
"minimum_topic_match": 0.16,
|
| 19 |
+
"minimum_critical_coverage": 0.22,
|
| 20 |
+
"minimum_negative_alignment": 0.34,
|
| 21 |
+
"conflict_weight_ratio": 0.62
|
| 22 |
+
},
|
| 23 |
+
"type_compatibility": {
|
| 24 |
+
"definition": {"definition": 1.0, "description": 0.45, "evidence": 0.25, "unclassified": 0.35},
|
| 25 |
+
"conditions": {"conditions": 1.0, "list": 0.92, "components": 0.78, "validity": 0.58, "ruling": 0.35, "unclassified": 0.35},
|
| 26 |
+
"pillars": {"pillars": 1.0, "list": 0.9, "components": 0.75, "conditions": 0.45, "unclassified": 0.35},
|
| 27 |
+
"duties": {"duties": 1.0, "list": 0.9, "components": 0.75, "ruling": 0.58, "unclassified": 0.35},
|
| 28 |
+
"ruling": {"ruling": 1.0, "validity": 0.74, "remedy": 0.72, "exception": 0.65, "evidence": 0.42, "unclassified": 0.38},
|
| 29 |
+
"validity": {"validity": 1.0, "ruling": 0.82, "conditions": 0.62, "unclassified": 0.35},
|
| 30 |
+
"remedy": {"remedy": 1.0, "ruling": 0.78, "duties": 0.58, "unclassified": 0.35},
|
| 31 |
+
"procedure": {"procedure": 1.0, "duties": 0.72, "components": 0.64, "ruling": 0.38, "unclassified": 0.35},
|
| 32 |
+
"timing": {"timing": 1.0, "procedure": 0.62, "ruling": 0.55, "unclassified": 0.35},
|
| 33 |
+
"amount": {"amount": 1.0, "remedy": 0.64, "list": 0.55, "unclassified": 0.35},
|
| 34 |
+
"location": {"location": 1.0, "procedure": 0.55, "conditions": 0.45, "unclassified": 0.35},
|
| 35 |
+
"cause": {"cause": 1.0, "evidence": 0.65, "ruling": 0.42, "unclassified": 0.35},
|
| 36 |
+
"comparison": {"comparison": 1.0, "definition": 0.72, "description": 0.65, "list": 0.55, "unclassified": 0.35},
|
| 37 |
+
"evidence": {"evidence": 1.0, "ruling": 0.62, "definition": 0.4, "unclassified": 0.35},
|
| 38 |
+
"exception": {"exception": 1.0, "ruling": 0.78, "conditions": 0.55, "unclassified": 0.35},
|
| 39 |
+
"list": {"list": 1.0, "conditions": 0.82, "pillars": 0.82, "duties": 0.82, "components": 0.82, "unclassified": 0.35},
|
| 40 |
+
"components": {"components": 1.0, "list": 0.9, "conditions": 0.7, "unclassified": 0.35},
|
| 41 |
+
"description": {"description": 1.0, "definition": 0.7, "procedure": 0.55, "unclassified": 0.35}
|
| 42 |
+
},
|
| 43 |
+
"limits": {
|
| 44 |
+
"max_list_items": 18,
|
| 45 |
+
"max_selected_sources": 6,
|
| 46 |
+
"max_dissent_sources": 3
|
| 47 |
+
}
|
| 48 |
+
}
|
|
@@ -0,0 +1,1142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0.0",
|
| 3 |
+
"description": "Generic bilingual semantic rules. No question, book, or fiqh answer is hard-coded.",
|
| 4 |
+
"decisive_request_types": [
|
| 5 |
+
"ruling",
|
| 6 |
+
"validity",
|
| 7 |
+
"remedy",
|
| 8 |
+
"timing",
|
| 9 |
+
"amount",
|
| 10 |
+
"location",
|
| 11 |
+
"exception"
|
| 12 |
+
],
|
| 13 |
+
"generic_question_words": {
|
| 14 |
+
"ar": [
|
| 15 |
+
"ما",
|
| 16 |
+
"ماذا",
|
| 17 |
+
"ماهو",
|
| 18 |
+
"ماهي",
|
| 19 |
+
"هو",
|
| 20 |
+
"هي",
|
| 21 |
+
"الحكم",
|
| 22 |
+
"الشخص",
|
| 23 |
+
"المسالة",
|
| 24 |
+
"السؤال",
|
| 25 |
+
"عن",
|
| 26 |
+
"في",
|
| 27 |
+
"من",
|
| 28 |
+
"على",
|
| 29 |
+
"اذا",
|
| 30 |
+
"هل"
|
| 31 |
+
],
|
| 32 |
+
"en": [
|
| 33 |
+
"what",
|
| 34 |
+
"which",
|
| 35 |
+
"who",
|
| 36 |
+
"when",
|
| 37 |
+
"where",
|
| 38 |
+
"how",
|
| 39 |
+
"is",
|
| 40 |
+
"are",
|
| 41 |
+
"the",
|
| 42 |
+
"ruling",
|
| 43 |
+
"person",
|
| 44 |
+
"case",
|
| 45 |
+
"question",
|
| 46 |
+
"if",
|
| 47 |
+
"whether"
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
"stopwords": {
|
| 51 |
+
"ar": [
|
| 52 |
+
"ما",
|
| 53 |
+
"ماذا",
|
| 54 |
+
"هل",
|
| 55 |
+
"هو",
|
| 56 |
+
"هي",
|
| 57 |
+
"هذا",
|
| 58 |
+
"هذه",
|
| 59 |
+
"ذلك",
|
| 60 |
+
"تلك",
|
| 61 |
+
"الذي",
|
| 62 |
+
"التي",
|
| 63 |
+
"من",
|
| 64 |
+
"في",
|
| 65 |
+
"على",
|
| 66 |
+
"عن",
|
| 67 |
+
"الى",
|
| 68 |
+
"إلى",
|
| 69 |
+
"ثم",
|
| 70 |
+
"او",
|
| 71 |
+
"أو",
|
| 72 |
+
"و",
|
| 73 |
+
"ف",
|
| 74 |
+
"ب",
|
| 75 |
+
"ل",
|
| 76 |
+
"ك",
|
| 77 |
+
"كما",
|
| 78 |
+
"عند",
|
| 79 |
+
"مع",
|
| 80 |
+
"اذا",
|
| 81 |
+
"إذا",
|
| 82 |
+
"الشخص",
|
| 83 |
+
"المسالة",
|
| 84 |
+
"السؤال",
|
| 85 |
+
"الحكم",
|
| 86 |
+
"يمكن",
|
| 87 |
+
"يكون",
|
| 88 |
+
"كانت",
|
| 89 |
+
"كان",
|
| 90 |
+
"لم",
|
| 91 |
+
"لا",
|
| 92 |
+
"لن",
|
| 93 |
+
"ليس",
|
| 94 |
+
"ليست",
|
| 95 |
+
"عنه",
|
| 96 |
+
"عنها",
|
| 97 |
+
"عليها",
|
| 98 |
+
"عليه",
|
| 99 |
+
"لها",
|
| 100 |
+
"له",
|
| 101 |
+
"التي",
|
| 102 |
+
"الذي",
|
| 103 |
+
"توفرها"
|
| 104 |
+
],
|
| 105 |
+
"en": [
|
| 106 |
+
"what",
|
| 107 |
+
"which",
|
| 108 |
+
"who",
|
| 109 |
+
"when",
|
| 110 |
+
"where",
|
| 111 |
+
"how",
|
| 112 |
+
"is",
|
| 113 |
+
"are",
|
| 114 |
+
"was",
|
| 115 |
+
"were",
|
| 116 |
+
"the",
|
| 117 |
+
"a",
|
| 118 |
+
"an",
|
| 119 |
+
"of",
|
| 120 |
+
"in",
|
| 121 |
+
"on",
|
| 122 |
+
"for",
|
| 123 |
+
"to",
|
| 124 |
+
"from",
|
| 125 |
+
"with",
|
| 126 |
+
"and",
|
| 127 |
+
"or",
|
| 128 |
+
"if",
|
| 129 |
+
"person",
|
| 130 |
+
"case",
|
| 131 |
+
"question",
|
| 132 |
+
"ruling",
|
| 133 |
+
"does",
|
| 134 |
+
"do",
|
| 135 |
+
"did",
|
| 136 |
+
"not",
|
| 137 |
+
"no",
|
| 138 |
+
"without",
|
| 139 |
+
"him",
|
| 140 |
+
"her",
|
| 141 |
+
"it",
|
| 142 |
+
"that",
|
| 143 |
+
"which"
|
| 144 |
+
]
|
| 145 |
+
},
|
| 146 |
+
"negation_tokens": {
|
| 147 |
+
"ar": [
|
| 148 |
+
"لا",
|
| 149 |
+
"لم",
|
| 150 |
+
"لن",
|
| 151 |
+
"ليس",
|
| 152 |
+
"ليست",
|
| 153 |
+
"بدون",
|
| 154 |
+
"دون",
|
| 155 |
+
"غير",
|
| 156 |
+
"تعذر",
|
| 157 |
+
"عجز",
|
| 158 |
+
"فقد"
|
| 159 |
+
],
|
| 160 |
+
"en": [
|
| 161 |
+
"not",
|
| 162 |
+
"no",
|
| 163 |
+
"never",
|
| 164 |
+
"without",
|
| 165 |
+
"cannot",
|
| 166 |
+
"unable",
|
| 167 |
+
"failed",
|
| 168 |
+
"missing"
|
| 169 |
+
]
|
| 170 |
+
},
|
| 171 |
+
"negation_markers": {
|
| 172 |
+
"ar": [
|
| 173 |
+
"(?:^|\\s)(?:لا|لم|لن|ليس|ليست|بدون|دون|غير)(?:\\s|$)",
|
| 174 |
+
"(?:تعذر|عجز|فقد|امتنع)"
|
| 175 |
+
],
|
| 176 |
+
"en": [
|
| 177 |
+
"\\b(?:not|no|never|without|cannot|can't|unable|failed|missing)\\b"
|
| 178 |
+
]
|
| 179 |
+
},
|
| 180 |
+
"asks_for_disagreement": {
|
| 181 |
+
"ar": [
|
| 182 |
+
"(?:هل|ما)\\s+(?:فيه|فيها)\\s+(?:خلاف|قولان|وجهان)",
|
| 183 |
+
"ما\\s+اقوال",
|
| 184 |
+
"اذكر\\s+الخلاف"
|
| 185 |
+
],
|
| 186 |
+
"en": [
|
| 187 |
+
"\\b(?:views|opinions|disagreement|difference of opinion)\\b"
|
| 188 |
+
]
|
| 189 |
+
},
|
| 190 |
+
"asks_for_sources": {
|
| 191 |
+
"ar": [
|
| 192 |
+
"(?:ما|اذكر)\\s+(?:الدليل|المصدر|المصادر|النص)",
|
| 193 |
+
"من\\s+اي\\s+كتاب"
|
| 194 |
+
],
|
| 195 |
+
"en": [
|
| 196 |
+
"\\b(?:source|sources|evidence|proof|citation|which book)\\b"
|
| 197 |
+
]
|
| 198 |
+
},
|
| 199 |
+
"dimensions": {
|
| 200 |
+
"linguistic": {
|
| 201 |
+
"patterns": {
|
| 202 |
+
"ar": [
|
| 203 |
+
"لغة",
|
| 204 |
+
"لغويا"
|
| 205 |
+
],
|
| 206 |
+
"en": [
|
| 207 |
+
"linguistically",
|
| 208 |
+
"language meaning"
|
| 209 |
+
]
|
| 210 |
+
}
|
| 211 |
+
},
|
| 212 |
+
"technical": {
|
| 213 |
+
"patterns": {
|
| 214 |
+
"ar": [
|
| 215 |
+
"شرعا",
|
| 216 |
+
"اصطلاحا"
|
| 217 |
+
],
|
| 218 |
+
"en": [
|
| 219 |
+
"legally",
|
| 220 |
+
"technically",
|
| 221 |
+
"in sharia"
|
| 222 |
+
]
|
| 223 |
+
}
|
| 224 |
+
},
|
| 225 |
+
"obligation": {
|
| 226 |
+
"patterns": {
|
| 227 |
+
"ar": [
|
| 228 |
+
"وجوب",
|
| 229 |
+
"يجب",
|
| 230 |
+
"واجب"
|
| 231 |
+
],
|
| 232 |
+
"en": [
|
| 233 |
+
"obligation",
|
| 234 |
+
"obligatory",
|
| 235 |
+
"required"
|
| 236 |
+
]
|
| 237 |
+
}
|
| 238 |
+
},
|
| 239 |
+
"validity": {
|
| 240 |
+
"patterns": {
|
| 241 |
+
"ar": [
|
| 242 |
+
"صحة",
|
| 243 |
+
"صحيح",
|
| 244 |
+
"يصح"
|
| 245 |
+
],
|
| 246 |
+
"en": [
|
| 247 |
+
"validity",
|
| 248 |
+
"valid",
|
| 249 |
+
"validly"
|
| 250 |
+
]
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
},
|
| 254 |
+
"request_types": {
|
| 255 |
+
"definition": {
|
| 256 |
+
"priority": 1.12,
|
| 257 |
+
"keywords": {
|
| 258 |
+
"ar": [
|
| 259 |
+
"تعريف",
|
| 260 |
+
"معنى",
|
| 261 |
+
"لغة",
|
| 262 |
+
"شرعا"
|
| 263 |
+
],
|
| 264 |
+
"en": [
|
| 265 |
+
"definition",
|
| 266 |
+
"meaning",
|
| 267 |
+
"linguistically",
|
| 268 |
+
"legally"
|
| 269 |
+
]
|
| 270 |
+
},
|
| 271 |
+
"patterns": {
|
| 272 |
+
"ar": [
|
| 273 |
+
"(?:ما|ماهو|ما هي)\\s+(?:تعريف|معنى)",
|
| 274 |
+
"لغة\\s+وشرعا",
|
| 275 |
+
"ما\\s+المقصود"
|
| 276 |
+
],
|
| 277 |
+
"en": [
|
| 278 |
+
"what\\s+(?:is|does).*(?:definition|mean)",
|
| 279 |
+
"linguistically.*legally"
|
| 280 |
+
]
|
| 281 |
+
},
|
| 282 |
+
"evidence_patterns": {
|
| 283 |
+
"ar": [
|
| 284 |
+
"\\bتعريف\\b",
|
| 285 |
+
"لغة.*شرعا",
|
| 286 |
+
"معناه",
|
| 287 |
+
"هو\\s+.+?\\s+شرعا"
|
| 288 |
+
],
|
| 289 |
+
"en": [
|
| 290 |
+
"\\bdefinition\\b",
|
| 291 |
+
"linguistically.*legally",
|
| 292 |
+
"means",
|
| 293 |
+
"is defined as"
|
| 294 |
+
]
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
"conditions": {
|
| 298 |
+
"priority": 1.1,
|
| 299 |
+
"keywords": {
|
| 300 |
+
"ar": [
|
| 301 |
+
"شروط",
|
| 302 |
+
"يشترط",
|
| 303 |
+
"شرط",
|
| 304 |
+
"يتوقف",
|
| 305 |
+
"وجوب",
|
| 306 |
+
"يجب",
|
| 307 |
+
"توفر",
|
| 308 |
+
"متطلبات"
|
| 309 |
+
],
|
| 310 |
+
"en": [
|
| 311 |
+
"conditions",
|
| 312 |
+
"requirements",
|
| 313 |
+
"prerequisites"
|
| 314 |
+
]
|
| 315 |
+
},
|
| 316 |
+
"patterns": {
|
| 317 |
+
"ar": [
|
| 318 |
+
"(?:ما|ماهي|ما هي)\\s+الشروط",
|
| 319 |
+
"شروط\\s+",
|
| 320 |
+
"ما\\s+الذي\\s+يشترط",
|
| 321 |
+
"يتوقف.*على"
|
| 322 |
+
],
|
| 323 |
+
"en": [
|
| 324 |
+
"what\\s+are\\s+the\\s+(?:conditions|requirements|prerequisites)",
|
| 325 |
+
"conditions?\\s+for",
|
| 326 |
+
"what\\s+is\\s+required\\s+for"
|
| 327 |
+
]
|
| 328 |
+
},
|
| 329 |
+
"evidence_patterns": {
|
| 330 |
+
"ar": [
|
| 331 |
+
"\\bشروط?\\b",
|
| 332 |
+
"يشترط",
|
| 333 |
+
"لا\\s+.+?\\s+الا\\s+ب",
|
| 334 |
+
"شرط\\s+(?:الوجوب|الصحة|الاجزاء)"
|
| 335 |
+
],
|
| 336 |
+
"en": [
|
| 337 |
+
"\\bconditions?\\b",
|
| 338 |
+
"\\brequirements?\\b",
|
| 339 |
+
"prerequisites",
|
| 340 |
+
"is conditional upon"
|
| 341 |
+
]
|
| 342 |
+
}
|
| 343 |
+
},
|
| 344 |
+
"pillars": {
|
| 345 |
+
"priority": 1.08,
|
| 346 |
+
"keywords": {
|
| 347 |
+
"ar": [
|
| 348 |
+
"اركان",
|
| 349 |
+
"ركن"
|
| 350 |
+
],
|
| 351 |
+
"en": [
|
| 352 |
+
"pillars",
|
| 353 |
+
"pillar"
|
| 354 |
+
]
|
| 355 |
+
},
|
| 356 |
+
"patterns": {
|
| 357 |
+
"ar": [
|
| 358 |
+
"ما\\s+(?:هي\\s+)?اركان",
|
| 359 |
+
"اركان\\s+"
|
| 360 |
+
],
|
| 361 |
+
"en": [
|
| 362 |
+
"what\\s+are\\s+the\\s+pillars",
|
| 363 |
+
"pillars?\\s+of"
|
| 364 |
+
]
|
| 365 |
+
},
|
| 366 |
+
"evidence_patterns": {
|
| 367 |
+
"ar": [
|
| 368 |
+
"\\bاركان?\\b",
|
| 369 |
+
"\\bركن\\b"
|
| 370 |
+
],
|
| 371 |
+
"en": [
|
| 372 |
+
"\\bpillars?\\b"
|
| 373 |
+
]
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
"duties": {
|
| 377 |
+
"priority": 1.07,
|
| 378 |
+
"keywords": {
|
| 379 |
+
"ar": [
|
| 380 |
+
"واجبات",
|
| 381 |
+
"الواجب",
|
| 382 |
+
"يلزم"
|
| 383 |
+
],
|
| 384 |
+
"en": [
|
| 385 |
+
"duties",
|
| 386 |
+
"obligations",
|
| 387 |
+
"required acts"
|
| 388 |
+
]
|
| 389 |
+
},
|
| 390 |
+
"patterns": {
|
| 391 |
+
"ar": [
|
| 392 |
+
"ما\\s+(?:هي\\s+)?الواجبات",
|
| 393 |
+
"ما\\s+الواجب",
|
| 394 |
+
"ماذا\\s+يلزم"
|
| 395 |
+
],
|
| 396 |
+
"en": [
|
| 397 |
+
"what\\s+are\\s+the\\s+(?:duties|obligations)",
|
| 398 |
+
"what\\s+is\\s+required"
|
| 399 |
+
]
|
| 400 |
+
},
|
| 401 |
+
"evidence_patterns": {
|
| 402 |
+
"ar": [
|
| 403 |
+
"\\bواجبات?\\b",
|
| 404 |
+
"\\bيلزم\\b",
|
| 405 |
+
"\\bيجب\\b"
|
| 406 |
+
],
|
| 407 |
+
"en": [
|
| 408 |
+
"\\bduties\\b",
|
| 409 |
+
"\\bobligations\\b",
|
| 410 |
+
"\\brequired\\b"
|
| 411 |
+
]
|
| 412 |
+
}
|
| 413 |
+
},
|
| 414 |
+
"validity": {
|
| 415 |
+
"priority": 1.09,
|
| 416 |
+
"keywords": {
|
| 417 |
+
"ar": [
|
| 418 |
+
"صحة",
|
| 419 |
+
"صحيح",
|
| 420 |
+
"يصح",
|
| 421 |
+
"يبطل",
|
| 422 |
+
"فساد"
|
| 423 |
+
],
|
| 424 |
+
"en": [
|
| 425 |
+
"valid",
|
| 426 |
+
"validity",
|
| 427 |
+
"invalid",
|
| 428 |
+
"invalidate"
|
| 429 |
+
]
|
| 430 |
+
},
|
| 431 |
+
"patterns": {
|
| 432 |
+
"ar": [
|
| 433 |
+
"هل\\s+(?:يصح|يبطل)",
|
| 434 |
+
"ما\\s+حكم\\s+صحة",
|
| 435 |
+
"صحة\\s+"
|
| 436 |
+
],
|
| 437 |
+
"en": [
|
| 438 |
+
"is\\s+.+?\\s+valid",
|
| 439 |
+
"does\\s+.+?\\s+invalidate",
|
| 440 |
+
"validity\\s+of"
|
| 441 |
+
]
|
| 442 |
+
},
|
| 443 |
+
"evidence_patterns": {
|
| 444 |
+
"ar": [
|
| 445 |
+
"\\b(?:يصح|صحيح|لا يصح|باطل|يبطل|فسد|يفسد)\\b"
|
| 446 |
+
],
|
| 447 |
+
"en": [
|
| 448 |
+
"\\b(?:valid|invalid|invalidates|void)\\b"
|
| 449 |
+
]
|
| 450 |
+
}
|
| 451 |
+
},
|
| 452 |
+
"remedy": {
|
| 453 |
+
"priority": 1.08,
|
| 454 |
+
"keywords": {
|
| 455 |
+
"ar": [
|
| 456 |
+
"فدية",
|
| 457 |
+
"دم",
|
| 458 |
+
"كفارة",
|
| 459 |
+
"قضاء",
|
| 460 |
+
"ماذا يلزم"
|
| 461 |
+
],
|
| 462 |
+
"en": [
|
| 463 |
+
"fidyah",
|
| 464 |
+
"sacrifice",
|
| 465 |
+
"expiation",
|
| 466 |
+
"make up",
|
| 467 |
+
"remedy"
|
| 468 |
+
]
|
| 469 |
+
},
|
| 470 |
+
"patterns": {
|
| 471 |
+
"ar": [
|
| 472 |
+
"ما\\s+(?:يلزمه|يلزم|عليه)",
|
| 473 |
+
"ما\\s+(?:الفدية|الكفارة)",
|
| 474 |
+
"هل\\s+عليه\\s+(?:دم|فدية)"
|
| 475 |
+
],
|
| 476 |
+
"en": [
|
| 477 |
+
"what\\s+(?:is|becomes)\\s+due",
|
| 478 |
+
"what\\s+(?:fidyah|sacrifice|expiation)",
|
| 479 |
+
"does\\s+.+?\\s+owe"
|
| 480 |
+
]
|
| 481 |
+
},
|
| 482 |
+
"evidence_patterns": {
|
| 483 |
+
"ar": [
|
| 484 |
+
"\\b(?:فدية|دم|كفارة|قضاء|شاة|بدنة|صيام|اطعام)\\b"
|
| 485 |
+
],
|
| 486 |
+
"en": [
|
| 487 |
+
"\\b(?:fidyah|sacrifice|expiation|make-up|camel|sheep|fasting|feeding)\\b"
|
| 488 |
+
]
|
| 489 |
+
}
|
| 490 |
+
},
|
| 491 |
+
"timing": {
|
| 492 |
+
"priority": 1.08,
|
| 493 |
+
"keywords": {
|
| 494 |
+
"ar": [
|
| 495 |
+
"متى",
|
| 496 |
+
"وقت",
|
| 497 |
+
"يبدا",
|
| 498 |
+
"ينتهي"
|
| 499 |
+
],
|
| 500 |
+
"en": [
|
| 501 |
+
"when",
|
| 502 |
+
"time",
|
| 503 |
+
"begin",
|
| 504 |
+
"end"
|
| 505 |
+
]
|
| 506 |
+
},
|
| 507 |
+
"patterns": {
|
| 508 |
+
"ar": [
|
| 509 |
+
"^متى",
|
| 510 |
+
"ما\\s+(?:هو\\s+)?وقت",
|
| 511 |
+
"متى\\s+(?:يبدا|ينتهي)",
|
| 512 |
+
"الى\\s+متى"
|
| 513 |
+
],
|
| 514 |
+
"en": [
|
| 515 |
+
"^when",
|
| 516 |
+
"what\\s+time",
|
| 517 |
+
"when\\s+does\\s+.+?\\s+(?:begin|end)"
|
| 518 |
+
]
|
| 519 |
+
},
|
| 520 |
+
"evidence_patterns": {
|
| 521 |
+
"ar": [
|
| 522 |
+
"\\bوقت\\b",
|
| 523 |
+
"يبدا",
|
| 524 |
+
"ينتهي",
|
| 525 |
+
"من\\s+.+?\\s+الى"
|
| 526 |
+
],
|
| 527 |
+
"en": [
|
| 528 |
+
"\\btime\\b",
|
| 529 |
+
"begins",
|
| 530 |
+
"ends",
|
| 531 |
+
"from\\s+.+?\\s+until"
|
| 532 |
+
]
|
| 533 |
+
}
|
| 534 |
+
},
|
| 535 |
+
"amount": {
|
| 536 |
+
"priority": 1.04,
|
| 537 |
+
"keywords": {
|
| 538 |
+
"ar": [
|
| 539 |
+
"كم",
|
| 540 |
+
"عدد",
|
| 541 |
+
"مقدار"
|
| 542 |
+
],
|
| 543 |
+
"en": [
|
| 544 |
+
"how many",
|
| 545 |
+
"how much",
|
| 546 |
+
"number",
|
| 547 |
+
"amount"
|
| 548 |
+
]
|
| 549 |
+
},
|
| 550 |
+
"patterns": {
|
| 551 |
+
"ar": [
|
| 552 |
+
"^كم",
|
| 553 |
+
"ما\\s+(?:هو\\s+)?(?:عدد|مقدار)"
|
| 554 |
+
],
|
| 555 |
+
"en": [
|
| 556 |
+
"^how\\s+(?:many|much)",
|
| 557 |
+
"what\\s+(?:number|amount)"
|
| 558 |
+
]
|
| 559 |
+
},
|
| 560 |
+
"evidence_patterns": {
|
| 561 |
+
"ar": [
|
| 562 |
+
"\\b(?:عدد|مقدار)\\b",
|
| 563 |
+
"\\b(?:واحد|اثنان|ثلاثة|اربعة|خمسة|ستة|سبعة|ثمانية|تسعة|عشرة)\\b",
|
| 564 |
+
"\\d+"
|
| 565 |
+
],
|
| 566 |
+
"en": [
|
| 567 |
+
"\\b(?:number|amount)\\b",
|
| 568 |
+
"\\d+"
|
| 569 |
+
]
|
| 570 |
+
}
|
| 571 |
+
},
|
| 572 |
+
"location": {
|
| 573 |
+
"priority": 1.04,
|
| 574 |
+
"keywords": {
|
| 575 |
+
"ar": [
|
| 576 |
+
"اين",
|
| 577 |
+
"مكان",
|
| 578 |
+
"من اين"
|
| 579 |
+
],
|
| 580 |
+
"en": [
|
| 581 |
+
"where",
|
| 582 |
+
"location",
|
| 583 |
+
"place"
|
| 584 |
+
]
|
| 585 |
+
},
|
| 586 |
+
"patterns": {
|
| 587 |
+
"ar": [
|
| 588 |
+
"^اين",
|
| 589 |
+
"من\\s+اين",
|
| 590 |
+
"ما\\s+(?:هو\\s+)?مكان"
|
| 591 |
+
],
|
| 592 |
+
"en": [
|
| 593 |
+
"^where",
|
| 594 |
+
"from\\s+where",
|
| 595 |
+
"what\\s+place"
|
| 596 |
+
]
|
| 597 |
+
},
|
| 598 |
+
"evidence_patterns": {
|
| 599 |
+
"ar": [
|
| 600 |
+
"\\bمكان\\b",
|
| 601 |
+
"في\\s+.+",
|
| 602 |
+
"من\\s+.+"
|
| 603 |
+
],
|
| 604 |
+
"en": [
|
| 605 |
+
"\\bplace\\b",
|
| 606 |
+
"\\blocation\\b",
|
| 607 |
+
"at\\s+.+",
|
| 608 |
+
"from\\s+.+"
|
| 609 |
+
]
|
| 610 |
+
}
|
| 611 |
+
},
|
| 612 |
+
"cause": {
|
| 613 |
+
"priority": 1.04,
|
| 614 |
+
"keywords": {
|
| 615 |
+
"ar": [
|
| 616 |
+
"لماذا",
|
| 617 |
+
"سبب",
|
| 618 |
+
"علة",
|
| 619 |
+
"حكمة"
|
| 620 |
+
],
|
| 621 |
+
"en": [
|
| 622 |
+
"why",
|
| 623 |
+
"reason",
|
| 624 |
+
"cause",
|
| 625 |
+
"wisdom"
|
| 626 |
+
]
|
| 627 |
+
},
|
| 628 |
+
"patterns": {
|
| 629 |
+
"ar": [
|
| 630 |
+
"^لماذا",
|
| 631 |
+
"ما\\s+(?:هو\\s+)?(?:السبب|العلة|الحكمة)"
|
| 632 |
+
],
|
| 633 |
+
"en": [
|
| 634 |
+
"^why",
|
| 635 |
+
"what\\s+(?:is\\s+)?the\\s+(?:reason|cause|wisdom)"
|
| 636 |
+
]
|
| 637 |
+
},
|
| 638 |
+
"evidence_patterns": {
|
| 639 |
+
"ar": [
|
| 640 |
+
"\\b(?:سبب|علة|حكمة|لان|لأن)\\b"
|
| 641 |
+
],
|
| 642 |
+
"en": [
|
| 643 |
+
"\\b(?:reason|cause|because|wisdom)\\b"
|
| 644 |
+
]
|
| 645 |
+
}
|
| 646 |
+
},
|
| 647 |
+
"comparison": {
|
| 648 |
+
"priority": 1.06,
|
| 649 |
+
"keywords": {
|
| 650 |
+
"ar": [
|
| 651 |
+
"الفرق",
|
| 652 |
+
"قارن",
|
| 653 |
+
"مقارنة"
|
| 654 |
+
],
|
| 655 |
+
"en": [
|
| 656 |
+
"difference",
|
| 657 |
+
"compare",
|
| 658 |
+
"comparison"
|
| 659 |
+
]
|
| 660 |
+
},
|
| 661 |
+
"patterns": {
|
| 662 |
+
"ar": [
|
| 663 |
+
"ما\\s+الفرق",
|
| 664 |
+
"قارن\\s+بين",
|
| 665 |
+
"مقارنة\\s+بين"
|
| 666 |
+
],
|
| 667 |
+
"en": [
|
| 668 |
+
"what\\s+is\\s+the\\s+difference",
|
| 669 |
+
"compare\\s+",
|
| 670 |
+
"comparison\\s+between"
|
| 671 |
+
]
|
| 672 |
+
},
|
| 673 |
+
"evidence_patterns": {
|
| 674 |
+
"ar": [
|
| 675 |
+
"الفرق\\s+بين",
|
| 676 |
+
"اما\\s+.+?\\s+واما",
|
| 677 |
+
"ينقسم\\s+الى"
|
| 678 |
+
],
|
| 679 |
+
"en": [
|
| 680 |
+
"difference\\s+between",
|
| 681 |
+
"whereas",
|
| 682 |
+
"types\\s+are"
|
| 683 |
+
]
|
| 684 |
+
}
|
| 685 |
+
},
|
| 686 |
+
"procedure": {
|
| 687 |
+
"priority": 1.03,
|
| 688 |
+
"keywords": {
|
| 689 |
+
"ar": [
|
| 690 |
+
"كيف",
|
| 691 |
+
"صفة",
|
| 692 |
+
"طريقة",
|
| 693 |
+
"ماذا يفعل"
|
| 694 |
+
],
|
| 695 |
+
"en": [
|
| 696 |
+
"how",
|
| 697 |
+
"procedure",
|
| 698 |
+
"method",
|
| 699 |
+
"what should do"
|
| 700 |
+
]
|
| 701 |
+
},
|
| 702 |
+
"patterns": {
|
| 703 |
+
"ar": [
|
| 704 |
+
"^كيف",
|
| 705 |
+
"ما\\s+(?:هي\\s+)?صفة",
|
| 706 |
+
"ما\\s+(?:هي\\s+)?طريقة",
|
| 707 |
+
"ماذا\\s+يفعل"
|
| 708 |
+
],
|
| 709 |
+
"en": [
|
| 710 |
+
"^how",
|
| 711 |
+
"what\\s+is\\s+the\\s+(?:procedure|method)",
|
| 712 |
+
"what\\s+should\\s+.+?\\s+do"
|
| 713 |
+
]
|
| 714 |
+
},
|
| 715 |
+
"evidence_patterns": {
|
| 716 |
+
"ar": [
|
| 717 |
+
"ثم\\s+",
|
| 718 |
+
"يفعل",
|
| 719 |
+
"يبدا",
|
| 720 |
+
"يذهب",
|
| 721 |
+
"يرجع",
|
| 722 |
+
"يطوف",
|
| 723 |
+
"يسعى"
|
| 724 |
+
],
|
| 725 |
+
"en": [
|
| 726 |
+
"then",
|
| 727 |
+
"should do",
|
| 728 |
+
"begins",
|
| 729 |
+
"goes",
|
| 730 |
+
"returns",
|
| 731 |
+
"performs"
|
| 732 |
+
]
|
| 733 |
+
}
|
| 734 |
+
},
|
| 735 |
+
"evidence": {
|
| 736 |
+
"priority": 1.02,
|
| 737 |
+
"keywords": {
|
| 738 |
+
"ar": [
|
| 739 |
+
"دليل",
|
| 740 |
+
"نص",
|
| 741 |
+
"مصدر"
|
| 742 |
+
],
|
| 743 |
+
"en": [
|
| 744 |
+
"evidence",
|
| 745 |
+
"proof",
|
| 746 |
+
"source",
|
| 747 |
+
"text"
|
| 748 |
+
]
|
| 749 |
+
},
|
| 750 |
+
"patterns": {
|
| 751 |
+
"ar": [
|
| 752 |
+
"ما\\s+(?:هو\\s+)?الدليل",
|
| 753 |
+
"اذكر\\s+(?:النص|المصدر)"
|
| 754 |
+
],
|
| 755 |
+
"en": [
|
| 756 |
+
"what\\s+is\\s+the\\s+(?:evidence|proof)",
|
| 757 |
+
"cite\\s+the\\s+source"
|
| 758 |
+
]
|
| 759 |
+
},
|
| 760 |
+
"evidence_patterns": {
|
| 761 |
+
"ar": [
|
| 762 |
+
"قال\\s+الله",
|
| 763 |
+
"قال\\s+النبي",
|
| 764 |
+
"الدليل",
|
| 765 |
+
"نص\\s+"
|
| 766 |
+
],
|
| 767 |
+
"en": [
|
| 768 |
+
"evidence",
|
| 769 |
+
"proof",
|
| 770 |
+
"the text states",
|
| 771 |
+
"reported"
|
| 772 |
+
]
|
| 773 |
+
}
|
| 774 |
+
},
|
| 775 |
+
"exception": {
|
| 776 |
+
"priority": 1.02,
|
| 777 |
+
"keywords": {
|
| 778 |
+
"ar": [
|
| 779 |
+
"استثناء",
|
| 780 |
+
"الا",
|
| 781 |
+
"متى يسقط"
|
| 782 |
+
],
|
| 783 |
+
"en": [
|
| 784 |
+
"exception",
|
| 785 |
+
"unless",
|
| 786 |
+
"when waived"
|
| 787 |
+
]
|
| 788 |
+
},
|
| 789 |
+
"patterns": {
|
| 790 |
+
"ar": [
|
| 791 |
+
"ما\\s+(?:هو\\s+)?الاستثناء",
|
| 792 |
+
"متى\\s+يسقط",
|
| 793 |
+
"هل\\s+يستثنى"
|
| 794 |
+
],
|
| 795 |
+
"en": [
|
| 796 |
+
"what\\s+is\\s+the\\s+exception",
|
| 797 |
+
"when\\s+is\\s+.+?\\s+waived",
|
| 798 |
+
"is\\s+.+?\\s+excepted"
|
| 799 |
+
]
|
| 800 |
+
},
|
| 801 |
+
"evidence_patterns": {
|
| 802 |
+
"ar": [
|
| 803 |
+
"الا\\s+",
|
| 804 |
+
"يستثنى",
|
| 805 |
+
"يسقط",
|
| 806 |
+
"يعفى"
|
| 807 |
+
],
|
| 808 |
+
"en": [
|
| 809 |
+
"unless",
|
| 810 |
+
"except",
|
| 811 |
+
"waived",
|
| 812 |
+
"exempt"
|
| 813 |
+
]
|
| 814 |
+
}
|
| 815 |
+
},
|
| 816 |
+
"list": {
|
| 817 |
+
"priority": 0.95,
|
| 818 |
+
"keywords": {
|
| 819 |
+
"ar": [
|
| 820 |
+
"اذكر",
|
| 821 |
+
"عدد",
|
| 822 |
+
"ما هي"
|
| 823 |
+
],
|
| 824 |
+
"en": [
|
| 825 |
+
"list",
|
| 826 |
+
"enumerate",
|
| 827 |
+
"what are"
|
| 828 |
+
]
|
| 829 |
+
},
|
| 830 |
+
"patterns": {
|
| 831 |
+
"ar": [
|
| 832 |
+
"^اذكر",
|
| 833 |
+
"عدد\\s+",
|
| 834 |
+
"ما\\s+هي\\s+"
|
| 835 |
+
],
|
| 836 |
+
"en": [
|
| 837 |
+
"^list",
|
| 838 |
+
"^enumerate",
|
| 839 |
+
"what\\s+are\\s+the"
|
| 840 |
+
]
|
| 841 |
+
},
|
| 842 |
+
"evidence_patterns": {
|
| 843 |
+
"ar": [
|
| 844 |
+
"(?:هي|وهي|خمسة|اربعة|ثلاثة|ستة|سبعة|ثمانية|تسعة|عشرة)\\s*[::]"
|
| 845 |
+
],
|
| 846 |
+
"en": [
|
| 847 |
+
"(?:are|include|consist of)\\s*[::]"
|
| 848 |
+
]
|
| 849 |
+
}
|
| 850 |
+
},
|
| 851 |
+
"components": {
|
| 852 |
+
"priority": 0.98,
|
| 853 |
+
"keywords": {
|
| 854 |
+
"ar": [
|
| 855 |
+
"عناصر",
|
| 856 |
+
"اجزاء",
|
| 857 |
+
"مكونات"
|
| 858 |
+
],
|
| 859 |
+
"en": [
|
| 860 |
+
"components",
|
| 861 |
+
"elements",
|
| 862 |
+
"parts"
|
| 863 |
+
]
|
| 864 |
+
},
|
| 865 |
+
"patterns": {
|
| 866 |
+
"ar": [
|
| 867 |
+
"ما\\s+(?:هي\\s+)?(?:العناصر|الاجزاء|المكونات)"
|
| 868 |
+
],
|
| 869 |
+
"en": [
|
| 870 |
+
"what\\s+are\\s+the\\s+(?:components|elements|parts)"
|
| 871 |
+
]
|
| 872 |
+
},
|
| 873 |
+
"evidence_patterns": {
|
| 874 |
+
"ar": [
|
| 875 |
+
"(?:العناصر|الاجزاء|المكونات)"
|
| 876 |
+
],
|
| 877 |
+
"en": [
|
| 878 |
+
"(?:components|elements|parts)"
|
| 879 |
+
]
|
| 880 |
+
}
|
| 881 |
+
},
|
| 882 |
+
"description": {
|
| 883 |
+
"priority": 0.82,
|
| 884 |
+
"keywords": {
|
| 885 |
+
"ar": [
|
| 886 |
+
"صف",
|
| 887 |
+
"بيان",
|
| 888 |
+
"ما هو"
|
| 889 |
+
],
|
| 890 |
+
"en": [
|
| 891 |
+
"describe",
|
| 892 |
+
"explain",
|
| 893 |
+
"what is"
|
| 894 |
+
]
|
| 895 |
+
},
|
| 896 |
+
"patterns": {
|
| 897 |
+
"ar": [
|
| 898 |
+
"^صف",
|
| 899 |
+
"^بين",
|
| 900 |
+
"^ما\\s+هو"
|
| 901 |
+
],
|
| 902 |
+
"en": [
|
| 903 |
+
"^describe",
|
| 904 |
+
"^explain",
|
| 905 |
+
"^what\\s+is"
|
| 906 |
+
]
|
| 907 |
+
},
|
| 908 |
+
"evidence_patterns": {
|
| 909 |
+
"ar": [
|
| 910 |
+
"بيان",
|
| 911 |
+
"صفة",
|
| 912 |
+
"وصف"
|
| 913 |
+
],
|
| 914 |
+
"en": [
|
| 915 |
+
"description",
|
| 916 |
+
"explains",
|
| 917 |
+
"is a"
|
| 918 |
+
]
|
| 919 |
+
}
|
| 920 |
+
},
|
| 921 |
+
"ruling": {
|
| 922 |
+
"priority": 1.0,
|
| 923 |
+
"keywords": {
|
| 924 |
+
"ar": [
|
| 925 |
+
"حكم",
|
| 926 |
+
"يجوز",
|
| 927 |
+
"يجب",
|
| 928 |
+
"يحرم",
|
| 929 |
+
"حكم",
|
| 930 |
+
"الحكم"
|
| 931 |
+
],
|
| 932 |
+
"en": [
|
| 933 |
+
"ruling",
|
| 934 |
+
"permissible",
|
| 935 |
+
"obligatory",
|
| 936 |
+
"prohibited"
|
| 937 |
+
]
|
| 938 |
+
},
|
| 939 |
+
"patterns": {
|
| 940 |
+
"ar": [
|
| 941 |
+
"ما\\s+(?:هو\\s+)?الحكم",
|
| 942 |
+
"ما\\s+حكم",
|
| 943 |
+
"هل\\s+(?:يجوز|يجب|يحرم|يلزم)",
|
| 944 |
+
"ماذا\\s+يترتب"
|
| 945 |
+
],
|
| 946 |
+
"en": [
|
| 947 |
+
"what\\s+is\\s+the\\s+ruling",
|
| 948 |
+
"is\\s+.+?\\s+(?:permissible|obligatory|prohibited)",
|
| 949 |
+
"what\\s+follows\\s+if"
|
| 950 |
+
]
|
| 951 |
+
},
|
| 952 |
+
"evidence_patterns": {
|
| 953 |
+
"ar": [
|
| 954 |
+
"\\b(?:يجوز|لا يجوز|يجب|لا يجب|يحرم|واجب|جائز|يسقط|يلزم|لا يلزم|وجهان|قولان|روايتان)\\b"
|
| 955 |
+
],
|
| 956 |
+
"en": [
|
| 957 |
+
"\\b(?:permissible|not permissible|obligatory|not obligatory|prohibited|required|waived|two views|disputed)\\b"
|
| 958 |
+
]
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
},
|
| 962 |
+
"outcomes": {
|
| 963 |
+
"obligatory": {
|
| 964 |
+
"decisiveness": 0.94,
|
| 965 |
+
"patterns": {
|
| 966 |
+
"ar": [
|
| 967 |
+
"(?:^|\\s)(?:يجب|واجب|يلزم|لازم)(?:\\s|$)"
|
| 968 |
+
],
|
| 969 |
+
"en": [
|
| 970 |
+
"\\b(?:obligatory|required|must|is due)\\b"
|
| 971 |
+
]
|
| 972 |
+
}
|
| 973 |
+
},
|
| 974 |
+
"not_obligatory": {
|
| 975 |
+
"decisiveness": 0.96,
|
| 976 |
+
"patterns": {
|
| 977 |
+
"ar": [
|
| 978 |
+
"(?:لا\\s+يجب|لا\\s+يلزم|غير\\s+واجب)"
|
| 979 |
+
],
|
| 980 |
+
"en": [
|
| 981 |
+
"\\b(?:not obligatory|not required|does not have to)\\b"
|
| 982 |
+
]
|
| 983 |
+
}
|
| 984 |
+
},
|
| 985 |
+
"obligation_dropped": {
|
| 986 |
+
"decisiveness": 0.98,
|
| 987 |
+
"patterns": {
|
| 988 |
+
"ar": [
|
| 989 |
+
"(?:يسقط|سقط).*?(?:الوجوب|وجوب)",
|
| 990 |
+
"يعفى.*?(?:الوجوب|الواجب)"
|
| 991 |
+
],
|
| 992 |
+
"en": [
|
| 993 |
+
"(?:obligation|duty).*?(?:is dropped|falls away|is waived)",
|
| 994 |
+
"no longer obligatory"
|
| 995 |
+
]
|
| 996 |
+
}
|
| 997 |
+
},
|
| 998 |
+
"permissible": {
|
| 999 |
+
"decisiveness": 0.94,
|
| 1000 |
+
"patterns": {
|
| 1001 |
+
"ar": [
|
| 1002 |
+
"(?:^|\\s)(?:يجوز|جائز|مباح)(?:\\s|$)"
|
| 1003 |
+
],
|
| 1004 |
+
"en": [
|
| 1005 |
+
"\\b(?:permissible|allowed|may)\\b"
|
| 1006 |
+
]
|
| 1007 |
+
}
|
| 1008 |
+
},
|
| 1009 |
+
"prohibited": {
|
| 1010 |
+
"decisiveness": 0.96,
|
| 1011 |
+
"patterns": {
|
| 1012 |
+
"ar": [
|
| 1013 |
+
"(?:لا\\s+يجوز|يحرم|حرام|محظور)"
|
| 1014 |
+
],
|
| 1015 |
+
"en": [
|
| 1016 |
+
"\\b(?:not permissible|prohibited|forbidden)\\b"
|
| 1017 |
+
]
|
| 1018 |
+
}
|
| 1019 |
+
},
|
| 1020 |
+
"valid": {
|
| 1021 |
+
"decisiveness": 0.93,
|
| 1022 |
+
"patterns": {
|
| 1023 |
+
"ar": [
|
| 1024 |
+
"(?:يصح|صحيح|اجزا|مجزئ)"
|
| 1025 |
+
],
|
| 1026 |
+
"en": [
|
| 1027 |
+
"\\b(?:valid|validly performed|sufficient)\\b"
|
| 1028 |
+
]
|
| 1029 |
+
}
|
| 1030 |
+
},
|
| 1031 |
+
"invalid": {
|
| 1032 |
+
"decisiveness": 0.97,
|
| 1033 |
+
"patterns": {
|
| 1034 |
+
"ar": [
|
| 1035 |
+
"(?:لا\\s+يصح|باطل|يبطل|فسد|يفسد)"
|
| 1036 |
+
],
|
| 1037 |
+
"en": [
|
| 1038 |
+
"\\b(?:invalid|void|invalidates)\\b"
|
| 1039 |
+
]
|
| 1040 |
+
}
|
| 1041 |
+
},
|
| 1042 |
+
"recommended": {
|
| 1043 |
+
"decisiveness": 0.87,
|
| 1044 |
+
"patterns": {
|
| 1045 |
+
"ar": [
|
| 1046 |
+
"(?:مستحب|سنة|مندوب)"
|
| 1047 |
+
],
|
| 1048 |
+
"en": [
|
| 1049 |
+
"\\b(?:recommended|sunnah)\\b"
|
| 1050 |
+
]
|
| 1051 |
+
}
|
| 1052 |
+
},
|
| 1053 |
+
"disliked": {
|
| 1054 |
+
"decisiveness": 0.86,
|
| 1055 |
+
"patterns": {
|
| 1056 |
+
"ar": [
|
| 1057 |
+
"(?:مكروه|كراهة)"
|
| 1058 |
+
],
|
| 1059 |
+
"en": [
|
| 1060 |
+
"\\b(?:disliked|makruh)\\b"
|
| 1061 |
+
]
|
| 1062 |
+
}
|
| 1063 |
+
},
|
| 1064 |
+
"remedy_required": {
|
| 1065 |
+
"decisiveness": 0.92,
|
| 1066 |
+
"patterns": {
|
| 1067 |
+
"ar": [
|
| 1068 |
+
"(?:عليه|يلزمه|يجب عليه).*?(?:دم|فدية|كفارة|قضاء|صيام|اطعام|شاة|بدنة)"
|
| 1069 |
+
],
|
| 1070 |
+
"en": [
|
| 1071 |
+
"(?:owes|must offer|is required).*?(?:fidyah|sacrifice|expiation|make up|fasting|feeding)"
|
| 1072 |
+
]
|
| 1073 |
+
}
|
| 1074 |
+
},
|
| 1075 |
+
"no_remedy": {
|
| 1076 |
+
"decisiveness": 0.93,
|
| 1077 |
+
"patterns": {
|
| 1078 |
+
"ar": [
|
| 1079 |
+
"(?:لا\\s+شيء\\s+عليه|لا\\s+دم\\s+عليه|لا\\s+فدية)"
|
| 1080 |
+
],
|
| 1081 |
+
"en": [
|
| 1082 |
+
"(?:nothing is due|no sacrifice is due|no fidyah)"
|
| 1083 |
+
]
|
| 1084 |
+
}
|
| 1085 |
+
},
|
| 1086 |
+
"disputed": {
|
| 1087 |
+
"decisiveness": 0.36,
|
| 1088 |
+
"patterns": {
|
| 1089 |
+
"ar": [
|
| 1090 |
+
"(?:وجهان|قولان|روايتان|فيه\\s+خلاف|اختلف)"
|
| 1091 |
+
],
|
| 1092 |
+
"en": [
|
| 1093 |
+
"(?:two views|two opinions|two reports|disputed|scholars differ)"
|
| 1094 |
+
]
|
| 1095 |
+
}
|
| 1096 |
+
},
|
| 1097 |
+
"condition": {
|
| 1098 |
+
"decisiveness": 0.78,
|
| 1099 |
+
"patterns": {
|
| 1100 |
+
"ar": [
|
| 1101 |
+
"(?:شرط|يشترط|من\\s+شروط)"
|
| 1102 |
+
],
|
| 1103 |
+
"en": [
|
| 1104 |
+
"\\b(?:condition|prerequisite|required condition)\\b"
|
| 1105 |
+
]
|
| 1106 |
+
}
|
| 1107 |
+
},
|
| 1108 |
+
"pillar": {
|
| 1109 |
+
"decisiveness": 0.82,
|
| 1110 |
+
"patterns": {
|
| 1111 |
+
"ar": [
|
| 1112 |
+
"(?:ركن|من\\s+اركان)"
|
| 1113 |
+
],
|
| 1114 |
+
"en": [
|
| 1115 |
+
"\\b(?:pillar|essential pillar)\\b"
|
| 1116 |
+
]
|
| 1117 |
+
}
|
| 1118 |
+
}
|
| 1119 |
+
},
|
| 1120 |
+
"incompatible_outcomes": [
|
| 1121 |
+
[
|
| 1122 |
+
"obligatory",
|
| 1123 |
+
"not_obligatory"
|
| 1124 |
+
],
|
| 1125 |
+
[
|
| 1126 |
+
"obligatory",
|
| 1127 |
+
"obligation_dropped"
|
| 1128 |
+
],
|
| 1129 |
+
[
|
| 1130 |
+
"permissible",
|
| 1131 |
+
"prohibited"
|
| 1132 |
+
],
|
| 1133 |
+
[
|
| 1134 |
+
"valid",
|
| 1135 |
+
"invalid"
|
| 1136 |
+
],
|
| 1137 |
+
[
|
| 1138 |
+
"remedy_required",
|
| 1139 |
+
"no_remedy"
|
| 1140 |
+
]
|
| 1141 |
+
]
|
| 1142 |
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Dict, Iterable, List, Sequence, Set, Tuple
|
| 5 |
+
|
| 6 |
+
from .text import TextProcessor
|
| 7 |
+
from .types import ConsensusResult, QueryFrame, ScoredEvidence
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AnswerSynthesizer:
|
| 11 |
+
def __init__(self, text: TextProcessor, semantic: dict, templates: dict, ranking: dict):
|
| 12 |
+
self.text = text
|
| 13 |
+
self.semantic = semantic
|
| 14 |
+
self.templates = templates
|
| 15 |
+
self.ranking = ranking
|
| 16 |
+
|
| 17 |
+
@staticmethod
|
| 18 |
+
def _clean(value: str) -> str:
|
| 19 |
+
return re.sub(r"\s+", " ", str(value or "")).strip()
|
| 20 |
+
|
| 21 |
+
def _source_label(self, item: ScoredEvidence, lang: str) -> str:
|
| 22 |
+
book = item.evidence.book or ("مصدر غير مسمى" if lang == "ar" else "Unnamed source")
|
| 23 |
+
page = item.evidence.page
|
| 24 |
+
return f"{book} (ص {page})" if lang == "ar" and page else (f"{book} (p. {page})" if page else book)
|
| 25 |
+
|
| 26 |
+
def _strip_preamble(self, text: str, lang: str) -> str:
|
| 27 |
+
value = self._clean(text)
|
| 28 |
+
patterns = self.templates.get("preamble_patterns", {}).get(lang, []) or []
|
| 29 |
+
for pattern in patterns:
|
| 30 |
+
value = re.sub(pattern, "", value, flags=re.I).strip(" .:؛-")
|
| 31 |
+
markers = self.templates.get("preferred_clause_markers", {}).get(lang, []) or []
|
| 32 |
+
for marker in markers:
|
| 33 |
+
match = re.search(marker, value, re.I)
|
| 34 |
+
if match and match.end() < len(value):
|
| 35 |
+
candidate = value[match.end():].strip(" .:؛-")
|
| 36 |
+
if len(candidate) >= 12:
|
| 37 |
+
value = candidate
|
| 38 |
+
return value
|
| 39 |
+
|
| 40 |
+
def _sentence_score(self, sentence: str, query: QueryFrame, item: ScoredEvidence) -> float:
|
| 41 |
+
sentence_terms = self.text.content_terms(sentence, query.language)
|
| 42 |
+
critical = self.text.fuzzy_term_overlap(query.critical_terms, sentence_terms)
|
| 43 |
+
subject = self.text.fuzzy_term_overlap(query.subject_terms, sentence_terms)
|
| 44 |
+
outcome_bonus = 0.0
|
| 45 |
+
normalized = self.text.normalize(sentence, query.language)
|
| 46 |
+
for outcome in item.evidence.outcomes:
|
| 47 |
+
for pattern in self.semantic.get("outcomes", {}).get(outcome, {}).get("patterns", {}).get(query.language, []) or []:
|
| 48 |
+
if self.text.phrase_hit(normalized, pattern):
|
| 49 |
+
outcome_bonus = max(outcome_bonus, 1.0)
|
| 50 |
+
return 0.43 * critical + 0.27 * subject + 0.22 * outcome_bonus + 0.08 * min(1.0, len(sentence) / 100.0)
|
| 51 |
+
|
| 52 |
+
def _best_extract(self, query: QueryFrame, item: ScoredEvidence) -> str:
|
| 53 |
+
candidates: List[str] = []
|
| 54 |
+
source = item.source
|
| 55 |
+
preferred_fields = self.templates.get("preferred_answer_fields", [])
|
| 56 |
+
for field in preferred_fields:
|
| 57 |
+
value = self._clean(source.get(field, ""))
|
| 58 |
+
if value:
|
| 59 |
+
candidates.extend(self.text.sentences(self._strip_preamble(value, query.language)) or (value,))
|
| 60 |
+
if not candidates:
|
| 61 |
+
candidates.extend(item.evidence.sentences)
|
| 62 |
+
ranked = sorted(candidates, key=lambda sentence: self._sentence_score(sentence, query, item), reverse=True)
|
| 63 |
+
return self._strip_preamble(ranked[0], query.language) if ranked else ""
|
| 64 |
+
|
| 65 |
+
def _dedupe_fragments(self, values: Iterable[str], lang: str) -> List[str]:
|
| 66 |
+
result: List[str] = []
|
| 67 |
+
normalized: List[str] = []
|
| 68 |
+
for value in values:
|
| 69 |
+
clean = self._clean(value).strip(" .؛،,-")
|
| 70 |
+
if lang == "ar":
|
| 71 |
+
clean = re.sub(r"^(?:و|ف|ثم|او|أو)\s*", "", clean).strip()
|
| 72 |
+
norm = self.text.normalize(clean, lang)
|
| 73 |
+
if len(norm) < 2:
|
| 74 |
+
continue
|
| 75 |
+
duplicate = False
|
| 76 |
+
for existing in normalized:
|
| 77 |
+
similarity = self.text.fuzzy_term_overlap(self.text.content_terms(norm, lang), self.text.content_terms(existing, lang))
|
| 78 |
+
if norm == existing or (similarity >= 0.90 and min(len(norm), len(existing)) / max(len(norm), len(existing)) >= 0.65):
|
| 79 |
+
duplicate = True
|
| 80 |
+
break
|
| 81 |
+
if not duplicate:
|
| 82 |
+
normalized.append(norm)
|
| 83 |
+
result.append(clean)
|
| 84 |
+
return result
|
| 85 |
+
|
| 86 |
+
def _list_answer(self, query: QueryFrame, selected: Sequence[ScoredEvidence]) -> str:
|
| 87 |
+
items: List[str] = []
|
| 88 |
+
for scored in selected:
|
| 89 |
+
raw_items = list(scored.evidence.list_items)
|
| 90 |
+
if not raw_items:
|
| 91 |
+
extract = self._best_extract(query, scored)
|
| 92 |
+
raw_items = list(self.text.split_list_items(extract, query.language))
|
| 93 |
+
items.extend(raw_items)
|
| 94 |
+
items = self._dedupe_fragments(items, query.language)
|
| 95 |
+
# Filter obvious preamble fragments and retain concise propositions.
|
| 96 |
+
filtered = []
|
| 97 |
+
for item in items:
|
| 98 |
+
normalized = self.text.normalize(item, query.language)
|
| 99 |
+
if any(self.text.phrase_hit(normalized, pattern) for pattern in self.templates.get("drop_item_patterns", {}).get(query.language, []) or []):
|
| 100 |
+
continue
|
| 101 |
+
if 1 <= len(normalized.split()) <= 18:
|
| 102 |
+
filtered.append(item)
|
| 103 |
+
items = filtered[: int(self.ranking.get("limits", {}).get("max_list_items", 18))]
|
| 104 |
+
if len(items) < 2:
|
| 105 |
+
return self._best_extract(query, selected[0])
|
| 106 |
+
bullet = "\n".join(f"- {item}" for item in items)
|
| 107 |
+
heading = self.templates.get("list_heading", {}).get(query.language, "Answer:")
|
| 108 |
+
return f"{heading}\n{bullet}"
|
| 109 |
+
|
| 110 |
+
def _conflict_answer(self, query: QueryFrame, consensus: ConsensusResult) -> str:
|
| 111 |
+
labels: List[str] = []
|
| 112 |
+
for key, cluster in sorted(consensus.clusters.items(), key=lambda pair: float(pair[1]["weight"]), reverse=True):
|
| 113 |
+
items = sorted(cluster["items"], key=lambda item: item.score, reverse=True)
|
| 114 |
+
if not items:
|
| 115 |
+
continue
|
| 116 |
+
extract = self._best_extract(query, items[0])
|
| 117 |
+
sources = "، ".join(self._source_label(item, query.language) for item in items[:3])
|
| 118 |
+
if query.language == "ar":
|
| 119 |
+
labels.append(f"- **قول أو اتجاه:** {extract}\n **مصادره:** {sources}")
|
| 120 |
+
else:
|
| 121 |
+
labels.append(f"- **View:** {extract}\n **Sources:** {sources}")
|
| 122 |
+
intro = self.templates.get("conflict_intro", {}).get(query.language, "")
|
| 123 |
+
return intro + "\n" + "\n".join(labels)
|
| 124 |
+
|
| 125 |
+
def synthesize(self, query: QueryFrame, consensus: ConsensusResult, *, style: str = "detailed", compare_sources: bool = True) -> str:
|
| 126 |
+
selected = consensus.selected
|
| 127 |
+
if not selected:
|
| 128 |
+
return self.templates.get("insufficient", {}).get(query.language, "")
|
| 129 |
+
if consensus.state == "conflict":
|
| 130 |
+
body = self._conflict_answer(query, consensus)
|
| 131 |
+
elif query.primary_request_type in {"conditions", "list", "components", "pillars", "duties"}:
|
| 132 |
+
body = self._list_answer(query, selected)
|
| 133 |
+
elif query.primary_request_type == "comparison":
|
| 134 |
+
parts = []
|
| 135 |
+
for item in selected[:5]:
|
| 136 |
+
extract = self._best_extract(query, item)
|
| 137 |
+
if extract:
|
| 138 |
+
parts.append(f"- **{self._source_label(item, query.language)}:** {extract}")
|
| 139 |
+
body = (self.templates.get("comparison_heading", {}).get(query.language, "") + "\n" + "\n".join(parts)).strip()
|
| 140 |
+
else:
|
| 141 |
+
extracts = self._dedupe_fragments((self._best_extract(query, item) for item in selected[:4]), query.language)
|
| 142 |
+
if not extracts:
|
| 143 |
+
return self.templates.get("insufficient", {}).get(query.language, "")
|
| 144 |
+
body = extracts[0]
|
| 145 |
+
if style != "short" and len(extracts) > 1:
|
| 146 |
+
complementary = [value for value in extracts[1:] if value not in body and len(value) >= 12]
|
| 147 |
+
if complementary:
|
| 148 |
+
body += "\n\n" + "\n".join(f"- {value}" for value in complementary[:3])
|
| 149 |
+
prefix = self.templates.get("answer_prefix", {}).get(query.language, "")
|
| 150 |
+
body = prefix + body
|
| 151 |
+
|
| 152 |
+
source_labels = []
|
| 153 |
+
seen: Set[str] = set()
|
| 154 |
+
for item in selected:
|
| 155 |
+
key = item.evidence.book_id or item.evidence.book or item.evidence.record_id
|
| 156 |
+
if key in seen:
|
| 157 |
+
continue
|
| 158 |
+
seen.add(key)
|
| 159 |
+
source_labels.append(self._source_label(item, query.language))
|
| 160 |
+
source_heading = self.templates.get("sources_heading", {}).get(query.language, "Sources")
|
| 161 |
+
body += f"\n\n**{source_heading}:** " + "، ".join(source_labels)
|
| 162 |
+
|
| 163 |
+
if consensus.dissent and compare_sources:
|
| 164 |
+
dissent_labels = []
|
| 165 |
+
seen_dissent: Set[str] = set()
|
| 166 |
+
for item in consensus.dissent:
|
| 167 |
+
key = item.evidence.book_id or item.evidence.book or item.evidence.record_id
|
| 168 |
+
if key in seen_dissent:
|
| 169 |
+
continue
|
| 170 |
+
seen_dissent.add(key)
|
| 171 |
+
extract = self._best_extract(query, item)
|
| 172 |
+
if extract:
|
| 173 |
+
dissent_labels.append(f"{self._source_label(item, query.language)}: {extract}")
|
| 174 |
+
if len(dissent_labels) >= 3:
|
| 175 |
+
break
|
| 176 |
+
if dissent_labels:
|
| 177 |
+
heading = self.templates.get("dissent_heading", {}).get(query.language, "Other formulations")
|
| 178 |
+
body += f"\n\n**{heading}:**\n" + "\n".join(f"- {value}" for value in dissent_labels)
|
| 179 |
+
return body.strip()
|
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
from hudanet_core import GenericEvidencePipeline
|
| 5 |
+
|
| 6 |
+
ROOT = Path(__file__).resolve().parents[1] / "resources"
|
| 7 |
+
pipeline = GenericEvidencePipeline(ROOT)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def src(record_id, book, title, question, ruling, answer, score=.7, dense=.8, cross=.7, **extra):
|
| 11 |
+
value = {
|
| 12 |
+
"record_id": record_id,
|
| 13 |
+
"book_id": book,
|
| 14 |
+
"book": book,
|
| 15 |
+
"title": title,
|
| 16 |
+
"question": question,
|
| 17 |
+
"ruling": ruling,
|
| 18 |
+
"answer": answer,
|
| 19 |
+
"score": score,
|
| 20 |
+
"dense_score": dense,
|
| 21 |
+
"cross_encoder_score": cross,
|
| 22 |
+
"source_kind": "نسخة منظفة معتمدة",
|
| 23 |
+
}
|
| 24 |
+
value.update(extra)
|
| 25 |
+
return value
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def assert_true(condition, label, value=None):
|
| 29 |
+
if not condition:
|
| 30 |
+
raise AssertionError(f"{label}: {value!r}")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def run():
|
| 34 |
+
checks = []
|
| 35 |
+
definition = src("definition", "الفروع", "معنى الحج لغة وشرعا", "ما معنى الحج؟", "تعريف", "الحج لغة القصد، وشرعا قصد مكة للنسك.", .98, .99, .95)
|
| 36 |
+
conditions = src("conditions", "دليل الطالب", "شروط وجوب الحج", "ما شروط وجوب الحج؟", "شروط", "شروط وجوب الحج: الإسلام، والعقل، والبلوغ، وكمال الحرية، والاستطاعة.")
|
| 37 |
+
result = pipeline.resolve("ما هي الشروط التي يجب توفرها لوجوب الحج؟", [definition, conditions], "ar")
|
| 38 |
+
checks.append(("definition rejected for conditions", not next(x for x in result.ranked if x.evidence.record_id == "definition").accepted))
|
| 39 |
+
checks.append(("conditions selected", result.consensus.selected[0].evidence.record_id == "conditions"))
|
| 40 |
+
checks.append(("conditions list", all(term in result.answer for term in ("الإسلام", "العقل", "البلوغ", "الاستطاعة"))))
|
| 41 |
+
|
| 42 |
+
extensions = [
|
| 43 |
+
src("road", "الإقناع", "أمن الطريق من الاستطاعة", "هل أمن الطريق شرط؟", "شرط", "يشترط في الاستطاعة أمن الطريق."),
|
| 44 |
+
src("time", "الكافي", "سعة الوقت", "ما حكم ضيق الوقت؟", "شرط", "ومن شروط الاستطاعة سعة الوقت للوصول وأداء النسك."),
|
| 45 |
+
src("money", "المنتهى", "الديون والنفقات", "ما ضابط المال؟", "شرط", "يشترط أن يفضل المال عن الديون والنفقات الشرعية والحوائج الأصلية."),
|
| 46 |
+
]
|
| 47 |
+
result = pipeline.resolve("ما هي الشروط التي يجب توفرها لوجوب الحج؟", [conditions] + extensions, "ar")
|
| 48 |
+
checks.append(("multi-source conditions merge", all(term in result.answer for term in ("أمن الطريق", "سعة الوقت", "الديون", "النفقات", "الحوائج الأصلية"))))
|
| 49 |
+
checks.append(("all selected sources named", all(book in result.answer for book in ("دليل الطالب", "الإقناع", "الكافي", "المنتهى"))))
|
| 50 |
+
|
| 51 |
+
decisive = src("dropped", "الروض المربع", "عجز من لا يجد نائبا", "ما حكم من لم يجد نائبا؟", "يسقط الوجوب", "إذا لم يجد الشخص نائبا للحج عنه، يسقط عنه وجوب الحج.", .55, .82, .71)
|
| 52 |
+
disputed = src("views", "الفروع", "من لم يجد نائبا", "من لم يجد نائبا للحج", "وجهان", "إن وجد مالا ولم يجد نائبا ففي وجوب الحج في ذمته وجهان.", .95, .98, .9)
|
| 53 |
+
result = pipeline.resolve("ما هو الحكم إذا لم يجد الشخص نائبا للحج عنه؟", [disputed, decisive], "ar")
|
| 54 |
+
checks.append(("decisive beats disputed", result.consensus.selected[0].evidence.record_id == "dropped"))
|
| 55 |
+
checks.append(("direct ruling in answer", "يسقط" in result.answer))
|
| 56 |
+
checks.append(("dissent stays visible", "وجهان" in result.answer and "الفروع" in result.answer))
|
| 57 |
+
|
| 58 |
+
permitted = src("p", "كتاب أ", "حكم الفعل", "هل يجوز الفعل؟", "جائز", "يجوز هذا الفعل عند تحقق شروطه.")
|
| 59 |
+
prohibited = src("x", "كتاب ب", "حكم الفعل", "هل يجوز الفعل؟", "محرم", "لا يجوز هذا الفعل وهو محرم.")
|
| 60 |
+
result = pipeline.resolve("هل يجوز هذا الفعل؟", [permitted, prohibited], "ar")
|
| 61 |
+
checks.append(("strong incompatible outcomes conflict", result.consensus.state == "conflict"))
|
| 62 |
+
checks.append(("conflict sources separated", "كتاب أ" in result.answer and "كتاب ب" in result.answer))
|
| 63 |
+
|
| 64 |
+
unrelated = src("u", "كتاب ج", "فضل العبادة", "ما فضلها؟", "فضيلة", "هذه العبادة عظيمة الأجر.", .99, .99, .99)
|
| 65 |
+
result = pipeline.resolve("متى يبدأ وقت هذا النسك؟", [unrelated], "ar")
|
| 66 |
+
checks.append(("unrelated high neural score rejected", not result.accepted))
|
| 67 |
+
|
| 68 |
+
en_def = src("en_d", "Book D", "Definition of Hajj", "What does Hajj mean?", "Definition", "Hajj means intending Makkah for the rites.")
|
| 69 |
+
en_cond = src("en_c", "Book C", "Conditions of Hajj", "What are the conditions?", "Conditions", "The conditions are Islam, sanity, puberty, freedom, and capability.")
|
| 70 |
+
result = pipeline.resolve("What are the conditions for the obligation of Hajj?", [en_def, en_cond], "en")
|
| 71 |
+
checks.append(("english type isolation", result.consensus.selected and result.consensus.selected[0].evidence.record_id == "en_c"))
|
| 72 |
+
|
| 73 |
+
search = {"exact": [definition], "related": [], "distant": [conditions], "stats": {}}
|
| 74 |
+
gated = pipeline.annotate_and_rebucket("ما شروط وجوب الحج؟", search, "ar")
|
| 75 |
+
checks.append(("compatible distant promoted", any(x["record_id"] == "conditions" for x in gated["related"] + gated["exact"])))
|
| 76 |
+
checks.append(("incompatible exact demoted", any(x["record_id"] == "definition" for x in gated["distant"])))
|
| 77 |
+
|
| 78 |
+
failed = [label for label, passed in checks if not passed]
|
| 79 |
+
assert_true(not failed, "failed checks", failed)
|
| 80 |
+
print(json.dumps({"passed": True, "tested": len(checks), "checks": checks}, ensure_ascii=False, indent=2))
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
run()
|
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import unicodedata
|
| 5 |
+
from difflib import SequenceMatcher
|
| 6 |
+
from typing import Any, Callable, Iterable, List, Sequence, Set, Tuple
|
| 7 |
+
|
| 8 |
+
_AR_DIACRITICS = re.compile(r"[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06ED]")
|
| 9 |
+
_TOKEN_RE = re.compile(r"[\w\u0600-\u06FF]+", re.UNICODE)
|
| 10 |
+
_SENTENCE_RE = re.compile(r"(?<=[.!؟?؛;])\s+|\n+")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TextProcessor:
|
| 14 |
+
def __init__(self, semantic: dict, normalizers: dict[str, Callable[[Any], str]] | None = None):
|
| 15 |
+
self.semantic = semantic
|
| 16 |
+
self.normalizers = normalizers or {}
|
| 17 |
+
self.stopwords = {
|
| 18 |
+
lang: set(values or []) for lang, values in semantic.get("stopwords", {}).items()
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
def normalize(self, value: Any, lang: str) -> str:
|
| 22 |
+
callback = self.normalizers.get(lang)
|
| 23 |
+
if callable(callback):
|
| 24 |
+
return re.sub(r"\s+", " ", callback(value)).strip()
|
| 25 |
+
text = unicodedata.normalize("NFKC", str(value or "")).replace("ـ", "")
|
| 26 |
+
if lang == "ar":
|
| 27 |
+
text = _AR_DIACRITICS.sub("", text)
|
| 28 |
+
text = text.translate(str.maketrans({
|
| 29 |
+
"أ": "ا", "إ": "ا", "آ": "ا", "ٱ": "ا", "ى": "ي",
|
| 30 |
+
"ؤ": "و", "ئ": "ي", "ک": "ك", "ی": "ي", "ۀ": "ة",
|
| 31 |
+
}))
|
| 32 |
+
text = text.casefold()
|
| 33 |
+
text = re.sub(r"[؟،؛:!.,\-_/\\]+", " ", text)
|
| 34 |
+
text = re.sub(r"[^\w\s\u0600-\u06FF]", " ", text)
|
| 35 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 36 |
+
|
| 37 |
+
def tokens(self, value: Any, lang: str, *, content_only: bool = False) -> List[str]:
|
| 38 |
+
normalized = self.normalize(value, lang)
|
| 39 |
+
values = [token for token in _TOKEN_RE.findall(normalized) if token]
|
| 40 |
+
if content_only:
|
| 41 |
+
stop = self.stopwords.get(lang, set())
|
| 42 |
+
values = [token for token in values if token not in stop and len(token) > 1]
|
| 43 |
+
return values
|
| 44 |
+
|
| 45 |
+
def content_terms(self, value: Any, lang: str) -> Tuple[str, ...]:
|
| 46 |
+
terms: List[str] = []
|
| 47 |
+
for token in self.tokens(value, lang, content_only=True):
|
| 48 |
+
stem = self.light_stem(token, lang)
|
| 49 |
+
if stem and stem not in terms:
|
| 50 |
+
terms.append(stem)
|
| 51 |
+
return tuple(terms)
|
| 52 |
+
|
| 53 |
+
def light_stem(self, token: str, lang: str) -> str:
|
| 54 |
+
token = self.normalize(token, lang)
|
| 55 |
+
if lang != "ar" or len(token) < 4:
|
| 56 |
+
return token
|
| 57 |
+
original = token
|
| 58 |
+
for prefix in ("وال", "بال", "كال", "فال", "لل", "ال"):
|
| 59 |
+
if token.startswith(prefix) and len(token) - len(prefix) >= 3:
|
| 60 |
+
token = token[len(prefix):]
|
| 61 |
+
break
|
| 62 |
+
for suffix in ("يات", "ات", "ون", "ين", "ان", "ها", "هم", "هن", "كم", "كن", "نا", "ية", "ه", "ة", "ي"):
|
| 63 |
+
if token.endswith(suffix) and len(token) - len(suffix) >= 3:
|
| 64 |
+
token = token[:-len(suffix)]
|
| 65 |
+
break
|
| 66 |
+
return token or original
|
| 67 |
+
|
| 68 |
+
def sentences(self, value: Any) -> Tuple[str, ...]:
|
| 69 |
+
text = str(value or "").strip()
|
| 70 |
+
parts = [re.sub(r"\s+", " ", part).strip(" -•\t") for part in _SENTENCE_RE.split(text)]
|
| 71 |
+
return tuple(part for part in parts if len(part) >= 3)
|
| 72 |
+
|
| 73 |
+
def phrase_hit(self, normalized_text: str, pattern: str) -> bool:
|
| 74 |
+
try:
|
| 75 |
+
return bool(re.search(pattern, normalized_text, re.I))
|
| 76 |
+
except re.error as exc:
|
| 77 |
+
raise RuntimeError(f"Invalid HUDA-Net semantic regex: {pattern}: {exc}") from exc
|
| 78 |
+
|
| 79 |
+
def fuzzy_term_overlap(self, query_terms: Sequence[str], evidence_terms: Sequence[str]) -> float:
|
| 80 |
+
q = [term for term in query_terms if term]
|
| 81 |
+
e = [term for term in evidence_terms if term]
|
| 82 |
+
if not q:
|
| 83 |
+
return 0.5
|
| 84 |
+
if not e:
|
| 85 |
+
return 0.0
|
| 86 |
+
matched = 0.0
|
| 87 |
+
used: Set[int] = set()
|
| 88 |
+
for query_term in q:
|
| 89 |
+
best_index = -1
|
| 90 |
+
best = 0.0
|
| 91 |
+
for index, evidence_term in enumerate(e):
|
| 92 |
+
if index in used:
|
| 93 |
+
continue
|
| 94 |
+
if query_term == evidence_term:
|
| 95 |
+
score = 1.0
|
| 96 |
+
elif min(len(query_term), len(evidence_term)) >= 4:
|
| 97 |
+
score = SequenceMatcher(None, query_term, evidence_term).ratio()
|
| 98 |
+
else:
|
| 99 |
+
score = 0.0
|
| 100 |
+
if score > best:
|
| 101 |
+
best = score
|
| 102 |
+
best_index = index
|
| 103 |
+
if best >= 0.78:
|
| 104 |
+
matched += best
|
| 105 |
+
used.add(best_index)
|
| 106 |
+
return min(1.0, matched / max(1, len(q)))
|
| 107 |
+
|
| 108 |
+
def jaccard(self, left: Sequence[str], right: Sequence[str]) -> float:
|
| 109 |
+
a, b = set(left), set(right)
|
| 110 |
+
if not a and not b:
|
| 111 |
+
return 1.0
|
| 112 |
+
if not a or not b:
|
| 113 |
+
return 0.0
|
| 114 |
+
return len(a & b) / len(a | b)
|
| 115 |
+
|
| 116 |
+
def split_list_items(self, value: Any, lang: str) -> Tuple[str, ...]:
|
| 117 |
+
text = str(value or "").strip()
|
| 118 |
+
if not text:
|
| 119 |
+
return ()
|
| 120 |
+
candidates: List[str] = []
|
| 121 |
+
for line in re.split(r"[\n\r]+|[؛;]", text):
|
| 122 |
+
line = line.strip(" -•\t")
|
| 123 |
+
if not line:
|
| 124 |
+
continue
|
| 125 |
+
if ":" in line:
|
| 126 |
+
left, right = line.split(":", 1)
|
| 127 |
+
if len(right.strip()) >= 3:
|
| 128 |
+
line = right.strip()
|
| 129 |
+
chunks = [chunk.strip(" -•\t.") for chunk in re.split(r"[،,]", line) if chunk.strip()]
|
| 130 |
+
if len(chunks) == 1 and lang == "ar" and line.count(" و") >= 2:
|
| 131 |
+
chunks = [chunk.strip(" -•\t.") for chunk in re.split(r"\s+و(?=[\u0600-\u06FF])", line) if chunk.strip()]
|
| 132 |
+
candidates.extend(chunks)
|
| 133 |
+
result: List[str] = []
|
| 134 |
+
seen: Set[str] = set()
|
| 135 |
+
for item in candidates:
|
| 136 |
+
item = re.sub(r"^(?:و|ثم|او|أو)\s+", "", item).strip()
|
| 137 |
+
normalized = self.normalize(item, lang)
|
| 138 |
+
if len(normalized) < 2 or len(normalized.split()) > 18:
|
| 139 |
+
continue
|
| 140 |
+
if normalized not in seen:
|
| 141 |
+
seen.add(normalized)
|
| 142 |
+
result.append(item)
|
| 143 |
+
return tuple(result)
|
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Any, Dict, List, Mapping, Sequence, Tuple
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass(frozen=True)
|
| 8 |
+
class QueryFrame:
|
| 9 |
+
raw: str
|
| 10 |
+
normalized: str
|
| 11 |
+
language: str
|
| 12 |
+
request_types: Tuple[str, ...]
|
| 13 |
+
primary_request_type: str
|
| 14 |
+
subject_terms: Tuple[str, ...]
|
| 15 |
+
critical_terms: Tuple[str, ...]
|
| 16 |
+
negated_terms: Tuple[str, ...]
|
| 17 |
+
polarity: str
|
| 18 |
+
dimensions: Tuple[str, ...]
|
| 19 |
+
asks_for_decisive_answer: bool
|
| 20 |
+
asks_for_sources: bool
|
| 21 |
+
confidence: float
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass(frozen=True)
|
| 25 |
+
class EvidenceFrame:
|
| 26 |
+
record_id: str
|
| 27 |
+
book_id: str
|
| 28 |
+
book: str
|
| 29 |
+
page: str
|
| 30 |
+
metadata_text: str
|
| 31 |
+
answer_text: str
|
| 32 |
+
normalized_metadata: str
|
| 33 |
+
normalized_answer: str
|
| 34 |
+
answer_types: Tuple[str, ...]
|
| 35 |
+
outcomes: Tuple[str, ...]
|
| 36 |
+
subject_terms: Tuple[str, ...]
|
| 37 |
+
sentences: Tuple[str, ...]
|
| 38 |
+
list_items: Tuple[str, ...]
|
| 39 |
+
decisiveness: float
|
| 40 |
+
completeness: float
|
| 41 |
+
raw: Mapping[str, Any]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class ScoredEvidence:
|
| 46 |
+
source: Dict[str, Any]
|
| 47 |
+
evidence: EvidenceFrame
|
| 48 |
+
score: float
|
| 49 |
+
accepted: bool
|
| 50 |
+
promoted: bool
|
| 51 |
+
hard_rejections: List[str] = field(default_factory=list)
|
| 52 |
+
reasons: List[str] = field(default_factory=list)
|
| 53 |
+
metrics: Dict[str, float] = field(default_factory=dict)
|
| 54 |
+
cluster_key: str = ""
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass
|
| 58 |
+
class ConsensusResult:
|
| 59 |
+
state: str
|
| 60 |
+
selected_cluster: str
|
| 61 |
+
selected: List[ScoredEvidence]
|
| 62 |
+
dissent: List[ScoredEvidence]
|
| 63 |
+
clusters: Dict[str, Dict[str, Any]]
|
| 64 |
+
confidence: float
|
| 65 |
+
source_count: int
|
| 66 |
+
book_count: int
|
| 67 |
+
explanation: str
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@dataclass
|
| 71 |
+
class ResolutionResult:
|
| 72 |
+
answer: str
|
| 73 |
+
query: QueryFrame
|
| 74 |
+
ranked: List[ScoredEvidence]
|
| 75 |
+
accepted: List[ScoredEvidence]
|
| 76 |
+
rejected: List[ScoredEvidence]
|
| 77 |
+
consensus: ConsensusResult
|
| 78 |
+
confidence: float
|
| 79 |
+
details: Dict[str, Any]
|
|
@@ -1,329 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"version": "
|
| 3 |
-
"description": "
|
| 4 |
-
"intents": {
|
| 5 |
-
|
| 6 |
-
"query_patterns": {
|
| 7 |
-
"ar": [
|
| 8 |
-
"(?:ما\\s+هي\\s+)?شروط(?:\\s+الوجوب)?(?:\\s+التي\\s+يجب\\s+توفرها)?\\s+لوجوب\\s+الحج",
|
| 9 |
-
"شروط\\s+وجوب\\s+الحج",
|
| 10 |
-
"على\\s+من\\s+يجب\\s+الحج",
|
| 11 |
-
"متى\\s+يجب\\s+الحج"
|
| 12 |
-
],
|
| 13 |
-
"en": [
|
| 14 |
-
"conditions?.*(?:obligation|obligatory).*(?:hajj)",
|
| 15 |
-
"requirements?.*(?:hajj).*(?:obligatory|obligation)",
|
| 16 |
-
"on\\s+whom\\s+is\\s+hajj\\s+obligatory"
|
| 17 |
-
]
|
| 18 |
-
},
|
| 19 |
-
"topic_clusters": [
|
| 20 |
-
{
|
| 21 |
-
"id": "general_conditions",
|
| 22 |
-
"weight": 1.0,
|
| 23 |
-
"patterns": {
|
| 24 |
-
"ar": [
|
| 25 |
-
"شروط\\s+وجوب\\s+الحج",
|
| 26 |
-
"شروط\\s+وجوب\\s+الحج\\s+والعمرة",
|
| 27 |
-
"على\\s+من\\s+يجب\\s+الحج"
|
| 28 |
-
],
|
| 29 |
-
"en": [
|
| 30 |
-
"conditions?\\s+for\\s+the\\s+obligation\\s+of\\s+hajj",
|
| 31 |
-
"on\\s+whom\\s+is\\s+hajj\\s+obligatory"
|
| 32 |
-
]
|
| 33 |
-
}
|
| 34 |
-
},
|
| 35 |
-
{
|
| 36 |
-
"id": "validity_and_sufficiency_conditions",
|
| 37 |
-
"weight": 0.97,
|
| 38 |
-
"patterns": {
|
| 39 |
-
"ar": [
|
| 40 |
-
"شروط\\s+الوجوب\\s+والصحة",
|
| 41 |
-
"شروط\\s+الوجوب\\s+والاجزاء",
|
| 42 |
-
"شروط\\s+الوجوب\\s+فقط"
|
| 43 |
-
],
|
| 44 |
-
"en": [
|
| 45 |
-
"conditions?\\s+of\\s+obligation\\s+and\\s+validity",
|
| 46 |
-
"conditions?\\s+of\\s+obligation\\s+and\\s+sufficiency"
|
| 47 |
-
]
|
| 48 |
-
}
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"id": "ability_provisions_transport",
|
| 52 |
-
"weight": 0.95,
|
| 53 |
-
"patterns": {
|
| 54 |
-
"ar": [
|
| 55 |
-
"ماهية\\s+القدرة",
|
| 56 |
-
"ضابط\\s+القدرة",
|
| 57 |
-
"الاستطاعة.*الزاد.*الراحلة",
|
| 58 |
-
"الزاد\\s+والراحلة"
|
| 59 |
-
],
|
| 60 |
-
"en": [
|
| 61 |
-
"ability.*provisions.*(?:mount|transport)",
|
| 62 |
-
"capacity.*hajj"
|
| 63 |
-
]
|
| 64 |
-
}
|
| 65 |
-
},
|
| 66 |
-
{
|
| 67 |
-
"id": "financial_obligations",
|
| 68 |
-
"weight": 0.93,
|
| 69 |
-
"patterns": {
|
| 70 |
-
"ar": [
|
| 71 |
-
"الاستطاعة\\s+والديون",
|
| 72 |
-
"قضاء\\s+الواجبات",
|
| 73 |
-
"الديون.*الواجبات"
|
| 74 |
-
],
|
| 75 |
-
"en": [
|
| 76 |
-
"ability\\s+and\\s+debts",
|
| 77 |
-
"financial\\s+obligations"
|
| 78 |
-
]
|
| 79 |
-
}
|
| 80 |
-
},
|
| 81 |
-
{
|
| 82 |
-
"id": "lawful_expenses",
|
| 83 |
-
"weight": 0.92,
|
| 84 |
-
"patterns": {
|
| 85 |
-
"ar": [
|
| 86 |
-
"الاستطاعة\\s+والنفقات\\s+الشرعية",
|
| 87 |
-
"النفقات\\s+الشرعية"
|
| 88 |
-
],
|
| 89 |
-
"en": [
|
| 90 |
-
"ability\\s+and\\s+lawful\\s+expenses",
|
| 91 |
-
"maintenance\\s+expenses"
|
| 92 |
-
]
|
| 93 |
-
}
|
| 94 |
-
},
|
| 95 |
-
{
|
| 96 |
-
"id": "basic_needs",
|
| 97 |
-
"weight": 0.92,
|
| 98 |
-
"patterns": {
|
| 99 |
-
"ar": [
|
| 100 |
-
"الاستطاعة\\s+والحوائج\\s+الاصلية",
|
| 101 |
-
"الحوائج\\s+الاصلية"
|
| 102 |
-
],
|
| 103 |
-
"en": [
|
| 104 |
-
"ability\\s+and\\s+essential\\s+needs",
|
| 105 |
-
"basic\\s+needs"
|
| 106 |
-
]
|
| 107 |
-
}
|
| 108 |
-
},
|
| 109 |
-
{
|
| 110 |
-
"id": "road_safety",
|
| 111 |
-
"weight": 0.91,
|
| 112 |
-
"patterns": {
|
| 113 |
-
"ar": [
|
| 114 |
-
"امن\\s+الطريق",
|
| 115 |
-
"امان\\s+الطريق"
|
| 116 |
-
],
|
| 117 |
-
"en": [
|
| 118 |
-
"safety\\s+of\\s+the\\s+(?:road|route)",
|
| 119 |
-
"safe\\s+route"
|
| 120 |
-
]
|
| 121 |
-
}
|
| 122 |
-
},
|
| 123 |
-
{
|
| 124 |
-
"id": "sufficient_time",
|
| 125 |
-
"weight": 0.9,
|
| 126 |
-
"patterns": {
|
| 127 |
-
"ar": [
|
| 128 |
-
"سعة\\s+الوقت",
|
| 129 |
-
"الوقت\\s+لا\\s+يكفي\\s+للوصول"
|
| 130 |
-
],
|
| 131 |
-
"en": [
|
| 132 |
-
"sufficient\\s+time",
|
| 133 |
-
"time.*reach"
|
| 134 |
-
]
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
],
|
| 138 |
-
"answer_items": [
|
| 139 |
-
{
|
| 140 |
-
"id": "islam",
|
| 141 |
-
"label_ar": "الإسلام",
|
| 142 |
-
"label_en": "Islam",
|
| 143 |
-
"terms_ar": [
|
| 144 |
-
"الإسلام",
|
| 145 |
-
"المسلم"
|
| 146 |
-
],
|
| 147 |
-
"terms_en": [
|
| 148 |
-
"islam",
|
| 149 |
-
"muslim"
|
| 150 |
-
]
|
| 151 |
-
},
|
| 152 |
-
{
|
| 153 |
-
"id": "sanity",
|
| 154 |
-
"label_ar": "العقل",
|
| 155 |
-
"label_en": "Sanity",
|
| 156 |
-
"terms_ar": [
|
| 157 |
-
"العقل",
|
| 158 |
-
"المكلف"
|
| 159 |
-
],
|
| 160 |
-
"terms_en": [
|
| 161 |
-
"sanity",
|
| 162 |
-
"sane",
|
| 163 |
-
"legally accountable"
|
| 164 |
-
]
|
| 165 |
-
},
|
| 166 |
-
{
|
| 167 |
-
"id": "puberty",
|
| 168 |
-
"label_ar": "البلوغ",
|
| 169 |
-
"label_en": "Puberty",
|
| 170 |
-
"terms_ar": [
|
| 171 |
-
"البلوغ",
|
| 172 |
-
"بالغ",
|
| 173 |
-
"المكلف"
|
| 174 |
-
],
|
| 175 |
-
"terms_en": [
|
| 176 |
-
"puberty",
|
| 177 |
-
"adult",
|
| 178 |
-
"legally accountable"
|
| 179 |
-
]
|
| 180 |
-
},
|
| 181 |
-
{
|
| 182 |
-
"id": "freedom",
|
| 183 |
-
"label_ar": "كمال الحرية",
|
| 184 |
-
"label_en": "Full freedom",
|
| 185 |
-
"terms_ar": [
|
| 186 |
-
"كمال الحرية",
|
| 187 |
-
"الحرية",
|
| 188 |
-
"الحر"
|
| 189 |
-
],
|
| 190 |
-
"terms_en": [
|
| 191 |
-
"full freedom",
|
| 192 |
-
"free person",
|
| 193 |
-
"freedom"
|
| 194 |
-
]
|
| 195 |
-
},
|
| 196 |
-
{
|
| 197 |
-
"id": "ability",
|
| 198 |
-
"label_ar": "الاستطاعة، ومن أهمها وجود الزاد والراحلة المناسبين",
|
| 199 |
-
"label_en": "Capability, including adequate provisions and transport",
|
| 200 |
-
"terms_ar": [
|
| 201 |
-
"الاستطاعة",
|
| 202 |
-
"القادر",
|
| 203 |
-
"الزاد",
|
| 204 |
-
"الراحلة"
|
| 205 |
-
],
|
| 206 |
-
"terms_en": [
|
| 207 |
-
"ability",
|
| 208 |
-
"capability",
|
| 209 |
-
"provisions",
|
| 210 |
-
"mount",
|
| 211 |
-
"transport"
|
| 212 |
-
],
|
| 213 |
-
"required_term_groups_ar": [
|
| 214 |
-
[
|
| 215 |
-
"الاستطاعة",
|
| 216 |
-
"القادر"
|
| 217 |
-
],
|
| 218 |
-
[
|
| 219 |
-
"الزاد"
|
| 220 |
-
],
|
| 221 |
-
[
|
| 222 |
-
"الراحلة"
|
| 223 |
-
]
|
| 224 |
-
],
|
| 225 |
-
"required_term_groups_en": [
|
| 226 |
-
[
|
| 227 |
-
"ability",
|
| 228 |
-
"capability"
|
| 229 |
-
],
|
| 230 |
-
[
|
| 231 |
-
"provisions"
|
| 232 |
-
],
|
| 233 |
-
[
|
| 234 |
-
"mount",
|
| 235 |
-
"transport"
|
| 236 |
-
]
|
| 237 |
-
]
|
| 238 |
-
},
|
| 239 |
-
{
|
| 240 |
-
"id": "road_safety",
|
| 241 |
-
"label_ar": "أمن الطريق",
|
| 242 |
-
"label_en": "Safety of the route",
|
| 243 |
-
"terms_ar": [
|
| 244 |
-
"أمن الطريق",
|
| 245 |
-
"امن الطريق",
|
| 246 |
-
"الطريق آمنا",
|
| 247 |
-
"الطريق امن"
|
| 248 |
-
],
|
| 249 |
-
"terms_en": [
|
| 250 |
-
"safe route",
|
| 251 |
-
"safety of the route",
|
| 252 |
-
"road is safe"
|
| 253 |
-
]
|
| 254 |
-
},
|
| 255 |
-
{
|
| 256 |
-
"id": "sufficient_time",
|
| 257 |
-
"label_ar": "سعة الوقت بما يكفي للوصول وأداء النسك",
|
| 258 |
-
"label_en": "Sufficient time to reach and perform the rites",
|
| 259 |
-
"terms_ar": [
|
| 260 |
-
"سعة الوقت",
|
| 261 |
-
"الوقت لا يكفي",
|
| 262 |
-
"يمكن السير فيه"
|
| 263 |
-
],
|
| 264 |
-
"terms_en": [
|
| 265 |
-
"sufficient time",
|
| 266 |
-
"time to reach",
|
| 267 |
-
"travel at a normal pace"
|
| 268 |
-
]
|
| 269 |
-
},
|
| 270 |
-
{
|
| 271 |
-
"id": "obligations_expenses",
|
| 272 |
-
"label_ar": "أن يكون المال فاضلًا بعد قضاء الواجبات والديون والنفقات الشرعية",
|
| 273 |
-
"label_en": "Wealth remaining after debts, obligations, and lawful maintenance expenses",
|
| 274 |
-
"terms_ar": [
|
| 275 |
-
"قضاء الواجبات",
|
| 276 |
-
"الديون",
|
| 277 |
-
"النفقات الشرعية",
|
| 278 |
-
"نفقة عياله"
|
| 279 |
-
],
|
| 280 |
-
"terms_en": [
|
| 281 |
-
"debts",
|
| 282 |
-
"financial obligations",
|
| 283 |
-
"lawful expenses",
|
| 284 |
-
"dependents"
|
| 285 |
-
],
|
| 286 |
-
"required_term_groups_ar": [
|
| 287 |
-
[
|
| 288 |
-
"قضاء الواجبات",
|
| 289 |
-
"الديون"
|
| 290 |
-
],
|
| 291 |
-
[
|
| 292 |
-
"النفقات الشرعية",
|
| 293 |
-
"نفقة عياله",
|
| 294 |
-
"النفقات"
|
| 295 |
-
]
|
| 296 |
-
],
|
| 297 |
-
"required_term_groups_en": [
|
| 298 |
-
[
|
| 299 |
-
"debts",
|
| 300 |
-
"financial obligations"
|
| 301 |
-
],
|
| 302 |
-
[
|
| 303 |
-
"lawful expenses",
|
| 304 |
-
"dependents",
|
| 305 |
-
"maintenance"
|
| 306 |
-
]
|
| 307 |
-
]
|
| 308 |
-
},
|
| 309 |
-
{
|
| 310 |
-
"id": "basic_needs",
|
| 311 |
-
"label_ar": "أن يكون المال فاضلًا عن الحوائج الأصلية",
|
| 312 |
-
"label_en": "Wealth exceeding essential needs",
|
| 313 |
-
"terms_ar": [
|
| 314 |
-
"الحوائج الأصلية",
|
| 315 |
-
"الحوائج الاصلية",
|
| 316 |
-
"ضروريات الحياة"
|
| 317 |
-
],
|
| 318 |
-
"terms_en": [
|
| 319 |
-
"essential needs",
|
| 320 |
-
"basic needs",
|
| 321 |
-
"necessities"
|
| 322 |
-
]
|
| 323 |
-
}
|
| 324 |
-
],
|
| 325 |
-
"minimum_items_for_structured_answer": 5,
|
| 326 |
-
"max_sources_per_cluster": 2
|
| 327 |
-
}
|
| 328 |
-
}
|
| 329 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"version": "2.0.0",
|
| 3 |
+
"description": "Legacy direct-intent table intentionally empty. All answer gating, ranking, consensus, and synthesis are handled generically by hudanet_core.",
|
| 4 |
+
"intents": {}
|
| 5 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"passed": true,
|
| 3 |
+
"tested": 14,
|
| 4 |
+
"checks": [
|
| 5 |
+
[
|
| 6 |
+
"definition rejected for conditions",
|
| 7 |
+
true
|
| 8 |
+
],
|
| 9 |
+
[
|
| 10 |
+
"conditions selected",
|
| 11 |
+
true
|
| 12 |
+
],
|
| 13 |
+
[
|
| 14 |
+
"conditions list",
|
| 15 |
+
true
|
| 16 |
+
],
|
| 17 |
+
[
|
| 18 |
+
"multi-source conditions merge",
|
| 19 |
+
true
|
| 20 |
+
],
|
| 21 |
+
[
|
| 22 |
+
"all selected sources named",
|
| 23 |
+
true
|
| 24 |
+
],
|
| 25 |
+
[
|
| 26 |
+
"decisive beats disputed",
|
| 27 |
+
true
|
| 28 |
+
],
|
| 29 |
+
[
|
| 30 |
+
"direct ruling in answer",
|
| 31 |
+
true
|
| 32 |
+
],
|
| 33 |
+
[
|
| 34 |
+
"dissent stays visible",
|
| 35 |
+
true
|
| 36 |
+
],
|
| 37 |
+
[
|
| 38 |
+
"strong incompatible outcomes conflict",
|
| 39 |
+
true
|
| 40 |
+
],
|
| 41 |
+
[
|
| 42 |
+
"conflict sources separated",
|
| 43 |
+
true
|
| 44 |
+
],
|
| 45 |
+
[
|
| 46 |
+
"unrelated high neural score rejected",
|
| 47 |
+
true
|
| 48 |
+
],
|
| 49 |
+
[
|
| 50 |
+
"english type isolation",
|
| 51 |
+
true
|
| 52 |
+
],
|
| 53 |
+
[
|
| 54 |
+
"compatible distant promoted",
|
| 55 |
+
true
|
| 56 |
+
],
|
| 57 |
+
[
|
| 58 |
+
"incompatible exact demoted",
|
| 59 |
+
true
|
| 60 |
+
]
|
| 61 |
+
]
|
| 62 |
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "37.0.0",
|
| 3 |
+
"architecture": "modular_generic_evidence_library",
|
| 4 |
+
"files": [
|
| 5 |
+
{
|
| 6 |
+
"path": "app.py",
|
| 7 |
+
"size": 529861,
|
| 8 |
+
"sha256": "1ccf29c63b47092a7f4d6320faa1a55f6e05d262a3f715a79e14ab90b60a7459"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"path": "hudanet_dialects.json",
|
| 12 |
+
"size": 61047,
|
| 13 |
+
"sha256": "9d73324aa9a53cef401b1b155691c5c594504ef5a89618709856d8ff86032413"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"path": "hudanet_retrieval_rules.json",
|
| 17 |
+
"size": 204,
|
| 18 |
+
"sha256": "a9710d648dcb07055d1256b9e414f7bf3f235e3f75b420ff6c05fdeeda0d4ec5"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"path": "DEPLOY_HUDANET_V37.md",
|
| 22 |
+
"size": 655,
|
| 23 |
+
"sha256": "51ef70330f3748703e2361d539ddc4025b2b0a9d4750ff4530d05b074fd5e044"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"path": "hudanet_v37_validation_report.json",
|
| 27 |
+
"size": 2022,
|
| 28 |
+
"sha256": "74c46eb7e2a4b613b6ad2c22537b198c334ae8b95c120cbb48778aa2485a1766"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"path": "hudanet_v37_generic_tests.json",
|
| 32 |
+
"size": 895,
|
| 33 |
+
"sha256": "c89730411206fa56ae77aaf589d02eaa78349b62986bf293e23b72b66d34fec4"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"path": "hudanet_core/README.md",
|
| 37 |
+
"size": 1982,
|
| 38 |
+
"sha256": "4cace1731877f91937ebbe03330850d3c9cf1461a569d2fd12fdc3d06a2d236c"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"path": "hudanet_core/__init__.py",
|
| 42 |
+
"size": 175,
|
| 43 |
+
"sha256": "c417d4feb14e04c42769554348a0fe5f9cd2764ae29fcce6bca48fb29ce1acb1"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"path": "hudanet_core/compatibility.py",
|
| 47 |
+
"size": 9275,
|
| 48 |
+
"sha256": "a4a98d3d2d973bc3087d1a662ffb62b064e2c2a89903b3bbad52bd2ee19f7bdb"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"path": "hudanet_core/consensus.py",
|
| 52 |
+
"size": 6332,
|
| 53 |
+
"sha256": "8fb742c3c72653d8b813df059d3603dcf10d99e20c80a43777802df50ef97fca"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"path": "hudanet_core/evidence.py",
|
| 57 |
+
"size": 3722,
|
| 58 |
+
"sha256": "7aa467f07fcb24dd314e5e9e541b22f152b74e49a71de0f0e6898dbbf2ca3297"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"path": "hudanet_core/pipeline.py",
|
| 62 |
+
"size": 5877,
|
| 63 |
+
"sha256": "b09767818b2ad3a153a507fdbe895cc47dee875f7f8b424ff3536befe826baa4"
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"path": "hudanet_core/query.py",
|
| 67 |
+
"size": 4124,
|
| 68 |
+
"sha256": "f0576b66a2d710b0c2cde91a7483848f72ad9d8eb7cdf8299dab1b97f07e91d2"
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"path": "hudanet_core/resources/answer_templates.json",
|
| 72 |
+
"size": 2457,
|
| 73 |
+
"sha256": "e8389097845d43ebcdaed9046b0ef61ce547d4c5d44e60f311f0166bb674506b"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"path": "hudanet_core/resources/evidence_schema.json",
|
| 77 |
+
"size": 701,
|
| 78 |
+
"sha256": "912073cee04802fa30eff22614f452433715d754bb2beb0512f2f4e7d094759f"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"path": "hudanet_core/resources/ranking_config.json",
|
| 82 |
+
"size": 2607,
|
| 83 |
+
"sha256": "ebb900b730084b884c3b69af8ea616305db227cb9c711f140d6ed1a054fe9048"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"path": "hudanet_core/resources/semantic_rules.json",
|
| 87 |
+
"size": 23721,
|
| 88 |
+
"sha256": "acd203d5210d3ff94c60a3c2acf4ff4b9af21b00bfbe3c6ffb93c7a16c4538cb"
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"path": "hudanet_core/resources.py",
|
| 92 |
+
"size": 2268,
|
| 93 |
+
"sha256": "d043d3df9da9ec9c43b9a269f395ba2754f08945a80f98ff6b6516fad3087e1d"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"path": "hudanet_core/synthesis.py",
|
| 97 |
+
"size": 9375,
|
| 98 |
+
"sha256": "743a4bb6dacad9a7cae5c7aa509ebec1cccab76450feac5535d4a9ac86d2151f"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"path": "hudanet_core/tests/test_generic_pipeline.py",
|
| 102 |
+
"size": 6204,
|
| 103 |
+
"sha256": "71ee43f050d64ae8d31625e4ec7ee7f4933df876d42f05ed98bee47071b22485"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"path": "hudanet_core/text.py",
|
| 107 |
+
"size": 6077,
|
| 108 |
+
"sha256": "cef781df2ca32c651afa86c0374c094559c0b63d4508a5e604909fef61757a7b"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"path": "hudanet_core/types.py",
|
| 112 |
+
"size": 1861,
|
| 113 |
+
"sha256": "91cb0fe6f7688fdd6c7d9e677450af8e8ca3a5ebc76007f3e9c0e43de057c984"
|
| 114 |
+
}
|
| 115 |
+
],
|
| 116 |
+
"file_count": 22,
|
| 117 |
+
"total_size": 681442,
|
| 118 |
+
"validation": {
|
| 119 |
+
"ast_json_regex": true,
|
| 120 |
+
"generic_regressions": 14,
|
| 121 |
+
"semantic_regexes": 229,
|
| 122 |
+
"full_space_runtime_executed": false
|
| 123 |
+
}
|
| 124 |
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "37.0.0",
|
| 3 |
+
"checks": [
|
| 4 |
+
{
|
| 5 |
+
"name": "python_ast_parse",
|
| 6 |
+
"passed": true,
|
| 7 |
+
"details": {
|
| 8 |
+
"files": 12
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"name": "json_valid",
|
| 13 |
+
"passed": true,
|
| 14 |
+
"details": {
|
| 15 |
+
"files": 6
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"name": "semantic_regex_valid",
|
| 20 |
+
"passed": true,
|
| 21 |
+
"details": {
|
| 22 |
+
"patterns": 229
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"name": "named_intents_absent",
|
| 27 |
+
"passed": true,
|
| 28 |
+
"details": {
|
| 29 |
+
"version": "2.0.0",
|
| 30 |
+
"description": "Legacy direct-intent table intentionally empty. All answer gating, ranking, consensus, and synthesis are handled generically by hudanet_core.",
|
| 31 |
+
"intents": {}
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"name": "forbidden_special_case_absent:no_hajj_deputy",
|
| 36 |
+
"passed": true,
|
| 37 |
+
"details": null
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"name": "forbidden_special_case_absent:preferred_answer_ar",
|
| 41 |
+
"passed": true,
|
| 42 |
+
"details": null
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"name": "forbidden_special_case_absent:إذا لم يجد الشخص نائبًا للحج عنه، يسقط عنه وجوب الحج",
|
| 46 |
+
"passed": true,
|
| 47 |
+
"details": null
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"name": "app_hook:VERSION = \"37.0.0\"",
|
| 51 |
+
"passed": true,
|
| 52 |
+
"details": null
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"name": "app_hook:UI_VERSION = \"37.0.0\"",
|
| 56 |
+
"passed": true,
|
| 57 |
+
"details": null
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "app_hook:apply_generic_evidence_gate_ui(search,intent_query,lang)",
|
| 61 |
+
"passed": true,
|
| 62 |
+
"details": null
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"name": "app_hook:from hudanet_core import GenericEvidencePipeline",
|
| 66 |
+
"passed": true,
|
| 67 |
+
"details": null
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"name": "conversation_context_disabled",
|
| 71 |
+
"passed": true,
|
| 72 |
+
"details": null
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "dialect_does_not_corrupt_tawaffur",
|
| 76 |
+
"passed": true,
|
| 77 |
+
"details": "توفرها"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"name": "dialect_does_not_split_wa_sharan",
|
| 81 |
+
"passed": true,
|
| 82 |
+
"details": "وشرعا"
|
| 83 |
+
}
|
| 84 |
+
],
|
| 85 |
+
"passed": true,
|
| 86 |
+
"tested": 14,
|
| 87 |
+
"failed": []
|
| 88 |
+
}
|