File size: 7,995 Bytes
112250c 874c881 bafa4b6 112250c 3a1e54b 112250c 874c881 112250c 874c881 112250c 874c881 112250c 874c881 112250c 874c881 112250c 874c881 112250c 874c881 112250c 874c881 112250c 874c881 112250c bafa4b6 112250c 874c881 112250c 874c881 112250c bafa4b6 112250c 874c881 bafa4b6 112250c 874c881 112250c bafa4b6 bad47fd 112250c 3a1e54b 112250c bafa4b6 3a1e54b 874c881 112250c 874c881 112250c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | import os
import tempfile
import hashlib
import sqlite3
from datetime import datetime
from typing import List, Tuple, Any
import gradio as gr
from dotenv import load_dotenv
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_groq import ChatGroq
load_dotenv()
CHROMA_DIR = "chroma_db"
DB_FILE = "uploaded_files.db"
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
GROQ_MODEL = "llama-3.1-8b-instant"
PROMPT_TEMPLATE = (
"You are a helpful study assistant. Use the provided context to answer "
"the student's question accurately. If the answer is not in the context, "
"say that you don't know based on the available materials.\n\n"
"Context:\n{context}\n\n"
"Question: {question}"
)
embeddings = None
vectorstore = None
llm = None
# ==========================================
# SQLITE DATABASE FOR FILE TRACKING & CACHE
# ==========================================
def init_db():
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS indexed_files (
file_hash TEXT PRIMARY KEY,
filename TEXT,
upload_date TEXT,
chunk_count INTEGER
)
''')
conn.commit()
conn.close()
def get_file_hash(file_path: str) -> str:
hasher = hashlib.md5()
with open(file_path, 'rb') as f:
buf = f.read()
hasher.update(buf)
return hasher.hexdigest()
def is_file_indexed(file_hash: str) -> bool:
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM indexed_files WHERE file_hash = ?", (file_hash,))
result = cursor.fetchone()
conn.close()
return result is not None
def add_file_to_db(file_hash: str, filename: str, chunk_count: int):
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO indexed_files (file_hash, filename, upload_date, chunk_count) VALUES (?, ?, ?, ?)",
(file_hash, filename, datetime.now().strftime("%Y-%m-%d %H:%M:%S"), chunk_count)
)
conn.commit()
conn.close()
def get_indexed_files() -> str:
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("SELECT filename, upload_date, chunk_count FROM indexed_files ORDER BY upload_date DESC")
results = cursor.fetchall()
conn.close()
if not results:
return "No files indexed yet."
formatted_list = []
for f, d, c in results:
formatted_list.append(f"📄 **{f}** (Chunks: {c} | Indexed on: {d})")
return "\n\n".join(formatted_list)
init_db()
# ==========================================
# LANGCHAIN & RAG LOGIC
# ==========================================
def get_embeddings():
global embeddings
if embeddings is None:
embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
return embeddings
def get_vectorstore():
global vectorstore
if vectorstore is None:
emb = get_embeddings()
vectorstore = Chroma(persist_directory=CHROMA_DIR, embedding_function=emb)
return vectorstore
def get_llm():
global llm
if llm is None:
api_key = os.environ.get("GROQ_API_KEY")
if not api_key:
raise ValueError("GROQ_API_KEY environment variable not set.")
llm = ChatGroq(model=GROQ_MODEL, groq_api_key=api_key, temperature=0.3)
return llm
def process_file(file_path: str) -> int:
ext = os.path.splitext(file_path)[1].lower()
if ext == ".pdf":
loader = PyPDFLoader(file_path)
elif ext in (".txt", ".md"):
loader = TextLoader(file_path)
else:
raise ValueError(f"Unsupported file type: {ext}")
docs = loader.load()
if not docs:
return 0
splitter = RecursiveCharacterTextSplitter(chunk_size=700, chunk_overlap=100)
chunks = splitter.split_documents(docs)
store = get_vectorstore()
store.add_documents(chunks)
return len(chunks)
def index_file(file) -> str:
if file is None:
return "No file uploaded."
try:
if isinstance(file, str):
file_path = file
original_name = os.path.basename(file_path)
else:
original_name = os.path.basename(file.name)
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(original_name)[1]) as tmp:
tmp.write(file.read())
file_path = tmp.name
file_hash = get_file_hash(file_path)
if is_file_indexed(file_hash):
return f"⚠️ **Skipped:** '{original_name}' is already in the database."
chunks = process_file(file_path)
if chunks > 0:
add_file_to_db(file_hash, original_name, chunks)
return f"✅ **Success:** Indexed {chunks} chunks from '{original_name}'."
else:
return f"⚠️ **Warning:** No readable text found in '{original_name}'."
except Exception as e:
return f"❌ **Error:** {str(e)}"
# ==========================================
# CHAT FUNCTION (FIXED: uses tuple-based history)
# ==========================================
def answer_question(question: str, history: List[Tuple[str, str]]) -> Tuple[str, List[Tuple[str, str]]]:
"""
Gradio Chatbot (default mode) expects history as a list of (user, bot) tuples.
We return the updated history with the new pair appended.
"""
if not question.strip():
return "", history
store = get_vectorstore()
llm_model = get_llm()
docs = store.similarity_search(question, k=4)
if not docs:
answer = "I couldn't find any relevant information in the uploaded documents. Please upload some study materials first!"
else:
context = "\n\n".join(doc.page_content for doc in docs)
prompt = PROMPT_TEMPLATE.format(context=context, question=question)
response = llm_model.invoke(prompt)
answer = response.content
# Append the new exchange as a tuple
history.append((question, answer))
return "", history
# ==========================================
# GRADIO UI
# ==========================================
with gr.Blocks(title="Source.AI – RAG Study Assistant") as demo:
gr.Markdown("# 🎓 Source.AI – RAG Study Assistant")
gr.Markdown("Upload your study materials (PDF, TXT) and ask questions about them.")
with gr.Tab("💬 Ask Questions"):
# IMPORTANT: No type="messages" – using classic tuple‑based history
chatbot = gr.Chatbot(label="Conversation")
msg = gr.Textbox(label="Your question", placeholder="Type your doubt here...")
clear = gr.Button("Clear Chat")
msg.submit(answer_question, inputs=[msg, chatbot], outputs=[msg, chatbot])
# Clears chatbot (empty list) and the input field
clear.click(lambda: ([], ""), outputs=[chatbot, msg])
with gr.Tab("📤 Upload Documents"):
file_input = gr.File(label="Upload a file", file_types=[".pdf", ".txt", ".md"])
upload_output = gr.Markdown(label="Status")
upload_btn = gr.Button("Index Document", variant="primary")
upload_btn.click(fn=index_file, inputs=[file_input], outputs=[upload_output])
with gr.Tab("🗄️ Database Info"):
gr.Markdown("### 📚 Currently Indexed Materials")
db_list_output = gr.Markdown(get_indexed_files())
refresh_btn = gr.Button("Refresh Database List")
refresh_btn.click(fn=get_indexed_files, inputs=[], outputs=[db_list_output])
gr.Markdown("---\n*Built with Groq, ChromaDB, and Hugging Face Spaces.*")
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
theme=gr.themes.Soft(),
share=False
) |