feat: initial RAG stack with README and env example

This commit is contained in:
2026-02-25 14:37:29 +00:00
commit f6b05c6f98
7 changed files with 339 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# OpenAI API Key (https://platform.openai.com/api-keys)
OPENAI_API_KEY=sk-proj-...
# PostgreSQL Passwort
POSTGRES_PASSWORD=dein-sicheres-passwort
+7
View File
@@ -0,0 +1,7 @@
```
```
postgres/
ollama/
open-webui/
*.sql
.env
+111
View File
@@ -0,0 +1,111 @@
# apply4jobs RAG Stack
KI-basierter Chat-Assistent für apply4jobs.de beantwortet Recruiter-Fragen zu Sebastian Fröhlich auf Basis seiner Bewerbungsunterlagen.
## Architektur
\`\`\`
apply4jobs.de (Next.js)
└── ChatWidget → https://ai.apply4jobs.de/api/v1/chat/completions
└── FastAPI RAG-Bridge (Port 8000)
├── multilingual-e5-small (Embeddings)
├── pgvector / PostgreSQL (Vektordatenbank)
└── OpenAI GPT-4o-mini (Generierung)
\`\`\`
## Stack
| Service | Image | Port |
|---------|-------|------|
| PostgreSQL + pgvector | pgvector/pgvector:pg16 | 5432 |
| Ollama | ollama/ollama:latest | 11434 |
| Open WebUI | ghcr.io/open-webui/open-webui:main | 3000 |
| RAG API | python:3.11-slim (custom) | 8000 |
## Voraussetzungen
- Docker + Docker Compose v2
- Ubuntu 22.04 LTS
- Domain mit SSL (Let's Encrypt)
- OpenAI API Key (GPT-4o-mini)
## Setup
### 1. Repository klonen
\`\`\`bash
git clone <repo-url> /opt/apply4jobs
cd /opt/apply4jobs
\`\`\`
### 2. Umgebungsvariablen konfigurieren
\`\`\`bash
cp .env.example .env
nano .env
\`\`\`
\`\`\`env
OPENAI_API_KEY=sk-proj-...
POSTGRES_PASSWORD=dein-sicheres-passwort
\`\`\`
### 3. Stack starten
\`\`\`bash
docker compose up -d
\`\`\`
### 4. Ollama-Modelle pullen
\`\`\`bash
docker exec ollama ollama pull llama3.2:3b
docker exec ollama ollama pull nomic-embed-text
\`\`\`
### 5. Datenbank importieren
\`\`\`bash
docker exec -i postgres psql -U anythingllm -d anythingllm < backup.sql
\`\`\`
## RAG API Endpunkte
| Methode | Endpoint | Beschreibung |
|---------|----------|-------------|
| GET | /health | Health Check |
| GET | /v1/models | Verfügbare Modelle |
| POST | /retrieve | Kontext aus pgvector abrufen |
| POST | /v1/chat/completions | OpenAI-kompatibler Chat-Endpunkt |
## Kosten
| Posten | Kosten/Monat |
|--------|-------------|
| Hetzner CPX42 | ~22 € |
| OpenAI GPT-4o-mini | ~1-3 € |
| **Gesamt** | **~23-25 €** |
## Wartung
\`\`\`bash
# Logs anzeigen
docker compose logs -f rag-api
# Container neu starten
docker compose restart rag-api
# Nach Code-Änderungen neu bauen
docker compose up -d --build rag-api
# SSL-Zertifikat testen
certbot renew --dry-run
\`\`\`
## Sicherheitshinweise
- .env niemals ins Git-Repository committen
- DSGVO: Datenschutzhinweis auf apply4jobs.de erforderlich
- Chat-Logs werden nicht persistiert
- Hetzner Rechenzentrum Deutschland = DSGVO-konform
+50
View File
@@ -0,0 +1,50 @@
services:
postgres:
image: pgvector/pgvector:pg16
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: anythingllm
POSTGRES_PASSWORD: any0203thing78llm
POSTGRES_DB: anythingllm
volumes:
- ./postgres:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
volumes:
- ./ollama:/root/.ollama
ports:
- "127.0.0.1:11434:11434"
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
restart: unless-stopped
depends_on:
- ollama
environment:
- OLLAMA_BASE_URL=http://ollama:11434
volumes:
- ./open-webui:/app/backend/data
ports:
- "127.0.0.1:3000:8080"
rag-api:
build: ./rag-api
container_name: rag-api
restart: unless-stopped
depends_on:
- postgres
env_file:
- .env
ports:
- "127.0.0.1:8000:8000"
networks:
default:
name: apply4jobs-network
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+151
View File
@@ -0,0 +1,151 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sentence_transformers import SentenceTransformer
import psycopg2
import httpx
import json
import os
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://apply4jobs.de", "https://www.apply4jobs.de", "http://localhost:3000"],
allow_methods=["POST", "GET", "OPTIONS"],
allow_headers=["*"],
)
model = SentenceTransformer("intfloat/multilingual-e5-small")
DB_CONFIG = {
"host": "postgres",
"port": 5432,
"dbname": "anythingllm",
"user": "anythingllm",
"password": "any0203thing78llm"
}
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_MODEL = "gpt-4o-mini"
SYSTEM_PROMPT = """Du bist ein professioneller Assistent für Sebastian Fröhlich.
Beantworte ausschließlich Fragen zu seiner Person, seinen Fähigkeiten,
Projekten und Berufserfahrung. Nutze nur die bereitgestellten Dokumente.
Antworte auf Deutsch oder Englisch je nach Sprache des Recruiters.
Wenn du eine Frage nicht aus den Dokumenten beantworten kannst, sage das ehrlich.
Antworte immer in vollständigen, professionellen Sätzen."""
def get_context(query: str, top_k: int = 5) -> str:
query_text = f"query: {query}"
embedding = model.encode(query_text).tolist()
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("""
SELECT metadata->>'text'
FROM anythingllm_vectors
WHERE namespace = 'mein-workspace'
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (embedding, top_k))
rows = cur.fetchall()
cur.close()
conn.close()
return "\n\n---\n\n".join([row[0] for row in rows])
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
model: str = OPENAI_MODEL
messages: list[Message]
stream: bool = False
class QueryRequest(BaseModel):
query: str
top_k: int = 5
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/v1/models")
def list_models():
return {
"object": "list",
"data": [{
"id": "sebastian-rag",
"object": "model",
"created": 1700000000,
"owned_by": "apply4jobs"
}]
}
@app.post("/retrieve")
def retrieve(req: QueryRequest):
context = get_context(req.query, req.top_k)
return {"context": context, "system_prompt": SYSTEM_PROMPT}
@app.post("/v1/chat/completions")
async def chat(req: ChatRequest):
user_message = next(
(m.content for m in reversed(req.messages) if m.role == "user"), ""
)
context = get_context(user_message)
enriched_messages = [
{
"role": "system",
"content": f"{SYSTEM_PROMPT}\n\n## Relevante Dokumente:\n{context}"
},
*[{"role": m.role, "content": m.content} for m in req.messages]
]
async def stream_response():
async with httpx.AsyncClient(timeout=60) as client:
async with client.stream(
"POST",
"https://api.openai.com/v1/chat/completions",
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": OPENAI_MODEL,
"messages": enriched_messages,
"stream": True
}
) as response:
async for chunk in response.aiter_bytes():
yield chunk
if req.stream:
return StreamingResponse(
stream_response(),
media_type="text/event-stream"
)
else:
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": OPENAI_MODEL,
"messages": enriched_messages,
"stream": False
}
)
return response.json()
+5
View File
@@ -0,0 +1,5 @@
fastapi
uvicorn
psycopg2-binary
pgvector
sentence-transformers