Spaces:
Sleeping
Sleeping
| """ | |
| VDHF - Streamlit Interface | |
| Verification-Driven Hallucination Firewall | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import streamlit as st | |
| # Add project root to path | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "sample_docs") | |
| # βββ Page Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title="VDHF - Hallucination Firewall", | |
| page_icon="π‘οΈ", | |
| layout="wide", | |
| initial_sidebar_state="expanded", | |
| ) | |
| # βββ Custom CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 2.2rem; | |
| font-weight: 700; | |
| color: #1E3A5F; | |
| text-align: center; | |
| padding: 0.5rem 0 0.2rem 0; | |
| } | |
| .sub-header { | |
| font-size: 1rem; | |
| color: #6B7280; | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .metric-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 1.2rem; | |
| border-radius: 12px; | |
| color: white; | |
| text-align: center; | |
| } | |
| .metric-card h3 { margin: 0; font-size: 2rem; } | |
| .metric-card p { margin: 0; font-size: 0.85rem; opacity: 0.9; } | |
| .claim-supported { | |
| background-color: #D1FAE5; | |
| border-left: 4px solid #10B981; | |
| padding: 0.7rem 1rem; | |
| border-radius: 6px; | |
| margin: 0.4rem 0; | |
| } | |
| .claim-unsupported { | |
| background-color: #FEE2E2; | |
| border-left: 4px solid #EF4444; | |
| padding: 0.7rem 1rem; | |
| border-radius: 6px; | |
| margin: 0.4rem 0; | |
| } | |
| .evidence-box { | |
| background-color: #EFF6FF; | |
| border-left: 4px solid #3B82F6; | |
| padding: 0.7rem 1rem; | |
| border-radius: 6px; | |
| margin: 0.4rem 0; | |
| font-size: 0.9rem; | |
| } | |
| .status-pass { | |
| background: linear-gradient(135deg, #10B981 0%, #059669 100%); | |
| color: white; | |
| padding: 0.8rem 1.5rem; | |
| border-radius: 10px; | |
| text-align: center; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| .status-fail { | |
| background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%); | |
| color: white; | |
| padding: 0.8rem 1.5rem; | |
| border-radius: 10px; | |
| text-align: center; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # βββ Pipeline Init (cached) βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_pipeline(): | |
| """Initialize the VDHF pipeline and load documents.""" | |
| from core.pipeline import VDHFPipeline | |
| pipeline = VDHFPipeline() | |
| # Load sample documents | |
| if os.path.exists(DATA_DIR): | |
| for filename in os.listdir(DATA_DIR): | |
| if filename.endswith('.txt'): | |
| filepath = os.path.join(DATA_DIR, filename) | |
| pipeline.ingest_file(filepath) | |
| return pipeline | |
| # βββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with st.sidebar: | |
| st.markdown("## βοΈ Configuration") | |
| similarity_threshold = st.slider( | |
| "Similarity Threshold (ΞΈ_sim)", | |
| min_value=0.0, max_value=1.0, value=0.75, step=0.05, | |
| help="Minimum semantic similarity for a claim to be considered supported." | |
| ) | |
| firewall_threshold = st.slider( | |
| "Firewall Threshold (Ο)", | |
| min_value=0.0, max_value=1.0, value=0.80, step=0.05, | |
| help="Minimum support ratio to pass the firewall." | |
| ) | |
| top_k = st.slider( | |
| "Top-K Evidence Chunks", | |
| min_value=1, max_value=15, value=7, | |
| help="Number of evidence chunks to retrieve." | |
| ) | |
| st.markdown("---") | |
| st.markdown("## π Knowledge Base") | |
| # Custom document upload | |
| uploaded_file = st.file_uploader( | |
| "Upload a document (.txt)", type=["txt"], | |
| help="Add your own document to the knowledge base." | |
| ) | |
| st.markdown("---") | |
| st.markdown("## βΉοΈ How It Works") | |
| st.markdown(""" | |
| 1. **Retrieve** relevant evidence from documents | |
| 2. **Generate** an LLM response using context | |
| 3. **Extract** atomic factual claims | |
| 4. **Verify** each claim against evidence | |
| 5. **Firewall** blocks hallucinated responses | |
| 6. **Regenerate** using only verified evidence | |
| """) | |
| # βββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown('<div class="main-header">π‘οΈ Hallucination Firewall</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">Verification-Driven Hallucination Firewall for RAG Systems</div>', unsafe_allow_html=True) | |
| # βββ Load Pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with st.spinner("π Loading models and documents... (first load may take a moment)"): | |
| pipeline = load_pipeline() | |
| # Handle file upload | |
| if uploaded_file is not None: | |
| content = uploaded_file.read().decode("utf-8") | |
| if f"uploaded_{uploaded_file.name}" not in st.session_state: | |
| pipeline.ingest_text(content, source=uploaded_file.name) | |
| st.session_state[f"uploaded_{uploaded_file.name}"] = True | |
| st.sidebar.success(f"β Uploaded: {uploaded_file.name}") | |
| # Show doc count | |
| st.sidebar.metric("Document Chunks Loaded", pipeline.document_count) | |
| # βββ Tabs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| tab_query, tab_analyze, tab_about = st.tabs(["π Query", "π§ͺ Analyze Claims", "π About"]) | |
| # βββ TAB 1: Query ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_query: | |
| query = st.text_input( | |
| "Ask a question about the knowledge base:", | |
| placeholder="e.g., When was Python released and who created it?", | |
| ) | |
| col_btn, col_examples = st.columns([1, 3]) | |
| with col_btn: | |
| run_query = st.button("π Run Query", type="primary", use_container_width=True) | |
| with col_examples: | |
| example = st.selectbox("Or try an example:", [ | |
| "", | |
| "When was Python released and who created it?", | |
| "What caused World War I?", | |
| "Tell me about artificial intelligence history.", | |
| "How does the human body work?", | |
| "What is climate change and what causes it?", | |
| "Tell me about the Renaissance period.", | |
| "How did the internet develop?", | |
| ], label_visibility="collapsed") | |
| if example and not query: | |
| query = example | |
| run_query = True | |
| if run_query and query: | |
| # Update pipeline thresholds | |
| pipeline.similarity_threshold = similarity_threshold | |
| pipeline.firewall_threshold = firewall_threshold | |
| pipeline.top_k = top_k | |
| pipeline.verifier.similarity_threshold = similarity_threshold | |
| pipeline.firewall.similarity_threshold = similarity_threshold | |
| pipeline.firewall.decision_engine.threshold = firewall_threshold | |
| pipeline.firewall.decision_engine.scoring_module.threshold = firewall_threshold | |
| with st.spinner("Processing query through the VDHF pipeline..."): | |
| start_time = time.time() | |
| result = pipeline.query(query, verbose=False) | |
| elapsed = time.time() - start_time | |
| # ββ Status Banner ββ | |
| if result.is_verified: | |
| st.markdown( | |
| f'<div class="status-pass">β VERIFIED β Support Ratio: {result.support_ratio:.0%} ' | |
| f'({result.supported_claims}/{result.total_claims} claims supported)</div>', | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.markdown( | |
| f'<div class="status-fail">β οΈ PARTIALLY VERIFIED β Support Ratio: {result.support_ratio:.0%} ' | |
| f'({result.supported_claims}/{result.total_claims} claims supported)</div>', | |
| unsafe_allow_html=True | |
| ) | |
| st.markdown("") | |
| # ββ Metrics Row ββ | |
| m1, m2, m3, m4 = st.columns(4) | |
| m1.metric("Support Ratio", f"{result.support_ratio:.0%}") | |
| m2.metric("Total Claims", result.total_claims) | |
| m3.metric("Supported", result.supported_claims) | |
| m4.metric("Regenerations", result.regeneration_attempts) | |
| st.markdown("---") | |
| # ββ Response ββ | |
| st.subheader("π Response") | |
| st.info(result.final_response) | |
| st.caption(f"β±οΈ Processed in {elapsed:.2f}s") | |
| # ββ Claims Breakdown ββ | |
| if result.verification_results: | |
| st.subheader("π¬ Claims Verification") | |
| for vr in result.verification_results: | |
| if vr.is_supported: | |
| st.markdown( | |
| f'<div class="claim-supported">' | |
| f'<strong>β SUPPORTED</strong> (similarity: {vr.similarity_score:.3f}, ' | |
| f'entailment: {vr.entailment_label})<br/>' | |
| f'{vr.claim.text}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.markdown( | |
| f'<div class="claim-unsupported">' | |
| f'<strong>β UNSUPPORTED</strong> (similarity: {vr.similarity_score:.3f}, ' | |
| f'entailment: {vr.entailment_label})<br/>' | |
| f'{vr.claim.text}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| # ββ Retrieved Evidence ββ | |
| if result.retrieved_evidence: | |
| with st.expander(f"π Retrieved Evidence ({len(result.retrieved_evidence)} chunks)", expanded=False): | |
| for i, ev in enumerate(result.retrieved_evidence, 1): | |
| source = os.path.basename(ev.metadata.get("source", "Unknown")) | |
| st.markdown( | |
| f'<div class="evidence-box">' | |
| f'<strong>[{i}]</strong> Score: {ev.similarity_score:.3f} | Source: {source}<br/>' | |
| f'{ev.content[:300]}{"..." if len(ev.content) > 300 else ""}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| # βββ TAB 2: Analyze Claims βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_analyze: | |
| st.subheader("Test Custom Claims Against the Knowledge Base") | |
| st.markdown("Enter individual claims to verify them against the loaded documents.") | |
| claims_input = st.text_area( | |
| "Enter claims (one per line):", | |
| placeholder="Python was created by Guido van Rossum.\nPython was released in 2005.\nPython is a compiled language.", | |
| height=150, | |
| ) | |
| if st.button("π Verify Claims", type="primary"): | |
| if claims_input.strip(): | |
| from core.claim_extractor import Claim | |
| from retrieval.retriever import RetrievedEvidence | |
| lines = [l.strip() for l in claims_input.strip().split("\n") if l.strip()] | |
| with st.spinner("Verifying claims..."): | |
| # Retrieve evidence for all claims combined | |
| combined_query = " ".join(lines) | |
| evidence_list = pipeline.retriever.retrieve(combined_query, top_k=top_k) | |
| claims = [Claim(text=line, claim_id=i) for i, line in enumerate(lines)] | |
| results = pipeline.verifier.verify_all_claims(claims, evidence_list) | |
| supported = sum(1 for r in results if r.is_supported) | |
| total = len(results) | |
| ratio = supported / total if total > 0 else 0 | |
| st.markdown(f"**Results: {supported}/{total} claims supported ({ratio:.0%})**") | |
| st.progress(ratio) | |
| for vr in results: | |
| if vr.is_supported: | |
| st.markdown( | |
| f'<div class="claim-supported">' | |
| f'<strong>β SUPPORTED</strong> (score: {vr.similarity_score:.3f})<br/>' | |
| f'{vr.claim.text}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.markdown( | |
| f'<div class="claim-unsupported">' | |
| f'<strong>β UNSUPPORTED</strong> (score: {vr.similarity_score:.3f})<br/>' | |
| f'{vr.claim.text}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| if vr.best_evidence: | |
| with st.expander(f"Evidence for: {vr.claim.text[:50]}..."): | |
| st.markdown( | |
| f'<div class="evidence-box">{vr.best_evidence[:500]}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.warning("Please enter at least one claim.") | |
| # βββ TAB 3: About ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_about: | |
| st.subheader("About VDHF") | |
| st.markdown(""" | |
| The **Verification-Driven Hallucination Firewall (VDHF)** is a post-generation | |
| verification system that detects and mitigates hallucinations in LLM-generated responses. | |
| ### Architecture | |
| ``` | |
| User Query | |
| β | |
| βΌ | |
| βββββββββββββββββββ | |
| β RAG Retrieval β β Sentence-BERT + ChromaDB | |
| ββββββββββ¬βββββββββ | |
| βΌ | |
| βββββββββββββββββββ | |
| β LLM Generation β β Groq API / Mock | |
| ββββββββββ¬βββββββββ | |
| βΌ | |
| βββββββββββββββββββ | |
| β Claim Extraction β β Rule-based decomposition | |
| ββββββββββ¬βββββββββ | |
| βΌ | |
| βββββββββββββββββββ | |
| β Verification β β Semantic Similarity + NLI | |
| ββββββββββ¬βββββββββ | |
| βΌ | |
| βββββββββββββββββββ | |
| β Firewall β β Support Ratio β₯ Ο ? | |
| ββββββββββ¬βββββββββ | |
| βββββ΄ββββ | |
| βΌ βΌ | |
| PASS REGENERATE | |
| ``` | |
| ### Key Parameters | |
| | Parameter | Default | Description | | |
| |-----------|---------|-------------| | |
| | Similarity Threshold (ΞΈ_sim) | 0.75 | Min cosine similarity for support | | |
| | Firewall Threshold (Ο) | 0.80 | Min support ratio to pass | | |
| | Top-K | 7 | Evidence chunks retrieved | | |
| | Max Regenerations | 2 | Retry attempts on failure | | |
| ### Models Used | |
| - **Embeddings**: `all-MiniLM-L6-v2` (Sentence-BERT) | |
| - **NLI**: `microsoft/deberta-base-mnli` | |
| - **LLM**: `llama-3.3-70b-versatile` (via Groq API) | |
| ### Knowledge Base | |
| The system comes preloaded with 12 sample documents covering: | |
| Python, Ancient Egypt, AI, Climate Change, Economics, Human Body, | |
| Internet Technology, Music History, Quantum Physics, Renaissance, | |
| Solar System, and World War II. | |
| """) | |