IC / app.py
Vanshcc's picture
Upload 20 files
b353d13 verified
Raw History Blame Contribute Delete
20.8 kB
import streamlit as st
import json
import torch
import numpy as np
from sentence_transformers import SentenceTransformer
from flashrank import Ranker, RerankRequest
import lancedb
import os
import requests
from groq import Groq
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# ===============================
# PAGE CONFIG & CONSTITUTION THEME
# ===============================
st.set_page_config(page_title="Constitution of India - Knowledge System", page_icon="⚖️", layout="wide")
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600;700&family=Lato:wght@300;400;700&display=swap');
/* INDIAN CONSTITUTION THEME - Saffron, White, Green */
/* Global Background */
html, body, [class*="css"] { font-family: 'Lato', sans-serif; }
.stApp {
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #0a0a0a 100%);
color: #e8e8e8;
}
/* ASHOKA CHAKRA ANIMATION */
@keyframes rotateChakra {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.chakra-container {
position: fixed;
width: 80px;
height: 80px;
z-index: 999;
opacity: 0.3;
pointer-events: none;
}
.chakra-left { left: 30px; top: 30px; }
.chakra-right { right: 30px; top: 30px; }
.chakra {
width: 100%;
height: 100%;
border: 4px solid #000080;
border-radius: 50%;
position: relative;
animation: rotateChakra 20s linear infinite;
}
.chakra::before {
content: '';
position: absolute;
width: 100%;
height: 2px;
background: #000080;
top: 50%;
left: 0;
}
.chakra::after {
content: '';
position: absolute;
width: 2px;
height: 100%;
background: #000080;
left: 50%;
top: 0;
}
/* TRICOLOR ACCENT BAR */
.tricolor-bar {
height: 6px;
background: linear-gradient(to right, #FF9933 33%, #FFFFFF 33%, #FFFFFF 66%, #138808 66%);
margin: 1rem 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
/* HEADERS */
.main-title {
font-family: 'Crimson Text', serif !important;
font-size: 3.5rem;
background: linear-gradient(to bottom, #FFD700, #FFA500);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
text-shadow: 0 4px 10px rgba(0,0,0,0.8);
font-weight: 700;
}
.sub-title {
font-family: 'Crimson Text', serif !important;
text-align: center;
color: #a0a0a0;
font-size: 1.1rem;
letter-spacing: 3px;
text-transform: uppercase;
margin-bottom: 2rem;
}
/* LEGAL DOCUMENT CARDS */
.answer-box, .glass-card {
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
border: 2px solid #4a4a6a;
color: #e8e8e8 !important;
padding: 2.5rem;
border-radius: 8px;
box-shadow:
0 10px 30px rgba(0,0,0,0.7),
inset 0 0 40px rgba(255, 153, 51, 0.05);
font-family: 'Lato', sans-serif !important;
line-height: 1.8;
margin-bottom: 2rem;
position: relative;
}
/* Tricolor left border accent */
.answer-box::before, .glass-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
background: linear-gradient(to bottom, #FF9933 33%, #FFFFFF 33%, #FFFFFF 66%, #138808 66%);
}
.answer-box h3, .glass-card h3 { color: #FFD700 !important; font-weight: 700; }
.answer-box ul li, .glass-card ul li { color: #d0d0d0 !important; }
/* ARTICLE PILLS (Legal Citation Style) */
.source-pill {
display: inline-block;
background: linear-gradient(135deg, #000080 0%, #0000CD 100%);
border: 1px solid #4169E1;
color: #FFFFFF !important;
padding: 0.4rem 1rem;
border-radius: 5px;
font-size: 0.8rem;
margin-right: 0.5rem;
font-family: 'Crimson Text', serif !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
font-weight: 600;
}
.source-pill:hover { transform: scale(1.05); background: linear-gradient(135deg, #0000CD 0%, #4169E1 100%); cursor: pointer; }
/* Sidebar */
.metadata-section {
background: rgba(30, 30, 46, 0.8);
border: 2px solid #4a4a6a;
border-radius: 10px;
padding: 1rem;
}
/* INPUT STYLING */
div[data-testid="stTextInput"] input,
div[data-testid="stChatInput"] textarea,
div[data-testid="stChatInput"] input {
background: linear-gradient(135deg, #2a2a3e 0%, #1e1e2e 100%) !important;
color: #FFD700 !important;
font-family: 'Lato', sans-serif !important;
font-weight: 500 !important;
border: 2px solid #4a4a6a !important;
border-radius: 12px !important;
padding: 1rem !important;
box-shadow: inset 0 0 15px rgba(255, 153, 51, 0.1) !important;
}
/* PIN INPUT TO BOTTOM */
[data-testid="stChatInput"],
[data-testid="stBottom"] {
position: fixed !important;
bottom: 10px !important;
left: 0 !important;
right: 0 !important;
margin-inline: auto !important;
width: 100% !important;
max-width: 800px !important;
padding: 0 !important;
z-index: 10000 !important;
background-color: transparent !important;
background-image: none !important;
}
[data-testid="stBottom"] > div {
background-color: transparent !important;
}
div[data-testid="stTextInput"] input::placeholder,
div[data-testid="stChatInput"] textarea::placeholder {
color: #a0a0a0 !important;
font-style: italic;
opacity: 0.8 !important;
}
div[data-testid="stTextInput"] div[data-baseweb="base-input-container"],
div[data-testid="stChatInput"] {
background-color: transparent !important;
border: none !important;
}
div[data-testid="stTextInput"] label {
color: #FFD700 !important;
font-family: 'Crimson Text', serif !important;
}
div[data-testid="stChatInput"] button {
color: #FFD700 !important;
}
/* TOKEN PILLS */
.token-pill {
display: inline-block;
background: #1e1e2e;
border: 1px solid #4a4a6a;
color: #FFD700 !important;
padding: 0.2rem 0.8rem;
border-radius: 4px;
font-size: 0.75rem;
margin-left: 0.3rem;
font-family: 'Lato', sans-serif !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
vertical-align: middle;
}
</style>
<!-- DECORATIVE ELEMENTS -->
<div class="chakra-container chakra-left"><div class="chakra"></div></div>
<div class="chakra-container chakra-right"><div class="chakra"></div></div>
""", unsafe_allow_html=True)
# ===============================
# LOAD RESOURCES
# ===============================
@st.cache_resource
def load_resources():
model = SentenceTransformer("BAAI/bge-m3", device='cpu')
ranker = Ranker(model_name="ms-marco-MiniLM-L-12-v2", cache_dir="./opt")
db = lancedb.connect("./.lancedb")
tbl = db.open_table("constitution_knowledge")
return model, ranker, tbl
model, ranker, tbl = load_resources()
# ===============================
# LLM LOGIC
# ===============================
def get_answer_from_llm(query, context):
system_prompt = """You are a constitutional law expert and scholar of the Indian Constitution.
Answer precisely using the provided context from the Constitution of India.
CRITICAL RULES:
1. Cite specific Articles, Parts, and Chapters when answering.
2. Maintain legal accuracy and constitutional interpretation.
3. If the context doesn't contain the answer, say so clearly."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Context from Constitution:\n{context}\n\nQuestion: {query}"}
]
# Token estimation
input_text = system_prompt + f"Context from Constitution:\n{context}\n\nQuestion: {query}"
in_tokens = len(input_text) // 4
errors = []
# 1. Try SambaNova First (Ramayana style model name)
s_key = os.getenv("SAMBANOVA_API_KEY")
if s_key and s_key != "your_key_here":
try:
url = "https://api.sambanova.ai/v1/chat/completions"
headers = {"Authorization": f"Bearer {s_key}", "Content-Type": "application/json"}
# Switched to 70B model without 'Meta-' prefix
data = {"model": "Llama-3.1-70B-Instruct", "messages": messages, "temperature": 0.1, "stream": False}
res = requests.post(url, headers=headers, json=data, timeout=10)
if res.status_code == 200:
ans = res.json()['choices'][0]['message']['content']
out_tokens = len(ans) // 4
return ans, "SambaNova (70B)", in_tokens, out_tokens
else:
errors.append(f"SambaNova Error: {res.status_code} - {res.text}")
except Exception as e:
errors.append(f"SambaNova Exception: {str(e)}")
# 2. Fallback to Groq
g_key = os.getenv("GROQ_API_KEY")
if g_key:
try:
client = Groq(api_key=g_key)
for m in ["llama-3.3-70b-versatile", "mixtral-8x7b-32768", "llama-3.1-8b-instant"]:
try:
res = client.chat.completions.create(model=m, messages=messages, temperature=0.2)
ans = res.choices[0].message.content
out_tokens = len(ans) // 4
return ans, m, in_tokens, out_tokens
except Exception as e:
errors.append(f"Groq {m} Error: {str(e)}")
if "429" in str(e) or "400" in str(e): continue
else: break
except Exception as e:
errors.append(f"Groq Setup Error: {str(e)}")
# 3. Fallback to Local Ollama
try:
url = "http://localhost:11434/api/chat"
data = {
"model": "llama3.1:8b-instruct-q4_K_M",
"messages": messages,
"stream": False,
"options": {"temperature": 0.2}
}
res = requests.post(url, json=data, timeout=30)
if res.status_code == 200:
ans = res.json()['message']['content']
out_tokens = len(ans) // 4
return ans, "Ollama (llama3.1)", in_tokens, out_tokens
else:
errors.append(f"Ollama Error: {res.status_code}")
except Exception as e:
errors.append(f"Ollama Exception: {str(e)}")
return f"[ERROR] All API calls failed. Errors: {errors}", "None", 0, 0
# ===============================
# MAIN UI
# ===============================
st.markdown("""
<style>
.block-container {
padding-top: 1rem;
padding-bottom: 5rem;
max-width: 90% !important;
margin: 0 auto !important;
}
html {
font-size: 80% !important;
}
body {
zoom: 100%;
-moz-transform: none;
}
</style>
""", unsafe_allow_html=True)
st.markdown('<h1 class="main-title">Constitution of India</h1>', unsafe_allow_html=True)
st.markdown('<p class="sub-title">Legal Knowledge System</p>', unsafe_allow_html=True)
# TRICOLOR DIVIDER
st.markdown('<div class="tricolor-bar"></div>', unsafe_allow_html=True)
# Session State for History
if "messages" not in st.session_state:
st.session_state.messages = []
# Layout: Content First
content_placeholder = st.empty()
# Render History or Landing Page
with content_placeholder.container():
if not st.session_state.messages:
st.markdown("""
<div class="glass-card">
<h3>Welcome to the Constitutional Knowledge System</h3>
<p>Access the structured knowledge of the Indian Constitution through semantic search and legal reasoning.</p>
<ul>
<li><b>Query Articles</b>: Ask about specific constitutional provisions.</li>
<li><b>Understand Rights</b>: Explore Fundamental Rights and Directive Principles.</li>
<li><b>Government Structure</b>: Learn about Union and State governance.</li>
<li><b>Legal Procedures</b>: Understand amendment processes and emergency provisions.</li>
</ul>
</div>
""", unsafe_allow_html=True)
# Render Conversation
for msg in st.session_state.messages:
if msg["role"] == "user":
st.markdown(f'<div style="text-align: right; color: #FFD700; font-family: Crimson Text; margin: 1rem; font-weight: 600;">Query: {msg["content"]}</div>', unsafe_allow_html=True)
elif msg["role"] == "assistant":
st.markdown(msg["content"], unsafe_allow_html=True)
# Input Pinned to Bottom
if user_query := st.chat_input("Ask a question about the Constitution..."):
# Append User Msg
st.session_state.messages.append({"role": "user", "content": user_query})
with content_placeholder.container():
st.markdown(f'<div style="text-align: right; color: #FFD700; font-family: Crimson Text; margin: 1rem; font-weight: 600;">Query: {user_query}</div>', unsafe_allow_html=True)
# Process Query
limit_n = 3
is_summary = any(k in user_query.lower() for k in ["summarize", "summary", "overview", "explain all"])
# PART DETECTION
target_part = None
parts = {
"fundamental rights": "PART III",
"directive principles": "PART IV",
"union": "PART V",
"states": "PART VI",
"judiciary": "PART V",
"elections": "PART XV",
"emergency": "PART XVIII",
"amendment": "PART XX"
}
for k, v in parts.items():
if k in user_query.lower():
target_part = v
break
# Increase search depth for better recall
limit_n = 10
if is_summary: limit_n = 15
if target_part: limit_n = 20
q_vec = model.encode(user_query)
# Search top 30-60 candidates to ensure we don't miss key Articles like Art 3
search_res = tbl.search(q_vec).limit(limit_n * 3).to_list()
# Filter by Part if detected
if target_part:
filtered_res = [r for r in search_res if target_part in r.get('part', '')]
if filtered_res:
search_res = filtered_res
passages = [{"id": i, "text": r['text'], "meta": r} for i, r in enumerate(search_res)]
# Rerank top 8 candidates (increased from 5)
reranked = ranker.rerank(RerankRequest(query=user_query, passages=passages))[:10 if is_summary or target_part else 8]
# Deduplicate chunks based on Article Number OR Text content (CRITICAL FIX)
seen_keys = set()
unique_reranked = []
for r in reranked:
# Create a unique key: Try Article Number first + Part, else full text
art_num = r['meta'].get('article_number', '').strip()
part = r['meta'].get('part', '').strip()
if art_num and "Article" in art_num:
key = f"{art_num}-{part}"
else:
key = r['meta']['text'][:100] # Use first 100 chars of text as proxy
if key not in seen_keys:
seen_keys.add(key)
unique_reranked.append(r)
# Take top 3 UNIQUE results
reranked = unique_reranked[:3]
ctx = "\n---\n".join([f"{r['meta'].get('article_number', 'Section')}: {r['meta']['text']}" for r in reranked])
if len(ctx) > 6000: ctx = ctx[:6000] + "... [TRUNCATED]"
# Generate Answer
with st.spinner("Analyzing constitutional provisions..."):
errors = []
# Prepare Prompt (Strict Context Only)
system_prompt = """You are a strict legal assistant.
Answer the question ONLY using the provided Context from the Constitution of India.
CRITICAL RULES:
1. Do NOT use your own internal knowledge. Use ONLY the provided context.
2. If the answer is not explicitly mentioned in the context, state: "The provided context does not contain information about [topic]."
3. Do not assume or hallucinate Articles not present in the text below.
4. Cite specific Articles from the context provided."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Context from Constitution:\n{ctx}\n\nQuestion: {user_query}"}
]
# Calculate Input Tokens (Approx)
in_tokens = len(system_prompt + ctx + user_query) // 4
# 1. Try SambaNova First (DISABLED)
# s_key = os.getenv("SAMBANOVA_API_KEY")
# ans = None # Ensure ans is initialized
# if s_key and s_key != "your_key_here":
# ... (Logic commented out) ...
ans = None
# 2. Fallback to Groq (DISABLED)
# if not ans:
# ... (Logic commented out) ...
# 3. Use Local Ollama (EXCLUSIVE)
if not ans:
try:
url = "http://localhost:11434/api/chat"
data = {
"model": "llama3.1:8b-instruct-q4_K_M",
"messages": messages,
"stream": False,
"options": {"temperature": 0.2, "num_ctx": 8192}
}
res = requests.post(url, json=data, timeout=120) # Increased timeout for local processing
if res.status_code == 200:
ans_text = res.json()['message']['content']
out_tokens = len(ans_text) // 4
ans, m_used, in_tok, out_tok = ans_text, "Ollama (local)", in_tokens, out_tokens
else:
errors.append(f"Ollama Error: {res.status_code} - {res.text}")
except Exception as e:
errors.append(f"Ollama Exception: {str(e)}")
# Final Check
if not ans:
ans = f"[ERROR] All API calls failed. Errors: {errors}"
m_used, in_tok, out_tok = "None", 0, 0
# Build HTML
sources_html = "".join([
f'<span class="source-pill">{r["meta"].get("article_number", "Section")} - {r["meta"].get("part", "")}</span>'
for r in reranked
])
answer_html = f"""
<div class="answer-box">
<h3>Legal Analysis</h3>
{ans}
<br><br>
<div style="font-size: 0.8rem; border-top: 2px solid #4a4a6a; padding-top: 10px; margin-top: 10px; display: flex; justify-content: space-between; align-items: center;">
<div><strong>Citations:</strong><br>{sources_html}</div>
<div style="text-align: right;">
<span class="token-pill">In: {in_tok}</span>
<span class="token-pill">Out: {out_tok}</span>
<span class="token-pill">{m_used}</span>
</div>
</div>
</div>
"""
# Append Assistant Msg
st.session_state.messages.append({"role": "assistant", "content": answer_html})
st.rerun()
# ===============================
# SIDEBAR
# ===============================
with st.sidebar:
st.markdown('<div class="metadata-section">', unsafe_allow_html=True)
st.markdown("### System Metadata")
st.write(f"**Index Type:** LanceDB Vector")
st.write(f"**Model:** BGE-M3 (1024D)")
st.write(f"**Reranker:** FlashRank")
st.write(f"**Total Articles:** {len(tbl)}")
with st.expander("Example Queries"):
st.markdown("""
<small>
- What are fundamental rights?<br>
- Role of the President<br>
- How to amend the Constitution?<br>
- Directive Principles explained<br>
- Emergency provisions
</small>
""", unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)