Upload Gradio_Ollama_Enhanced_RAG_chatbot_WebUI.py

#2
by IW2025 - opened
Gradio_Ollama_Enhanced_RAG_chatbot_WebUI.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # THis code includes:
2
+ # 1. Uploading and indexing PDFs
3
+ # 2. Querying with or without RAG
4
+ # 1. Streams responses from local LLaMA 3.1
5
+ # For this uses LlamaIndex instead of LangChain, because:
6
+ # a. LangChainLLM is designed to wrap LangChain-compatible models, but not all of them
7
+ # expose streaming in a way LlamaIndex can detect.
8
+ # b. The native llama_index.llms.ollama.Ollama class is built specifically for this
9
+ # use case and fully supports streaming.
10
+ # 2. Uses RAG when collection is selected
11
+ # 3. Skips RAG when β€œπŸ”Œ Don’t use RAG” is chosen
12
+ # 4. Supports PDF uploads for live indexing
13
+ # 5. Displays source citations when available
14
+
15
+ import os
16
+ import sys
17
+ import argparse
18
+ import gradio as gr
19
+ import chromadb
20
+ from pathlib import Path
21
+ from llama_index.core import (
22
+ VectorStoreIndex,
23
+ StorageContext,
24
+ Document,
25
+ SimpleDirectoryReader
26
+ )
27
+ from llama_index.embeddings.huggingface import HuggingFaceEmbedding
28
+ from llama_index.vector_stores.chroma import ChromaVectorStore
29
+ from llama_index.llms.ollama import Ollama # βœ… Native LlamaIndex Ollama integration
30
+
31
+ NO_RAG_LABEL = "Don't use RAG" # Match exactly what get_collection_names() returns
32
+
33
+ def sanitize_metadata(metadata):
34
+ return {k: str(v) if v is not None else "" for k, v in metadata.items()}
35
+
36
+
37
+ def sanitize_name(value):
38
+ import re
39
+ return re.sub(r"[^\w]+", "_", value).strip("_").lower()
40
+
41
+
42
+ def get_collection_names(persist_dir):
43
+ try:
44
+ client = chromadb.PersistentClient(path=persist_dir)
45
+ return [NO_RAG_LABEL] + [col.name for col in client.list_collections()]
46
+ except Exception as e:
47
+ print(f"Failed to list collections: {e}")
48
+ return [NO_RAG_LABEL]
49
+
50
+
51
+ def index_pdf(file_obj, topic, persist_dir):
52
+ try:
53
+ pdf_path = Path(file_obj.name)
54
+ topic_safe = sanitize_name(topic or "untagged")
55
+ pdf_safe = sanitize_name(pdf_path.stem)
56
+ collection_name = f"{pdf_safe}_{topic_safe}"
57
+
58
+ chroma_client = chromadb.PersistentClient(path=persist_dir)
59
+ collection = chroma_client.get_or_create_collection(name=collection_name)
60
+ vector_store = ChromaVectorStore(chroma_collection=collection)
61
+ embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
62
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
63
+
64
+ docs = SimpleDirectoryReader(input_files=[str(pdf_path)]).load_data()
65
+ documents = []
66
+ for doc in docs:
67
+ meta = sanitize_metadata(doc.metadata or {})
68
+ meta["topic"] = topic
69
+ meta["source"] = pdf_path.name
70
+ # Try to include page label if available
71
+ if hasattr(doc, "page_label"):
72
+ meta["page"] = str(doc.page_label)
73
+ documents.append(Document(text=doc.text, metadata=meta))
74
+
75
+ VectorStoreIndex.from_documents(documents, embed_model=embed_model, storage_context=storage_context)
76
+ return f"βœ… Indexed: {pdf_path.name} as collection `{collection_name}`"
77
+ except Exception as e:
78
+ return f"❌ Indexing failed: {e}"
79
+
80
+
81
+ def query_index(persist_dir, collection_name, question, verbose=False):
82
+ try:
83
+ if not question.strip():
84
+ return "⚠️ Please enter a valid question."
85
+
86
+ llm = Ollama(model="llama3.1", streaming=False)
87
+
88
+ if collection_name.strip() == NO_RAG_LABEL:
89
+ if verbose:
90
+ print("⚑ Using LLM only (no retrieval)...")
91
+ return llm.complete(question)
92
+
93
+ chroma_client = chromadb.PersistentClient(path=persist_dir)
94
+ if collection_name not in [col.name for col in chroma_client.list_collections()]:
95
+ return f"❌ Collection '{collection_name}' not found."
96
+
97
+ # Step 1: Set up vector index
98
+ collection = chroma_client.get_collection(name=collection_name)
99
+ vector_store = ChromaVectorStore(chroma_collection=collection)
100
+ embed_model = HuggingFaceEmbedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
101
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
102
+ index = VectorStoreIndex.from_vector_store(vector_store=vector_store, embed_model=embed_model)
103
+
104
+ # Step 2: Create query engine with your LLM
105
+ query_engine = index.as_query_engine(llm=llm, streaming=False)
106
+
107
+ # Step 3: Query the engine directly
108
+ response = query_engine.query(question)
109
+
110
+ # Step 4: Check if any source nodes were returned
111
+ if not response.source_nodes:
112
+ print("⚠️ No relevant embeddings found. Using LLM only.")
113
+ return llm.complete(question)
114
+
115
+ # Step 5: Deduplicate citations
116
+ seen_sources = set()
117
+ unique_citations = []
118
+ for node in response.source_nodes:
119
+ source = node.metadata.get("source", "Unknown source")
120
+ if source not in seen_sources:
121
+ seen_sources.add(source)
122
+ unique_citations.append(source)
123
+
124
+ citation_text = ""
125
+ if unique_citations:
126
+ citation_text = "\n\nπŸ“š **Sources:**\n" + "\n".join(
127
+ [f"[{i+1}] {src}" for i, src in enumerate(unique_citations)]
128
+ )
129
+
130
+ # Step 6: Return final response
131
+ return (response.response or "⚠️ No answer generated.") + citation_text
132
+
133
+ except Exception as e:
134
+ return f"Error: {e}"
135
+
136
+
137
+ def build_ui(persist_dir, verbose=False):
138
+ collections = get_collection_names(persist_dir)
139
+ default_collection = collections[0]
140
+
141
+ with gr.Blocks(title="RAG Chatbot") as demo:
142
+ gr.Markdown("## 🧠 RAG Chatbot with LLaMA 3.1 (Ollama)")
143
+ gr.Markdown("Ask questions with or without retrieval. Upload PDFs to create new collections.")
144
+
145
+ with gr.Row():
146
+ question = gr.Textbox(label="πŸ” Ask a question", placeholder="e.g. What does the tablet support?")
147
+ collection_select = gr.Dropdown(label="πŸ“ Collection", choices=collections, value=default_collection)
148
+
149
+ answer_output = gr.Textbox(label="πŸ’¬ Answer", lines=10, interactive=False)
150
+ question_button = gr.Button("Ask")
151
+ question_button.click(
152
+ fn=query_index,
153
+ inputs=[gr.State(persist_dir), collection_select, question, gr.State(verbose)],
154
+ outputs=answer_output
155
+ )
156
+
157
+ gr.Markdown("---")
158
+ gr.Markdown("### πŸ“₯ Upload PDF for Live Indexing")
159
+
160
+ with gr.Row():
161
+ file = gr.File(label="PDF File", file_types=[".pdf"])
162
+ topic = gr.Textbox(label="Topic", placeholder="e.g. HP Tablet User Guide")
163
+ upload_status = gr.Textbox(label="Status", interactive=False)
164
+
165
+ upload_button = gr.Button("πŸ“„ Index PDF")
166
+ upload_button.click(fn=index_pdf, inputs=[file, topic, gr.State(persist_dir)], outputs=upload_status)
167
+
168
+ demo.launch()
169
+
170
+
171
+ if __name__ == "__main__":
172
+ parser = argparse.ArgumentParser(description="Gradio RAG chatbot with LLaMA 3.1 via Ollama")
173
+ parser.add_argument("--persist_dir", required=True, help="Path to ChromaDB index directory")
174
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
175
+ args = parser.parse_args()
176
+
177
+ try:
178
+ build_ui(args.persist_dir, verbose=args.verbose)
179
+ except Exception as e:
180
+ print(f"❌ Failed to launch app: {e}")
181
+ sys.exit(1)
182
+