botInfinity's picture
Update app.py
bde8ba0 verified
import os
import streamlit as st
from qdrant_client import QdrantClient
from langchain_qdrant import (
QdrantVectorStore,
RetrievalMode,
FastEmbedSparse
)
from langchain_huggingface import HuggingFaceEmbeddings
from sentence_transformers import CrossEncoder
from langchain_groq import ChatGroq
# ------------------------------
# Streamlit Config (MUST RUN FAST)
# ------------------------------
st.set_page_config(
page_title="Nepal Constitution AI",
page_icon="πŸ§‘β€βš–οΈ",
layout="wide"
)
st.title("πŸ§‘β€βš–οΈ Nepal Constitution – AI Legal Assistant")
st.caption("Hybrid RAG (Dense + BM25) + Cross-Encoder Reranking")
# πŸ”₯ EARLY VISIBILITY (HF health check helper)
st.write("βœ… App booted successfully.")
# ------------------------------
# Hard stop if DB missing (NO SILENT FAIL)
# ------------------------------
if not os.path.exists("./qdrant_db"):
st.error("❌ qdrant_db folder not found. You must commit it to the repo.")
st.stop()
# ------------------------------
# User Input
# ------------------------------
query = st.text_input(
"Ask a constitutional or legal question:",
placeholder="e.g. What does Article 275 say about local governance?"
)
# ------------------------------
# Cached Heavy Stuff
# ------------------------------
@st.cache_resource
def load_embeddings():
return HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
@st.cache_resource
def load_sparse_embeddings():
return FastEmbedSparse(model_name="Qdrant/bm25")
@st.cache_resource
def load_reranker():
return CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
@st.cache_resource
def load_vector_store():
embeddings = load_embeddings()
sparse_embeddings = load_sparse_embeddings()
client = QdrantClient(path="./qdrant_db")
return QdrantVectorStore(
client = client,
collection_name="nepal_law",
embedding=embeddings,
sparse_embedding=sparse_embeddings,
retrieval_mode=RetrievalMode.HYBRID
)
@st.cache_resource
def load_llm():
return ChatGroq(
model="llama-3.1-8b-instant",
temperature=0.2,
max_tokens=600
)
# ------------------------------
# Reranking
# ------------------------------
def rerank(query, docs, top_k=8):
reranker = load_reranker()
pairs = [(query, d.page_content) for d in docs]
scores = reranker.predict(pairs)
ranked = sorted(
zip(docs, scores),
key=lambda x: x[1],
reverse=True
)
return [doc for doc, _ in ranked[:top_k]]
if query:
with st.spinner("πŸ” Searching constitution..."):
vector_store = load_vector_store()
retrieved = vector_store.similarity_search(query, k=20)
reranked = rerank(query, retrieved)
context = "\n\n".join(
f"[Source {i+1}]\n{doc.page_content}"
for i, doc in enumerate(reranked)
)
prompt = f"""
You are a constitutional law assistant for Nepal.
RULES:
- Use ONLY the provided context.
- Do NOT invent articles, clauses, or interpretations.
- If the answer is not found, say so explicitly.
- Use formal, neutral legal language.
- Reference article/section numbers when mentioned.
CONTEXT:
{context}
QUESTION:
{query}
ANSWER:
"""
with st.spinner("🧠 Generating answer..."):
llm = load_llm()
response = llm.invoke(prompt)
st.markdown("### βœ… Answer")
st.write(response.content)
with st.expander("πŸ“š Retrieved Constitutional Sources"):
for i, doc in enumerate(reranked):
st.markdown(f"**Source {i+1}**")
st.write(doc.page_content)
st.markdown("---")