- added three iwm articles as test data

- added simple webview and FastAPI server
This commit is contained in:
Philipp Mock 2026-02-03 10:57:05 +01:00
parent b1405f3b84
commit 3fd6b8dd0d
8 changed files with 76090 additions and 14 deletions

View File

@ -4,12 +4,16 @@ Minimal RAG implementation with LangChain, Ollama, and FAISS.
## Dependencies ## Dependencies
Only 5 packages:
- `langchain` - Core framework - `langchain` - Core framework
- `langchain-community` - Community integrations (loaders, vectorstores)
- `langchain-ollama` - Ollama integration - `langchain-ollama` - Ollama integration
- `langchain-text-splitters` - Text splitting utilities
- `langchain-huggingface` - HuggingFace embeddings
- `faiss-cpu` - Vector search - `faiss-cpu` - Vector search
- `sentence-transformers` - Embeddings - `sentence-transformers` - Embeddings
- `pypdf` - PDF loading - `pypdf` - PDF loading
- `fastapi` - Web server and API
- `uvicorn` - ASGI server
## Installation ## Installation
@ -54,6 +58,16 @@ Run:
python local_rag.py python local_rag.py
``` ```
## Chat GUI (FastAPI)
A simple web chat interface is included. Start the server:
```bash
uvicorn server:app --reload
```
Then open [http://localhost:8000](http://localhost:8000) in your browser. The chat view uses the same RAG system: your messages are answered using the vector store and Ollama. Ensure your vector store is populated (e.g. by running the document-add steps in `local_rag.py` once) and that Ollama is running.
## How it works ## How it works
1. **Load documents** - PDFs or text files 1. **Load documents** - PDFs or text files

BIN
data/dok1.pdf Normal file

Binary file not shown.

70594
data/dok2.PDF Normal file

File diff suppressed because one or more lines are too long

5153
data/dok3.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,14 +5,14 @@ Minimal dependencies, simple code
import os import os
from pathlib import Path from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader, TextLoader from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS from langchain_community.vectorstores import FAISS
from langchain_ollama import ChatOllama from langchain_ollama import ChatOllama
class LocalRAG: class LocalRAG:
def __init__(self, vectorstore_path="./vectorstore", ollama_model="llama2"): def __init__(self, vectorstore_path="./vectorstore", ollama_model="mistral:7b"):
"""Initialize local RAG system""" """Initialize local RAG system"""
self.vectorstore_path = vectorstore_path self.vectorstore_path = vectorstore_path
self.ollama_model = ollama_model self.ollama_model = ollama_model
@ -97,6 +97,42 @@ class LocalRAG:
self.vectorstore.save_local(self.vectorstore_path) self.vectorstore.save_local(self.vectorstore_path)
print(f"Vector store saved to {self.vectorstore_path}") print(f"Vector store saved to {self.vectorstore_path}")
def list_documents(self):
"""List all documents in the vector store"""
if self.vectorstore is None:
print("No documents in vector store.")
return []
# Get all documents from the vector store
# We'll retrieve a large number to get all documents
all_docs = self.vectorstore.similarity_search("", k=10000) # Large k to get all
# Extract unique document sources from metadata
documents = {}
for doc in all_docs:
source = doc.metadata.get('source', 'Unknown')
if source not in documents:
documents[source] = {
'source': source,
'chunks': 0,
'page': doc.metadata.get('page', None)
}
documents[source]['chunks'] += 1
# Convert to list and sort
doc_list = list(documents.values())
doc_list.sort(key=lambda x: x['source'])
print(f"\nDocuments in vector store ({len(doc_list)} unique documents):")
print("-" * 60)
for doc_info in doc_list:
print(f" - {doc_info['source']}")
print(f" Chunks: {doc_info['chunks']}")
if doc_info['page'] is not None:
print(f" Page: {doc_info['page']}")
return doc_list
def query(self, question, k=4): def query(self, question, k=4):
"""Query the RAG system""" """Query the RAG system"""
if self.vectorstore is None: if self.vectorstore is None:
@ -134,21 +170,25 @@ def main():
print("=" * 60) print("=" * 60)
# Initialize # Initialize
rag = LocalRAG(ollama_model="llama2") rag = LocalRAG(ollama_model="mistral:7b")
# Add documents (uncomment and add your file paths) # Add documents (uncomment and add your file paths)
rag.add_documents([ # rag.add_documents([
"diverses/local_rag/test1.pdf", # "data/dok1.pdf",
"diverses/local_rag/test2.txt" # "data/dok2.pdf",
]) # "data/dok3.pdf"
# ])
# List documents
rag.list_documents()
# Query # Query
# question = "What is this document about?" question = "What do the documents say about modality for perceived message perception?"
# answer = rag.query(question) answer = rag.query(question)
# print(f"\nQuestion: {question}") print(f"\nQuestion: {question}")
# print(f"Answer: {answer}") print(f"Answer: {answer}")
print("\nSetup complete! Uncomment the code above to add documents and query.") # print("\nSetup complete! Uncomment the code above to add documents and query.")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,5 +1,11 @@
langchain langchain
langchain-community
langchain-ollama langchain-ollama
langchain-text-splitters
langchain-huggingface
faiss-cpu faiss-cpu
sentence-transformers sentence-transformers
pypdf pypdf
numpy<2.0
fastapi
uvicorn[standard]

60
server.py Normal file
View File

@ -0,0 +1,60 @@
"""
FastAPI server for Local RAG with chat GUI.
Run with: uvicorn server:app --reload
"""
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from local_rag import LocalRAG
# Initialize RAG once at startup
VECTORSTORE_PATH = "./vectorstore"
OLLAMA_MODEL = "mistral:7b"
rag = LocalRAG(vectorstore_path=VECTORSTORE_PATH, ollama_model=OLLAMA_MODEL)
app = FastAPI(title="Local RAG Chat", version="1.0.0")
class ChatRequest(BaseModel):
message: str
class ChatResponse(BaseModel):
answer: str
error: str | None = None
@app.get("/", response_class=HTMLResponse)
def chat_view():
"""Serve the chat GUI."""
html_path = Path(__file__).parent / "templates" / "chat.html"
if not html_path.exists():
raise HTTPException(status_code=500, detail="Chat template not found")
return HTMLResponse(content=html_path.read_text(encoding="utf-8"))
@app.post("/api/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
"""Handle a chat message and return the RAG answer."""
if not request.message or not request.message.strip():
return ChatResponse(answer="", error="Message cannot be empty")
try:
answer = rag.query(request.message.strip())
return ChatResponse(answer=answer)
except Exception as e:
return ChatResponse(answer="", error=str(e))
@app.get("/api/health")
def health():
"""Health check and vector store status."""
has_docs = rag.vectorstore is not None
return {"status": "ok", "vectorstore_loaded": has_docs}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

209
templates/chat.html Normal file
View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Local RAG Chat</title>
<style>
* { box-sizing: border-box; }
body {
font-family: "SF Mono", "Consolas", "Monaco", monospace;
margin: 0;
min-height: 100vh;
background: #0f0f12;
color: #e4e4e7;
display: flex;
flex-direction: column;
}
header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #27272a;
background: #18181b;
}
header h1 {
margin: 0;
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.02em;
}
header p {
margin: 0.25rem 0 0;
font-size: 0.75rem;
color: #71717a;
}
#messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.msg {
max-width: 85%;
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.msg.user {
align-self: flex-end;
background: #3f3f46;
color: #fafafa;
}
.msg.assistant {
align-self: flex-start;
background: #27272a;
border: 1px solid #3f3f46;
}
.msg.error {
background: #451a1a;
border: 1px solid #7f1d1d;
color: #fecaca;
}
.msg .label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #71717a;
margin-bottom: 0.5rem;
display: block;
}
#input-area {
padding: 1rem 1.5rem 1.5rem;
border-top: 1px solid #27272a;
background: #18181b;
}
#input-row {
display: flex;
gap: 0.5rem;
align-items: flex-end;
}
#input {
flex: 1;
min-height: 44px;
max-height: 160px;
padding: 0.6rem 1rem;
font: inherit;
font-size: 0.9rem;
color: #e4e4e7;
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 8px;
resize: none;
outline: none;
}
#input::placeholder { color: #71717a; }
#input:focus { border-color: #52525b; }
#send {
padding: 0.6rem 1.2rem;
font: inherit;
font-size: 0.85rem;
font-weight: 500;
color: #0f0f12;
background: #e4e4e7;
border: none;
border-radius: 8px;
cursor: pointer;
}
#send:hover { background: #fafafa; }
#send:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
align-self: flex-start;
padding: 0.5rem 1rem;
font-size: 0.8rem;
color: #71717a;
}
</style>
</head>
<body>
<header>
<h1>Local RAG Chat</h1>
<p>Ask questions about your documents. Answers are generated from the vector store + Ollama.</p>
</header>
<div id="messages"></div>
<div id="input-area">
<div id="input-row">
<textarea id="input" rows="1" placeholder="Ask a question…" autofocus></textarea>
<button type="button" id="send">Send</button>
</div>
</div>
<script>
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send');
function appendMessage(role, text, isError = false) {
const div = document.createElement('div');
div.className = 'msg ' + (isError ? 'error' : role);
const label = role === 'user' ? 'You' : 'RAG';
div.innerHTML = '<span class="label">' + label + '</span>' + escapeHtml(text);
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function setLoading(show) {
if (show) {
const el = document.createElement('div');
el.className = 'loading';
el.id = 'loading-dot';
el.textContent = 'Thinking…';
messagesEl.appendChild(el);
messagesEl.scrollTop = messagesEl.scrollHeight;
} else {
const el = document.getElementById('loading-dot');
if (el) el.remove();
}
sendBtn.disabled = show;
}
async function send() {
const text = inputEl.value.trim();
if (!text) return;
inputEl.value = '';
appendMessage('user', text);
setLoading(true);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text })
});
const data = await res.json();
setLoading(false);
if (data.error) {
appendMessage('assistant', data.error, true);
} else {
appendMessage('assistant', (data.answer || '(No response)').trim());
}
} catch (err) {
setLoading(false);
appendMessage('assistant', 'Request failed: ' + err.message, true);
}
}
sendBtn.addEventListener('click', send);
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
send();
}
});
</script>
</body>
</html>