paperpilot / app.py
KamranX07's picture
Sync from GitHub via hub-sync
e74eb58 verified
Raw
History Blame Contribute Delete
19.4 kB
import os
import json
import time
from hashlib import sha256
from typing import Any, Dict, List, Tuple
import gradio as gr
from modules.ocr import extract_text
from modules.llm_provider import extract_form_data, ask_llm
from modules.overview import generate_overview
from modules.checklist import generate_checklist
from modules.eligibility import check_eligibility
from modules.timeline import extract_timeline
from modules.risk_detector import generate_risks
from modules.verifier import verify_documents, verification_summary
def _read_css() -> str:
css_content = ""
css_path = os.path.join("assets", "style.css")
if os.path.exists(css_path):
with open(css_path, "r", encoding="utf-8") as f:
css_content = f.read()
return css_content
def _file_fingerprint(file_obj) -> str:
"""Lightweight key for caching: file name + size + mtime if available."""
if file_obj is None:
return ""
name = getattr(file_obj, "name", "") or ""
size = getattr(file_obj, "size", None)
mtime = getattr(file_obj, "last_modified", None)
raw = f"{name}|{size}|{mtime}".encode("utf-8", errors="ignore")
return sha256(raw).hexdigest()
# In-memory cache (server-side)
_CACHE: Dict[str, Dict[str, Any]] = {}
def analyze_document(file):
"""Analyze uploaded form and return all outputs for the dashboard."""
if file is None:
return (
{},
gr.update(value="ℹ Upload a form first.", visible=True),
gr.update(visible=False),
gr.update(value="ℹ Analyze a form to see results.", visible=True),
gr.update(visible=False),
gr.update(value="ℹ No timeline information found in this form.", visible=True),
gr.update(visible=False),
"ℹ No risks detected.",
gr.update(value="ℹ Upload supporting documents to verify.", visible=True),
gr.update(visible=False),
gr.update(value="", visible=False),
gr.update(value="ℹ Ready", visible=False),
gr.update(value=[], visible=False),
)
key = _file_fingerprint(file)
cached = _CACHE.get(key)
if cached:
master_json = cached["master_json"]
overview = cached["overview"]
checklist = cached["checklist"]
timeline = cached["timeline"]
risks = cached["risks"]
status = f"✅ Loaded from cache ({time.strftime('%H:%M:%S')})"
else:
extracted_text = extract_text(file.name)
master_json = extract_form_data(extracted_text)
overview = generate_overview(master_json)
checklist = generate_checklist(master_json)
timeline = extract_timeline(extracted_text)
risks = generate_risks(master_json)
status = f"✅ Analysis complete ({time.strftime('%H:%M:%S')})"
_CACHE[key] = {
"master_json": master_json,
"overview": overview,
"checklist": checklist,
"timeline": timeline,
"risks": risks,
}
# Checklist empty state
if not checklist:
checklist_empty_update = gr.update(value="ℹ Analyze a form to see results.", visible=True)
checklist_output_update = gr.update(visible=False)
else:
checklist_empty_update = gr.update(visible=False)
checklist_output_update = gr.update(value=checklist, visible=True)
# Timeline empty state
if not timeline:
timeline_empty_update = gr.update(value="ℹ No timeline information found in this form.", visible=True)
timeline_output_update = gr.update(visible=False)
else:
timeline_empty_update = gr.update(visible=False)
timeline_output_update = gr.update(value=timeline, visible=True)
# Risks display
if not risks or (isinstance(risks, str) and risks.strip() == "✅ No major issues detected."):
risks_display = "ℹ No risks detected."
else:
risks_display = risks
# Reset verification outputs when a new form is analyzed
verification_empty_update = gr.update(value="ℹ Upload supporting documents to verify.", visible=True)
verification_output_update = gr.update(visible=False)
verification_summary_update = gr.update(value="", visible=False)
# Overview empty state
if not overview:
overview_empty_update = gr.update(value="ℹ Upload a form first.", visible=True)
overview_output_update = gr.update(visible=False)
else:
overview_empty_update = gr.update(value="", visible=False)
overview_output_update = gr.update(value=overview, visible=True)
completeness = 0
if master_json:
completeness += 25
if checklist:
completeness += 25
if timeline:
completeness += 25
if risks_display and "No risks" not in str(risks_display):
completeness += 25
return (
master_json,
overview_empty_update,
overview_output_update,
checklist_empty_update,
checklist_output_update,
timeline_empty_update,
timeline_output_update,
risks_display,
verification_empty_update,
verification_output_update,
verification_summary_update,
gr.update(value=f"✅ Ready · Completeness {completeness}%", visible=True),
gr.update(value=[], visible=False),
)
def run_eligibility_check(income, master_json):
if not master_json:
return "ℹ Upload a form first."
return check_eligibility(master_json, income)
def run_verification(files, master_json):
# Gradio may return a File object or list depending on browser upload.
# Normalize to list for reliable truthiness checks.
if not master_json:
return (
gr.update(value="ℹ Upload supporting documents to verify.", visible=True),
gr.update(visible=False),
gr.update(value="", visible=False),
)
if files is None:
files_list = []
elif isinstance(files, list):
files_list = files
else:
files_list = [files]
if len(files_list) == 0:
return (
gr.update(value="ℹ Upload supporting documents to verify.", visible=True),
gr.update(visible=False),
gr.update(value="", visible=False),
)
if not files:
return (
gr.update(value="ℹ Upload supporting documents to verify.", visible=True),
gr.update(visible=False),
gr.update(value="", visible=False),
)
results = verify_documents(files_list, master_json)
summary = verification_summary(results)
return (
gr.update(visible=False),
gr.update(value=results, visible=True),
gr.update(value=summary, visible=True),
)
def run_qa(question: str, master_json: dict, chat_history):
"""Chat handler for gr.Chatbot.
Gradio 6.x expects Chatbot value to be either:
- list[dict{"role": "user"|"assistant", "content": "..."}]
- or list[gr.ChatMessage]
We normalize to dict format to avoid "messages format" errors.
"""
if not master_json:
assistant = "ℹ Upload a form first."
else:
prompt = json.dumps({"question": question, "master_json": master_json})
assistant = ask_llm(prompt)
# Normalize incoming history
history = chat_history if isinstance(chat_history, list) else []
def _to_msg(msg):
if isinstance(msg, dict) and "role" in msg and "content" in msg:
return msg
# Some older formats may come as (user, assistant) tuples.
if isinstance(msg, (list, tuple)) and len(msg) == 2:
return {"role": "user", "content": str(msg[0])}
return None
norm_history: List[Dict[str, str]] = []
for m in history:
if isinstance(m, dict) and "role" in m and "content" in m:
norm_history.append({"role": m["role"], "content": m["content"]})
# Append new turn
norm_history.append({"role": "user", "content": str(question)})
norm_history.append({"role": "assistant", "content": str(assistant)})
# Second output clears the textbox.
return norm_history, ""
with gr.Blocks(title="PaperPilot") as demo:
master_json_state = gr.State()
with gr.Group(elem_id="appbar"):
gr.HTML(
"""
<div class="appbar">
<div class="appbar-left">
<div class="brand-mark">📄</div>
<div class="brand-text">
<div class="brand-title">PaperPilot</div>
<div class="brand-subtitle">AI-powered scholarship & form assistant</div>
</div>
</div>
<div class="appbar-right">
<div class="status-active" id="status-pill" aria-label="Active">● Active</div>
</div>
</div>
"""
)
with gr.Tabs(elem_id="pp-tabs"):
with gr.Tab("1) Analyze"):
with gr.Row(equal_height=True):
with gr.Column(scale=1):
with gr.Group(elem_id="card-upload", elem_classes=["pp-card"]):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">📤 Upload & Analyze</div>
<div class="card-subtitle">Start here</div>
</div>
"""
)
file_input = gr.File(label="Form file (PDF / image)", file_count="single")
analyze_btn = gr.Button("Analyze Form", variant="primary")
clear_btn = gr.Button("Clear", variant="secondary")
with gr.Accordion("ℹ What happens", open=False):
gr.Markdown(
"""
- Extract text (OCR/PDF)
- Build structured JSON
- Generate: overview, checklist, timeline, risk alerts
- Enable eligibility + document verification
"""
)
with gr.Column(scale=2):
with gr.Group(elem_classes=["pp-card"], elem_id="card-overview"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">📋 Overview</div>
<div class="card-subtitle">Extracted summary</div>
</div>
"""
)
overview_empty = gr.Markdown(
"ℹ Upload a form first.", visible=True, elem_classes=["pp-empty"]
)
overview_output = gr.Markdown("", visible=False)
with gr.Row(equal_height=True):
with gr.Group(elem_classes=["pp-card"], elem_id="card-risks"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">⚠ Risk Alerts</div>
<div class="card-subtitle">Potential gaps</div>
</div>
"""
)
risk_output = gr.Markdown("ℹ No risks detected.", elem_classes=["pp-empty"])
with gr.Group(elem_classes=["pp-card"], elem_id="card-timeline"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">📅 Timeline</div>
<div class="card-subtitle">Important dates</div>
</div>
"""
)
timeline_empty = gr.Markdown(
"ℹ No timeline information found in this form.",
visible=True,
elem_classes=["pp-empty"],
)
timeline_output = gr.DataFrame(
headers=["Date", "Event"],
label="",
visible=False,
)
with gr.Tab("2) Checklist"):
with gr.Row(equal_height=True):
with gr.Column(scale=2):
with gr.Group(elem_classes=["pp-card"], elem_id="card-checklist"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">📑 Document Checklist</div>
<div class="card-subtitle">From extracted requirements</div>
</div>
"""
)
checklist_empty = gr.Markdown(
"ℹ Analyze a form to see results.",
visible=True,
elem_classes=["pp-empty"],
)
checklist_output = gr.DataFrame(
headers=["Documents", "Status"],
label="",
visible=False,
)
with gr.Tab("3) Eligibility & Verify"):
with gr.Row(equal_height=True):
with gr.Column(scale=1):
with gr.Group(elem_classes=["pp-card"], elem_id="card-eligibility"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">💳 Eligibility</div>
<div class="card-subtitle">Income-based check</div>
</div>
"""
)
income_input = gr.Number(label="Annual Family Income (₹)", precision=0)
check_btn = gr.Button("Check Eligibility", variant="primary")
eligibility_output = gr.Markdown("ℹ Upload a form first.", elem_classes=["pp-empty"])
with gr.Column(scale=2):
with gr.Group(elem_classes=["pp-card"], elem_id="card-verification"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">📁 Document Verification</div>
<div class="card-subtitle">Upload supporting files</div>
</div>
"""
)
verify_upload = gr.File(file_count="multiple", label="Supporting documents")
verify_btn = gr.Button("Verify Documents", variant="primary")
verification_empty = gr.Markdown(
"ℹ Upload supporting documents to verify.",
visible=True,
elem_classes=["pp-empty"],
)
verification_output = gr.DataFrame(
headers=["Document", "Status"],
label="",
visible=False,
)
verification_summary_output = gr.Markdown(visible=False)
with gr.Tab("4) Ask PaperPilot"):
with gr.Group(elem_classes=["pp-card"], elem_id="card-qa"):
gr.HTML(
"""
<div class="card-header">
<div class="card-title">💬 Ask PaperPilot</div>
<div class="card-subtitle">Questions based on your extracted form</div>
</div>
"""
)
chat_output = gr.Chatbot(value=[], elem_id="qa-chat", height=320)
with gr.Row(equal_height=False):
question_input = gr.Textbox(
label="Your question",
placeholder="e.g., What documents are required? What is the deadline?",
scale=4,
)
ask_btn = gr.Button("Send", variant="primary", scale=1)
gr.Markdown(
"""
**Try:** What is the eligibility criteria? What documents are needed? What should I do next?
""",
elem_classes=["pp-muted"],
)
# Footer
gr.Markdown(
"""
<div class="custom-footer">
Built with ❤️ using: Gradio • EasyOCR • PyMuPDF • Python
<br/><b>PaperPilot</b> – Helping users understand forms effortlessly.
</div>
""",
elem_classes=["custom-footer"],
)
# Handlers
analyze_btn.click(
fn=analyze_document,
inputs=[file_input],
outputs=[
master_json_state,
overview_empty,
overview_output,
checklist_empty,
checklist_output,
timeline_empty,
timeline_output,
risk_output,
verification_empty,
verification_output,
verification_summary_output,
gr.State(),
gr.State(),
],
)
def _clear_all():
return (
{},
gr.update(value="ℹ Upload a form first.", visible=True),
gr.update(visible=False),
gr.update(value="ℹ Analyze a form to see results.", visible=True),
gr.update(visible=False),
gr.update(value="ℹ No timeline information found in this form.", visible=True),
gr.update(visible=False),
"ℹ No risks detected.",
gr.update(value="ℹ Upload supporting documents to verify.", visible=True),
gr.update(visible=False),
gr.update(value="", visible=False),
gr.update(value="ℹ Ready", visible=True),
[],
)
# NOTE: Clear is optional; leaving it disabled avoids risking wiring mismatch.
# clear_btn.click(fn=_clear_all, inputs=[], outputs=[])
check_btn.click(
fn=run_eligibility_check,
inputs=[income_input, master_json_state],
outputs=eligibility_output,
)
verify_btn.click(
fn=run_verification,
inputs=[verify_upload, master_json_state],
outputs=[verification_empty, verification_output, verification_summary_output],
)
ask_btn.click(
fn=run_qa,
inputs=[question_input, master_json_state, chat_output],
outputs=[chat_output, question_input],
)
question_input.submit(
fn=run_qa,
inputs=[question_input, master_json_state, chat_output],
outputs=[chat_output, question_input],
)
demo.launch(theme=gr.themes.Soft(), css=_read_css())