| import os |
| import gc |
| from pypdf import PdfReader |
| from langchain_text_splitters import RecursiveCharacterTextSplitter |
| from langchain_community.embeddings import HuggingFaceEmbeddings |
| 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 |
| |
| from langchain_core.documents import Document |
|
|
| |
| |
| MODEL_REPO_ID = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF" |
| MODEL_FILENAME = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf" |
| |
| MODEL_CACHE_DIR = os.environ.get("MODEL_CACHE_DIR", "/app/models") |
|
|
| |
| _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 = 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, |
| token=hf_token |
| ) |
| 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, |
| n_ctx=16384, |
| n_batch=512, |
| n_gpu_layers=-1, |
| streaming=True, |
| stop=["<|eot_id|>", "<|end_of_text|>", "Human:", "user:"], |
| 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. |
| """ |
| |
| pdf_reader = PdfReader(uploaded_file) |
| docs = [] |
| |
| |
| for i, page in enumerate(pdf_reader.pages): |
| page_text = page.extract_text() |
| if page_text: |
| |
| docs.append(Document(page_content=page_text, metadata={"page": i + 1})) |
| |
| |
| |
| text_splitter = RecursiveCharacterTextSplitter( |
| chunk_size=1000, |
| chunk_overlap=200 |
| ) |
| |
| text_chunks = text_splitter.split_documents(docs) |
| |
| |
| embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") |
| |
| 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. |
| """ |
| |
| llm = _get_llm() |
| |
| |
| |
| |
| 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) |
| |
| question_answer_chain = create_stuff_documents_chain(llm, prompt_template) |
| |
| |
| retriever = vectorstore.as_retriever(search_kwargs={"k": 30}) |
| |
| |
| rag_chain = create_retrieval_chain(retriever, question_answer_chain) |
| |
| |
| |
| for chunk in rag_chain.stream({"input": user_question}): |
| if "answer" in chunk: |
| yield chunk["answer"] |