Головна / Статті / Обгортання Multimodal RAG у інтерфейс чату Gradio

Обгортання Multimodal RAG у інтерфейс чату Gradio

Переробити CLI-мультимодальний двигун RAG у додаток Gradio з функціями перетягування елементів, відображенням прогресу, посиланнями та потоковими відповідями.

1866 слів

У першій частині була представлена стек-система багатомодального RAG у командному рядку: завантаження PDF-файлів, вилучення тексту та графіків, надання відповідей локально. Це потужний інструмент, але складний для використання нефаховими співробітниками. У цьому продовженні той самий двигун обгортається у веб-інтерфейс: достатньо просто перетягнути PDF-файл, стежити за прогресом та спілкуватися з посиланнями на джерела.

Двигун вже знав, як завантажувати дані, створювати індекси, знаходити відповідні фрагменти та надавати відповіді з посиланнями на джерела. Інтерфейс не повинен розривати цю логіку — він має її викликати.

rag_engine.py

Функції завантаження даних та виконання запитів слід перетворити на зрозумілі функції, які може викликати інтерфейс:

# rag_engine.py - Core functions

def ingest_pdf(pdf_path: str, progress=None, status=None):
    """
    Parse PDF, extract text, process images, and store in Chroma.

    Args:
        pdf_path: Path to the PDF file
        progress: Gradio progress object (optional)
        status: Callback for status updates (optional)
    """
    # ... same logic as Part 1, but with progress callbacks

def query_rag(question: str) -> dict:
    """
    Retrieve relevant context and generate an answer.

    Args:
        question: User's question

    Returns:
        dict: {"answer": str, "sources": list[str]}
    """
    # ... returns answer with source parent IDs

def clear_database():
    """Reset the database and reinitialize collections."""
    # ... handles collection re-creation

def get_ingested_documents():
    """Return list of ingested PDFs."""
    # ... utility for status display

Функції повідомлення про прогрес

Під час завантаження великих PDF-файлів необхідно надавати оновлення статусу у браузері:

# In rag_engine.py
def ingest_pdf(pdf_path: str, progress=None, status=None):
    if status:
        status(f"📄 Processing page {page_num + 1}/{total_pages}...")
    if progress:
        progress((page_num + 1) / total_pages, desc=f"Processing page {page_num + 1}/{total_pages}")
    # ... rest of the logic

Передавайте з Gradio функції progress / status, щоб користувачі бачили кількість сторінок замість застиглого індикатора завантаження.

Метадані для цитування

Зберігайте читабельні поля джерел під час розділення на частини:

# During ingestion in rag_engine.py
metadata = {
    "source": os.path.basename(pdf_path),
    "page": page_num + 1,
    "parent_id": f"Page_{page_num + 1}",
    "section": "Unknown",  # Optional: extract from heading
    "chunk_index": chunk_idx,
    "type": "hybrid"
}

Отримання даних батьківських та дочірніх елементів (короткий огляд)

Дочірні частини отримуються з точністю; вікна батьківських елементів надають контекст для генерації. Інтерфейс має відображати ті самі джерела, що й CLI.

Шлях запиту

def query_rag(question: str) -> dict:
    # 1. Retrieve top matching child chunks
    results = child_collection.query(
        query_texts=[question],
        n_results=3
    )

    # 2. Fetch the full Parent contexts with metadata
    parent_ids = list(set([m["parent_ref"] for m in results["metadatas"][0]]))
    parent_results = parent_collection.get(ids=parent_ids)

    # 3. Build full context from parent documents
    full_context = "\n\n---\n\n".join(parent_results["documents"])

    # 4. Get metadata for source display
    source_metadata = []
    for meta in parent_results["metadatas"]:
        page = meta.get("page", "Unknown")
        section = meta.get("section", "Section")
        source_metadata.append({"page": page, "section": section})

    # 5. Build prompt and generate answer
    prompt = f"""
    You are a financial research assistant. Answer the question based strictly on the context below.
    If the context contains chart summaries or tables, use those numbers specifically.
    If you cannot answer from the context, say "I don't have that information."

    Context:
    {full_context}

    Question: {question}
    Answer:
    """

    response = ollama.chat(
        model=TEXT_MODEL,
        messages=[{"role": "user", "content": prompt}]
    )

    return {
        "answer": response["message"]["content"],
        "sources": source_metadata  # Now contains page and section info
    }

