From c9f8c5403033038b856f8c9eb26e8f9a5f0c8806 Mon Sep 17 00:00:00 2001 From: Martin Rattensberger Date: Mon, 13 Jan 2025 15:22:54 +0100 Subject: [PATCH] Titeltext angepasst --- RAG-Demo.py | 142 +++++++++++------- ...iff im Vergleich (Teil I) - beck-online.pdf | Bin ...ragung auf andere Körperschaften - DruckenAM.pdf | Bin .../Dötsch_Pung_Möhlenbrock_1.pdf | Bin .../Dötsch_Pung_Möhlenbrock_2.pdf | Bin ...0-67ee0130-5203-440d-8ec2-ca8cd9316cdf.txn | 1 + ...1-8d587c71-015a-41f9-8503-e6e8568abf89.txn | Bin 0 -> 43 bytes ...2-ed119f5c-2bb6-458d-af90-d8d523b8187f.txn | Bin 0 -> 109 bytes .../pdf_embeddings.lance/_versions/1.manifest | Bin 0 -> 343 bytes .../pdf_embeddings.lance/_versions/2.manifest | Bin 0 -> 343 bytes .../pdf_embeddings.lance/_versions/3.manifest | Bin 0 -> 409 bytes ...da484e8c-ffdf-4684-ab37-13c30efbd25a.lance | Bin 0 -> 22779 bytes 12 files changed, 88 insertions(+), 55 deletions(-) rename dateien/{Steuerdokumente => Dokumente}/Der nationale und der europäische Teilbetriebsbegriff im Vergleich (Teil I) - beck-online.pdf (100%) rename dateien/{Steuerdokumente => Dokumente}/Dötsch_Pung_Möhlenbrock (D_P_M), Die Körperschaftsteuer, UmwStG § 15 (SEStEG) Aufspaltung, Abspaltung und Teilübertragung auf andere Körperschaften - DruckenAM.pdf (100%) rename dateien/{Steuerdokumente => Dokumente}/Dötsch_Pung_Möhlenbrock_1.pdf (100%) rename dateien/{Steuerdokumente => Dokumente}/Dötsch_Pung_Möhlenbrock_2.pdf (100%) create mode 100644 lancedb/pdf_embeddings.lance/_transactions/0-67ee0130-5203-440d-8ec2-ca8cd9316cdf.txn create mode 100644 lancedb/pdf_embeddings.lance/_transactions/1-8d587c71-015a-41f9-8503-e6e8568abf89.txn create mode 100644 lancedb/pdf_embeddings.lance/_transactions/2-ed119f5c-2bb6-458d-af90-d8d523b8187f.txn create mode 100644 lancedb/pdf_embeddings.lance/_versions/1.manifest create mode 100644 lancedb/pdf_embeddings.lance/_versions/2.manifest create mode 100644 lancedb/pdf_embeddings.lance/_versions/3.manifest create mode 100644 lancedb/pdf_embeddings.lance/data/da484e8c-ffdf-4684-ab37-13c30efbd25a.lance diff --git a/RAG-Demo.py b/RAG-Demo.py index 58413ee..bfe3503 100644 --- a/RAG-Demo.py +++ b/RAG-Demo.py @@ -2,13 +2,14 @@ File: RAG-Demo.py Author: Martin Rattensberger Description: A GUI application for interacting with a local Llama vision model. - Users can select a directory with PDF files and ask questions about them. + Users can select a directory with PDF files, load them into a vector database, + and ask questions about them. Date: 11.11.2024 # Replace with actual date -Version: 1.1 +Version: 1.2 Development Environment: Visual Studio Code with Continue.ai (Claude Sonnet 3.5) This script creates a tkinter-based GUI for selecting a directory with PDFs, -sending them to a local Llama 3.2 vision model, and displaying the results. +loading them into a LanceDB vector database, and querying them using a local Llama 3.2 vision model. """ import tkinter as tk @@ -21,11 +22,30 @@ import base64 import threading import time import os +import lancedb +import numpy as np +import pyarrow as pa +from sentence_transformers import SentenceTransformer + class LlamaVisionApp: def __init__(self, master): self.master = master - master.title("Llama Vision Interface") + master.title("Llama Vision Interface RAG") + + # Initialize LanceDB and sentence transformer + self.db = lancedb.connect("./lancedb") + self.db.drop_table("pdf_embeddings") + schema = pa.schema([ + ('id', pa.int64()), + ('filename', pa.string()), + ('page', pa.int64()), + ('text', pa.string()), + ("embedding", pa.list_(pa.float32(), 384)) + ]) + + self.table = self.db.create_table("pdf_embeddings", schema=schema) + self.model = SentenceTransformer('all-MiniLM-L6-v2') # Directory selection button self.select_dir_button = tk.Button(master, text="Select PDF Directory", command=self.select_directory) @@ -35,14 +55,14 @@ class LlamaVisionApp: self.dir_label = tk.Label(master, text="No directory selected") self.dir_label.pack() - # PDF file listbox - self.pdf_listbox = tk.Listbox(master, width=50, height=5) - self.pdf_listbox.pack(pady=10) + # Load PDFs button + self.load_pdfs_button = tk.Button(master, text="Load PDFs into Database", command=self.load_pdfs_to_db) + self.load_pdfs_button.pack(pady=10) # Question input self.question_entry = tk.Text(master, width=50, height=3) self.question_entry.pack(pady=10) - self.question_entry.insert(tk.END, "What is in this PDF?") + self.question_entry.insert(tk.END, "What is in these PDFs?") # Submit button self.submit_button = tk.Button(master, text="Submit", command=self.submit_question) @@ -54,51 +74,79 @@ class LlamaVisionApp: self.directory_path = None self.pdf_files = [] - self.image_data = None self.processing = False def select_directory(self): self.directory_path = filedialog.askdirectory() if self.directory_path: self.dir_label.config(text=f"Selected directory: {self.directory_path}") - self.load_pdf_files() + self.pdf_files = [f for f in os.listdir(self.directory_path) if f.lower().endswith('.pdf')] - def load_pdf_files(self): - self.pdf_files = [f for f in os.listdir(self.directory_path) if f.lower().endswith('.pdf')] - self.pdf_listbox.delete(0, tk.END) - for pdf in self.pdf_files: - self.pdf_listbox.insert(tk.END, pdf) - - def load_selected_pdf(self): - selected_indices = self.pdf_listbox.curselection() - if not selected_indices: - return None - selected_pdf = self.pdf_files[selected_indices[0]] - pdf_path = os.path.join(self.directory_path, selected_pdf) - - pdf_document = fitz.open(pdf_path) - first_page = pdf_document[0] - image = first_page.get_pixmap() - img = Image.frombytes("RGB", [image.width, image.height], image.samples) - buffer = io.BytesIO() - img.save(buffer, format="PNG") - image_data = base64.b64encode(buffer.getvalue()).decode('utf-8') - pdf_document.close() - return image_data - - def submit_question(self): - self.image_data = self.load_selected_pdf() - if not self.image_data: + def load_pdfs_to_db(self): + if not self.directory_path: self.response_text.delete('1.0', tk.END) - self.response_text.insert(tk.END, "Please select a PDF file first.\n") + self.response_text.insert(tk.END, "Please select a directory first.\n") return + self.processing = True + threading.Thread(target=self.processing_animation).start() + threading.Thread(target=self.process_pdfs).start() + + def process_pdfs(self): + data = [] + id_counter = 0 + for pdf_file in self.pdf_files: + pdf_path = os.path.join(self.directory_path, pdf_file) + doc = fitz.open(pdf_path) + for page_num in range(len(doc)): + page = doc[page_num] + text = page.get_text() + embedding = self.model.encode(text) + data.append({ + "id": id_counter, + "filename": pdf_file, + "page": page_num, + "text": text, + "embedding": embedding.tolist() + }) + id_counter += 1 + doc.close() + + self.table.add(data) + self.processing = False + self.master.after(0, self.update_response, "Load Complete", f"Loaded {len(data)} pages from {len(self.pdf_files)} PDFs into the database.") + + def submit_question(self): question = self.question_entry.get('1.0', tk.END).strip() self.response_text.delete('1.0', tk.END) self.processing = True threading.Thread(target=self.processing_animation).start() - threading.Thread(target=self.run_llama_model, args=(question,)).start() + threading.Thread(target=self.query_database, args=(question,)).start() + + def query_database(self, question): + try: + question_embedding = self.model.encode(question) + results = self.table.search(question_embedding).limit(5).to_list() + + context = "\n".join([f"From {r['filename']} (Page {r['page']+1}):\n{r['text'][:500]}..." for r in results]) + + response = ollama.chat( + model='llama3.2-vision', + messages=[{ + 'role': 'system', + 'content': f"You are an AI assistant that answers questions based on the following context:\n\n{context}" + }, + { + 'role': 'user', + 'content': question + }] + ) + self.processing = False + self.master.after(0, self.update_response, question, response['message']['content']) + except Exception as e: + self.processing = False + self.master.after(0, self.update_response, question, f"Error: {str(e)}") def processing_animation(self): animation = "|/-\\" @@ -110,26 +158,10 @@ class LlamaVisionApp: time.sleep(0.1) i += 1 - def run_llama_model(self, question): - try: - response = ollama.chat( - model='llama3.2-vision', - messages=[{ - 'role': 'user', - 'content': question, - 'images': [self.image_data] - }] - ) - self.processing = False - self.master.after(0, self.update_response, question, response['message']['content']) - except Exception as e: - self.processing = False - self.master.after(0, self.update_response, question, f"Error: {str(e)}") - def update_response(self, question, answer): self.response_text.delete('1.0', tk.END) self.response_text.insert(tk.END, f"Q: {question}\nA: {answer}\n\n") root = tk.Tk() app = LlamaVisionApp(root) -root.mainloop() +root.mainloop() \ No newline at end of file diff --git a/dateien/Steuerdokumente/Der nationale und der europäische Teilbetriebsbegriff im Vergleich (Teil I) - beck-online.pdf b/dateien/Dokumente/Der nationale und der europäische Teilbetriebsbegriff im Vergleich (Teil I) - beck-online.pdf similarity index 100% rename from dateien/Steuerdokumente/Der nationale und der europäische Teilbetriebsbegriff im Vergleich (Teil I) - beck-online.pdf rename to dateien/Dokumente/Der nationale und der europäische Teilbetriebsbegriff im Vergleich (Teil I) - beck-online.pdf diff --git a/dateien/Steuerdokumente/Dötsch_Pung_Möhlenbrock (D_P_M), Die Körperschaftsteuer, UmwStG § 15 (SEStEG) Aufspaltung, Abspaltung und Teilübertragung auf andere Körperschaften - DruckenAM.pdf b/dateien/Dokumente/Dötsch_Pung_Möhlenbrock (D_P_M), Die Körperschaftsteuer, UmwStG § 15 (SEStEG) Aufspaltung, Abspaltung und Teilübertragung auf andere Körperschaften - DruckenAM.pdf similarity index 100% rename from dateien/Steuerdokumente/Dötsch_Pung_Möhlenbrock (D_P_M), Die Körperschaftsteuer, UmwStG § 15 (SEStEG) Aufspaltung, Abspaltung und Teilübertragung auf andere Körperschaften - DruckenAM.pdf rename to dateien/Dokumente/Dötsch_Pung_Möhlenbrock (D_P_M), Die Körperschaftsteuer, UmwStG § 15 (SEStEG) Aufspaltung, Abspaltung und Teilübertragung auf andere Körperschaften - DruckenAM.pdf diff --git a/dateien/Steuerdokumente/Dötsch_Pung_Möhlenbrock_1.pdf b/dateien/Dokumente/Dötsch_Pung_Möhlenbrock_1.pdf similarity index 100% rename from dateien/Steuerdokumente/Dötsch_Pung_Möhlenbrock_1.pdf rename to dateien/Dokumente/Dötsch_Pung_Möhlenbrock_1.pdf diff --git a/dateien/Steuerdokumente/Dötsch_Pung_Möhlenbrock_2.pdf b/dateien/Dokumente/Dötsch_Pung_Möhlenbrock_2.pdf similarity index 100% rename from dateien/Steuerdokumente/Dötsch_Pung_Möhlenbrock_2.pdf rename to dateien/Dokumente/Dötsch_Pung_Möhlenbrock_2.pdf diff --git a/lancedb/pdf_embeddings.lance/_transactions/0-67ee0130-5203-440d-8ec2-ca8cd9316cdf.txn b/lancedb/pdf_embeddings.lance/_transactions/0-67ee0130-5203-440d-8ec2-ca8cd9316cdf.txn new file mode 100644 index 0000000..d5cb4d8 --- /dev/null +++ b/lancedb/pdf_embeddings.lance/_transactions/0-67ee0130-5203-440d-8ec2-ca8cd9316cdf.txn @@ -0,0 +1 @@ +$67ee0130-5203-440d-8ec2-ca8cd9316cdf²è#id ÿÿÿÿÿÿÿÿÿ*int6408Zdefault,filename ÿÿÿÿÿÿÿÿÿ*string08Zdefault'page ÿÿÿÿÿÿÿÿÿ*int6408Zdefault(text ÿÿÿÿÿÿÿÿÿ*string08Zdefault@ embedding ÿÿÿÿÿÿÿÿÿ*fixed_size_list:float:38408Zdefault \ No newline at end of file diff --git a/lancedb/pdf_embeddings.lance/_transactions/1-8d587c71-015a-41f9-8503-e6e8568abf89.txn b/lancedb/pdf_embeddings.lance/_transactions/1-8d587c71-015a-41f9-8503-e6e8568abf89.txn new file mode 100644 index 0000000000000000000000000000000000000000..a9809202443a6038a5174d43ebe490bbab40e935 GIT binary patch literal 43 ycmd;J6jHHBF|{yHHaFBYFf>inH8D)H)U_}*FxE{qOSLdHvq(%zv#?ym#sC1wg$d^X literal 0 HcmV?d00001 diff --git a/lancedb/pdf_embeddings.lance/_transactions/2-ed119f5c-2bb6-458d-af90-d8d523b8187f.txn b/lancedb/pdf_embeddings.lance/_transactions/2-ed119f5c-2bb6-458d-af90-d8d523b8187f.txn new file mode 100644 index 0000000000000000000000000000000000000000..058c475dc16f918e5a28fde61f675eb5ac44fe43 GIT binary patch literal 109 zcmXBFF%H5o3;;j~1yvn0cFMqnWYi>1<4O#OpV)CcAs*G2cHov9oxgXDakJ7OiL8Ky z6xg-}l(eKQ6sg*N`%G{DnjXqX*r|b;8sJI@EV)9=Eic_fQ{m^Af35rL>Fh4U{4cmL E9>y0Kq5uE@ literal 0 HcmV?d00001 diff --git a/lancedb/pdf_embeddings.lance/_versions/1.manifest b/lancedb/pdf_embeddings.lance/_versions/1.manifest new file mode 100644 index 0000000000000000000000000000000000000000..a946f5da3aac003c66b94b3f2b4cc22a0043d97a GIT binary patch literal 343 zcmZ=}WMJS@7GlawQTPu7j9RRjc_n5h28U+zWq-CIM zW}cdAU}$WhYieX*tZQOokfLjmnrx(-oM@4pVrgt>mYkBNS5lFe#m~i>lbDyBD#T`> ZXK1NsRK)`mU^db-V1NTg1|LU1X8=BYdO`pI literal 0 HcmV?d00001 diff --git a/lancedb/pdf_embeddings.lance/_versions/2.manifest b/lancedb/pdf_embeddings.lance/_versions/2.manifest new file mode 100644 index 0000000000000000000000000000000000000000..9d5d9c3dc7b96b50bb08377db3a621a8640cf4b1 GIT binary patch literal 343 zcmZ=}WMJS@7GlawQTPu7j9RRjc_n5h28s#&UqshLG$QksROUP(n>7C#qjPGVkist}uj Zo}s0lQ56qNfZ0gTfB_B|8GIc5oB-BIrylL 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 literal 0 HcmV?d00001