Upload folder using huggingface_hub
Browse files- .env.example +1 -1
- .gitignore +1 -1
- README.md +2 -2
- app/analysis.py +59 -8
- app/arabic_nlp.py +10 -3
- app/llm.py +16 -6
- app/prompts.py +78 -74
- app/routers/chat.py +2 -43
- app/routers/hadith.py +39 -8
- app/routers/ops.py +3 -35
- app/routers/quran.py +23 -18
- app/search.py +54 -14
- app/state.py +63 -5
- main.py +4 -8
.env.example
CHANGED
|
@@ -27,7 +27,7 @@ OLLAMA_MODEL=minimax-m2.7:cloud
|
|
| 27 |
# ─────────────────────────────────────────────────────────────────────
|
| 28 |
# GGUF BACKEND (if LLM_BACKEND=gguf)
|
| 29 |
# ─────────────────────────────────────────────────────────────────────
|
| 30 |
-
# GGUF_MODEL_PATH=./models/
|
| 31 |
# GGUF_N_CTX=4096 # Context window size
|
| 32 |
# GGUF_N_GPU_LAYERS=-1 # -1 = offload all layers to GPU (Metal on Mac)
|
| 33 |
|
|
|
|
| 27 |
# ─────────────────────────────────────────────────────────────────────
|
| 28 |
# GGUF BACKEND (if LLM_BACKEND=gguf)
|
| 29 |
# ─────────────────────────────────────────────────────────────────────
|
| 30 |
+
# GGUF_MODEL_PATH=./models/Qwen3-32B-Q4_K_M.gguf
|
| 31 |
# GGUF_N_CTX=4096 # Context window size
|
| 32 |
# GGUF_N_GPU_LAYERS=-1 # -1 = offload all layers to GPU (Metal on Mac)
|
| 33 |
|
.gitignore
CHANGED
|
@@ -209,4 +209,4 @@ data/
|
|
| 209 |
|
| 210 |
QModel.index
|
| 211 |
metadata.json
|
| 212 |
-
models/
|
|
|
|
| 209 |
|
| 210 |
QModel.index
|
| 211 |
metadata.json
|
| 212 |
+
models/Qwen3-32B-Q4_K_M.gguf
|
README.md
CHANGED
|
@@ -19,12 +19,12 @@ language:
|
|
| 19 |
- en
|
| 20 |
---
|
| 21 |
|
| 22 |
-
# QModel
|
| 23 |
**Specialized Qur'an & Hadith Knowledge System with Dual LLM Support**
|
| 24 |
|
| 25 |
> A production-ready Retrieval-Augmented Generation system specialized exclusively in authenticated Islamic knowledge. No hallucinations, no outside knowledge—only content from verified sources.
|
| 26 |
|
| 27 |
-

|
| 29 |

|
| 30 |
|
|
|
|
| 19 |
- en
|
| 20 |
---
|
| 21 |
|
| 22 |
+
# QModel v6 — Islamic RAG System
|
| 23 |
**Specialized Qur'an & Hadith Knowledge System with Dual LLM Support**
|
| 24 |
|
| 25 |
> A production-ready Retrieval-Augmented Generation system specialized exclusively in authenticated Islamic knowledge. No hallucinations, no outside knowledge—only content from verified sources.
|
| 26 |
|
| 27 |
+

|
| 28 |

|
| 29 |