Отримуйте дочірні елементи, розгортайте батьківські, створюйте запит та повертайте відповідь разом із джерелами.

Відображення джерел у інтерфейсі

# In app.py
if result["sources"]:
    source_text = "\n\n📚 **Sources:** "
    sources_list = []
    for source in result["sources"][:3]:
        # Assuming source contains metadata
        page = source.get("page", "Unknown")
        section = source.get("section", "Section")
        sources_list.append(f"Page {page} ({section})")
    answer += source_text + ", ".join(sources_list)
if result["sources"]:
    source_text = "\n\n **Sources:** "
    sources_list = []
    for source in result["sources"][:3]:
        page = source.get("page", "Unknown")
        section = source.get("section", "Section")
        sources_list.append(f"Page {page} ({section})")
    answer += source_text + ", ".join(sources_list)

Приклад формату обміну даними:

User: What was the total revenue shown in the financial statements?

The total revenue shown in the financial statements is €180,462 for the year ended 31 December 2025 and €160,465 for the year ended 31 December 2024.

📚 Sources: Page 35 (Section 1), Page 79 (Section 2), Page 159 (Section 3)
User: What was the total revenue shown in the financial statements?

The total revenue shown in the financial statements is €180,462 for the year ended 31 December 2025 and €160,465 for the year ended 31 December 2024.

📚 Sources: Page 35 (Section 1), Page 79 (Section 2), Page 159 (Section 3)

Створення app.py за допомогою Gradio

Gradio надає віджети для завантаження, історію чату та можливості стрімінгу з мінімальною кількістю шаблонного коду.

Стрімінг відповідей

Краще використовувати метод yield для досягнення більшої швидкості відповідей:

def chat_response(message, history):
    # ... retrieve context ...

    # Instead of returning, use yield to stream tokens
    full_response = ""
    for chunk in ollama.chat(model=TEXT_MODEL, messages=[...], stream=True):
        full_response += chunk["message"]["content"]
        yield history + [("user", message), ("assistant", full_response)]

Повний ескіз додатку

#!/usr/bin/env python3
"""
Multimodal RAG Gradio UI
Run with: python app.py
"""

import os
import shutil
from pathlib import Path

import gradio as gr

# Import the core engine
from rag_engine import ingest_pdf, query_rag, clear_database, get_ingested_documents

# ---------- CONFIG ----------
UPLOAD_DIR = Path("./data")
UPLOAD_DIR.mkdir(exist_ok=True)

# ---------- UI FUNCTIONS ----------
def process_upload(file_obj, progress=gr.Progress()):
    """Handle PDF upload and ingestion with progress bar."""
    if file_obj is None:
        return " Please upload a PDF file first."

    pdf_path = UPLOAD_DIR / os.path.basename(file_obj.name)
    shutil.copy(file_obj.name, pdf_path)

    try:
        result = ingest_pdf(str(pdf_path), progress=progress)
        return f"{result}\n\n📄 File saved to: {pdf_path}"
    except Exception as e:
        return f" Error during ingestion: {str(e)}"

def chat_response(message, history):
    """
    Handle user questions and return responses.

    Note: This uses synchronous return. For streaming responses,
    consider using yield with ollama's stream=True parameter.
    """
    if not message or not message.strip():
        return history

    docs = get_ingested_documents()
    if not docs:
        history.append({"role": "user", "content": message})
        history.append({"role": "assistant", "content": " No documents ingested. Please upload and ingest a PDF first."})
        return history

    result = query_rag(message)
    answer = result["answer"]

    # Display readable sources with page and section info
    if result["sources"]:
        source_text = "\n\n **Sources:** "
        sources_list = []
        for source in result["sources"][:3]:
            page = source.get("page", "Unknown")
            section = source.get("section", "Section")
            sources_list.append(f"Page {page} ({section})")
        answer += source_text + ", ".join(sources_list)

    history.append({"role": "user", "content": message})
    history.append({"role": "assistant", "content": answer})
    return history

def reset_database():
    """Clear the database, chat history, and reset file upload."""
    result = clear_database()
    return result, [], gr.update(value=None)

