| 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 |
|
|
| |
| |
| |
| 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") |
|
|
| |
| st.write("β
App booted successfully.") |
|
|
| |
| |
| |
| if not os.path.exists("./qdrant_db"): |
| st.error("β qdrant_db folder not found. You must commit it to the repo.") |
| st.stop() |
|
|
| |
| |
| |
| query = st.text_input( |
| "Ask a constitutional or legal question:", |
| placeholder="e.g. What does Article 275 say about local governance?" |
| ) |
|
|
| |
| |
| |
| @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 |
| ) |
|
|
| |
| |
| |
| 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("---") |
|
|