|
| 30 |
|
app/analysis.py
CHANGED
|
@@ -132,26 +132,77 @@ async def detect_analysis_intent(query: str, rewrite: dict) -> Optional[str]:
|
|
| 132 |
kw_text = " ".join(kws)
|
| 133 |
if any(w in kw_text for w in ("آيات", "آية", "verses", "ayat")):
|
| 134 |
return None
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
if not (_COUNT_EN.search(query) or _COUNT_AR.search(query)):
|
| 138 |
return None
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
for pat in (_COUNT_EN, _COUNT_AR):
|
| 141 |
m = pat.search(query)
|
| 142 |
if m:
|
| 143 |
tail = query[m.end():].strip().split()
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
return None
|
| 147 |
|
| 148 |
|
| 149 |
# ═══════════════════════════════════════════════════════════════════════
|
| 150 |
# OCCURRENCE COUNTING
|
| 151 |
# ═══════════════════════════════════════════════════════════════════════
|
| 152 |
-
async def count_occurrences(
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
if cached:
|
| 156 |
return cached
|
| 157 |
|
|
@@ -162,7 +213,7 @@ async def count_occurrences(keyword: str, dataset: list) -> dict:
|
|
| 162 |
examples: list = []
|
| 163 |
|
| 164 |
for item in dataset:
|
| 165 |
-
if item.get("type") !=
|
| 166 |
continue
|
| 167 |
|
| 168 |
ar_norm = normalize_arabic(item.get("arabic", ""), aggressive=True).lower()
|
|
@@ -195,7 +246,7 @@ async def count_occurrences(keyword: str, dataset: list) -> dict:
|
|
| 195 |
"by_surah": dict(sorted(by_surah.items())),
|
| 196 |
"examples": examples,
|
| 197 |
}
|
| 198 |
-
await analysis_cache.set(result, keyword)
|
| 199 |
return result
|
| 200 |
|
| 201 |
|
|
|
|
| 132 |
kw_text = " ".join(kws)
|
| 133 |
if any(w in kw_text for w in ("آيات", "آية", "verses", "ayat")):
|
| 134 |
return None
|
| 135 |
+
# The rewriter is instructed to put the target word as first keyword
|
| 136 |
+
if kws:
|
| 137 |
+
return kws[0]
|
| 138 |
+
# Fallback: extract from query
|
| 139 |
+
keyword = _extract_count_keyword(query)
|
| 140 |
+
return keyword
|
| 141 |
|
| 142 |
if not (_COUNT_EN.search(query) or _COUNT_AR.search(query)):
|
| 143 |
return None
|
| 144 |
|
| 145 |
+
keyword = _extract_count_keyword(query)
|
| 146 |
+
return keyword
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _extract_count_keyword(query: str) -> Optional[str]:
|
| 150 |
+
"""Extract the keyword being counted from various question patterns."""
|
| 151 |
+
# Arabic patterns: كم مرة ذكرت كلمة X / كم مرة وردت X / عدد مرات ذكر X
|
| 152 |
+
ar_patterns = [
|
| 153 |
+
re.compile(r"(?:كلمة|لفظ|لفظة)\s+([\u0600-\u06FF\u0750-\u077F]+)"),
|
| 154 |
+
re.compile(r"(?:ذ[ُ]?كر(?:ت|)|وردت?|تكرر(?:ت|))\s+(?:كلمة\s+)?([\u0600-\u06FF\u0750-\u077F]+)"),
|
| 155 |
+
re.compile(r"(?:عدد\s+مرات\s+(?:ذكر|ورود))\s+(?:كلمة\s+)?([\u0600-\u06FF\u0750-\u077F]+)"),
|
| 156 |
+
re.compile(r"كم\s+(?:مرة|مره)\s+(?:ذ[ُ]?كر(?:ت|)|وردت?)\s+(?:كلمة\s+)?([\u0600-\u06FF\u0750-\u077F]+)"),
|
| 157 |
+
]
|
| 158 |
+
for pat in ar_patterns:
|
| 159 |
+
m = pat.search(query)
|
| 160 |
+
if m:
|
| 161 |
+
word = m.group(1).strip()
|
| 162 |
+
# Skip common non-keyword words
|
| 163 |
+
if word not in ("في", "من", "عن", "إلى", "على", "هل", "ما", "كم"):
|
| 164 |
+
return word
|
| 165 |
+
|
| 166 |
+
# English patterns: how many times is X mentioned / count of X / occurrences of X
|
| 167 |
+
en_patterns = [
|
| 168 |
+
re.compile(r"(?:word|term)\s+['\"]?(\w+)['\"]?", re.I),
|
| 169 |
+
re.compile(r"(?:times?\s+(?:is|does|has)\s+)(\w+)", re.I),
|
| 170 |
+
re.compile(r"(?:occurrences?\s+of|frequency\s+of|count\s+of)\s+['\"]?(\w+)['\"]?", re.I),
|
| 171 |
+
re.compile(r"(?:mentioned|appear[s]?|occur[s]?)\s+.*?['\"]?(\w+)['\"]?\s+(?:in|throughout)", re.I),
|
| 172 |
+
re.compile(r"(?:how many times)\s+(?:is\s+)?['\"]?(\w+)['\"]?", re.I),
|
| 173 |
+
]
|
| 174 |
+
for pat in en_patterns:
|
| 175 |
+
m = pat.search(query)
|
| 176 |
+
if m:
|
| 177 |
+
word = m.group(1).strip()
|
| 178 |
+
if word.lower() not in ("the", "a", "an", "in", "of", "is", "are", "was", "how", "many", "quran"):
|
| 179 |
+
return word
|
| 180 |
+
|
| 181 |
+
# Last resort: find the first meaningful word after count-related keywords
|
| 182 |
for pat in (_COUNT_EN, _COUNT_AR):
|
| 183 |
m = pat.search(query)
|
| 184 |
if m:
|
| 185 |
tail = query[m.end():].strip().split()
|
| 186 |
+
for word in tail:
|
| 187 |
+
clean = re.sub(r"[؟?!.,،]", "", word).strip()
|
| 188 |
+
if clean and clean.lower() not in (
|
| 189 |
+
"في", "من", "عن", "القرآن", "الكريم", "the", "quran", "in", "of",
|
| 190 |
+
"كلمة", "لفظ", "word", "term",
|
| 191 |
+
):
|
| 192 |
+
return clean
|
| 193 |
return None
|
| 194 |
|
| 195 |
|
| 196 |
# ═══════════════════════════════════════════════════════════════════════
|
| 197 |
# OCCURRENCE COUNTING
|
| 198 |
# ═══════════════════════════════════════════════════════════════════════
|
| 199 |
+
async def count_occurrences(
|
| 200 |
+
keyword: str,
|
| 201 |
+
dataset: list,
|
| 202 |
+
source_type: Optional[str] = "quran",
|
| 203 |
+
) -> dict:
|
| 204 |
+
"""Count keyword occurrences with surah/collection grouping."""
|
| 205 |
+
cached = await analysis_cache.get(keyword, source_type or "all")
|
| 206 |
if cached:
|
| 207 |
return cached
|
| 208 |
|
|
|
|
| 213 |
examples: list = []
|
| 214 |
|
| 215 |
for item in dataset:
|
| 216 |
+
if source_type and item.get("type") != source_type:
|
| 217 |
continue
|
| 218 |
|
| 219 |
ar_norm = normalize_arabic(item.get("arabic", ""), aggressive=True).lower()
|
|
|
|
| 246 |
"by_surah": dict(sorted(by_surah.items())),
|
| 247 |
"examples": examples,
|
| 248 |
}
|
| 249 |
+
await analysis_cache.set(result, keyword, source_type or "all")
|
| 250 |
return result
|
| 251 |
|
| 252 |
|
app/arabic_nlp.py
CHANGED
|
@@ -88,11 +88,18 @@ def language_instruction(lang: str) -> str:
|
|
| 88 |
return {
|
| 89 |
"arabic": (
|
| 90 |
"يجب أن تكون الإجابة كاملةً باللغة العربية الفصحى تماماً. "
|
| 91 |
-
"لا تستخدم الإنجليزية أو أي لغة أخرى في أي جزء من الإجابة
|
|
|
|
|
|
|
| 92 |
),
|
| 93 |
"mixed": (
|
| 94 |
"The question mixes Arabic and English. Reply primarily in Arabic (الفصحى) "
|
| 95 |
-
"but you may
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
),
|
| 97 |
-
"english": "You MUST reply entirely in clear, formal English.",
|
| 98 |
}.get(lang, "You MUST reply entirely in clear, formal English.")
|
|
|
|
| 88 |
return {
|
| 89 |
"arabic": (
|
| 90 |
"يجب أن تكون الإجابة كاملةً باللغة العربية الفصحى تماماً. "
|
| 91 |
+
"لا تستخدم الإنجليزية أو أي لغة أخرى في أي جزء من الإجابة، "
|
| 92 |
+
"باستثناء الاقتباسات الموجودة في صناديق الأدلة فقط. "
|
| 93 |
+
"إذا كان السؤال بالعربية، أجب بالعربية حصراً."
|
| 94 |
),
|
| 95 |
"mixed": (
|
| 96 |
"The question mixes Arabic and English. Reply primarily in Arabic (الفصحى) "
|
| 97 |
+
"but you may include English transliterations for key Islamic terms where essential. "
|
| 98 |
+
"Match the dominant language of the question."
|
| 99 |
+
),
|
| 100 |
+
"english": (
|
| 101 |
+
"You MUST reply entirely in clear, formal English. "
|
| 102 |
+
"Do NOT use Arabic in your explanation — only inside evidence quotation boxes. "
|
| 103 |
+
"The user asked in English and expects an English answer."
|
| 104 |
),
|
|
|
|
| 105 |
}.get(lang, "You MUST reply entirely in clear, formal English.")
|
app/llm.py
CHANGED
|
@@ -43,6 +43,7 @@ class OllamaProvider(LLMProvider):
|
|
| 43 |
model=self.model,
|
| 44 |
messages=messages,
|
| 45 |
options={"temperature": temperature, "num_predict": max_tokens},
|
|
|
|
| 46 |
),
|
| 47 |
)
|
| 48 |
return result["message"]["content"].strip()
|
|
@@ -69,12 +70,20 @@ class GGUFProvider(LLMProvider):
|
|
| 69 |
async def chat(
|
| 70 |
self, messages: List[dict], temperature: float, max_tokens: int
|
| 71 |
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
loop = asyncio.get_event_loop()
|
| 73 |
try:
|
| 74 |
result = await loop.run_in_executor(
|
| 75 |
None,
|
| 76 |
lambda: self.llm.create_chat_completion(
|
| 77 |
-
messages=
|
| 78 |
temperature=temperature,
|
| 79 |
max_tokens=max_tokens,
|
| 80 |
),
|
|
@@ -177,18 +186,19 @@ class HuggingFaceProvider(LLMProvider):
|
|
| 177 |
|
| 178 |
def get_llm_provider() -> LLMProvider:
|
| 179 |
"""Factory function to get the configured LLM provider."""
|
| 180 |
-
|
|
|
|
| 181 |
logger.info("Using Ollama backend: %s @ %s", cfg.OLLAMA_MODEL, cfg.OLLAMA_HOST)
|
| 182 |
return OllamaProvider(cfg.OLLAMA_HOST, cfg.OLLAMA_MODEL)
|
| 183 |
-
elif
|
| 184 |
logger.info("Using HuggingFace backend: %s on %s", cfg.HF_MODEL_NAME, cfg.HF_DEVICE)
|
| 185 |
return HuggingFaceProvider(cfg.HF_MODEL_NAME, cfg.HF_DEVICE)
|
| 186 |
-
elif
|
| 187 |
logger.info("Using GGUF backend: %s (ctx=%d, gpu_layers=%d)",
|
| 188 |
cfg.GGUF_MODEL_PATH, cfg.GGUF_N_CTX, cfg.GGUF_N_GPU_LAYERS)
|
| 189 |
return GGUFProvider(cfg.GGUF_MODEL_PATH, cfg.GGUF_N_CTX, cfg.GGUF_N_GPU_LAYERS)
|
| 190 |
-
elif
|
| 191 |
logger.info("Using LM Studio backend: %s @ %s", cfg.LMSTUDIO_MODEL, cfg.LMSTUDIO_URL)
|
| 192 |
return LMStudioProvider(cfg.LMSTUDIO_URL, cfg.LMSTUDIO_MODEL)
|
| 193 |
else:
|
| 194 |
-
raise ValueError(f"Unknown LLM_BACKEND: {cfg.LLM_BACKEND}")
|
|
|
|
| 43 |
model=self.model,
|
| 44 |
messages=messages,
|
| 45 |
options={"temperature": temperature, "num_predict": max_tokens},
|
| 46 |
+
think=False,
|
| 47 |
),
|
| 48 |
)
|
| 49 |
return result["message"]["content"].strip()
|
|
|
|
| 70 |
async def chat(
|
| 71 |
self, messages: List[dict], temperature: float, max_tokens: int
|
| 72 |
) -> str:
|
| 73 |
+
# Disable Qwen3 thinking mode by appending /no_think to the system message
|
| 74 |
+
patched = []
|
| 75 |
+
for msg in messages:
|
| 76 |
+
if msg["role"] == "system" and "/no_think" not in msg["content"]:
|
| 77 |
+
patched.append({"role": "system", "content": msg["content"] + "\n/no_think"})
|
| 78 |
+
else:
|
| 79 |
+
patched.append(msg)
|
| 80 |
+
|
| 81 |
loop = asyncio.get_event_loop()
|
| 82 |
try:
|
| 83 |
result = await loop.run_in_executor(
|
| 84 |
None,
|
| 85 |
lambda: self.llm.create_chat_completion(
|
| 86 |
+
messages=patched,
|
| 87 |
temperature=temperature,
|
| 88 |
max_tokens=max_tokens,
|
| 89 |
),
|
|
|
|
| 186 |
|
| 187 |
def get_llm_provider() -> LLMProvider:
|
| 188 |
"""Factory function to get the configured LLM provider."""
|
| 189 |
+
backend = cfg.LLM_BACKEND.lower()
|
| 190 |
+
if backend == "ollama":
|
| 191 |
logger.info("Using Ollama backend: %s @ %s", cfg.OLLAMA_MODEL, cfg.OLLAMA_HOST)
|
| 192 |
return OllamaProvider(cfg.OLLAMA_HOST, cfg.OLLAMA_MODEL)
|
| 193 |
+
elif backend == "hf":
|
| 194 |
logger.info("Using HuggingFace backend: %s on %s", cfg.HF_MODEL_NAME, cfg.HF_DEVICE)
|
| 195 |
return HuggingFaceProvider(cfg.HF_MODEL_NAME, cfg.HF_DEVICE)
|
| 196 |
+
elif backend == "gguf":
|
| 197 |
logger.info("Using GGUF backend: %s (ctx=%d, gpu_layers=%d)",
|
| 198 |
cfg.GGUF_MODEL_PATH, cfg.GGUF_N_CTX, cfg.GGUF_N_GPU_LAYERS)
|
| 199 |
return GGUFProvider(cfg.GGUF_MODEL_PATH, cfg.GGUF_N_CTX, cfg.GGUF_N_GPU_LAYERS)
|
| 200 |
+
elif backend == "lmstudio":
|
| 201 |
logger.info("Using LM Studio backend: %s @ %s", cfg.LMSTUDIO_MODEL, cfg.LMSTUDIO_URL)
|
| 202 |
return LMStudioProvider(cfg.LMSTUDIO_URL, cfg.LMSTUDIO_MODEL)
|
| 203 |
else:
|
| 204 |
+
raise ValueError(f"Unknown LLM_BACKEND: {cfg.LLM_BACKEND!r}")
|
app/prompts.py
CHANGED
|
@@ -17,72 +17,78 @@ PERSONA = (
|
|
| 17 |
|
| 18 |
TASK_INSTRUCTIONS: Dict[str, str] = {
|
| 19 |
"tafsir": (
|
| 20 |
-
"The user asks about a Quranic verse. Steps:\n"
|
| 21 |
-
"1. Identify the verse(s) from
|
| 22 |
-
"2.
|
| 23 |
-
"3.
|
| 24 |
-
"4.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
),
|
| 26 |
"hadith": (
|
| 27 |
-
"The user asks about a Hadith
|
| 28 |
-
"1.
|
| 29 |
-
"2.
|
| 30 |
-
"
|
| 31 |
-
"
|
| 32 |
-
"
|
| 33 |
-
"
|
| 34 |
-
"
|
| 35 |
-
"CRITICAL: If the Hadith is NOT in
|
| 36 |
-
"Quote hadith text VERBATIM from context — never paraphrase the matn."
|
| 37 |
),
|
| 38 |
"auth": (
|
| 39 |
-
"The user asks about Hadith authenticity
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
"Provide
|
| 47 |
-
"
|
| 48 |
-
"
|
| 49 |
-
"
|
| 50 |
-
"
|
| 51 |
-
"
|
| 52 |
-
"
|
| 53 |
-
"الخلاصة — Comprehensive summary restating the verdict with key evidence.\n\n"
|
| 54 |
-
"RULES:\n"
|
| 55 |
-
"• If found in Sahih Bukhari or Sahih Muslim → assert AUTHENTIC (Sahih).\n"
|
| 56 |
-
"• Quote hadith text VERBATIM from context — never paraphrase the matn.\n"
|
| 57 |
-
"• You may add scholarly commentary to explain significance and context.\n"
|
| 58 |
-
"• If NOT found in context → clearly state it is absent from the dataset.\n"
|
| 59 |
-
"• NEVER fabricate hadith text, grades, or source citations."
|
| 60 |
),
|
| 61 |
"fatwa": (
|
| 62 |
-
"The user seeks a religious ruling. Steps:\n"
|
| 63 |
-
"1.
|
| 64 |
-
"2.
|
| 65 |
-
"3.
|
|
|
|
|
|
|
|
|
|
| 66 |
),
|
| 67 |
"count": (
|
| 68 |
-
"The user asks
|
| 69 |
-
"1. State the ANALYSIS RESULT
|
| 70 |
-
"2.
|
| 71 |
-
"3.
|
|
|
|
|
|
|
|
|
|
| 72 |
),
|
| 73 |
"surah_info": (
|
| 74 |
-
"The user asks about surah metadata. Steps:\n"
|
| 75 |
-
"1.
|
| 76 |
-
"2. Use the total_verses number
|
| 77 |
-
"3.
|
| 78 |
-
"4.
|
|
|
|
|
|
|
|
|
|
| 79 |
),
|
| 80 |
"general": (
|
| 81 |
-
"The user has a general Islamic question.
|
| 82 |
-
"1.
|
| 83 |
-
"2.
|
| 84 |
-
"
|
| 85 |
-
"
|
| 86 |
),
|
| 87 |
}
|
| 88 |
|
|
@@ -96,14 +102,21 @@ For EVERY supporting evidence, use this exact format:
|
|
| 96 |
└─────────────────────────────────────────────┘
|
| 97 |
|
| 98 |
ABSOLUTE RULES:
|
| 99 |
-
•
|
| 100 |
-
•
|
| 101 |
-
• NEVER fabricate hadith text, grades, verse numbers, or source citations.
|
| 102 |
• If a specific Hadith/verse is NOT in context → respond with:
|
| 103 |
"هذا الحديث/الآية غير موجود في قاعدة البيانات." (Arabic)
|
| 104 |
or "This Hadith/verse is not in the available dataset." (English)
|
| 105 |
• Never invent or guess content.
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
"""
|
| 108 |
|
| 109 |
_SYSTEM_TEMPLATE = """\
|
|
@@ -116,18 +129,10 @@ _SYSTEM_TEMPLATE = """\
|
|
| 116 |
|
| 117 |
=== OUTPUT FORMAT ===
|
| 118 |
{fmt}
|
| 119 |
-
"""
|
| 120 |
|
| 121 |
-
|
| 122 |
-
IMPORTANT: The database has already been searched for you.
|
| 123 |
-
The relevant results are provided below — use ONLY this data to formulate your answer.
|
| 124 |
-
Do NOT state that you need a database or ask the user for data. Answer from the context below.
|
| 125 |
-
|
| 126 |
-
=== RETRIEVED DATABASE RESULTS ===
|
| 127 |
{context}
|
| 128 |
-
=== END
|
| 129 |
-
|
| 130 |
-
Now answer the following question using ONLY the data above:
|
| 131 |
"""
|
| 132 |
|
| 133 |
|
|
@@ -169,18 +174,17 @@ def build_messages(
|
|
| 169 |
lang_instruction=language_instruction(lang),
|
| 170 |
task=TASK_INSTRUCTIONS.get(intent, TASK_INSTRUCTIONS["general"]),
|
| 171 |
fmt=FORMAT_RULES,
|
|
|
|
| 172 |
)
|
| 173 |
|
| 174 |
-
context_block = _CONTEXT_TEMPLATE.format(context=context)
|
| 175 |
-
|
| 176 |
cot = {
|
| 177 |
-
"arabic": "فكّر خطوةً بخطوة، ثم أجب: ",
|
| 178 |
-
"mixed": "
|
| 179 |
-
}.get(lang, "Think step by step: ")
|
| 180 |
|
| 181 |
return [
|
| 182 |
{"role": "system", "content": system},
|
| 183 |
-
{"role": "user", "content":
|
| 184 |
]
|
| 185 |
|
| 186 |
|
|
|
|
| 17 |
|
| 18 |
TASK_INSTRUCTIONS: Dict[str, str] = {
|
| 19 |
"tafsir": (
|
| 20 |
+
"The user asks about a Quranic verse — by partial text, topic, or meaning. Steps:\n"
|
| 21 |
+
"1. Identify the matching verse(s) from the RETRIEVED RESULTS.\n"
|
| 22 |
+
"2. Quote the Arabic verse text EXACTLY from the results.\n"
|
| 23 |
+
"3. Provide the full reference: Surah name (Arabic & English), number, and Ayah number.\n"
|
| 24 |
+
"4. Provide the English translation EXACTLY as given in the results.\n"
|
| 25 |
+
"5. If the user searched by partial text, confirm the full verse found.\n"
|
| 26 |
+
"6. Provide Tafsir: explain the meaning, context, and significance.\n"
|
| 27 |
+
"7. If related verses appear in the results, draw connections.\n"
|
| 28 |
+
"8. Answer the user's specific question directly.\n"
|
| 29 |
+
"9. Do NOT reference verses that are not in the results."
|
| 30 |
),
|
| 31 |
"hadith": (
|
| 32 |
+
"The user asks about a Hadith — by partial text, topic, or meaning. Steps:\n"
|
| 33 |
+
"1. Find the best matching Hadith from the RETRIEVED RESULTS.\n"
|
| 34 |
+
"2. Quote the Hadith text EXACTLY — both Arabic and English from the results.\n"
|
| 35 |
+
"3. State the full reference: collection name, book/chapter, hadith number.\n"
|
| 36 |
+
"4. State the grade/authenticity (Sahih, Hasan, Da'if) if available in the results.\n"
|
| 37 |
+
"5. If the user searched by partial text, present the complete hadith found.\n"
|
| 38 |
+
"6. Explain the meaning, context, and scholarly implications.\n"
|
| 39 |
+
"7. Note any related Hadiths from the results.\n"
|
| 40 |
+
"CRITICAL: If the Hadith is NOT in the results, say so clearly — do NOT fabricate."
|
|
|
|
| 41 |
),
|
| 42 |
"auth": (
|
| 43 |
+
"The user asks about Hadith authenticity or grade. YOU MUST:\n"
|
| 44 |
+
"1. Search the RETRIEVED RESULTS carefully for the Hadith.\n"
|
| 45 |
+
"2. If FOUND:\n"
|
| 46 |
+
" a. State the grade (Sahih, Hasan, Da'if, etc.) PROMINENTLY at the start.\n"
|
| 47 |
+
" b. Hadiths from Sahih al-Bukhari or Sahih Muslim are AUTHENTIC (Sahih).\n"
|
| 48 |
+
" c. Hadiths from Sunan an-Nasa'i are generally Sahih.\n"
|
| 49 |
+
" d. Hadiths from Jami' at-Tirmidhi, Sunan Abu Dawud, Sunan Ibn Majah are generally Hasan.\n"
|
| 50 |
+
" e. Provide the full reference: collection, hadith number, chapter.\n"
|
| 51 |
+
" f. Quote the full Hadith text from the results.\n"
|
| 52 |
+
" g. Explain why this grade applies.\n"
|
| 53 |
+
"3. If NOT FOUND in the results:\n"
|
| 54 |
+
" a. Clearly state: the hadith was not found in the authenticated dataset.\n"
|
| 55 |
+
" b. Do NOT guess or fabricate a grade.\n"
|
| 56 |
+
"CRITICAL: Base authenticity ONLY on the retrieved results and collection source."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
),
|
| 58 |
"fatwa": (
|
| 59 |
+
"The user seeks a religious ruling or asks about Islamic law. Steps:\n"
|
| 60 |
+
"1. Give a direct answer to the ruling question first.\n"
|
| 61 |
+
"2. Gather supporting evidence from Quran and Hadith in the results.\n"
|
| 62 |
+
"3. Quote verses and hadiths with exact references from the results.\n"
|
| 63 |
+
"4. Present scholarly reasoning based ONLY on the evidence found.\n"
|
| 64 |
+
"5. If multiple scholarly opinions exist, mention them briefly.\n"
|
| 65 |
+
"6. If the results lack sufficient evidence, state so explicitly."
|
| 66 |
),
|
| 67 |
"count": (
|
| 68 |
+
"The user asks about word frequency or occurrence count. Steps:\n"
|
| 69 |
+
"1. State the ANALYSIS RESULT count PROMINENTLY and FIRST.\n"
|
| 70 |
+
"2. Use the EXACT numbers from the ANALYSIS RESULT — do NOT recalculate.\n"
|
| 71 |
+
"3. List the top example occurrences with Surah name (Arabic & English) and Ayah number.\n"
|
| 72 |
+
"4. Show the per-Surah breakdown from the analysis.\n"
|
| 73 |
+
"5. Comment on the significance and patterns of usage.\n"
|
| 74 |
+
"CRITICAL: The numbers in the ANALYSIS RESULT block are authoritative."
|
| 75 |
),
|
| 76 |
"surah_info": (
|
| 77 |
+
"The user asks about surah metadata (verse count, revelation type, etc.). Steps:\n"
|
| 78 |
+
"1. Answer the SPECIFIC question FIRST using the SURAH INFORMATION block.\n"
|
| 79 |
+
"2. Use the total_verses number EXACTLY as given — do NOT guess or calculate.\n"
|
| 80 |
+
"3. State the revelation type (Meccan/Medinan) from the data.\n"
|
| 81 |
+
"4. Mention the surah name in Arabic, English, and transliteration.\n"
|
| 82 |
+
"5. Mention the surah number.\n"
|
| 83 |
+
"6. Optionally add brief scholarly context about the surah.\n"
|
| 84 |
+
"CRITICAL: The SURAH INFORMATION block is the ONLY authoritative source."
|
| 85 |
),
|
| 86 |
"general": (
|
| 87 |
+
"The user has a general Islamic question. Steps:\n"
|
| 88 |
+
"1. Give a direct, clear answer first.\n"
|
| 89 |
+
"2. Support with evidence from the RETRIEVED RESULTS.\n"
|
| 90 |
+
"3. Quote relevant verses or hadiths from the results with references.\n"
|
| 91 |
+
"4. Conclude with a brief summary."
|
| 92 |
),
|
| 93 |
}
|
| 94 |
|
|
|
|
| 102 |
└─────────────────────────────────────────────┘
|
| 103 |
|
| 104 |
ABSOLUTE RULES:
|
| 105 |
+
• Use ONLY content from the Islamic Context block. Zero outside knowledge.
|
| 106 |
+
• Copy Arabic text and translations VERBATIM from context. Never paraphrase.
|
|
|
|
| 107 |
• If a specific Hadith/verse is NOT in context → respond with:
|
| 108 |
"هذا الحديث/الآية غير موجود في قاعدة البيانات." (Arabic)
|
| 109 |
or "This Hadith/verse is not in the available dataset." (English)
|
| 110 |
• Never invent or guess content.
|
| 111 |
+
|
| 112 |
+
LANGUAGE RULE (CRITICAL — MUST FOLLOW):
|
| 113 |
+
• You MUST answer in the SAME language as the user's question.
|
| 114 |
+
• Arabic question → answer ENTIRELY in Arabic (العربية الفصحى). No English except inside evidence boxes.
|
| 115 |
+
• English question → answer ENTIRELY in English. No Arabic except inside evidence boxes.
|
| 116 |
+
• Mixed question → answer primarily in Arabic with English transliterations where helpful.
|
| 117 |
+
• The evidence boxes always show both Arabic text and English translation regardless of language.
|
| 118 |
+
|
| 119 |
+
• End with: "والله أعلم." (Arabic response) or "And Allah knows best." (English response)
|
| 120 |
"""
|
| 121 |
|
| 122 |
_SYSTEM_TEMPLATE = """\
|
|
|
|
| 129 |
|
| 130 |
=== OUTPUT FORMAT ===
|
| 131 |
{fmt}
|
|
|
|
| 132 |
|
| 133 |
+
=== ISLAMIC CONTEXT ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
{context}
|
| 135 |
+
=== END CONTEXT ===
|
|
|
|
|
|
|
| 136 |
"""
|
| 137 |
|
| 138 |
|
|
|
|
| 174 |
lang_instruction=language_instruction(lang),
|
| 175 |
task=TASK_INSTRUCTIONS.get(intent, TASK_INSTRUCTIONS["general"]),
|
| 176 |
fmt=FORMAT_RULES,
|
| 177 |
+
context=context,
|
| 178 |
)
|
| 179 |
|
|
|
|
|
|
|
| 180 |
cot = {
|
| 181 |
+
"arabic": "فكّر خطوةً بخطوة، ثم أجب باللغة العربية فقط: ",
|
| 182 |
+
"mixed": "فكّر خطوةً بخطوة، ثم أجب: ",
|
| 183 |
+
}.get(lang, "Think step by step, answer in English: ")
|
| 184 |
|
| 185 |
return [
|
| 186 |
{"role": "system", "content": system},
|
| 187 |
+
{"role": "user", "content": cot + question},
|
| 188 |
]
|
| 189 |
|
| 190 |
|
app/routers/chat.py
CHANGED
|
@@ -1,23 +1,20 @@
|
|
| 1 |
-
"""Chat / inference endpoints — OpenAI-compatible
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
| 6 |
import logging
|
| 7 |
import time
|
| 8 |
-
from typing import Optional
|
| 9 |
|
| 10 |
-
from fastapi import APIRouter, HTTPException
|
| 11 |
from fastapi.responses import StreamingResponse
|
| 12 |
|
| 13 |
from app.config import cfg
|
| 14 |
from app.models import (
|
| 15 |
-
AskResponse,
|
| 16 |
ChatCompletionChoice,
|
| 17 |
ChatCompletionMessage,
|
| 18 |
ChatCompletionRequest,
|
| 19 |
ChatCompletionResponse,
|
| 20 |
-
SourceItem,
|
| 21 |
)
|
| 22 |
from app.state import check_ready, run_rag_pipeline, state
|
| 23 |
|
|
@@ -123,41 +120,3 @@ async def _stream_response(result: dict, model: str):
|
|
| 123 |
}
|
| 124 |
yield f"data: {json.dumps(final)}\n\n"
|
| 125 |
yield "data: [DONE]\n\n"
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# ───────────────────────────────────────────────────────
|
| 129 |
-
# GET /ask — main inference endpoint
|
| 130 |
-
# ───────────────────────────────────────────────────────
|
| 131 |
-
@router.get("/ask", response_model=AskResponse)
|
| 132 |
-
async def ask(
|
| 133 |
-
q: str = Query(..., min_length=1, max_length=1000, description="Your Islamic question"),
|
| 134 |
-
top_k: int = Query(cfg.TOP_K_RETURN, ge=1, le=20, description="Number of sources"),
|
| 135 |
-
source_type: Optional[str] = Query(None, description="Filter: quran|hadith"),
|
| 136 |
-
grade_filter: Optional[str] = Query(None, description="Filter Hadith: sahih|hasan|all"),
|
| 137 |
-
):
|
| 138 |
-
"""Main inference endpoint — runs the full RAG pipeline."""
|
| 139 |
-
check_ready()
|
| 140 |
-
result = await run_rag_pipeline(q, top_k, source_type, grade_filter)
|
| 141 |
-
|
| 142 |
-
sources = [
|
| 143 |
-
SourceItem(
|
| 144 |
-
source=r.get("source") or r.get("reference") or "Unknown",
|
| 145 |
-
type=r.get("type", "unknown"),
|
| 146 |
-
grade=r.get("grade"),
|
| 147 |
-
arabic=r.get("arabic", ""),
|
| 148 |
-
english=r.get("english", ""),
|
| 149 |
-
_score=r.get("_score", 0.0),
|
| 150 |
-
)
|
| 151 |
-
for r in result["sources"]
|
| 152 |
-
]
|
| 153 |
-
|
| 154 |
-
return AskResponse(
|
| 155 |
-
question=q,
|
| 156 |
-
answer=result["answer"],
|
| 157 |
-
language=result["language"],
|
| 158 |
-
intent=result["intent"],
|
| 159 |
-
analysis=result["analysis"],
|
| 160 |
-
sources=sources,
|
| 161 |
-
top_score=result["top_score"],
|
| 162 |
-
latency_ms=result["latency_ms"],
|
| 163 |
-
)
|
|
|
|
| 1 |
+
"""Chat / inference endpoints — OpenAI-compatible."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import json
|
| 6 |
import logging
|
| 7 |
import time
|
|
|
|
| 8 |
|
| 9 |
+
from fastapi import APIRouter, HTTPException
|
| 10 |
from fastapi.responses import StreamingResponse
|
| 11 |
|
| 12 |
from app.config import cfg
|
| 13 |
from app.models import (
|
|
|
|
| 14 |
ChatCompletionChoice,
|
| 15 |
ChatCompletionMessage,
|
| 16 |
ChatCompletionRequest,
|
| 17 |
ChatCompletionResponse,
|
|
|
|
| 18 |
)
|
| 19 |
from app.state import check_ready, run_rag_pipeline, state
|
| 20 |
|
|
|
|
| 120 |
}
|
| 121 |
yield f"data: {json.dumps(final)}\n\n"
|
| 122 |
yield "data: [DONE]\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/hadith.py
CHANGED
|
@@ -30,12 +30,36 @@ async def hadith_text_search(
|
|
| 30 |
):
|
| 31 |
"""Search for Hadith by partial text match (Arabic or English).
|
| 32 |
|
| 33 |
-
Performs exact substring matching plus word-overlap scoring.
|
|
|
|
| 34 |
Use this to find a hadith when you know part of the text.
|
| 35 |
"""
|
| 36 |
check_ready()
|
| 37 |
results = text_search(q, state.dataset, source_type="hadith", limit=limit)
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Optional collection filter
|
| 40 |
if collection:
|
| 41 |
col_lower = collection.lower()
|
|
@@ -72,7 +96,11 @@ async def hadith_topic_search(
|
|
| 72 |
top_k: int = Query(10, ge=1, le=20),
|
| 73 |
grade_filter: Optional[str] = Query(None, description="Grade filter: sahih|hasan|all"),
|
| 74 |
):
|
| 75 |
-
"""Search for Hadith related to a topic/theme using semantic search.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
check_ready()
|
| 77 |
rewrite = await rewrite_query(topic, state.llm)
|
| 78 |
results = await hybrid_search(
|
|
@@ -110,12 +138,13 @@ async def verify_hadith(
|
|
| 110 |
"""Verify if a Hadith is in authenticated collections and check its grade.
|
| 111 |
|
| 112 |
Uses both semantic search and text matching for best accuracy.
|
|
|
|
| 113 |
"""
|
| 114 |
check_ready()
|
| 115 |
t0 = time.perf_counter()
|
| 116 |
|
| 117 |
# 1. Try text search first for exact matches
|
| 118 |
-
text_results = text_search(q, state.dataset, source_type="hadith", limit=
|
| 119 |
if collection:
|
| 120 |
col_lower = collection.lower()
|
| 121 |
text_results = [
|
|
@@ -123,15 +152,17 @@ async def verify_hadith(
|
|
| 123 |
if col_lower in (r.get("collection", "") or r.get("reference", "")).lower()
|
| 124 |
]
|
| 125 |
|
| 126 |
-
# 2. Also try semantic search
|
|
|
|
|
|
|
| 127 |
semantic_results = await hybrid_search(
|
| 128 |
-
q,
|
| 129 |
-
{"ar_query": q, "en_query": q, "keywords": q.split()[:7], "intent": "auth"},
|
| 130 |
state.embed_model, state.faiss_index, state.dataset,
|
| 131 |
-
top_n=
|
| 132 |
)
|
| 133 |
|
| 134 |
-
# 3. Pick best result
|
|
|
|
| 135 |
best = None
|
| 136 |
if text_results and text_results[0].get("_score", 0) > 2.0:
|
| 137 |
best = text_results[0]
|
|
|
|
| 30 |
):
|
| 31 |
"""Search for Hadith by partial text match (Arabic or English).
|
| 32 |
|
| 33 |
+
Performs exact substring matching plus word-overlap and n-gram scoring.
|
| 34 |
+
Falls back to semantic search when text matching yields few results.
|
| 35 |
Use this to find a hadith when you know part of the text.
|
| 36 |
"""
|
| 37 |
check_ready()
|
| 38 |
results = text_search(q, state.dataset, source_type="hadith", limit=limit)
|
| 39 |
|
| 40 |
+
# Optional collection filter
|
| 41 |
+
if collection:
|
| 42 |
+
col_lower = collection.lower()
|
| 43 |
+
results = [
|
| 44 |
+
r for r in results
|
| 45 |
+
if col_lower in (r.get("collection", "") or r.get("reference", "")).lower()
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
# If text search returns few results, augment with semantic search
|
| 49 |
+
if len(results) < 3:
|
| 50 |
+
rewrite = await rewrite_query(q, state.llm)
|
| 51 |
+
sem_results = await hybrid_search(
|
| 52 |
+
q, rewrite,
|
| 53 |
+
state.embed_model, state.faiss_index, state.dataset,
|
| 54 |
+
top_n=limit, source_type="hadith",
|
| 55 |
+
)
|
| 56 |
+
seen_ids = {r.get("id") for r in results}
|
| 57 |
+
for sr in sem_results:
|
| 58 |
+
if sr.get("id") not in seen_ids:
|
| 59 |
+
results.append(sr)
|
| 60 |
+
seen_ids.add(sr.get("id"))
|
| 61 |
+
results = sorted(results, key=lambda x: x.get("_score", 0), reverse=True)[:limit]
|
| 62 |
+
|
| 63 |
# Optional collection filter
|
| 64 |
if collection:
|
| 65 |
col_lower = collection.lower()
|
|
|
|
| 96 |
top_k: int = Query(10, ge=1, le=20),
|
| 97 |
grade_filter: Optional[str] = Query(None, description="Grade filter: sahih|hasan|all"),
|
| 98 |
):
|
| 99 |
+
"""Search for Hadith related to a topic/theme using semantic search.
|
| 100 |
+
|
| 101 |
+
Finds hadiths about a topic even when the exact words don't appear (e.g. "patience", "charity").
|
| 102 |
+
Optionally filter by authenticity grade.
|
| 103 |
+
"""
|
| 104 |
check_ready()
|
| 105 |
rewrite = await rewrite_query(topic, state.llm)
|
| 106 |
results = await hybrid_search(
|
|
|
|
| 138 |
"""Verify if a Hadith is in authenticated collections and check its grade.
|
| 139 |
|
| 140 |
Uses both semantic search and text matching for best accuracy.
|
| 141 |
+
Returns authenticity grade based on collection source.
|
| 142 |
"""
|
| 143 |
check_ready()
|
| 144 |
t0 = time.perf_counter()
|
| 145 |
|
| 146 |
# 1. Try text search first for exact matches
|
| 147 |
+
text_results = text_search(q, state.dataset, source_type="hadith", limit=10)
|
| 148 |
if collection:
|
| 149 |
col_lower = collection.lower()
|
| 150 |
text_results = [
|
|
|
|
| 152 |
if col_lower in (r.get("collection", "") or r.get("reference", "")).lower()
|
| 153 |
]
|
| 154 |
|
| 155 |
+
# 2. Also try semantic search with auth intent for better matching
|
| 156 |
+
rewrite = await rewrite_query(q, state.llm)
|
| 157 |
+
rewrite["intent"] = "auth" # Force auth intent for grade-aware ranking
|
| 158 |
semantic_results = await hybrid_search(
|
| 159 |
+
q, rewrite,
|
|
|
|
| 160 |
state.embed_model, state.faiss_index, state.dataset,
|
| 161 |
+
top_n=10, source_type="hadith",
|
| 162 |
)
|
| 163 |
|
| 164 |
+
# 3. Pick best result — prefer high-confidence text matches,
|
| 165 |
+
# then semantic results, then lower-confidence text matches
|
| 166 |
best = None
|
| 167 |
if text_results and text_results[0].get("_score", 0) > 2.0:
|
| 168 |
best = text_results[0]
|
app/routers/ops.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
| 1 |
-
"""Operational endpoints — health, models
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import time
|
| 6 |
-
from typing import Optional
|
| 7 |
|
| 8 |
-
from fastapi import APIRouter
|
| 9 |
|
| 10 |
from app.config import cfg
|
| 11 |
from app.models import ModelInfo, ModelsListResponse
|
| 12 |
-
from app.
|
| 13 |
-
from app.state import check_ready, state
|
| 14 |
|
| 15 |
router = APIRouter(tags=["ops"])
|
| 16 |
|
|
@@ -37,33 +35,3 @@ def list_models():
|
|
| 37 |
ModelInfo(id="qmodel", created=int(time.time()), owned_by="elgendy"),
|
| 38 |
]
|
| 39 |
)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
@router.get("/debug/scores")
|
| 43 |
-
async def debug_scores(
|
| 44 |
-
q: str = Query(..., min_length=1, max_length=1000),
|
| 45 |
-
top_k: int = Query(10, ge=1, le=20),
|
| 46 |
-
):
|
| 47 |
-
"""Debug: inspect raw retrieval scores without LLM generation."""
|
| 48 |
-
check_ready()
|
| 49 |
-
rewrite = await rewrite_query(q, state.llm)
|
| 50 |
-
results = await hybrid_search(
|
| 51 |
-
q, rewrite,
|
| 52 |
-
state.embed_model, state.faiss_index, state.dataset, top_k,
|
| 53 |
-
)
|
| 54 |
-
return {
|
| 55 |
-
"intent": rewrite.get("intent"),
|
| 56 |
-
"threshold": cfg.CONFIDENCE_THRESHOLD,
|
| 57 |
-
"results": [
|
| 58 |
-
{
|
| 59 |
-
"rank": i + 1,
|
| 60 |
-
"source": r.get("source") or r.get("reference"),
|
| 61 |
-
"type": r.get("type"),
|
| 62 |
-
"grade": r.get("grade"),
|
| 63 |
-
"_dense": round(r.get("_dense", 0), 4),
|
| 64 |
-
"_sparse": round(r.get("_sparse", 0), 4),
|
| 65 |
-
"_score": round(r.get("_score", 0), 4),
|
| 66 |
-
}
|
| 67 |
-
for i, r in enumerate(results)
|
| 68 |
-
],
|
| 69 |
-
}
|
|
|
|
| 1 |
+
"""Operational endpoints — health, models."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import time
|
|
|
|
| 6 |
|
| 7 |
+
from fastapi import APIRouter
|
| 8 |
|
| 9 |
from app.config import cfg
|
| 10 |
from app.models import ModelInfo, ModelsListResponse
|
| 11 |
+
from app.state import state
|
|
|
|
| 12 |
|
| 13 |
router = APIRouter(tags=["ops"])
|
| 14 |
|
|
|
|
| 35 |
ModelInfo(id="qmodel", created=int(time.time()), owned_by="elgendy"),
|
| 36 |
]
|
| 37 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/quran.py
CHANGED
|
@@ -35,7 +35,7 @@ async def quran_text_search(
|
|
| 35 |
):
|
| 36 |
"""Search for Quran verses by partial text match (Arabic or English).
|
| 37 |
|
| 38 |
-
This performs exact substring matching
|
| 39 |
Use this to find a verse when you know part of the text.
|
| 40 |
"""
|
| 41 |
check_ready()
|
|
@@ -45,14 +45,15 @@ async def quran_text_search(
|
|
| 45 |
count=len(results),
|
| 46 |
results=[
|
| 47 |
{
|
| 48 |
-
"surah_number":
|
| 49 |
-
"surah_name_ar":
|
| 50 |
-
"surah_name_en":
|
| 51 |
-
"
|
| 52 |
-
"
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"
|
|
|
|
| 56 |
}
|
| 57 |
for r in results
|
| 58 |
],
|
|
@@ -67,7 +68,10 @@ async def quran_topic_search(
|
|
| 67 |
topic: str = Query(..., min_length=1, max_length=500, description="Topic or theme to search for"),
|
| 68 |
top_k: int = Query(10, ge=1, le=20),
|
| 69 |
):
|
| 70 |
-
"""Search for Quran verses related to a topic/theme using semantic search.
|
|
|
|
|
|
|
|
|
|
| 71 |
check_ready()
|
| 72 |
rewrite = await rewrite_query(topic, state.llm)
|
| 73 |
results = await hybrid_search(
|
|
@@ -80,14 +84,15 @@ async def quran_topic_search(
|
|
| 80 |
count=len(results),
|
| 81 |
results=[
|
| 82 |
{
|
| 83 |
-
"surah_number":
|
| 84 |
-
"surah_name_ar":
|
| 85 |
-
"surah_name_en":
|
| 86 |
-
"
|
| 87 |
-
"
|
| 88 |
-
"
|
| 89 |
-
"
|
| 90 |
-
"
|
|
|
|
| 91 |
}
|
| 92 |
for r in results
|
| 93 |
],
|
|
|
|
| 35 |
):
|
| 36 |
"""Search for Quran verses by partial text match (Arabic or English).
|
| 37 |
|
| 38 |
+
This performs exact substring matching, n-gram phrase matching, and word-overlap matching.
|
| 39 |
Use this to find a verse when you know part of the text.
|
| 40 |
"""
|
| 41 |
check_ready()
|
|
|
|
| 45 |
count=len(results),
|
| 46 |
results=[
|
| 47 |
{
|
| 48 |
+
"surah_number": r.get("surah_number"),
|
| 49 |
+
"surah_name_ar": r.get("surah_name_ar", ""),
|
| 50 |
+
"surah_name_en": r.get("surah_name_en", ""),
|
| 51 |
+
"surah_name_transliteration": r.get("surah_name_transliteration", ""),
|
| 52 |
+
"ayah": r.get("ayah_number") or r.get("verse_number"),
|
| 53 |
+
"arabic": r.get("arabic", ""),
|
| 54 |
+
"english": r.get("english", ""),
|
| 55 |
+
"source": r.get("source", ""),
|
| 56 |
+
"score": round(r.get("_score", 0), 4),
|
| 57 |
}
|
| 58 |
for r in results
|
| 59 |
],
|
|
|
|
| 68 |
topic: str = Query(..., min_length=1, max_length=500, description="Topic or theme to search for"),
|
| 69 |
top_k: int = Query(10, ge=1, le=20),
|
| 70 |
):
|
| 71 |
+
"""Search for Quran verses related to a topic/theme using semantic search.
|
| 72 |
+
|
| 73 |
+
Finds verses about a topic even when the exact words don't appear (e.g. "patience", "charity").
|
| 74 |
+
"""
|
| 75 |
check_ready()
|
| 76 |
rewrite = await rewrite_query(topic, state.llm)
|
| 77 |
results = await hybrid_search(
|
|
|
|
| 84 |
count=len(results),
|
| 85 |
results=[
|
| 86 |
{
|
| 87 |
+
"surah_number": r.get("surah_number"),
|
| 88 |
+
"surah_name_ar": r.get("surah_name_ar", ""),
|
| 89 |
+
"surah_name_en": r.get("surah_name_en", ""),
|
| 90 |
+
"surah_name_transliteration": r.get("surah_name_transliteration", ""),
|
| 91 |
+
"ayah": r.get("ayah_number") or r.get("verse_number"),
|
| 92 |
+
"arabic": r.get("arabic", ""),
|
| 93 |
+
"english": r.get("english", ""),
|
| 94 |
+
"source": r.get("source", ""),
|
| 95 |
+
"score": round(r.get("_score", 0), 4),
|
| 96 |
}
|
| 97 |
for r in results
|
| 98 |
],
|
app/search.py
CHANGED
|
@@ -36,24 +36,47 @@ Reply ONLY with a valid JSON object — no markdown, no preamble:
|
|
| 36 |
}
|
| 37 |
|
| 38 |
Intent Detection Rules (CRITICAL):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
- 'surah_info' intent = asking about surah metadata: verse count, revelation type, surah number
|
| 40 |
(كم عدد آيات سورة, كم آية في سورة, how many verses in surah, is surah X meccan/medinan)
|
| 41 |
-
- 'count' intent = asking for WORD frequency/occurrence count
|
|
|
|
| 42 |
NOTE: "كم عدد آيات سورة" is surah_info NOT count!
|
| 43 |
-
|
| 44 |
-
- '
|
| 45 |
-
- '
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
Examples:
|
| 49 |
-
- "
|
|
|
|
|
|
|
|
|
|
| 50 |
- "كم آية في سورة البقرة" → intent: surah_info
|
| 51 |
- "how many verses in surah al-baqara" → intent: surah_info
|
| 52 |
- "هل سورة الفاتحة مكية أم مدنية" → intent: surah_info
|
| 53 |
-
- "كم مرة ذُكرت كلمة مريم" → intent: count
|
| 54 |
-
- "
|
| 55 |
-
- "
|
| 56 |
-
- "ما
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
"""
|
| 58 |
|
| 59 |
|
|
@@ -240,18 +263,35 @@ def text_search(
|
|
| 240 |
|
| 241 |
score = 0.0
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
# Exact substring in normalized Arabic
|
| 244 |
if q_norm and q_norm in ar_norm:
|
| 245 |
# Boost for shorter docs (more specific match)
|
| 246 |
-
score = 3.0 + (1.0 / max(len(ar_norm), 1)) * 100
|
| 247 |
|
| 248 |
# Exact substring in English
|
| 249 |
if q_lower and q_lower in en_lower:
|
| 250 |
score = max(score, 2.0 + (1.0 / max(len(en_lower), 1)) * 100)
|
| 251 |
|
| 252 |
-
#
|
| 253 |
-
if
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
# Word-level overlap for lower-confidence matches
|
| 257 |
if score == 0.0:
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
Intent Detection Rules (CRITICAL):
|
| 39 |
+
- 'tafsir' intent = looking up Quranic verse(s) by partial text, topic, word, or asking about meaning
|
| 40 |
+
(ابحث عن آية, find verse, ما تفسير, verse about X, آية عن, الآية التي فيها, verse that says)
|
| 41 |
+
IMPORTANT: When user provides Arabic verse text to find, put that text in ar_query verbatim.
|
| 42 |
+
- 'hadith' intent = looking up Hadith by text, topic, or asking about meaning (NOT authenticity)
|
| 43 |
+
(ابحث عن حديث, find hadith, hadith about, حديث عن, ما معنى حديث, hadith that says)
|
| 44 |
+
IMPORTANT: When user provides Arabic hadith text to find, put that text in ar_query verbatim.
|
| 45 |
+
- 'auth' intent = asking about Hadith authenticity/grade/verification
|
| 46 |
+
(صحيح؟, هل صحيح, is it authentic, verify hadith, درجة الحديث, is this hadith real, هل هذا حديث صحيح)
|
| 47 |
+
IMPORTANT: Include the hadith text fragment in ar_query for matching.
|
| 48 |
- 'surah_info' intent = asking about surah metadata: verse count, revelation type, surah number
|
| 49 |
(كم عدد آيات سورة, كم آية في سورة, how many verses in surah, is surah X meccan/medinan)
|
| 50 |
+
- 'count' intent = asking for WORD frequency/occurrence count
|
| 51 |
+
(كم مرة ذُكرت كلمة, how many times is word X mentioned, عدد مرات ذكر كلمة)
|
| 52 |
NOTE: "كم عدد آيات سورة" is surah_info NOT count!
|
| 53 |
+
IMPORTANT: The word being counted MUST be the first keyword.
|
| 54 |
+
- 'fatwa' intent = asking for a religious ruling (ما حكم, is X halal/haram, حلال أم حرام)
|
| 55 |
+
- 'general' intent = other Islamic questions
|
| 56 |
+
|
| 57 |
+
Rewriting Rules:
|
| 58 |
+
- For verse/hadith text lookups: include the EXACT Arabic text fragment in ar_query
|
| 59 |
+
- For topic searches: expand the topic with Arabic synonyms and related terms in keywords
|
| 60 |
+
- For word frequency: extract the EXACT keyword being counted as the FIRST keyword
|
| 61 |
+
- keywords MUST include core Arabic terms for matching (e.g. صبر, رحمة, صلاة)
|
| 62 |
|
| 63 |
Examples:
|
| 64 |
+
- "ابحث عن الآية التي فيها إنا أعطيناك الكوثر" → intent: tafsir, ar_query: "إنا أعطيناك الكوثر"
|
| 65 |
+
- "Find the verse about patience" → intent: tafsir, keywords: ["صبر", "patience", "الصبر"]
|
| 66 |
+
- "ما الآية التي تتحدث عن الصدقة" → intent: tafsir, keywords: ["صدقة", "الصدقة", "إنفاق"]
|
| 67 |
+
- "كم عدد آيات سورة آل عمران" → intent: surah_info
|
| 68 |
- "كم آية في سورة البقرة" → intent: surah_info
|
| 69 |
- "how many verses in surah al-baqara" → intent: surah_info
|
| 70 |
- "هل سورة الفاتحة مكية أم مدنية" → intent: surah_info
|
| 71 |
+
- "كم مرة ذُكرت كلمة مريم في القرآن" → intent: count, keywords: ["مريم", ...]
|
| 72 |
+
- "how many times is mercy mentioned in Quran" → intent: count, keywords: ["رحمة", "mercy", "الرحمة"]
|
| 73 |
+
- "هل حديث إنما الأعمال بالنيات صحيح" → intent: auth, ar_query: "إنما الأعمال بالنيات"
|
| 74 |
+
- "is the hadith about actions by intentions authentic" → intent: auth, keywords: ["إنما الأعمال بالنيات", "actions", "intentions"]
|
| 75 |
+
- "ما معنى حديث إنما الأعمال" → intent: hadith, ar_query: "إنما الأعمال"
|
| 76 |
+
- "ابحث عن حديث عن الصبر" → intent: hadith, keywords: ["صبر", "الصبر", "patience"]
|
| 77 |
+
- "find hadith about fasting" → intent: hadith, keywords: ["صيام", "صوم", "fasting"]
|
| 78 |
+
- "ما حكم الربا في الإسلام" → intent: fatwa, keywords: ["ربا", "الربا", "usury"]
|
| 79 |
+
- "هل الحديث ده صحيح: من كان يؤمن بالله" → intent: auth, ar_query: "من كان يؤمن بالله"
|
| 80 |
"""
|
| 81 |
|
| 82 |
|
|
|
|
| 263 |
|
| 264 |
score = 0.0
|
| 265 |
|
| 266 |
+
# Exact substring in raw Arabic (with diacritics) — highest priority
|
| 267 |
+
if query.strip() in ar_raw:
|
| 268 |
+
score = max(score, 5.0)
|
| 269 |
+
|
| 270 |
# Exact substring in normalized Arabic
|
| 271 |
if q_norm and q_norm in ar_norm:
|
| 272 |
# Boost for shorter docs (more specific match)
|
| 273 |
+
score = max(score, 3.0 + (1.0 / max(len(ar_norm), 1)) * 100)
|
| 274 |
|
| 275 |
# Exact substring in English
|
| 276 |
if q_lower and q_lower in en_lower:
|
| 277 |
score = max(score, 2.0 + (1.0 / max(len(en_lower), 1)) * 100)
|
| 278 |
|
| 279 |
+
# N-gram phrase matching for partial Arabic text (3+ word sequences)
|
| 280 |
+
if score == 0.0 and q_norm:
|
| 281 |
+
q_words = q_norm.split()
|
| 282 |
+
if len(q_words) >= 3:
|
| 283 |
+
# Check sliding windows of 3 words from query against doc
|
| 284 |
+
for i in range(len(q_words) - 2):
|
| 285 |
+
trigram = " ".join(q_words[i:i+3])
|
| 286 |
+
if trigram in ar_norm:
|
| 287 |
+
score = max(score, 2.0 + (i == 0) * 0.5)
|
| 288 |
+
break
|
| 289 |
+
if score == 0.0 and len(q_words) >= 2:
|
| 290 |
+
for i in range(len(q_words) - 1):
|
| 291 |
+
bigram = " ".join(q_words[i:i+2])
|
| 292 |
+
if bigram in ar_norm or bigram in en_lower:
|
| 293 |
+
score = max(score, 1.5)
|
| 294 |
+
break
|
| 295 |
|
| 296 |
# Word-level overlap for lower-confidence matches
|
| 297 |
if score == 0.0:
|
app/state.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
| 5 |
import asyncio
|
| 6 |
import json
|
| 7 |
import logging
|
|
|
|
| 8 |
import time
|
| 9 |
from contextlib import asynccontextmanager
|
| 10 |
from typing import Literal, Optional
|
|
@@ -28,6 +29,47 @@ from app.search import build_context, hybrid_search, rewrite_query, text_search
|
|
| 28 |
logger = logging.getLogger("qmodel.state")
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# ═══════════════════════════════════════════════════════════════════════
|
| 32 |
# HADITH GRADE INFERENCE
|
| 33 |
# ═══════════════════════════════════════════════════════════════════════
|
|
@@ -145,15 +187,27 @@ async def run_rag_pipeline(
|
|
| 145 |
)
|
| 146 |
|
| 147 |
# 2b. Text search fallback — catches exact matches missed by FAISS
|
| 148 |
-
#
|
| 149 |
-
#
|
| 150 |
seen_ids = {r.get("id") for r in results}
|
| 151 |
ar_q = rewrite.get("ar_query", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
for q in dict.fromkeys([ar_q, question]): # deduplicated, ar_query first
|
| 153 |
if not q:
|
| 154 |
continue
|
| 155 |
-
for hit in text_search(q, state.dataset,
|
| 156 |
if hit.get("id") not in seen_ids:
|
|
|
|
|
|
|
|
|
|
| 157 |
results.append(hit)
|
| 158 |
seen_ids.add(hit.get("id"))
|
| 159 |
if len(results) > top_k:
|
|
@@ -176,8 +230,9 @@ async def run_rag_pipeline(
|
|
| 176 |
# 3b. Word frequency count
|
| 177 |
analysis = None
|
| 178 |
if analysis_kw and not surah_info:
|
| 179 |
-
|
| 180 |
-
|
|
|
|
| 181 |
|
| 182 |
# 4. Language detection
|
| 183 |
lang = detect_language(question)
|
|
@@ -218,6 +273,9 @@ async def run_rag_pipeline(
|
|
| 218 |
logger.error("LLM call failed: %s", exc)
|
| 219 |
raise HTTPException(status_code=502, detail="LLM service unavailable")
|
| 220 |
|
|
|
|
|
|
|
|
|
|
| 221 |
latency = int((time.perf_counter() - t0) * 1000)
|
| 222 |
logger.info(
|
| 223 |
"Pipeline done | intent=%s | lang=%s | top_score=%.3f | %d ms",
|
|
|
|
| 5 |
import asyncio
|
| 6 |
import json
|
| 7 |
import logging
|
| 8 |
+
import re
|
| 9 |
import time
|
| 10 |
from contextlib import asynccontextmanager
|
| 11 |
from typing import Literal, Optional
|
|
|
|
| 29 |
logger = logging.getLogger("qmodel.state")
|
| 30 |
|
| 31 |
|
| 32 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 33 |
+
# POST-GENERATION HALLUCINATION CHECK
|
| 34 |
+
# ═══════════════════════════════════════════════════════════════════════
|
| 35 |
+
_QUOTE_RE = re.compile(r"❝\s*(.+?)\s*❞", re.DOTALL)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _verify_citations(answer: str, results: list) -> str:
|
| 39 |
+
"""Check that quoted Arabic text in the answer actually appears in retrieved results.
|
| 40 |
+
|
| 41 |
+
If a quoted block doesn't match any source, replace it with a warning.
|
| 42 |
+
This prevents the model from fabricating hadith or verse text.
|
| 43 |
+
"""
|
| 44 |
+
source_texts = set()
|
| 45 |
+
for r in results:
|
| 46 |
+
for field in ("arabic", "english", "text"):
|
| 47 |
+
val = r.get(field, "")
|
| 48 |
+
if val:
|
| 49 |
+
# Normalize whitespace for comparison
|
| 50 |
+
source_texts.add(re.sub(r"\s+", " ", val.strip()))
|
| 51 |
+
|
| 52 |
+
def _check_quote(m: re.Match) -> str:
|
| 53 |
+
quoted = re.sub(r"\s+", " ", m.group(1).strip())
|
| 54 |
+
# Check if any source text contains a significant portion of the quote
|
| 55 |
+
for src in source_texts:
|
| 56 |
+
# Use a substring match — LLMs sometimes trim edges
|
| 57 |
+
if len(quoted) < 10:
|
| 58 |
+
return m.group(0) # too short to verify
|
| 59 |
+
if quoted in src or src in quoted:
|
| 60 |
+
return m.group(0) # verified
|
| 61 |
+
# Check overlap: at least 60% of words match
|
| 62 |
+
q_words = set(quoted.split())
|
| 63 |
+
s_words = set(src.split())
|
| 64 |
+
if q_words and len(q_words & s_words) / len(q_words) >= 0.6:
|
| 65 |
+
return m.group(0) # close enough match
|
| 66 |
+
# Quote not found in any source — flag it
|
| 67 |
+
logger.warning("Hallucination detected: quoted text not in sources: %.80s...", quoted)
|
| 68 |
+
return "❝ ⚠️ [تم حذف نص غير موثق — النص غير موجود في قاعدة البيانات] ❞"
|
| 69 |
+
|
| 70 |
+
return _QUOTE_RE.sub(_check_quote, answer)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
# ═══════════════════════════════════════════════════════════════════════
|
| 74 |
# HADITH GRADE INFERENCE
|
| 75 |
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
| 187 |
)
|
| 188 |
|
| 189 |
# 2b. Text search fallback — catches exact matches missed by FAISS
|
| 190 |
+
# For auth/hadith/tafsir intents, also search with the rewritten ar_query
|
| 191 |
+
# which should contain the actual text fragment to look up.
|
| 192 |
seen_ids = {r.get("id") for r in results}
|
| 193 |
ar_q = rewrite.get("ar_query", "")
|
| 194 |
+
|
| 195 |
+
# Determine text search source filter based on intent
|
| 196 |
+
text_src = source_type
|
| 197 |
+
if not text_src and intent in ("tafsir", "count", "surah_info"):
|
| 198 |
+
text_src = "quran"
|
| 199 |
+
elif not text_src and intent in ("hadith", "auth"):
|
| 200 |
+
text_src = "hadith"
|
| 201 |
+
|
| 202 |
+
text_limit = top_k * 2 if intent in ("auth", "hadith", "tafsir") else top_k
|
| 203 |
for q in dict.fromkeys([ar_q, question]): # deduplicated, ar_query first
|
| 204 |
if not q:
|
| 205 |
continue
|
| 206 |
+
for hit in text_search(q, state.dataset, text_src, limit=text_limit):
|
| 207 |
if hit.get("id") not in seen_ids:
|
| 208 |
+
# Boost text search hits for auth intent (exact text match is crucial)
|
| 209 |
+
if intent == "auth" and hit.get("_score", 0) > 2.0:
|
| 210 |
+
hit["_score"] = hit["_score"] + 1.0
|
| 211 |
results.append(hit)
|
| 212 |
seen_ids.add(hit.get("id"))
|
| 213 |
if len(results) > top_k:
|
|
|
|
| 230 |
# 3b. Word frequency count
|
| 231 |
analysis = None
|
| 232 |
if analysis_kw and not surah_info:
|
| 233 |
+
count_src = "hadith" if intent in ("hadith", "auth") else "quran"
|
| 234 |
+
analysis = await count_occurrences(analysis_kw, state.dataset, source_type=count_src)
|
| 235 |
+
logger.info("Analysis: kw=%s src=%s count=%d", analysis_kw, count_src, analysis["total_count"])
|
| 236 |
|
| 237 |
# 4. Language detection
|
| 238 |
lang = detect_language(question)
|
|
|
|
| 273 |
logger.error("LLM call failed: %s", exc)
|
| 274 |
raise HTTPException(status_code=502, detail="LLM service unavailable")
|
| 275 |
|
| 276 |
+
# 7. Post-generation hallucination check — verify quoted text exists in sources
|
| 277 |
+
answer = _verify_citations(answer, results)
|
| 278 |
+
|
| 279 |
latency = int((time.perf_counter() - t0) * 1000)
|
| 280 |
logger.info(
|
| 281 |
"Pipeline done | intent=%s | lang=%s | top_score=%.3f | %d ms",
|
main.py
CHANGED
|
@@ -33,7 +33,7 @@ logging.basicConfig(
|
|
| 33 |
|
| 34 |
from app.config import cfg
|
| 35 |
from app.state import lifespan
|
| 36 |
-
from app.routers import chat,
|
| 37 |
|
| 38 |
# ═══════════════════════════════════════════════════════════════════════
|
| 39 |
# FASTAPI APP
|
|
@@ -43,11 +43,9 @@ app = FastAPI(
|
|
| 43 |
description=(
|
| 44 |
"Specialized Quran & Hadith system with dual LLM backend.\n\n"
|
| 45 |
"**Capabilities:**\n"
|
| 46 |
-
"-
|
| 47 |
-
"-
|
| 48 |
-
"-
|
| 49 |
-
"- Hadith authenticity verification\n"
|
| 50 |
-
"- OpenAI-compatible chat completions"
|
| 51 |
),
|
| 52 |
version="5.0.0",
|
| 53 |
lifespan=lifespan,
|
|
@@ -64,8 +62,6 @@ app.add_middleware(
|
|
| 64 |
# Register routers
|
| 65 |
app.include_router(ops.router)
|
| 66 |
app.include_router(chat.router)
|
| 67 |
-
app.include_router(quran.router)
|
| 68 |
-
app.include_router(hadith.router)
|
| 69 |
|
| 70 |
|
| 71 |
if __name__ == "__main__":
|
|
|
|
| 33 |
|
| 34 |
from app.config import cfg
|
| 35 |
from app.state import lifespan
|
| 36 |
+
from app.routers import chat, ops
|
| 37 |
|
| 38 |
# ═══════════════════════════════════════════════════════════════════════
|
| 39 |
# FASTAPI APP
|
|
|
|
| 43 |
description=(
|
| 44 |
"Specialized Quran & Hadith system with dual LLM backend.\n\n"
|
| 45 |
"**Capabilities:**\n"
|
| 46 |
+
"- OpenAI-compatible chat completions\n"
|
| 47 |
+
"- Streaming support\n"
|
| 48 |
+
"- Islamic knowledge RAG pipeline"
|
|
|
|
|
|
|
| 49 |
),
|
| 50 |
version="5.0.0",
|
| 51 |
lifespan=lifespan,
|
|
|
|
| 62 |
# Register routers
|
| 63 |
app.include_router(ops.router)
|
| 64 |
app.include_router(chat.router)
|
|
|
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
if __name__ == "__main__":
|