feat: initial RAG stack with README and env example
This commit is contained in:
commit
f6b05c6f98
7 files changed
+339
No files matched your search
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
```
|
||||||
|
```
|
||||||
|
postgres/
|
||||||
|
ollama/
|
||||||
|
open-webui/
|
||||||
|
*.sql
|
||||||
|
.env
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
psycopg2-binary
|
||||||
|
pgvector
|
||||||
|
sentence-transformers
|
||||||
Reference in new issue
Block a user