File size: 3,756 Bytes
d074d09
 
 
c7de1c8
 
 
 
 
d074d09
 
 
 
 
c7de1c8
d074d09
 
 
 
 
 
 
 
c7de1c8
 
 
 
 
 
 
 
 
 
 
d074d09
 
 
 
 
 
 
 
 
 
c7de1c8
d074d09
 
 
 
 
 
 
 
 
c7de1c8
 
 
 
d074d09
 
 
 
 
 
 
c7de1c8
bde8ba0
d074d09
 
bde8ba0
d074d09
 
c7de1c8
d074d09
 
 
 
 
 
 
 
 
 
 
 
c7de1c8
d074d09
c7de1c8
d074d09
 
 
 
 
 
 
 
 
 
 
 
c7de1c8
d074d09
c7de1c8
d074d09
c7de1c8
 
d074d09
 
c7de1c8
 
d074d09
 
 
 
 
c7de1c8
 
d074d09
c7de1c8
 
 
d074d09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c7de1c8
d074d09
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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("---")