Update app.py
Browse files
app.py
CHANGED
|
@@ -16,33 +16,43 @@ client = OpenAI(
|
|
| 16 |
|
| 17 |
FINAL_NOTE = "Responses are generated from retrieved hadith evidence and the system is still under improvement."
|
| 18 |
|
|
|
|
| 19 |
SYSTEM_PROMPT = """
|
| 20 |
You are Hadithi AI, a professional assistant for explaining retrieved hadith evidence.
|
| 21 |
|
| 22 |
-
The user
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
STRICT OUTPUT FORMAT:
|
| 29 |
-
|
| 30 |
|
| 31 |
Answer:
|
| 32 |
-
Write
|
| 33 |
-
|
| 34 |
-
- begin
|
| 35 |
-
|
| 36 |
-
-
|
| 37 |
-
-
|
| 38 |
-
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
-
-
|
|
|
|
|
|
|
| 43 |
|
| 44 |
Hadith Evidence:
|
| 45 |
-
|
| 46 |
For each hadith, use exactly this structure:
|
| 47 |
|
| 48 |
- Source: ...
|
|
@@ -51,89 +61,248 @@ For each hadith, use exactly this structure:
|
|
| 51 |
- Text: ...
|
| 52 |
|
| 53 |
FINAL LINE:
|
| 54 |
-
End with this exact
|
| 55 |
Responses are generated from retrieved hadith evidence and the system is still under improvement.
|
| 56 |
|
| 57 |
-
|
| 58 |
-
- Do not create
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
return False
|
| 72 |
-
return bool(re.search(r'[\u0600-\u06FF]', text))
|
| 73 |
|
| 74 |
|
| 75 |
def normalize_quotes(text: str) -> str:
|
| 76 |
if not text:
|
| 77 |
return ""
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
if
|
| 85 |
-
return ""
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
text = re.sub(rf"(?s)\n*{re.escape(FINAL_NOTE)}\s*$", "", text).strip()
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
|
| 121 |
-
#
|
| 122 |
-
if "
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
|
| 129 |
-
# If Arabic requested, lightly relabel only headings, keep body as model returned
|
| 130 |
-
if is_arabic_request(user_message):
|
| 131 |
-
text = text.replace("Answer:", "الجواب:")
|
| 132 |
-
text = text.replace("Hadith Evidence:", "الأحاديث المسترجعة:")
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
|
| 139 |
def chat(message, history):
|
|
@@ -141,21 +310,22 @@ def chat(message, history):
|
|
| 141 |
|
| 142 |
for user_msg, assistant_msg in history:
|
| 143 |
if user_msg:
|
| 144 |
-
messages.append({"role": "user", "content": user_msg})
|
| 145 |
if assistant_msg:
|
| 146 |
messages.append({"role": "assistant", "content": assistant_msg})
|
| 147 |
|
| 148 |
-
|
|
|
|
| 149 |
|
| 150 |
try:
|
| 151 |
response = client.chat.completions.create(
|
| 152 |
model=MODEL_ID,
|
| 153 |
messages=messages,
|
| 154 |
-
temperature=0.
|
| 155 |
-
max_tokens=
|
| 156 |
)
|
| 157 |
-
|
| 158 |
-
answer = clean_answer(
|
| 159 |
except Exception as e:
|
| 160 |
answer = f"Error: {str(e)}"
|
| 161 |
|
|
|
|
| 16 |
|
| 17 |
FINAL_NOTE = "Responses are generated from retrieved hadith evidence and the system is still under improvement."
|
| 18 |
|
| 19 |
+
|
| 20 |
SYSTEM_PROMPT = """
|
| 21 |
You are Hadithi AI, a professional assistant for explaining retrieved hadith evidence.
|
| 22 |
|
| 23 |
+
The user message contains:
|
| 24 |
+
1) a question or topic
|
| 25 |
+
2) retrieved hadith evidence from the API
|
| 26 |
|
| 27 |
+
Your task:
|
| 28 |
+
- Read the retrieved hadiths carefully
|
| 29 |
+
- Explain only what is supported by the retrieved hadiths
|
| 30 |
+
- Do not invent extra hadiths or unsupported claims
|
| 31 |
+
- Do not produce a generic bullet summary
|
| 32 |
+
- Do not produce the old flat style like:
|
| 33 |
+
"The hadiths provided emphasize..."
|
| 34 |
+
followed by bullet points
|
| 35 |
|
| 36 |
STRICT OUTPUT FORMAT:
|
| 37 |
+
You must output exactly these parts and in this order:
|
| 38 |
|
| 39 |
Answer:
|
| 40 |
+
Write one single polished paragraph in natural English.
|
| 41 |
+
This paragraph must:
|
| 42 |
+
- begin in a smooth explanatory style similar to:
|
| 43 |
+
"The retrieved hadiths show that..."
|
| 44 |
+
- explain the meaning of the hadith evidence clearly
|
| 45 |
+
- sound human, thoughtful, and elegant
|
| 46 |
+
- include short Arabic phrases only when useful
|
| 47 |
+
- place Arabic phrases naturally inside the explanation, with meaning
|
| 48 |
+
- not use bullet points
|
| 49 |
+
- not sound robotic
|
| 50 |
+
- not repeat the same idea
|
| 51 |
+
- not quote long hadith text
|
| 52 |
+
- summarize the evidence faithfully
|
| 53 |
|
| 54 |
Hadith Evidence:
|
| 55 |
+
Then list all retrieved hadiths in a clean way.
|
| 56 |
For each hadith, use exactly this structure:
|
| 57 |
|
| 58 |
- Source: ...
|
|
|
|
| 61 |
- Text: ...
|
| 62 |
|
| 63 |
FINAL LINE:
|
| 64 |
+
End with this exact line:
|
| 65 |
Responses are generated from retrieved hadith evidence and the system is still under improvement.
|
| 66 |
|
| 67 |
+
FORBIDDEN:
|
| 68 |
+
- Do not create headings like:
|
| 69 |
+
Main Insight
|
| 70 |
+
What the Hadiths Show
|
| 71 |
+
Key meanings
|
| 72 |
+
Supporting evidence summary
|
| 73 |
+
- Do not start with bullet points
|
| 74 |
+
- Do not write a short outline before the paragraph
|
| 75 |
+
- Do not say "The hadiths provided emphasize..." and then list bullets
|
| 76 |
+
- Do not skip the Answer paragraph
|
| 77 |
+
|
| 78 |
+
EXAMPLE OF THE DESIRED ANSWER STYLE:
|
| 79 |
+
Answer:
|
| 80 |
+
The retrieved hadiths show that mercy (raḥma / الرحمة) in Islam is both something believers ask from Allah and something they hope to experience in healing, forgiveness, and worship. One hadith includes the supplication "place Your mercy on earth" ("فاجعل رحمتك في الأرض"), presenting mercy as a source of relief and cure, while another links moments of worship with pausing at verses of mercy and praying for it, which shows that mercy is not only a theological idea but also a lived spiritual practice. Together, these narrations present mercy as divine care that the believer actively seeks in prayer, illness, and devotion.
|
| 81 |
|
| 82 |
+
Hadith Evidence:
|
| 83 |
+
- Source: Example
|
| 84 |
+
- Grade: Example
|
| 85 |
+
- Why it matters: Example
|
| 86 |
+
- Text: Example
|
| 87 |
|
| 88 |
+
Responses are generated from retrieved hadith evidence and the system is still under improvement.
|
| 89 |
+
""".strip()
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
def normalize_quotes(text: str) -> str:
|
| 93 |
if not text:
|
| 94 |
return ""
|
| 95 |
+
return (
|
| 96 |
+
text.replace("“", '"')
|
| 97 |
+
.replace("”", '"')
|
| 98 |
+
.replace("‘", "'")
|
| 99 |
+
.replace("’", "'")
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def extract_answer_and_evidence(raw_text: str):
|
| 104 |
+
"""
|
| 105 |
+
Try to split model output into Answer and Hadith Evidence.
|
| 106 |
+
If headings are missing, do a best-effort fallback.
|
| 107 |
+
"""
|
| 108 |
+
text = raw_text.strip()
|
| 109 |
+
|
| 110 |
+
# Normalize common variants
|
| 111 |
+
text = re.sub(r"(?im)^#+\s*answer\s*:?\s*$", "Answer:", text)
|
| 112 |
+
text = re.sub(r"(?im)^#+\s*hadith evidence\s*:?\s*$", "Hadith Evidence:", text)
|
| 113 |
+
text = text.replace("Supporting Hadiths:", "Hadith Evidence:")
|
| 114 |
+
text = text.replace("Referenced Hadiths:", "Hadith Evidence:")
|
| 115 |
+
text = text.replace("Evidence:", "Hadith Evidence:")
|
| 116 |
|
| 117 |
+
answer_match = re.search(r"(?is)Answer:\s*(.*?)(?:\n\s*Hadith Evidence:|\Z)", text)
|
| 118 |
+
evidence_match = re.search(r"(?is)Hadith Evidence:\s*(.*)$", text)
|
| 119 |
|
| 120 |
+
answer = answer_match.group(1).strip() if answer_match else ""
|
| 121 |
+
evidence = evidence_match.group(1).strip() if evidence_match else ""
|
|
|
|
| 122 |
|
| 123 |
+
if not answer and not evidence:
|
| 124 |
+
return text.strip(), ""
|
| 125 |
+
|
| 126 |
+
return answer, evidence
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def clean_intro_bullets(text: str) -> str:
|
| 130 |
+
"""
|
| 131 |
+
Remove old-style bullet summaries at the beginning of the answer if the model adds them.
|
| 132 |
+
"""
|
| 133 |
+
lines = [line.rstrip() for line in text.splitlines()]
|
| 134 |
+
cleaned = []
|
| 135 |
+
bullet_phase = True
|
| 136 |
+
|
| 137 |
+
for line in lines:
|
| 138 |
+
stripped = line.strip()
|
| 139 |
+
|
| 140 |
+
if bullet_phase and (
|
| 141 |
+
stripped.startswith("- ")
|
| 142 |
+
or re.match(r"^\d+\.\s+", stripped)
|
| 143 |
+
or stripped.lower().startswith("the hadiths provided")
|
| 144 |
+
):
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
if stripped:
|
| 148 |
+
bullet_phase = False
|
| 149 |
+
|
| 150 |
+
cleaned.append(line)
|
| 151 |
+
|
| 152 |
+
result = "\n".join(cleaned).strip()
|
| 153 |
+
return result
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def clean_answer_paragraph(answer: str) -> str:
|
| 157 |
+
answer = normalize_quotes(answer)
|
| 158 |
+
answer = clean_intro_bullets(answer)
|
| 159 |
+
|
| 160 |
+
# remove obvious unwanted headings inside answer
|
| 161 |
+
bad_headings = [
|
| 162 |
+
"Main Insight:",
|
| 163 |
+
"What the Hadiths Show:",
|
| 164 |
+
"Key meanings:",
|
| 165 |
+
"Supporting evidence summary:",
|
| 166 |
+
"Short answer:",
|
| 167 |
+
]
|
| 168 |
+
for h in bad_headings:
|
| 169 |
+
answer = answer.replace(h, "")
|
| 170 |
+
|
| 171 |
+
# remove leftover bullets inside answer start
|
| 172 |
+
answer = re.sub(r"(?m)^\s*-\s+", "", answer)
|
| 173 |
+
answer = re.sub(r"\n{2,}", "\n\n", answer).strip()
|
| 174 |
+
|
| 175 |
+
# force single paragraph
|
| 176 |
+
answer = re.sub(r"\s*\n\s*", " ", answer)
|
| 177 |
+
answer = re.sub(r"\s{2,}", " ", answer).strip()
|
| 178 |
+
|
| 179 |
+
# if model starts badly, nudge it
|
| 180 |
+
if answer and not answer.lower().startswith("the retrieved hadiths show"):
|
| 181 |
+
answer = "The retrieved hadiths show that " + answer[0].lower() + answer[1:] if len(answer) > 1 else "The retrieved hadiths show that " + answer
|
| 182 |
|
| 183 |
+
return answer
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def parse_hadith_blocks_from_user_message(user_message: str):
|
| 187 |
+
"""
|
| 188 |
+
Fallback parser:
|
| 189 |
+
Extract hadith entries directly from the user message if the model fails
|
| 190 |
+
to produce a proper Hadith Evidence section.
|
| 191 |
+
"""
|
| 192 |
+
lines = user_message.splitlines()
|
| 193 |
+
blocks = []
|
| 194 |
+
i = 0
|
| 195 |
+
|
| 196 |
+
source_line_pattern = re.compile(r'^[A-Za-z0-9_\-]+(?:\s+[A-Za-z0-9_\-]+)*\s+#\d+', re.IGNORECASE)
|
| 197 |
+
|
| 198 |
+
while i < len(lines):
|
| 199 |
+
line = lines[i].strip()
|
| 200 |
+
|
| 201 |
+
if source_line_pattern.match(line):
|
| 202 |
+
source = line
|
| 203 |
+
grade = ""
|
| 204 |
+
text_lines = []
|
| 205 |
+
score = ""
|
| 206 |
+
|
| 207 |
+
if i + 1 < len(lines) and lines[i + 1].strip().lower().startswith("grade:"):
|
| 208 |
+
grade = lines[i + 1].strip()
|
| 209 |
+
# keep score if included on same line
|
| 210 |
+
i += 2
|
| 211 |
+
else:
|
| 212 |
+
i += 1
|
| 213 |
+
|
| 214 |
+
while i < len(lines):
|
| 215 |
+
current = lines[i].strip()
|
| 216 |
+
if source_line_pattern.match(current):
|
| 217 |
+
break
|
| 218 |
+
if current:
|
| 219 |
+
text_lines.append(current)
|
| 220 |
+
i += 1
|
| 221 |
+
|
| 222 |
+
full_text = " ".join(text_lines).strip()
|
| 223 |
+
|
| 224 |
+
why = infer_why_it_matters(full_text)
|
| 225 |
+
|
| 226 |
+
blocks.append({
|
| 227 |
+
"source": source,
|
| 228 |
+
"grade": grade if grade else "Grade: Unknown grade",
|
| 229 |
+
"why": why,
|
| 230 |
+
"text": full_text if full_text else "[No text provided]",
|
| 231 |
+
})
|
| 232 |
+
else:
|
| 233 |
+
i += 1
|
| 234 |
+
|
| 235 |
+
return blocks
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def infer_why_it_matters(hadith_text: str) -> str:
|
| 239 |
+
t = hadith_text
|
| 240 |
|
| 241 |
+
if "ارحم" in t or "رحمتك" in t or "رحمة" in t:
|
| 242 |
+
if "شفاء" in t or "اشف" in t or "الوجع" in t:
|
| 243 |
+
return "Connects mercy with healing, relief, and supplication."
|
| 244 |
+
return "Directly relates to mercy as a theme in prayer or belief."
|
| 245 |
+
|
| 246 |
+
if "شفاء" in t or "اشف" in t:
|
| 247 |
+
return "Relates to healing and seeking Allah’s cure."
|
| 248 |
+
|
| 249 |
+
if "آية رحمة" in t or "باية رحمة" in t:
|
| 250 |
+
return "Shows how verses of mercy were treated in worship and recitation."
|
| 251 |
+
|
| 252 |
+
if "غفر" in t or "اغفر" in t:
|
| 253 |
+
return "Relates to forgiveness and seeking Allah’s pardon."
|
| 254 |
+
|
| 255 |
+
return "Provides supporting context connected to the retrieved topic."
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def format_hadith_evidence(blocks) -> str:
|
| 259 |
+
if not blocks:
|
| 260 |
+
return "- Source: Not available\n- Grade: Not available\n- Why it matters: The retrieved evidence could not be formatted automatically.\n- Text: Not available"
|
| 261 |
+
|
| 262 |
+
formatted = []
|
| 263 |
+
for b in blocks:
|
| 264 |
+
formatted.append(
|
| 265 |
+
f"- Source: {b['source']}\n"
|
| 266 |
+
f"- Grade: {b['grade']}\n"
|
| 267 |
+
f"- Why it matters: {b['why']}\n"
|
| 268 |
+
f"- Text: {b['text']}"
|
| 269 |
+
)
|
| 270 |
+
return "\n\n".join(formatted)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def clean_answer(model_text: str, user_message: str) -> str:
|
| 274 |
+
text = normalize_quotes(model_text.strip())
|
| 275 |
+
|
| 276 |
+
# Remove final note if model already included it; we add once at the end
|
| 277 |
text = re.sub(rf"(?s)\n*{re.escape(FINAL_NOTE)}\s*$", "", text).strip()
|
| 278 |
|
| 279 |
+
answer, evidence = extract_answer_and_evidence(text)
|
| 280 |
+
answer = clean_answer_paragraph(answer if answer else text)
|
| 281 |
|
| 282 |
+
# If model failed to give usable evidence, build it from the user message
|
| 283 |
+
if not evidence or "- Source:" not in evidence:
|
| 284 |
+
parsed_blocks = parse_hadith_blocks_from_user_message(user_message)
|
| 285 |
+
evidence = format_hadith_evidence(parsed_blocks)
|
| 286 |
+
else:
|
| 287 |
+
evidence = normalize_quotes(evidence).strip()
|
| 288 |
|
| 289 |
+
final = f"Answer:\n{answer}\n\nHadith Evidence:\n{evidence}\n\n{FINAL_NOTE}"
|
| 290 |
+
final = re.sub(r"\n{3,}", "\n\n", final).strip()
|
| 291 |
+
return final
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
+
def build_user_message(message: str) -> str:
|
| 295 |
+
"""
|
| 296 |
+
Wrap the incoming API text so the model better understands that the user's
|
| 297 |
+
message contains both the question/topic and retrieved hadith evidence.
|
| 298 |
+
"""
|
| 299 |
+
return f"""User request and retrieved hadith evidence are below.
|
| 300 |
+
|
| 301 |
+
Please answer using only this evidence.
|
| 302 |
+
|
| 303 |
+
Retrieved content:
|
| 304 |
+
{message}
|
| 305 |
+
""".strip()
|
| 306 |
|
| 307 |
|
| 308 |
def chat(message, history):
|
|
|
|
| 310 |
|
| 311 |
for user_msg, assistant_msg in history:
|
| 312 |
if user_msg:
|
| 313 |
+
messages.append({"role": "user", "content": build_user_message(user_msg)})
|
| 314 |
if assistant_msg:
|
| 315 |
messages.append({"role": "assistant", "content": assistant_msg})
|
| 316 |
|
| 317 |
+
wrapped_message = build_user_message(message)
|
| 318 |
+
messages.append({"role": "user", "content": wrapped_message})
|
| 319 |
|
| 320 |
try:
|
| 321 |
response = client.chat.completions.create(
|
| 322 |
model=MODEL_ID,
|
| 323 |
messages=messages,
|
| 324 |
+
temperature=0.08,
|
| 325 |
+
max_tokens=1200,
|
| 326 |
)
|
| 327 |
+
model_text = response.choices[0].message.content.strip()
|
| 328 |
+
answer = clean_answer(model_text, user_message=message)
|
| 329 |
except Exception as e:
|
| 330 |
answer = f"Error: {str(e)}"
|
| 331 |
|