- added three iwm articles as test data
- added simple webview and FastAPI server
This commit is contained in:
parent
b1405f3b84
commit
3fd6b8dd0d
16
README.md
16
README.md
@ -4,12 +4,16 @@ Minimal RAG implementation with LangChain, Ollama, and FAISS.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Only 5 packages:
|
||||
- `langchain` - Core framework
|
||||
- `langchain-community` - Community integrations (loaders, vectorstores)
|
||||
- `langchain-ollama` - Ollama integration
|
||||
- `langchain-text-splitters` - Text splitting utilities
|
||||
- `langchain-huggingface` - HuggingFace embeddings
|
||||
- `faiss-cpu` - Vector search
|
||||
- `sentence-transformers` - Embeddings
|
||||
- `pypdf` - PDF loading
|
||||
- `fastapi` - Web server and API
|
||||
- `uvicorn` - ASGI server
|
||||
|
||||
## Installation
|
||||
|
||||
@ -54,6 +58,16 @@ Run:
|
||||
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
|
||||
|
||||
1. **Load documents** - PDFs or text files
|
||||
|
||||
BIN
data/dok1.pdf
Normal file
BIN
data/dok1.pdf
Normal file
Binary file not shown.
70594
data/dok2.PDF
Normal file
70594
data/dok2.PDF
Normal file
File diff suppressed because one or more lines are too long
5153
data/dok3.pdf
Normal file
5153
data/dok3.pdf
Normal file
File diff suppressed because one or more lines are too long
66
local_rag.py
66
local_rag.py
@ -5,14 +5,14 @@ Minimal dependencies, simple code
|
||||
import os
|
||||
from pathlib import Path
|
||||
from langchain_community.document_loaders import PyPDFLoader, TextLoader
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain_community.embeddings import HuggingFaceEmbeddings
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
from langchain_huggingface import HuggingFaceEmbeddings
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_ollama import ChatOllama
|
||||
|
||||
|
||||
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"""
|
||||
self.vectorstore_path = vectorstore_path
|
||||
self.ollama_model = ollama_model
|
||||
@ -97,6 +97,42 @@ class LocalRAG:
|
||||
self.vectorstore.save_local(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):
|
||||
"""Query the RAG system"""
|
||||
if self.vectorstore is None:
|
||||
@ -134,21 +170,25 @@ def main():
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize
|
||||
rag = LocalRAG(ollama_model="llama2")
|
||||
rag = LocalRAG(ollama_model="mistral:7b")
|
||||
|
||||
# Add documents (uncomment and add your file paths)
|
||||
rag.add_documents([
|
||||
"diverses/local_rag/test1.pdf",
|
||||
"diverses/local_rag/test2.txt"
|
||||
])
|
||||
# rag.add_documents([
|
||||
# "data/dok1.pdf",
|
||||
# "data/dok2.pdf",
|
||||
# "data/dok3.pdf"
|
||||
# ])
|
||||
|
||||
# List documents
|
||||
rag.list_documents()
|
||||
|
||||
# Query
|
||||
# question = "What is this document about?"
|
||||
# answer = rag.query(question)
|
||||
# print(f"\nQuestion: {question}")
|
||||
# print(f"Answer: {answer}")
|
||||
question = "What do the documents say about modality for perceived message perception?"
|
||||
answer = rag.query(question)
|
||||
print(f"\nQuestion: {question}")
|
||||
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__":
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
langchain
|
||||
langchain-community
|
||||
langchain-ollama
|
||||
langchain-text-splitters
|
||||
langchain-huggingface
|
||||
faiss-cpu
|
||||
sentence-transformers
|
||||
pypdf
|
||||
numpy<2.0
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
|
||||
60
server.py
Normal file
60
server.py
Normal 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
209
templates/chat.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user