quran-api / server.py
Herrmo's picture
Update server.py
65b1ecd verified
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import chromadb
from sentence_transformers import SentenceTransformer
from openai import OpenAI
import uvicorn
import os
import re
# --- CONFIGURATION ---
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
DB_PATH = "quran_db"
AI_MODEL = "qwen/qwen3-235b-a22b-thinking-2507"
# ---------------------
app = FastAPI(title="Quran AI API", version="2.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- QURAN-FOCUSED SYSTEM PROMPT ---
BASE_SYSTEM_PROMPT = """You are an Imam and a knowledgeable scholar of the Holy Quran. You have spent your life studying the Quran, Tafsir, and the Hadith of the Prophet Muhammad (peace be upon him). People come to you seeking guidance, understanding, and clarity about the Quran and Islam.
You speak like a real person, a warm and wise Imam sitting with someone in a masjid, not like a machine or a chatbot. You are patient, humble, and caring. You share knowledge the way a teacher shares with a student, with sincerity and compassion.
YOUR GUIDELINES:
1. SPEAK NATURALLY
Talk like a real Imam having a conversation. No bullet points. No numbered lists. No markdown formatting. No asterisks. No slashes. Just flowing, natural speech as if you are sitting across from the person.
2. ALWAYS INCLUDE ARABIC TEXT
When you reference any Quran verse, always include the Arabic text first, then provide the meaning in English. This honors the original words of Allah. For example say the Arabic ayah then follow it with its meaning.
3. INCLUDE HADITH WHEN RELEVANT
When a Hadith of the Prophet Muhammad (peace be upon him) is relevant to the question, mention it naturally. State the Hadith, mention the narrator (like Abu Hurairah, Aisha, etc.) and the source (like Sahih Bukhari, Sahih Muslim, Sunan Abu Dawud, etc.). Only use well-known authentic (sahih) or good (hasan) hadith. If you are not certain about a hadith, do not mention it.
4. ALWAYS SAY PEACE BE UPON HIM
After mentioning any Prophet, always say "peace be upon him." After mentioning Prophet Muhammad, say "peace and blessings be upon him." This is the adab (manners) of a Muslim scholar.
5. QURAN AND ISLAMIC TOPICS ONLY
You only discuss the Quran, its verses, its surahs, its tafsir, the Prophets mentioned in the Quran, Hadith, Islamic teachings, prayer, fasting, and matters of faith. If someone asks about something unrelated, gently say: "My dear brother or sister, my knowledge is in the Quran and the teachings of Islam. Please feel free to ask me anything about the Holy Quran, the Hadith, or Islamic guidance."
6. BE HONEST AND HUMBLE
Use only the verses provided to you and your knowledge of authentic sources. If you are unsure about something, say something like "Allah knows best, but from what I have learned..." or simply "Allahu Alam, Allah knows best." Never fabricate a verse or hadith. This is a great sin and you would never do it.
7. GIVE COMPLETE BUT CONCISE ANSWERS
Answer thoroughly enough that the person truly understands, but do not ramble. Usually 4 to 10 sentences is the right amount. If the topic is deep, you may go longer, but always stay focused.
8. DIRECT ANSWERS ONLY
Never explain your thinking process. Never say things like "let me check" or "I should explain" or "the user is asking." Never write internal thoughts. Just give the answer directly the way a real Imam would speak to someone.
9. USE ISLAMIC PHRASES NATURALLY
Use phrases like "Bismillah," "SubhanAllah," "Alhamdulillah," "MashaAllah," and "InshaAllah" naturally in your speech the way any Imam would. Do not overuse them but let them flow where appropriate.
10. SHOW WISDOM AND DEPTH
You are not just giving information. You are guiding someone. Connect the verses to their meaning, to their life, to the wisdom behind them. Help the person understand not just what the Quran says but why it matters.
11. NO THINKING OUT LOUD
Do not include any thinking tags or internal reasoning. Do not wrap any part of your response in tags. Just give the final answer directly."""
# --- GLOBAL VARIABLES ---
collection = None
embed_model = None
openrouter_client = None
# --- HELPER FUNCTION TO CLEAN RESPONSE ---
def clean_response(text):
if not text:
return text
# Remove thinking tags and everything inside them
text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
text = re.sub(r'<thinking>.*?</thinking>', '', text, flags=re.DOTALL)
text = re.sub(r'<reason>.*?</reason>', '', text, flags=re.DOTALL)
text = re.sub(r'<reflection>.*?</reflection>', '', text, flags=re.DOTALL)
# Remove unclosed thinking tags (model sometimes doesn't close them)
text = re.sub(r'<think>.*', '', text, flags=re.DOTALL)
text = re.sub(r'<thinking>.*', '', text, flags=re.DOTALL)
# Remove any remaining tags
text = re.sub(r'<\/?(?:think|thinking|reason|reflection|output)>', '', text)
# Remove any "thinking out loud" patterns at the start
thinking_patterns = [
r'^.*?(?:the user is asking|let me check|I should|I need to|I will|Alright,|Okay,|So,|First,|Let me).*?(?:\.|:)\s*',
r'^.*?(?:Looking at|Checking|Based on the).*?(?:\.|:)\s*',
r'^.*?(?:I\'ll explain|I\'ll answer|I\'ll discuss).*?(?:\.|:)\s*',
]
for pattern in thinking_patterns:
text = re.sub(pattern, '', text, flags=re.IGNORECASE | re.DOTALL)
# Remove lines that start with thinking phrases
lines = text.split('\n')
clean_lines = []
skip_phrases = [
'alright', 'okay', 'let me', 'i should', 'i need to', 'i will check',
'the user', 'first,', 'looking at', 'checking', 'based on',
'i\'ll explain', 'i\'ll answer', 'so the question', 'now,',
'hmm', 'well,', 'so,', 'right,', 'sure,',
'the question is', 'they are asking', 'this question'
]
for line in lines:
line_lower = line.strip().lower()
should_skip = False
for phrase in skip_phrases:
if line_lower.startswith(phrase):
should_skip = True
break
if not should_skip:
clean_lines.append(line)
text = '\n'.join(clean_lines)
# Remove asterisks
text = re.sub(r'\*+', '', text)
# Remove markdown
text = re.sub(r'__(.+?)__', r'\1', text)
text = re.sub(r'_(.+?)_', r'\1', text)
text = re.sub(r'^#{1,6}\s*', '', text, flags=re.MULTILINE)
# Remove bullet points
text = re.sub(r'^\s*[-•]\s*', '', text, flags=re.MULTILINE)
# Remove numbered lists
text = re.sub(r'^\s*\d+[\.\)]\s*', '', text, flags=re.MULTILINE)
# Fix slashes
text = re.sub(r'(\w)/(\w)', r'\1 or \2', text)
# Clean whitespace
text = re.sub(r'\n{3,}', '\n\n', text)
text = text.strip()
return text
# --- AUTO-STARTUP LOGIC ---
@app.on_event("startup")
def startup_event():
global collection, embed_model, openrouter_client
print("Starting server...")
print(f"Using AI Model: {AI_MODEL}")
print(f"API Key present: {bool(OPENROUTER_API_KEY)}")
if not os.path.exists(DB_PATH) or not os.listdir(DB_PATH):
print("Database missing. Building now.")
try:
import ingest_multilingual
ingest_multilingual.ingest_multilingual()
print("Database built successfully.")
except ImportError:
print("Could not find ingest_multilingual.py")
except Exception as e:
print(f"Error building database: {e}")
print("Loading AI Models...")
try:
chroma_client = chromadb.PersistentClient(path=DB_PATH)
collection = chroma_client.get_collection(name="quran_verses")
embed_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
openrouter_client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=OPENROUTER_API_KEY
)
print("Systems Ready.")
except Exception as e:
print(f"Critical Error: {e}")
# --- API ENDPOINTS ---
class QueryRequest(BaseModel):
question: str
class VerseReference(BaseModel):
surah_name: str
surah_num: int
ayah_num: int
arabic: str
translation: str
tafsir: str
deep_link: str
class APIResponse(BaseModel):
answer: str
sources: list[VerseReference]
@app.get("/")
async def root():
return {"status": "running", "message": "Quran AI is ready."}
@app.get("/health")
async def health_check():
return {
"status": "healthy" if openrouter_client and collection else "starting",
"database": "connected" if collection else "loading"
}
@app.post("/ask", response_model=APIResponse)
async def ask_quran(request: QueryRequest):
if not openrouter_client or not collection:
raise HTTPException(status_code=503, detail="Server starting up. Please wait.")
question = request.question.strip()
if not question:
raise HTTPException(status_code=400, detail="Please enter a question.")
if len(question) > 1000:
raise HTTPException(status_code=400, detail="Question too long.")
print(f"Question: {question}")
# --- Search verses ---
try:
question_vector = embed_model.encode(question).tolist()
results = collection.query(
query_embeddings=[question_vector],
n_results=10
)
except Exception as e:
print(f"Database Error: {e}")
raise HTTPException(status_code=500, detail="Database Error")
# --- Build context ---
sources_list = []
context_text = ""
if results['ids'] and results['ids'][0]:
metas = results['metadatas'][0]
for meta in metas:
translation_text = meta.get('english', meta.get('translation', ''))
surah_name = meta.get('surah_name_en', meta.get('name', 'Surah'))
tafsir_text = meta.get('tafsir_en', meta.get('tafsir', ''))
arabic_text = meta.get('arabic', '')
context_text += f"""
Surah {surah_name} verse {meta['surah']}:{meta['ayah']}
Arabic: {arabic_text}
Meaning: {translation_text}
Tafsir: {tafsir_text[:400] if tafsir_text else 'N/A'}
"""
sources_list.append(VerseReference(
surah_name=surah_name,
surah_num=meta['surah'],
ayah_num=meta['ayah'],
arabic=arabic_text,
translation=translation_text,
tafsir=tafsir_text if tafsir_text else 'No tafsir available',
deep_link=f"https://quran.com/{meta['surah']}/{meta['ayah']}"
))
# --- Build prompt ---
if context_text:
full_system_prompt = f"""{BASE_SYSTEM_PROMPT}
QURAN VERSES FOR REFERENCE:
{context_text}
Remember: You are an Imam speaking to someone who came to you for guidance. Speak directly and naturally. Include the Arabic text of relevant verses. Mention authentic Hadith if relevant. Do not explain your thinking process. Do not use markdown or bullet points. Do not use any thinking tags. Just speak from the heart as a scholar would."""
else:
full_system_prompt = BASE_SYSTEM_PROMPT
# --- Call API ---
try:
print(f"Calling API with model: {AI_MODEL}")
chat_completion = openrouter_client.chat.completions.create(
messages=[
{"role": "system", "content": full_system_prompt},
{"role": "user", "content": question}
],
model=AI_MODEL,
temperature=0.3,
max_tokens=900,
)
if not chat_completion.choices:
print("ERROR: No choices returned from API")
answer = "Forgive me, I could not generate a response right now. Please try again, InshaAllah."
else:
answer = chat_completion.choices[0].message.content
print(f"Raw response length: {len(answer) if answer else 0}")
answer = clean_response(answer)
print(f"Cleaned response length: {len(answer) if answer else 0}")
if not answer or len(answer.strip()) < 5:
print("ERROR: Response was empty after cleaning")
answer = "Forgive me, I could not generate a proper response. Please rephrase your question, InshaAllah."
except Exception as e:
print(f"OpenRouter Error Type: {type(e).__name__}")
print(f"OpenRouter Error Details: {e}")
answer = "Forgive me, something went wrong on my end. Please ask your question again and I will do my best to help you, InshaAllah."
print("Response sent")
return APIResponse(
answer=answer,
sources=sources_list[:6]
)
if __name__ == "__main__":
port = int(os.environ.get("PORT", 7860))
uvicorn.run(app, host="0.0.0.0", port=port)