| """Gradio frontend for the LangGraph Agentic RAG application.""" |
|
|
| import gradio as gr |
|
|
| from src.config import setup_logger, logger_frontend |
| from src.graph import rag_app |
|
|
| |
| setup_logger(name="agentic_rag", level=20) |
|
|
| |
| CUSTOM_CSS = """ |
| .documents-container details.doc-detail { |
| margin: 10px 0 !important; |
| border: 1px solid var(--border-color-primary, #e0e0e0) !important; |
| border-radius: 8px !important; |
| padding: 8px !important; |
| background-color: var(--background-fill-secondary, #fafafa) !important; |
| } |
| .documents-container summary.doc-summary { |
| cursor: pointer !important; |
| font-weight: bold !important; |
| color: var(--body-text-color, #1a1a1a) !important; |
| padding: 4px !important; |
| } |
| .documents-container .doc-body { |
| margin-top: 8px !important; |
| padding: 8px !important; |
| border-radius: 4px !important; |
| background-color: var(--background-fill-primary, #ffffff) !important; |
| color: var(--body-text-color, #1a1a1a) !important; |
| } |
| .documents-container .doc-body p { |
| color: var(--body-text-color, #1a1a1a) !important; |
| margin: 4px 0 !important; |
| } |
| .documents-container pre.doc-pre { |
| white-space: pre-wrap !important; |
| word-wrap: break-word !important; |
| background-color: var(--input-background-fill, #f0f0f0) !important; |
| color: var(--body-text-color, #1a1a1a) !important; |
| padding: 12px !important; |
| border-radius: 4px !important; |
| max-height: 300px !important; |
| overflow-y: auto !important; |
| font-size: 12px !important; |
| line-height: 1.4 !important; |
| border: 1px solid var(--border-color-primary, #e0e0e0) !important; |
| } |
| .documents-container p { |
| color: var(--body-text-color, #1a1a1a) !important; |
| } |
| """ |
|
|
|
|
| def format_documents_html(documents): |
| """Format documents as HTML with expandable accordions.""" |
| if not documents: |
| return "<p>No documents retrieved</p>" |
| |
| html = f"<p><strong>Total documents retrieved: {len(documents)}</strong></p>\n" |
| |
| for i, doc in enumerate(documents, 1): |
| source = doc.get("source", "unknown") |
| title = doc.get("title", "N/A") |
| content = doc.get("content", "") |
| |
| |
| content_escaped = content.replace("&", "&").replace("<", "<").replace(">", ">") |
| |
| |
| source_display = source if len(source) <= 60 else source[:57] + "..." |
| |
| html += f""" |
| <details class="doc-detail"> |
| <summary class="doc-summary">📄 Document {i}: {source_display}</summary> |
| <div class="doc-body"> |
| <p><strong>Title:</strong> {title[:80]}...</p> |
| <p><strong>Source:</strong> {source}</p> |
| <p><strong>Content:</strong></p> |
| <pre class="doc-pre">{content_escaped}</pre> |
| </div> |
| </details> |
| """ |
| |
| return html |
|
|
|
|
| def stream_response(question: str, search_type: str, k_documents: float, |
| fetch_k: float, lambda_diversity: float, relevance_threshold: float, |
| max_web_results: float): |
| """ |
| Stream RAG response with node-by-node updates and configurable retrieval settings. |
| |
| Args: |
| question: The user's question |
| search_type: Type of search ("similarity" or "mmr") |
| k_documents: Number of documents to retrieve |
| fetch_k: Number of candidates to fetch before selection (MMR only) |
| lambda_diversity: Balance between relevance and diversity (0.0-1.0, MMR only) |
| relevance_threshold: Minimum relevance score threshold |
| |
| Yields: |
| Tuple of (chat_history, status_text, documents_html) |
| """ |
| chat_history = [] |
| status_updates = [] |
| all_documents = [] |
| seen_document_hashes = set() |
| |
| |
| config_msg = f"⚙️ Retrieval: {search_type.upper()} | k={k_documents}" |
| if search_type == "mmr": |
| config_msg += f" | fetch_k={fetch_k} | λ={lambda_diversity:.1f}" |
| status_updates.append(config_msg) |
| |
| |
| chat_history.append({"role": "user", "content": question}) |
| yield chat_history, "\n".join(status_updates), "<p>No documents retrieved</p>" |
| |
| logger_frontend.info(f"User question received: {question}") |
| logger_frontend.info(f"Retrieval config: type={search_type}, k={k_documents}, fetch_k={fetch_k}, lambda={lambda_diversity}") |
| logger_frontend.info("Starting RAG processing pipeline") |
| |
| |
| retrieval_config = { |
| "search_type": search_type, |
| "k": int(k_documents), |
| "fetch_k": int(fetch_k), |
| "lambda_mult": float(lambda_diversity), |
| "score_threshold": float(relevance_threshold), |
| "max_web_results": int(max_web_results) |
| } |
| |
| |
| logger_frontend.info("=" * 60) |
| logger_frontend.info("FRONTEND - Retrieval Configuration") |
| logger_frontend.info("=" * 60) |
| logger_frontend.info(f"Search type: {search_type}") |
| logger_frontend.info(f"k (documents): {int(k_documents)}") |
| logger_frontend.info(f"fetch_k: {int(fetch_k)}") |
| logger_frontend.info(f"lambda: {float(lambda_diversity)}") |
| logger_frontend.info(f"threshold: {float(relevance_threshold)}") |
| logger_frontend.info(f"max_web_results: {int(max_web_results)}") |
| logger_frontend.info("=" * 60) |
| |
| |
| full_response = "" |
| for chunk in rag_app.stream(input={"question": question, "retrieval_config": retrieval_config}): |
| for node, update in chunk.items(): |
| status_msg = f"▶ Processing node: {node}" |
| status_updates.append(status_msg) |
| logger_frontend.info(f"Processing node: {node}") |
| |
| |
| if "documents" in update: |
| docs = update["documents"] |
| if docs: |
| logger_frontend.info(f"Retrieved {len(docs)} documents from {node}") |
| |
| for i, doc in enumerate(docs, 1): |
| source = doc.metadata.get("source", "unknown") |
| title = doc.metadata.get("title", "N/A") |
| content = doc.page_content |
| |
| |
| doc_hash = hash((content[:200], source)) |
| |
| if doc_hash not in seen_document_hashes: |
| seen_document_hashes.add(doc_hash) |
| |
| doc_data = { |
| "id": f"doc_{len(all_documents) + 1}", |
| "source": source, |
| "title": title, |
| "content": content, |
| "metadata": doc.metadata |
| } |
| all_documents.append(doc_data) |
| logger_frontend.debug(f"Added document {len(all_documents)}: {source}") |
| |
| |
| if "web_search" in update: |
| if update["web_search"]: |
| status_msg = "🔍 Web search enabled" |
| status_updates.append(status_msg) |
| logger_frontend.info("Web search enabled") |
| else: |
| status_msg = "📚 Using vector store only" |
| status_updates.append(status_msg) |
| logger_frontend.info("Using vector store only") |
| |
| |
| if "generation" in update: |
| full_response = update["generation"] |
| |
| if chat_history and chat_history[-1].get("role") == "assistant": |
| chat_history[-1]["content"] = full_response |
| else: |
| chat_history.append({"role": "assistant", "content": full_response}) |
| logger_frontend.info("Response generated successfully") |
| |
| |
| status_text = "\n".join(status_updates[-10:]) |
| docs_html = format_documents_html(all_documents) |
| yield chat_history, status_text, docs_html |
| |
| logger_frontend.info("RAG processing pipeline complete") |
| |
| status_text = "\n".join(status_updates) if status_updates else "✅ Complete" |
| docs_html = format_documents_html(all_documents) |
| yield chat_history, status_text, docs_html |
| |
| |
| status_text = "\n".join(status_updates) if status_updates else "✅ Complete" |
| docs_html = format_documents_html(all_documents) |
| yield chat_history, status_text, docs_html |
|
|
|
|
| def create_interface() -> gr.Blocks: |
| """Create and return the Gradio interface with retrieval settings sidebar.""" |
| with gr.Blocks(title="LangGraph Agentic RAG") as app: |
| gr.Markdown("# LangGraph Agentic RAG") |
| gr.Markdown("Ask questions about AI concepts. The system will automatically decide whether to use the knowledge base or web search.") |
| |
| with gr.Sidebar(label="⚙️ Retrieval Settings") as sidebar: |
| gr.Markdown("### Configure Document Retrieval") |
| |
| |
| search_type = gr.Dropdown( |
| choices=["mmr", "similarity"], |
| value="mmr", |
| label="Search Method", |
| info="MMR: Diverse results | Similarity: Top matches only" |
| ) |
| |
| |
| k_documents = gr.Slider( |
| minimum=1, |
| maximum=20, |
| value=6, |
| step=1, |
| label="Documents to Retrieve (k)", |
| info="Number of final documents to return" |
| ) |
| |
| |
| relevance_threshold = gr.Slider( |
| minimum=0.0, |
| maximum=1.0, |
| value=0.3, |
| step=0.05, |
| label="Minimum Relevance Score", |
| info="Documents below this score will be filtered out" |
| ) |
| |
| |
| with gr.Accordion("MMR Advanced Settings", open=False): |
| fetch_k = gr.Slider( |
| minimum=10, |
| maximum=50, |
| value=20, |
| step=5, |
| label="Fetch Candidates (fetch_k)", |
| info="Number of candidates to fetch before MMR selection" |
| ) |
| |
| lambda_diversity = gr.Slider( |
| minimum=0.0, |
| maximum=1.0, |
| value=0.5, |
| step=0.1, |
| label="Diversity Balance (λ)", |
| info="0.0 = Max diversity, 1.0 = Max relevance" |
| ) |
| |
| |
| with gr.Accordion("Web Search Settings", open=False): |
| max_web_results = gr.Slider( |
| minimum=1, |
| maximum=5, |
| value=2, |
| step=1, |
| label="Max Web Search Results", |
| info="Number of web results to fetch when needed" |
| ) |
| |
| |
| reset_btn = gr.Button("Reset to Defaults", variant="secondary") |
| |
| def reset_settings(): |
| return ["mmr", 6, 20, 0.5, 0.3, 2] |
| |
| reset_btn.click( |
| fn=reset_settings, |
| outputs=[search_type, k_documents, fetch_k, lambda_diversity, relevance_threshold, max_web_results] |
| ) |
| |
| with gr.Row(): |
| with gr.Column(scale=2): |
| |
| chatbot = gr.Chatbot( |
| height=500, |
| label="Conversation" |
| ) |
| |
| with gr.Row(): |
| question_input = gr.Textbox( |
| placeholder="Enter your question here...", |
| label="Question", |
| scale=4 |
| ) |
| submit_btn = gr.Button("Submit", variant="primary", scale=1) |
| |
| |
| gr.Markdown("**Example Questions**") |
| gr.Examples( |
| examples=[ |
| "What is agent memory?", |
| "Can you explain concept of few-shot prompting?", |
| ], |
| inputs=question_input, |
| label="From knowledge base" |
| ) |
| gr.Examples( |
| examples=[ |
| "What is definition of Context Engineering and when did it get popular?", |
| "What are best places to visit in Indonesia?", |
| ], |
| inputs=question_input, |
| label="Require web search" |
| ) |
| |
| clear_btn = gr.Button("Clear Conversation", variant="secondary") |
| |
| with gr.Column(scale=1): |
| |
| gr.Markdown("### Processing Status") |
| status_text = gr.Textbox( |
| label="Node Updates", |
| lines=10, |
| max_lines=15, |
| interactive=False |
| ) |
| |
| gr.Markdown("### Retrieved Documents") |
| documents_html = gr.Markdown( |
| label="Document Details", |
| value="No documents retrieved", |
| elem_classes=["documents-container"] |
| ) |
| gr.Markdown("*Click on any document to expand and see full content*") |
| |
| |
| submit_btn.click( |
| fn=stream_response, |
| inputs=[question_input, search_type, k_documents, fetch_k, lambda_diversity, relevance_threshold, max_web_results], |
| outputs=[chatbot, status_text, documents_html] |
| ) |
| |
| question_input.submit( |
| fn=stream_response, |
| inputs=[question_input, search_type, k_documents, fetch_k, lambda_diversity, relevance_threshold, max_web_results], |
| outputs=[chatbot, status_text, documents_html] |
| ) |
| |
| clear_btn.click( |
| fn=lambda: ([], "", "No documents retrieved"), |
| outputs=[chatbot, status_text, documents_html] |
| ) |
| |
| return app |
|
|
|
|
| |
| app = create_interface() |
|
|
| if __name__ == "__main__": |
| app.launch(theme=gr.themes.Soft(), share=False, css=CUSTOM_CSS) |
|
|