JanMat / app.py
BEASTZZZ's picture
Update app.py
2935b46 verified
Raw History Blame Contribute Delete
8.1 kB
"""
JanMat β€” single Gradio Space.
Replaces the old bills/themes/sentiment dashboard with a chat interface that
answers questions using Groq, grounded in the full text of scraped PRS bill
pages + PDFs + articles (stored in Supabase), with optional voice in/out via
Sarvam AI.
Everything lives in this one Space, same as your original setup β€” no
separate frontend needed.
"""
import logging
import threading
import gradio as gr
import db
import rag
import voice
from config import CRON_SECRET
from ingest_pipeline import run_ingestion
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("janmat.app")
# This Space's hardware tier is set to ZeroGPU (HF's free GPU allowance).
# The actual workload here is 100% CPU (embeddings, HTTP calls to Supabase/
# Groq/Sarvam) β€” nothing needs a GPU. However, ZeroGPU hardware expects at
# least one @spaces.GPU-decorated function to exist so HF's runtime
# recognizes the Space as compliant with that hardware tier. This mirrors
# what the original app.py did for the same reason (see git history).
try:
import spaces
@spaces.GPU
def _zerogpu_keepalive():
"""No-op β€” exists only to satisfy the ZeroGPU hardware requirement."""
return "ok"
try:
_zerogpu_keepalive()
except Exception:
logger.info("ZeroGPU keepalive call skipped (no GPU allocated at import time) β€” harmless.")
except ImportError:
logger.info("`spaces` package not available β€” assuming non-ZeroGPU hardware, skipping keepalive.")
_ingestion_lock = threading.Lock()
_ingestion_state = {"running": False, "last_summary": None}
def _ensure_session(session_id):
return session_id or db.create_session()
def _clean_source_title(title: str) -> str:
"""
Source titles are often raw PDF filenames like
'Corporate_Laws_(A)_Bill_2026_Text.pdf' β€” turn those into something
readable before showing them to a citizen.
"""
name = title.strip()
if name.lower().endswith(".pdf"):
name = name[:-4]
name = name.replace("_", " ").replace("%2C", ",")
# Collapse the double spaces that underscore-stripping can leave behind.
return " ".join(name.split())
def _format_answer_with_sources(answer: str, citations: list) -> str:
if not citations:
return answer
lines = "\n".join(
f"{c['n']}. [{_clean_source_title(c['title'])}]({c['url']})" for c in citations
)
return f"{answer}\n\n---\n\n**Sources**\n\n{lines}"
def chat_fn(message, history, session_id):
if not message or not message.strip():
return history, "", session_id
session_id = _ensure_session(session_id)
db.save_message(session_id, "user", message, input_mode="text")
result = rag.answer_question(session_id, message)
db.save_message(session_id, "assistant", result["answer"], citations=result["citations"], input_mode="text")
display_answer = _format_answer_with_sources(result["answer"], result["citations"])
history = history + [(message, display_answer)]
return history, "", session_id
def voice_fn(audio_path, history, session_id):
if audio_path is None:
return history, session_id, None
session_id = _ensure_session(session_id)
try:
with open(audio_path, "rb") as f:
audio_bytes = f.read()
stt = voice.transcribe_audio(audio_bytes, filename="question.wav")
transcript = stt["transcript"]
language = stt.get("language_code") or "unknown"
if not transcript.strip():
history = history + [(None, "I couldn't hear a question in that clip β€” please try again.")]
return history, session_id, None
db.save_message(session_id, "user", transcript, input_mode="voice", language_code=language)
result = rag.answer_question(session_id, transcript)
db.save_message(session_id, "assistant", result["answer"], citations=result["citations"],
input_mode="voice", language_code=language)
tts_language = language if language != "unknown" else "en-IN"
answer_audio_bytes = voice.synthesize_speech(result["answer"], language_code=tts_language)
out_path = "/tmp/janmat_answer.wav"
with open(out_path, "wb") as f:
f.write(answer_audio_bytes)
display_answer = _format_answer_with_sources(result["answer"], result["citations"])
history = history + [(transcript, display_answer)]
return history, session_id, out_path
except Exception as exc:
logger.exception("Voice turn failed")
history = history + [(None, f"Voice processing failed: {exc}")]
return history, session_id, None
def run_ingestion_fn(secret):
if not CRON_SECRET or secret != CRON_SECRET:
return "❌ Incorrect secret."
with _ingestion_lock:
if _ingestion_state["running"]:
return "⏳ Ingestion is already running β€” check back shortly."
_ingestion_state["running"] = True
def _background_run():
try:
summary = run_ingestion()
_ingestion_state["last_summary"] = summary
logger.info("Ingestion finished: %s", summary)
except Exception:
logger.exception("Ingestion run crashed")
finally:
_ingestion_state["running"] = False
threading.Thread(target=_background_run, daemon=True).start()
return "βœ… Ingestion started in the background. This can take a few minutes for a full run."
def ingestion_status_fn(secret):
if not CRON_SECRET or secret != CRON_SECRET:
return "❌ Incorrect secret."
if _ingestion_state["running"]:
return "⏳ Still running..."
summary = _ingestion_state["last_summary"]
if not summary:
return "No ingestion has completed yet in this session."
return (
f"Last run β€” seen: {summary.get('documents_seen', 0)}, "
f"ingested: {summary.get('ingested', 0)}, "
f"unchanged: {summary.get('unchanged', 0)}, "
f"skipped: {summary.get('skipped', 0)}, "
f"failed: {summary.get('failed', 0)}"
)
with gr.Blocks(title="JanMat β€” Ask about Indian Bills") as demo:
gr.Markdown(
"# πŸ›οΈ JanMat\n"
"Ask questions about Indian legislative bills, their full PDF briefs, "
"related articles, and public opinion β€” by text or voice. Answers are "
"grounded in what's actually been read into the knowledge base, with sources cited."
)
session_state = gr.State(value=None)
chatbot = gr.Chatbot(height=460, label="Conversation")
with gr.Row():
msg = gr.Textbox(placeholder="e.g. What does the Digital Data Protection Bill say about consent?",
scale=4, show_label=False)
send_btn = gr.Button("Send", scale=1, variant="primary")
with gr.Row():
mic = gr.Audio(sources=["microphone"], type="filepath", label="Or ask by voice")
answer_audio = gr.Audio(label="Spoken answer", autoplay=True)
send_btn.click(chat_fn, [msg, chatbot, session_state], [chatbot, msg, session_state], api_name="chat")
msg.submit(chat_fn, [msg, chatbot, session_state], [chatbot, msg, session_state], api_name=False)
mic.stop_recording(voice_fn, [mic, chatbot, session_state], [chatbot, session_state, answer_audio], api_name="chat_voice")
with gr.Accordion("Admin: refresh the knowledge base", open=False):
gr.Markdown(
"Triggers a fresh scrape + ingest of PRS bill pages and their PDFs. "
"Requires the CRON_SECRET you set as a Space secret."
)
secret_box = gr.Textbox(label="Cron secret", type="password")
with gr.Row():
run_btn = gr.Button("Run ingestion now")
status_btn = gr.Button("Check status")
status_box = gr.Textbox(label="Status", interactive=False)
run_btn.click(run_ingestion_fn, [secret_box], [status_box])
status_btn.click(ingestion_status_fn, [secret_box], [status_box])
if __name__ == "__main__":
demo.queue().launch(server_name="0.0.0.0", server_port=7860)