Ask-PDFi / src /rag_core.py
EXOSHIVA's picture
refactor: clean up LlamaCpp initialization parameters to meet Python syntax standards
1f76485 verified
Raw
History Blame Contribute Delete
7.08 kB
import os
import gc
from pypdf import PdfReader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings # LOKAL!
from langchain_community.vectorstores import FAISS
from langchain_classic.chains import create_retrieval_chain
from langchain_classic.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_community.llms import LlamaCpp
from huggingface_hub import hf_hub_download
# WICHTIG: Import für die Metadaten-Struktur
from langchain_core.documents import Document
# --- Konfiguration des lokalen Modells ---
# Wechsel zurück auf 8B für höhere Präzision bei komplexen Analysen
MODEL_REPO_ID = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF"
MODEL_FILENAME = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf"
# Lokaler Cache-Pfad im Container
MODEL_CACHE_DIR = os.environ.get("MODEL_CACHE_DIR", "/app/models")
# Globaler LLM-Slot - wird nur einmal geladen und dann wiederverwendet
_llm_instance: LlamaCpp | None = None
def _get_local_model_path() -> str:
"""
Gibt den lokalen Pfad zur .gguf-Datei zurück.
Ist die Datei noch nicht vorhanden, wird sie automatisch von Hugging Face
in MODEL_CACHE_DIR heruntergeladen.
"""
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
local_path = os.path.join(MODEL_CACHE_DIR, MODEL_FILENAME)
# HF_TOKEN aus den Hugging Face Secrets laden (wichtig für Llama 3.2)
hf_token = os.getenv("HF_TOKEN")
if not os.path.exists(local_path):
print(f"[RAG] Modell nicht gefunden – lade '{MODEL_FILENAME}' herunter …")
local_path = hf_hub_download(
repo_id=MODEL_REPO_ID,
filename=MODEL_FILENAME,
local_dir=MODEL_CACHE_DIR,
local_dir_use_symlinks=False, # echte Kopie statt Symlink → sicherer im Container
token=hf_token # Auth-Token für gated Models übergeben
)
print(f"[RAG] Modell erfolgreich gespeichert unter: {local_path}")
else:
print(f"[RAG] Modell bereits im Cache: {local_path}")
return local_path
def _get_llm() -> LlamaCpp:
"""
Gibt die singleton-LlamaCpp-Instanz zurück.
Beim ersten Aufruf wird das Modell geladen; danach wird dieselbe Instanz
wiederverwendet (spart RAM und Ladezeit).
"""
global _llm_instance
if _llm_instance is None:
model_path = _get_local_model_path()
print("[RAG] Lade Modell in den Arbeitsspeicher …")
_llm_instance = LlamaCpp(
model_path=model_path,
temperature=0.0,
max_tokens=1024, # ERHÖHT: Damit Antworten nicht mehr abgeschnitten werden
n_ctx=16384, # Kontextfenster (in Token) - 16384 für Llama 3.2 <-- Komma entfernt!
n_batch=512, # Token-Batch-Größe beim Prompt-Verarbeiten
n_gpu_layers=-1, # -1 = ALLES in den VRAM (CUDA); 0 = reine CPU <--UPDATE!
streaming=True, # Live-Streaming aktivieren für sofortige UI-Updates!
stop=["<|eot_id|>", "<|end_of_text|>", "Human:", "user:"], # <--- DIE NOTBREMSE!
verbose=False,
)
print("[RAG] Modell geladen und bereit.")
return _llm_instance
def unload_llm() -> None:
"""
Gibt das LLM explizit aus dem RAM frei (nützlich in ressourcenbeschränkten
Containern, wenn das Modell nach einer Session nicht mehr gebraucht wird).
"""
global _llm_instance
if _llm_instance is not None:
del _llm_instance
_llm_instance = None
gc.collect()
print("[RAG] LLM-Instanz aus dem Arbeitsspeicher entfernt.")
# ---------------------------------------------------------------------------
#
# ---------------------------------------------------------------------------
def process_pdf_to_vectorstore(uploaded_file):
"""
Nimmt das PDF, liest den Text, zerteilt ihn und speichert ihn als Embeddings.
Gibt den fertigen Vectorstore zurück.
"""
# A. Text aus dem PDF extrahieren (NEU: Mit Metadaten für Seitenzahlen)
pdf_reader = PdfReader(uploaded_file)
docs = []
# Nutzt enumerate, um die Seiten zu zählen (i + 1 für echte Seitenzahlen)
for i, page in enumerate(pdf_reader.pages):
page_text = page.extract_text()
if page_text:
# Erstellt Dokument-Objekt mit Metadaten-Tag für die Seite
docs.append(Document(page_content=page_text, metadata={"page": i + 1}))
# B. Text in kleine "Häppchen" (Chunks) zerteilen
# LLMs können nicht endlos viel Text auf einmal lesen, daher zerlegen wir ihn.
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # Jeder Chunk hat ca. 1000 Zeichen
chunk_overlap=200 # 200 Zeichen Überlappung, damit kein Satz in der Mitte abreißt
)
# Nutzt split_documents statt split_text, um Seitenzahlen beizubehalten
text_chunks = text_splitter.split_documents(docs)
# C. EIGENE INFRASTRUKTUR: Lokale Embeddings
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# Nutzt from_documents statt from_texts
vectorstore = FAISS.from_documents(documents=text_chunks, embedding=embeddings)
return vectorstore
def get_rag_answer(vectorstore, user_question):
"""
Nimmt die Frage des Users, sucht im Vectorstore nach passendem Kontext
und generiert mit dem LLM eine Antwort.
"""
# A. LOKALES LLM: GGUF-Modell via llama-cpp-python (Singleton)
llm = _get_llm()
# B. Llama 3.2 Instruct Prompt Format
# Die Steuerzeichen nutzen, auf die Llama 3.2 trainiert wurde!
# HINWEIS: <|begin_of_text|> entfernt, um doppelte Tokens zu vermeiden
llama3_template = """<|start_header_id|>system<|end_header_id|>
Du bist PDF'i, eine schlaue, hilfsbereite und moderne Technik-Eule. Du hast das hochgeladene Dokument mit deinen scharfen Eulenaugen analysiert.
Beantworte die Fragen des Nutzers sympathisch und präzise, aber nutze NUR den folgenden Kontext. Wenn die Antwort nicht im Text steht, gib ehrlich zu,
dass du sie in den vorliegenden Seiten nicht erblicken kannst. Erfinde niemals Informationen dazu.
Kontext:
{context}<|eot_id|><|start_header_id|>user<|end_header_id|>
{input}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
prompt_template = PromptTemplate.from_template(llama3_template)
# C. Die RAG-Kette zusammenbauen
question_answer_chain = create_stuff_documents_chain(llm, prompt_template)
# Holt die relevantesten Chunks aus der Datenbank
retriever = vectorstore.as_retriever(search_kwargs={"k": 30})
# Verbindet Retriever und LLM zur finalen Kette
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
# D. DIE NEUE STREAMING LOGIK
# Statt invoke() nutzen wir stream() und feuern jedes Wort per 'yield' an die UI
for chunk in rag_chain.stream({"input": user_question}):
if "answer" in chunk:
yield chunk["answer"]