def get_status():
    """Get current system status."""
    docs = get_ingested_documents()
    if docs:
        return f" {len(docs)} document(s) ingested: {', '.join(docs)}"
    return " No documents ingested. Upload and ingest a PDF to get started."

# ---------- BUILD UI ----------
def create_ui():
    with gr.Blocks(title="Multimodal RAG Assistant") as demo:
        gr.Markdown("""
        # Zero-Cost Local Multimodal RAG

        A privacy-first assistant that answers questions from complex PDFs containing text, tables, and charts.

        **How it works:**
        1. Upload a PDF and click **Ingest**
        2. Wait for processing (charts will be analyzed by the VLM)
        3. Ask questions about the document content

        **100% local** — No data ever leaves your machine.

        **Sources** shown in answers include page numbers and sections from your PDF.
        """)

        # Status Bar
        with gr.Row():
            status_bar = gr.Textbox(value=get_status(), label=" Status", interactive=False, scale=3)
            refresh_btn = gr.Button(" Refresh", size="sm", scale=0)
            clear_db_btn = gr.Button(" Clear Database", size="sm", variant="stop", scale=0)

        # Upload & Chat
        with gr.Row():
            with gr.Column(scale=1):
                gr.Markdown("### Upload & Ingest")
                file_upload = gr.File(label="Upload PDF", file_types=[".pdf"], height=100)
                ingest_btn = gr.Button(" Ingest PDF", variant="primary", size="lg")
                upload_status = gr.Textbox(label="Upload Status", interactive=False, lines=3)

            with gr.Column(scale=2):
                gr.Markdown("### 💬 Ask Questions")
                chatbot = gr.Chatbot(label="Chat", height=400, avatar_images=(None, "🤖"))
                with gr.Row():
                    msg = gr.Textbox(label="Your question", placeholder="e.g., What was the total revenue?", scale=4, container=False)
                    send_btn = gr.Button("Send", variant="primary", scale=0)
                clear_chat_btn = gr.Button(" Clear Chat", size="sm")

        # Event Handlers
        ingest_btn.click(fn=process_upload, inputs=[file_upload], outputs=[upload_status]).then(
            fn=get_status, inputs=[], outputs=[status_bar])
        msg.submit(fn=chat_response, inputs=[msg, chatbot], outputs=[chatbot]).then(fn=lambda: "", outputs=[msg])
        send_btn.click(fn=chat_response, inputs=[msg, chatbot], outputs=[chatbot]).then(fn=lambda: "", outputs=[msg])
        clear_chat_btn.click(fn=lambda: [], outputs=[chatbot])
        clear_db_btn.click(fn=reset_database, inputs=[], outputs=[upload_status, chatbot, file_upload]).then(
            fn=get_status, inputs=[], outputs=[status_bar])
        refresh_btn.click(fn=get_status, inputs=[], outputs=[status_bar])
        demo.load(fn=get_status, inputs=[], outputs=[status_bar])

    return demo

if __name__ == "__main__":
    print(" Starting Gradio UI...")
    print(" Opening at: http://127.0.0.1:7860")
    demo = create_ui()
    demo.launch(server_name="127.0.0.1", server_port=7860, share=False, theme=gr.themes.Soft(), css="footer {visibility: hidden}")
def chat_response(message, history):
    """
    Note: This uses synchronous return. For streaming responses,
    consider using yield with ollama's stream=True parameter.
    """

Запустіть його

python app.py
git clone https://github.com/froilan-sia/m1_multimodal_rag.git
cd m1_multimodal_rag
python app.py

Клонуйте проект, встановіть залежності з частини 1, запустіть app.py, завантажте PDF, поставте конкретне запитання та розширіть джерела інформації.

Заключення

CLI довела ефективність цього процесу; Gradio дозволяє його легко поширювати. Зберігайте rag_engine.py як єдине джерело істини, щоб робота інтерфейсу ніколи не впливала на якість отримання даних.

Забезпечуйте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими мімікрії, коли у системі CI немає GPU.

Забезпечуйте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими мімікрії, коли у системі CI немає GPU.

Забезпечуйте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими мімікрії, коли у системі CI немає GPU.

Забезпечуйте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими мімікрії, коли у системі CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.

Забезпечте відтворюваність демонстрацій: фіксуйте теги моделей, документуйте порти та використовуйте режими-емулятори, коли у CI немає GPU.