feat: v2 pipeline mit OCR + KI-Normalisierung

- extractor.py: Text-Extraktion für MD/TXT/PDF/DOCX/Bilder inkl. OCR (Tesseract)
- normalizer.py: KI-Normalisierung via Claude Haiku (Typ-Erkennung) + Sonnet (Transformation)
- ingest.py: Vollpipeline v2 mit watch, delete, dry-run, quality-min Flags
- ingest_anythingllm.py: Einfaches Script v1 (nur MD/TXT/PDF, kein OCR)
- README.md: Alle drei Tools dokumentiert
This commit is contained in:
2026-04-28 11:21:53 +02:00
parent e7bb3b60b4
commit 4fea148d61
13 changed files with 1134 additions and 52 deletions
+11
View File
@@ -1,5 +1,16 @@
# Umgebung
.env .env
.env.job_matching
venv/ venv/
__pycache__/ __pycache__/
*.pyc *.pyc
*.pyo
# macOS
.DS_Store .DS_Store
# Embedding-Modell Cache (wird automatisch heruntergeladen)
.cache/
# Ausgefüllte persönliche Dokumente nicht ins Repo
templates/ausgefuellt/
+173 -52
View File
@@ -1,62 +1,183 @@
# RAG Ingestion Script # RAG Ingestion Tools
Lokales Python-Script um Dokumente (MD, TXT, PDF) in die pgvector-Datenbank auf dem Hetzner-Server zu importieren. Lokale Python-Tools zum Importieren von Dokumenten in pgvector-Datenbanken.
Drei eigenständige Tools für unterschiedliche Zieldatenbanken und Anwendungsfälle.
## Setup ---
## Übersicht
| Tool | Zieldatenbank | Verbindung | Besonderheit |
|---|---|---|---|
| `ingest.py` | Hetzner (anythingllm) | SSH-Tunnel | OCR + KI-Normalisierung |
| `ingest_anythingllm.py` | Hetzner (anythingllm) | SSH-Tunnel | Einfach, nur MD/TXT/PDF |
| `ingest_job_matching.py` | NAS 192.168.178.128 | Direkt (Port 5433) | Job-Matching-Datenbank |
---
## Tool 1: `ingest.py` — Vollpipeline mit OCR + KI-Normalisierung
Das Haupt-Tool der neuen Generation. Verarbeitet beliebige Dokumente (inkl. Scans
und Bilder) und normalisiert sie via Claude KI in die einheitliche Template-Struktur.
**Abhängigkeiten:** `extractor.py`, `normalizer.py` (müssen im gleichen Verzeichnis liegen)
### Unterstützte Formate
`.md` `.txt` `.pdf` `.docx` `.png` `.jpg` `.jpeg` `.tif` `.tiff` `.bmp` `.webp`
### Voraussetzungen
```bash
# Tesseract (OCR, einmalig)
brew install tesseract tesseract-lang
# Python-Pakete
pip install -r requirements.txt
# .env konfigurieren
cp .env.example .env
# ANTHROPIC_API_KEY eintragen
```
### Verwendung
```bash
# Einzelne Datei (mit KI-Normalisierung)
python ingest.py file ~/Downloads/fremdes_cv.pdf
# Erst testen ohne DB-Speicherung
python ingest.py file ~/Desktop/scan.png --dry-run
# Ohne KI-Normalisierung (z.B. eigene fertige Templates)
python ingest.py file ~/templates/ausgefuellt/mein_cv.md --no-normalize
# Ganzes Verzeichnis
python ingest.py dir ~/Dokumente/bewerbungsunterlagen/ --force
# Ordner live beobachten (neue Dateien automatisch verarbeiten)
python ingest.py watch ~/Desktop/scan-eingang/
# DB-Inhalt anzeigen
python ingest.py list
# Dokument löschen
python ingest.py delete "fremdes_cv.pdf"
```
### Flags
| Flag | Beschreibung |
|---|---|
| `--force` | Bestehende Chunks überschreiben |
| `--no-normalize` | KI-Normalisierung überspringen |
| `--dry-run` | Nur extrahieren + normalisieren, nicht speichern |
| `--quality-min 0.5` | Mindest-Qualitäts-Score (0.01.0) |
### KI-Normalisierung
Erkennt den Dokumenttyp automatisch und transformiert den Inhalt in die passende
Template-Struktur aus `templates/`. Nutzt die ausgefüllten Beispiele aus
`templates/ausgefuellt/` als Few-Shot-Referenz.
- Typ-Erkennung: Claude Haiku (günstig + schnell)
- Transformation: Claude Sonnet
- Fehlende Felder werden mit `[FEHLT]` markiert, nicht halluziniert
---
## Tool 2: `ingest_anythingllm.py` — Einfaches Ingestion-Script
Schlankes Tool ohne OCR und ohne KI-Normalisierung. Direkte Übernahme von
MD/TXT/PDF-Dateien in die anythingllm-Datenbank auf dem Hetzner-Server.
Geeignet für: eigene, bereits fertig strukturierte Dokumente.
### Unterstützte Formate
`.md` `.txt` `.pdf`
### Voraussetzungen
```bash
pip install openai psycopg2-binary pgvector sshtunnel python-dotenv pypdf tiktoken sentence-transformers
cp .env.example .env
# OPENAI_API_KEY eintragen (wird nur für Client-Init benötigt, Embeddings laufen lokal)
```
### Verwendung
```bash
# Einzelne Datei
python ingest_anythingllm.py file ~/Dokumente/Lebenslauf.md
# Mit --force (bestehende Chunks überschreiben)
python ingest_anythingllm.py file ~/Dokumente/Lebenslauf.md --force
# Ganzes Verzeichnis
python ingest_anythingllm.py dir ~/Dokumente/bewerbung/
# DB-Inhalt anzeigen
python ingest_anythingllm.py list
```
---
## Tool 3: `ingest_job_matching.py` — Job-Matching-Datenbank (NAS)
Importiert Dokumente direkt in die `job_matching`-Datenbank auf dem lokalen NAS
(192.168.178.128, Port 5433). Kein SSH-Tunnel erforderlich.
### Voraussetzungen
```bash
pip install -r requirements.txt
cp .env.job_matching .env.job_matching.local
# Werte in .env.job_matching.local anpassen
```
### Verwendung
```bash
python ingest_job_matching.py file ~/Dokumente/stellenanzeige.pdf
python ingest_job_matching.py dir ~/Dokumente/stellen/
python ingest_job_matching.py list
```
---
## Gemeinsame Infrastruktur
### Embedding-Modell
Alle Tools verwenden `intfloat/multilingual-e5-small` (384 Dimensionen) lokal.
Wird beim ersten Aufruf automatisch heruntergeladen (~120 MB).
### Templates
```
templates/
├── *_TEMPLATE.md Ziel-Strukturvorlagen (leer)
└── ausgefuellt/ Ausgefüllte Referenzbeispiele (für KI-Few-Shot)
```
### Umgebungsvariablen
| Datei | Verwendet von |
|---|---|
| `.env` | `ingest.py`, `ingest_anythingllm.py` |
| `.env.job_matching` | `ingest_job_matching.py` |
---
## Setup (komplett, einmalig)
```bash ```bash
cd ~/Projekte/rag-ingestion cd ~/Projekte/rag-ingestion
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
``` brew install tesseract tesseract-lang # nur für ingest.py (OCR)
Umgebungsvariablen konfigurieren:
```bash
cp .env.example .env cp .env.example .env
nano .env nano .env # API-Keys + DB-Zugangsdaten eintragen
``` ```
## Verwendung
### Einzelne Datei importieren
```bash
python ingest.py file ~/Dokumente/Lebenslauf.md
```
### Mit --force (bestehende Chunks überschreiben)
```bash
python ingest.py file ~/Dokumente/Lebenslauf.md --force
```
### Ganzes Verzeichnis importieren
```bash
python ingest.py dir ~/Dokumente/bewerbung/
```
### Alle Dokumente in der DB anzeigen
```bash
python ingest.py list
```
## Unterstützte Formate
- `.md` Markdown
- `.txt` Plaintext
- `.pdf` PDF (via pypdf)
## Embedding-Modell
Verwendet `intfloat/multilingual-e5-small` (384 Dimensionen) identisch mit der RAG-API auf dem Server. Das Modell wird beim ersten Aufruf automatisch heruntergeladen (~120MB).
## Geplante Erweiterungen
- [ ] Tkinter UI für komfortableres Einpflegen
- [ ] Drag & Drop von Dateien
- [ ] Fortschrittsanzeige
- [ ] Dokument-Verwaltung (löschen, aktualisieren)
+323
View File
@@ -0,0 +1,323 @@
#!/usr/bin/env python3
"""
RAG Ingestion Script für apply4jobs.de
Liest Dokumente (MD, TXT, PDF), chunked sie und speichert Embeddings in pgvector.
Späterer Ausbau: Tkinter UI
"""
import os
import uuid
import json
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
import psycopg2
from psycopg2.extras import execute_values
from sshtunnel import SSHTunnelForwarder
import tiktoken
# PDF Support (optional)
try:
from pypdf import PdfReader
PDF_SUPPORT = True
except ImportError:
PDF_SUPPORT = False
load_dotenv()
# --- Konfiguration ---
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
SSH_HOST = os.getenv("SSH_HOST")
SSH_USER = os.getenv("SSH_USER", "root")
SSH_KEY_PATH = os.path.expanduser(os.getenv("SSH_KEY_PATH", "~/.ssh/id_ed25519"))
SSH_KEY_PASSPHRASE = os.getenv("SSH_KEY_PASSPHRASE", None)
DB_HOST = os.getenv("DB_HOST", "127.0.0.1")
DB_PORT = int(os.getenv("DB_PORT", 5432))
DB_NAME = os.getenv("DB_NAME", "anythingllm")
DB_USER = os.getenv("DB_USER", "anythingllm")
DB_PASSWORD = os.getenv("DB_PASSWORD")
NAMESPACE = os.getenv("NAMESPACE", "mein-workspace")
CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", 500))
CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", 50))
EMBED_MODEL = "intfloat/multilingual-e5-small"
client = OpenAI(api_key=OPENAI_API_KEY)
embed_model = None # lazy load
# --- Text-Extraktion ---
def read_file(path: Path) -> str:
"""Liest MD, TXT oder PDF und gibt den Text zurück."""
suffix = path.suffix.lower()
if suffix in (".md", ".txt"):
return path.read_text(encoding="utf-8")
elif suffix == ".pdf":
if not PDF_SUPPORT:
raise ImportError("pypdf nicht installiert: pip install pypdf")
reader = PdfReader(str(path))
return "\n\n".join(page.extract_text() or "" for page in reader.pages)
else:
raise ValueError(f"Nicht unterstütztes Dateiformat: {suffix}")
# --- Chunking ---
def chunk_text(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list[str]:
"""Teilt Text in überlappende Chunks auf (token-basiert)."""
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = min(start + chunk_size, len(tokens))
chunk_tokens = tokens[start:end]
chunks.append(enc.decode(chunk_tokens))
start += chunk_size - overlap
return chunks
# --- Embeddings ---
def get_embeddings(texts: list[str]) -> list[list[float]]:
"""Erstellt Embeddings via multilingual-e5-small (lokal)."""
global embed_model
if embed_model is None:
from sentence_transformers import SentenceTransformer
print(" ⏳ Lade Embedding-Modell (einmalig)...")
embed_model = SentenceTransformer(EMBED_MODEL)
prefixed = [f"passage: {t}" for t in texts]
return embed_model.encode(prefixed).tolist()
# --- Datenbank ---
def get_db_connection(tunnel_port: int):
return psycopg2.connect(
host="127.0.0.1",
port=tunnel_port,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)
def check_existing(cur, source_title: str) -> int:
"""Prüft ob Dokument bereits in der DB vorhanden ist."""
cur.execute(
"SELECT COUNT(*) FROM anythingllm_vectors WHERE metadata->>'title' = %s AND namespace = %s",
(source_title, NAMESPACE)
)
return cur.fetchone()[0]
def delete_existing(cur, source_title: str):
"""Löscht alle Chunks eines Dokuments."""
cur.execute(
"DELETE FROM anythingllm_vectors WHERE metadata->>'title' = %s AND namespace = %s",
(source_title, NAMESPACE)
)
print(f" 🗑️ Bestehende Chunks für '{source_title}' gelöscht.")
def insert_chunks(cur, chunks: list[str], embeddings: list[list[float]], source_path: Path):
"""Fügt Chunks mit Embeddings in pgvector ein."""
now = datetime.now().isoformat()
records = []
for chunk, embedding in zip(chunks, embeddings):
metadata = {
"id": str(uuid.uuid4()),
"url": f"file://{source_path.resolve()}",
"text": chunk,
"title": source_path.name,
"docSource": "rag-ingestion-script",
"published": now,
"wordCount": len(chunk.split()),
"chunkSource": str(source_path.resolve()),
}
records.append((
str(uuid.uuid4()),
json.dumps(metadata),
NAMESPACE,
embedding
))
execute_values(
cur,
"""
INSERT INTO anythingllm_vectors (id, metadata, namespace, embedding)
VALUES %s
""",
records,
template="(%s::uuid, %s::jsonb, %s, %s::vector)"
)
# --- Haupt-Ingestion ---
def ingest_file(file_path: str, force: bool = False):
"""
Verarbeitet eine einzelne Datei:
1. Text lesen
2. Chunken
3. Embeddings erstellen
4. In pgvector speichern (via SSH Tunnel)
"""
path = Path(file_path).resolve()
if not path.exists():
print(f"❌ Datei nicht gefunden: {path}")
return False
print(f"\n📄 Verarbeite: {path.name}")
# Text lesen
try:
text = read_file(path)
print(f" ✅ Text gelesen ({len(text)} Zeichen)")
except Exception as e:
print(f" ❌ Fehler beim Lesen: {e}")
return False
# Chunken
chunks = chunk_text(text)
print(f"{len(chunks)} Chunks erstellt")
# Embeddings
print(f" ⏳ Erstelle Embeddings via OpenAI...")
try:
embeddings = get_embeddings(chunks)
print(f"{len(embeddings)} Embeddings erstellt")
except Exception as e:
print(f" ❌ Fehler bei Embeddings: {e}")
return False
# SSH Tunnel + DB
print(f" ⏳ Verbinde mit Datenbank via SSH Tunnel...")
try:
with SSHTunnelForwarder(
(SSH_HOST, 22),
ssh_username=SSH_USER,
ssh_pkey=SSH_KEY_PATH,
ssh_private_key_password=SSH_KEY_PASSPHRASE,
remote_bind_address=("127.0.0.1", DB_PORT)
) as tunnel:
conn = get_db_connection(tunnel.local_bind_port)
cur = conn.cursor()
# Prüfen ob Dokument bereits existiert
existing = check_existing(cur, path.name)
if existing > 0:
if force:
delete_existing(cur, path.name)
else:
print(f" ⚠️ '{path.name}' bereits in DB ({existing} Chunks). Nutze --force zum Überschreiben.")
cur.close()
conn.close()
return False
# Einfügen
insert_chunks(cur, chunks, embeddings, path)
conn.commit()
cur.close()
conn.close()
print(f"{len(chunks)} Chunks in pgvector gespeichert")
except Exception as e:
print(f" ❌ Datenbankfehler: {e}")
return False
return True
def ingest_directory(dir_path: str, force: bool = False):
"""Verarbeitet alle MD, TXT und PDF Dateien in einem Verzeichnis."""
path = Path(dir_path).resolve()
if not path.is_dir():
print(f"❌ Verzeichnis nicht gefunden: {path}")
return
files = list(path.glob("*.md")) + list(path.glob("*.txt")) + list(path.glob("*.pdf"))
if not files:
print(f"❌ Keine unterstützten Dateien in: {path}")
return
print(f"\n📁 Verarbeite {len(files)} Dateien aus: {path}")
success = 0
for f in files:
if ingest_file(str(f), force=force):
success += 1
print(f"\n✅ Fertig: {success}/{len(files)} Dateien erfolgreich importiert.")
def list_documents():
"""Zeigt alle Dokumente in der Datenbank an."""
print(f"\n📋 Dokumente in Namespace '{NAMESPACE}':")
try:
with SSHTunnelForwarder(
(SSH_HOST, 22),
ssh_username=SSH_USER,
ssh_pkey=SSH_KEY_PATH,
ssh_private_key_password=SSH_KEY_PASSPHRASE,
remote_bind_address=("127.0.0.1", DB_PORT)
) as tunnel:
conn = get_db_connection(tunnel.local_bind_port)
cur = conn.cursor()
cur.execute("""
SELECT metadata->>'title', COUNT(*), MAX(metadata->>'published')
FROM anythingllm_vectors
WHERE namespace = %s
GROUP BY metadata->>'title'
ORDER BY MAX(metadata->>'published') DESC
""", (NAMESPACE,))
rows = cur.fetchall()
cur.close()
conn.close()
if not rows:
print(" Keine Dokumente gefunden.")
else:
print(f" {'Titel':<50} {'Chunks':>6} {'Erstellt'}")
print(" " + "-" * 75)
for title, count, published in rows:
print(f" {(title or 'Unknown'):<50} {count:>6} {published or '-'}")
print()
except Exception as e:
print(f"❌ Fehler: {e}")
# --- CLI ---
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="RAG Ingestion Script Dokumente in pgvector importieren"
)
subparsers = parser.add_subparsers(dest="command")
# ingest file
p_file = subparsers.add_parser("file", help="Einzelne Datei importieren")
p_file.add_argument("path", help="Pfad zur Datei (MD, TXT, PDF)")
p_file.add_argument("--force", action="store_true", help="Bestehende Chunks überschreiben")
# ingest directory
p_dir = subparsers.add_parser("dir", help="Verzeichnis importieren")
p_dir.add_argument("path", help="Pfad zum Verzeichnis")
p_dir.add_argument("--force", action="store_true", help="Bestehende Chunks überschreiben")
# list
subparsers.add_parser("list", help="Alle Dokumente in der DB anzeigen")
args = parser.parse_args()
if args.command == "file":
ingest_file(args.path, force=args.force)
elif args.command == "dir":
ingest_directory(args.path, force=args.force)
elif args.command == "list":
list_documents()
else:
parser.print_help()
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""
RAG Ingestion Script für Job-Matching Workflow
Liest MD/TXT/PDF Dokumente, chunked sie und speichert Embeddings
in der lokalen pgvector Instanz auf der NAS (Port 5433).
Verwendung:
python ingest_job_matching.py file <pfad> # Einzelne Datei
python ingest_job_matching.py dir <pfad> # Verzeichnis
python ingest_job_matching.py list # Alle Dokumente anzeigen
python ingest_job_matching.py clear <filename> # Dokument löschen
"""
import os
import uuid
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
import psycopg2
from psycopg2.extras import execute_values
import tiktoken
# PDF Support (optional)
try:
from pypdf import PdfReader
PDF_SUPPORT = True
except ImportError:
PDF_SUPPORT = False
# Lade job_matching spezifische .env Datei
env_path = Path(__file__).parent / ".env.job_matching"
load_dotenv(dotenv_path=env_path)
# --- Konfiguration ---
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
DB_HOST = os.getenv("JM_DB_HOST", "192.168.178.128")
DB_PORT = int(os.getenv("JM_DB_PORT", 5433))
DB_NAME = os.getenv("JM_DB_NAME", "job_matching")
DB_USER = os.getenv("JM_DB_USER", "jobmatch")
DB_PASSWORD = os.getenv("JM_DB_PASSWORD")
CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", 500))
CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", 50))
EMBED_MODEL = "text-embedding-3-small"
EMBED_DIM = 1536
client = OpenAI(api_key=OPENAI_API_KEY)
# --- Datenbank ---
def get_db_connection():
"""Direkte Verbindung zur pgvector Instanz auf der NAS."""
return psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)
def ensure_table(conn):
"""Stellt sicher dass die profile_documents Tabelle existiert."""
with conn.cursor() as cur:
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
cur.execute(f"""
CREATE TABLE IF NOT EXISTS profile_documents (
id SERIAL PRIMARY KEY,
filename TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
embedding vector({EMBED_DIM}),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(filename, chunk_index)
);
""")
cur.execute("""
CREATE INDEX IF NOT EXISTS profile_documents_embedding_idx
ON profile_documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 10);
""")
conn.commit()
print(" ✅ Tabelle profile_documents bereit")
# --- Text-Extraktion ---
def read_file(path: Path) -> str:
"""Liest MD, TXT oder PDF und gibt den Text zurück."""
suffix = path.suffix.lower()
if suffix in (".md", ".txt"):
return path.read_text(encoding="utf-8")
elif suffix == ".pdf":
if not PDF_SUPPORT:
raise ImportError("pypdf nicht installiert: pip install pypdf")
reader = PdfReader(str(path))
return "\n\n".join(page.extract_text() or "" for page in reader.pages)
else:
raise ValueError(f"Nicht unterstütztes Dateiformat: {suffix}")
# --- Chunking ---
def chunk_text(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list[str]:
"""Teilt Text in überlappende Chunks auf (token-basiert)."""
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = min(start + chunk_size, len(tokens))
chunk_tokens = tokens[start:end]
chunks.append(enc.decode(chunk_tokens))
start += chunk_size - overlap
return chunks
# --- Embeddings ---
def get_embeddings(texts: list[str]) -> list[list[float]]:
"""Erstellt Embeddings via OpenAI text-embedding-3-small."""
response = client.embeddings.create(
model=EMBED_MODEL,
input=texts
)
return [item.embedding for item in response.data]
# --- Ingestion ---
def check_existing(conn, filename: str) -> int:
"""Prüft ob Dokument bereits in der DB vorhanden ist."""
with conn.cursor() as cur:
cur.execute(
"SELECT COUNT(*) FROM profile_documents WHERE filename = %s",
(filename,)
)
return cur.fetchone()[0]
def delete_existing(conn, filename: str):
"""Löscht alle Chunks eines Dokuments."""
with conn.cursor() as cur:
cur.execute(
"DELETE FROM profile_documents WHERE filename = %s",
(filename,)
)
conn.commit()
print(f" 🗑️ Bestehende Chunks für '{filename}' gelöscht.")
def insert_chunks(conn, chunks: list[str], embeddings: list[list[float]], filename: str):
"""Fügt Chunks mit Embeddings in pgvector ein."""
records = [
(filename, i, chunk, embedding)
for i, (chunk, embedding) in enumerate(zip(chunks, embeddings))
]
with conn.cursor() as cur:
execute_values(
cur,
"""
INSERT INTO profile_documents (filename, chunk_index, content, embedding)
VALUES %s
ON CONFLICT (filename, chunk_index) DO UPDATE
SET content = EXCLUDED.content,
embedding = EXCLUDED.embedding,
created_at = NOW()
""",
records,
template="(%s, %s, %s, %s::vector)"
)
conn.commit()
def ingest_file(file_path: str, force: bool = False) -> bool:
"""Verarbeitet eine einzelne Datei."""
path = Path(file_path).resolve()
if not path.exists():
print(f"❌ Datei nicht gefunden: {path}")
return False
print(f"\n📄 Verarbeite: {path.name}")
# Text lesen
try:
text = read_file(path)
print(f" ✅ Text gelesen ({len(text)} Zeichen)")
except Exception as e:
print(f" ❌ Fehler beim Lesen: {e}")
return False
# Chunken
chunks = chunk_text(text)
print(f"{len(chunks)} Chunks erstellt")
# Embeddings
print(f" ⏳ Erstelle Embeddings via OpenAI ({EMBED_MODEL})...")
try:
embeddings = []
batch_size = 100
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
embeddings.extend(get_embeddings(batch))
print(f"{len(embeddings)} Embeddings erstellt")
except Exception as e:
print(f" ❌ Fehler bei Embeddings: {e}")
return False
# Datenbank
print(f" ⏳ Verbinde mit pgvector auf {DB_HOST}:{DB_PORT}...")
try:
conn = get_db_connection()
ensure_table(conn)
existing = check_existing(conn, path.name)
if existing > 0:
if force:
delete_existing(conn, path.name)
else:
print(f" ⚠️ '{path.name}' bereits in DB ({existing} Chunks). Nutze --force zum Überschreiben.")
conn.close()
return False
insert_chunks(conn, chunks, embeddings, path.name)
conn.close()
print(f"{len(chunks)} Chunks in pgvector gespeichert")
except Exception as e:
print(f" ❌ Datenbankfehler: {e}")
return False
return True
def ingest_directory(dir_path: str, force: bool = False):
"""Verarbeitet alle MD, TXT und PDF Dateien in einem Verzeichnis."""
path = Path(dir_path).resolve()
if not path.is_dir():
print(f"❌ Verzeichnis nicht gefunden: {path}")
return
files = (
list(path.glob("*.md")) +
list(path.glob("*.txt")) +
list(path.glob("*.pdf"))
)
files = [f for f in files if not f.name.startswith(".")]
if not files:
print(f"❌ Keine unterstützten Dateien in: {path}")
return
print(f"\n📁 Verarbeite {len(files)} Dateien aus: {path}")
success = 0
for f in sorted(files):
if ingest_file(str(f), force=force):
success += 1
print(f"\n✅ Fertig: {success}/{len(files)} Dateien erfolgreich importiert.")
def list_documents():
"""Zeigt alle Dokumente in der Datenbank an."""
print(f"\n📋 Dokumente in profile_documents ({DB_HOST}:{DB_PORT}/{DB_NAME}):")
try:
conn = get_db_connection()
with conn.cursor() as cur:
cur.execute("""
SELECT filename, COUNT(*) as chunks, MAX(created_at) as imported_at
FROM profile_documents
GROUP BY filename
ORDER BY MAX(created_at) DESC
""")
rows = cur.fetchall()
conn.close()
if not rows:
print(" Keine Dokumente gefunden.")
else:
print(f"\n {'Dateiname':<55} {'Chunks':>6} {'Importiert'}")
print(" " + "-" * 80)
for filename, chunks, imported_at in rows:
ts = imported_at.strftime("%d.%m.%Y %H:%M") if imported_at else "-"
print(f" {filename:<55} {chunks:>6} {ts}")
print()
except Exception as e:
print(f"❌ Fehler: {e}")
def clear_document(filename: str):
"""Löscht ein Dokument aus der Datenbank."""
try:
conn = get_db_connection()
existing = check_existing(conn, filename)
if existing == 0:
print(f"⚠️ '{filename}' nicht in der Datenbank gefunden.")
else:
delete_existing(conn, filename)
print(f"'{filename}' gelöscht ({existing} Chunks).")
conn.close()
except Exception as e:
print(f"❌ Fehler: {e}")
# --- CLI ---
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Job-Matching RAG Ingestion Profil-Dokumente in pgvector importieren"
)
subparsers = parser.add_subparsers(dest="command")
p_file = subparsers.add_parser("file", help="Einzelne Datei importieren")
p_file.add_argument("path", help="Pfad zur Datei (MD, TXT, PDF)")
p_file.add_argument("--force", action="store_true", help="Bestehende Chunks überschreiben")
p_dir = subparsers.add_parser("dir", help="Verzeichnis importieren")
p_dir.add_argument("path", help="Pfad zum Verzeichnis")
p_dir.add_argument("--force", action="store_true", help="Bestehende Chunks überschreiben")
subparsers.add_parser("list", help="Alle Dokumente in der DB anzeigen")
p_clear = subparsers.add_parser("clear", help="Dokument aus DB löschen")
p_clear.add_argument("filename", help="Dateiname (z.B. 'Lebenslauf - 2026 - Sebastian Fröhlich.md')")
args = parser.parse_args()
if args.command == "file":
ingest_file(args.path, force=args.force)
elif args.command == "dir":
ingest_directory(args.path, force=args.force)
elif args.command == "list":
list_documents()
elif args.command == "clear":
clear_document(args.filename)
else:
parser.print_help()
+29
View File
@@ -0,0 +1,29 @@
# Karriereziele
## Kurzfristig (nächste 12 Jahre)
- **Zielrolle:** [z.B. Senior Fullstack-Entwickler / Tech Lead]
- **Zielbranche:** [z.B. Fintech, SaaS, KI/ML]
- **Fokus:** [z.B. "Vertiefung in Cloud-Architektur und KI-Integration"]
## Mittelfristig (35 Jahre)
- **Zielrolle:** [z.B. Lead Developer / Engineering Manager / CTO Startup]
- **Fokus:** [z.B. "Technische Führung eines Teams, Architekturentscheidungen"]
## Langfristig
- **Vision:** [z.B. "Eigenes Produkt aufbauen" / "Technische Beratung" / "Führungsposition"]
## Was ich suche
- [z.B. Technisch anspruchsvolle Projekte]
- [z.B. Moderner Tech-Stack]
- [z.B. Ein Team, in dem ich wachsen kann]
- [z.B. Möglichkeit, KI/ML in Produkte zu integrieren]
## Was ich NICHT suche
- [z.B. Reine Wartung von Legacy-Systemen]
- [z.B. 100% Vor-Ort ohne Remote-Option]
- [z.B. Reiner Management-Job ohne Coding]
## Branchen-Interesse
- **Sehr interessiert:** [z.B. KI/ML, Fintech, Developer Tools]
- **Interessiert:** [z.B. E-Commerce, SaaS, Healthtech]
- **Weniger interessiert:** [z.B. Rüstung, Glücksspiel]
+30
View File
@@ -0,0 +1,30 @@
# Muster-Anschreiben
## Angaben zur Bewerbung
- **Zielposition:** [z.B. Senior Fullstack-Entwickler]
- **Unternehmen:** [Name]
- **Branche:** [z.B. Finanzwesen, E-Commerce]
- **Stellenreferenz:** [z.B. Job-ID oder Link]
- **Datum:** [TT.MM.YYYY]
## Anschreiben
Sehr geehrte(r) [Frau/Herr Nachname],
### Einstieg / Bezug zur Stelle
[1-2 Sätze: Warum diese Stelle? Was hat dich angesprochen? Bezug zum Unternehmen.]
### Qualifikation & Erfahrung
[2-3 Sätze: Relevante Erfahrung, die zur Stelle passt. Konkrete Projekte oder Erfolge nennen.]
### Motivation & Mehrwert
[2-3 Sätze: Was motiviert dich? Welchen Mehrwert bringst du dem Unternehmen?]
### Rahmenbedingungen
[1 Satz: Verfügbarkeit, Gehaltsvorstellung falls gefordert.]
### Abschluss
Ich freue mich auf ein persönliches Gespräch und stehe Ihnen gerne für Rückfragen zur Verfügung.
Mit freundlichen Grüßen
Sebastian Fröhlich
+31
View File
@@ -0,0 +1,31 @@
# Projektbeschreibung: [Projektname]
## Überblick
- **Kunde/Arbeitgeber:** [Name]
- **Branche:** [z.B. Finanzwesen, Automotive, Energie]
- **Zeitraum:** [MM/YYYY] [MM/YYYY]
- **Rolle:** [z.B. Fullstack-Entwickler, Tech Lead, DevOps Engineer]
- **Teamgröße:** [z.B. 5 Entwickler, 2 Designer]
## Projektbeschreibung
[2-4 Sätze: Was war das Ziel des Projekts? Welches Problem wurde gelöst? Für wen?]
## Aufgaben & Verantwortlichkeiten
- [Aufgabe 1, z.B. "Konzeption und Umsetzung der REST-API"]
- [Aufgabe 2, z.B. "Migration der bestehenden Datenbank auf PostgreSQL"]
- [Aufgabe 3]
## Technologien
- **Sprachen:** [z.B. Python, TypeScript, Java]
- **Frameworks:** [z.B. React, Spring Boot, FastAPI]
- **Datenbanken:** [z.B. PostgreSQL, MongoDB, Redis]
- **Infrastruktur:** [z.B. Docker, Kubernetes, AWS, Hetzner]
- **Tools:** [z.B. Git, Jira, Jenkins, GitHub Actions]
## Ergebnisse & Erfolge
- [Konkretes Ergebnis, z.B. "Ladezeit um 60% reduziert"]
- [Konkretes Ergebnis, z.B. "System verarbeitet 10.000 Anfragen/Min"]
- [Konkretes Ergebnis, z.B. "Erfolgreicher Go-Live mit 500 Nutzern"]
## Besonderheiten / Lessons Learned
[Optional: Was war besonders herausfordernd? Was hast du gelernt?]
+26
View File
@@ -0,0 +1,26 @@
# Rahmenbedingungen
## Verfügbarkeit
- **Frühester Starttermin:** [z.B. sofort / 01.05.2026 / 3 Monate Kündigungsfrist]
- **Arbeitsumfang:** [z.B. Vollzeit / Teilzeit ab 30h / Projektbasis]
## Arbeitsmodell
- **Bevorzugt:** [z.B. Remote / Hybrid / Vor-Ort]
- **Remote-Anteil:** [z.B. 100% Remote gewünscht / mind. 3 Tage Remote]
- **Büro-Standorte möglich:** [z.B. Frankfurt, Darmstadt, Rhein-Main-Gebiet]
- **Reisebereitschaft:** [z.B. gelegentlich / bis 20% / nein]
- **Umzugsbereitschaft:** [z.B. ja, innerhalb Deutschlands / nein]
## Gehaltsvorstellung
- **Festanstellung:** [z.B. 75.00085.000 EUR brutto/Jahr]
- **Freiberuflich:** [z.B. 90110 EUR/h]
- **Verhandlungsbasis:** [z.B. abhängig vom Gesamtpaket / Benefits]
## Vertragsform
- **Bevorzugt:** [z.B. Festanstellung / Freelance / beides möglich]
- **Befristung:** [z.B. unbefristet bevorzugt / Projektbasis ok]
## Sonstiges
- **Führerschein:** [z.B. Klasse B vorhanden]
- **Sprachen:** [z.B. Deutsch (Muttersprache), Englisch (fließend)]
- **Besondere Anforderungen:** [z.B. keine Sicherheitsüberprüfung nötig / SÜ2 vorhanden]
+25
View File
@@ -0,0 +1,25 @@
# Referenzen
## Referenz 1
- **Name:** [Vor- und Nachname]
- **Position:** [z.B. Teamleiter Softwareentwicklung]
- **Unternehmen:** [Name]
- **Verhältnis:** [z.B. Direkter Vorgesetzter, 20212026]
- **Kontakt:** [auf Anfrage / E-Mail / Telefon]
- **Kontext:** [z.B. "Hat meine Arbeit als Fullstack-Entwickler im Banking-Projekt begleitet"]
## Referenz 2
- **Name:** [Vor- und Nachname]
- **Position:** [z.B. Projektmanager]
- **Unternehmen:** [Name]
- **Verhältnis:** [z.B. Projektleiter im gemeinsamen Kundenprojekt]
- **Kontakt:** [auf Anfrage / E-Mail / Telefon]
- **Kontext:** [z.B. "Zusammenarbeit im Bereich Mobile App Entwicklung"]
## Referenz 3
- **Name:** [Vor- und Nachname]
- **Position:** [Position]
- **Unternehmen:** [Name]
- **Verhältnis:** [Beziehung]
- **Kontakt:** [auf Anfrage]
- **Kontext:** [Kurzbeschreibung der Zusammenarbeit]
@@ -0,0 +1,64 @@
# Technologie-Bewertung / Skill-Matrix
## Legende
- **Erfahrungslevel:** Grundlagen | Solide | Fortgeschritten | Experte
- **Letzte Nutzung:** Aktuell | < 1 Jahr | 13 Jahre | > 3 Jahre
- **Kontext:** Beruflich (B), Privat (P), Weiterbildung (W)
## Programmiersprachen
| Technologie | Level | Jahre | Letzte Nutzung | Kontext | Anmerkung |
|---|---|---|---|---|---|
| [z.B. TypeScript] | [Fortgeschritten] | [5] | [Aktuell] | [B, P] | [z.B. "Hauptsprache seit 2021"] |
| [z.B. Python] | [Solide] | [3] | [Aktuell] | [B, P] | [z.B. "RAG, Scripting, APIs"] |
| [z.B. Java] | [Fortgeschritten] | [8] | [< 1 Jahr] | [B] | [z.B. "Spring Boot Projekte"] |
## Frontend
| Technologie | Level | Jahre | Letzte Nutzung | Kontext | Anmerkung |
|---|---|---|---|---|---|
| [z.B. React] | [Fortgeschritten] | [4] | [Aktuell] | [B, P] | |
| [z.B. HTML/CSS] | [Experte] | [12] | [Aktuell] | [B, P] | |
| [z.B. Tailwind CSS] | [Solide] | [2] | [Aktuell] | [P] | |
## Backend / Frameworks
| Technologie | Level | Jahre | Letzte Nutzung | Kontext | Anmerkung |
|---|---|---|---|---|---|
| [z.B. Node.js/Express] | [Fortgeschritten] | [5] | [Aktuell] | [B] | |
| [z.B. Spring Boot] | [Fortgeschritten] | [6] | [< 1 Jahr] | [B] | |
| [z.B. FastAPI] | [Solide] | [1] | [Aktuell] | [P] | |
## Datenbanken
| Technologie | Level | Jahre | Letzte Nutzung | Kontext | Anmerkung |
|---|---|---|---|---|---|
| [z.B. PostgreSQL] | [Fortgeschritten] | [6] | [Aktuell] | [B, P] | [inkl. pgvector] |
| [z.B. MongoDB] | [Solide] | [3] | [13 Jahre] | [B] | |
| [z.B. Redis] | [Grundlagen] | [1] | [< 1 Jahr] | [B] | |
## DevOps / Infrastruktur
| Technologie | Level | Jahre | Letzte Nutzung | Kontext | Anmerkung |
|---|---|---|---|---|---|
| [z.B. Docker] | [Fortgeschritten] | [4] | [Aktuell] | [B, P] | |
| [z.B. Linux/Bash] | [Fortgeschritten] | [10] | [Aktuell] | [B, P] | |
| [z.B. GitHub Actions] | [Solide] | [2] | [Aktuell] | [B, P] | |
| [z.B. AWS] | [Grundlagen] | [1] | [13 Jahre] | [W] | |
## KI / Machine Learning
| Technologie | Level | Jahre | Letzte Nutzung | Kontext | Anmerkung |
|---|---|---|---|---|---|
| [z.B. OpenAI API] | [Solide] | [2] | [Aktuell] | [B, P] | |
| [z.B. RAG / Embeddings] | [Solide] | [1] | [Aktuell] | [P] | [apply4jobs.de] |
| [z.B. LangChain] | [Grundlagen] | [< 1] | [Aktuell] | [W] | |
## Methoden & Practices
| Thema | Level | Anmerkung |
|---|---|---|
| [z.B. Agile/Scrum] | [Fortgeschritten] | [z.B. "6 Jahre in Scrum-Teams"] |
| [z.B. Git/Git-Flow] | [Experte] | |
| [z.B. Code Review] | [Fortgeschritten] | |
| [z.B. TDD] | [Solide] | |
@@ -0,0 +1,21 @@
# Über mich / Elevator Pitch
## Kurzvorstellung (1 Satz)
[z.B. "Erfahrener Fullstack-Entwickler mit 12+ Jahren Berufserfahrung in den Bereichen Fintech, Automotive und Mobile Development."]
## Mittlere Version (3 Sätze)
[z.B. "Ich bin Sebastian Fröhlich, Fullstack-Entwickler mit über 12 Jahren Erfahrung in der Softwareentwicklung. Meine Schwerpunkte liegen in [Technologien/Bereichen]. Aktuell suche ich eine neue Herausforderung im Bereich [Zielbereich]."]
## Ausführliche Version (1 Absatz)
[z.B. "Als leidenschaftlicher Softwareentwickler bringe ich über 12 Jahre Erfahrung aus verschiedenen Branchen mit — von Automotive über Energie bis hin zu Fintech. Bei [letzter Arbeitgeber] habe ich [wichtigstes Projekt/Ergebnis]. Besonders stark bin ich in [Top-Skills]. Was mich antreibt, ist [Motivation]. Für meine nächste Station suche ich [was du suchst]."]
## Persönliche Werte & Arbeitsweise
- [z.B. Qualitätsbewusst — sauberer Code ist mir wichtig]
- [z.B. Teamplayer — ich teile Wissen gerne und lerne von anderen]
- [z.B. Pragmatisch — funktionierende Lösungen vor Over-Engineering]
- [z.B. Lernbereit — ich bilde mich ständig weiter]
## Was mich von anderen unterscheidet
- [z.B. Breite Branchenerfahrung: Automotive, Energie, Fintech]
- [z.B. Fullstack-Profil: Von der Datenbank bis zum Frontend]
- [z.B. Eigene Projekte: apply4jobs.de als Beispiel für Eigeninitiative]
@@ -0,0 +1,25 @@
# Zertifikate & Weiterbildungen
## Zertifikate
| Zertifikat | Anbieter | Datum | Gültigkeit | Nachweis |
|---|---|---|---|---|
| [z.B. AWS Solutions Architect] | [z.B. Amazon] | [MM/YYYY] | [unbefristet / bis MM/YYYY] | [Link/Datei] |
| [z.B. Professional Scrum Master I] | [z.B. Scrum.org] | [MM/YYYY] | [unbefristet] | [Link/Datei] |
## Weiterbildungen / Kurse
| Kurs | Plattform | Zeitraum | Umfang | Themengebiet |
|---|---|---|---|---|
| [z.B. Machine Learning Specialization] | [z.B. Coursera] | [MM/YYYY] | [z.B. 40h] | [z.B. KI/ML] |
| [z.B. Docker & Kubernetes Masterclass] | [z.B. Udemy] | [MM/YYYY] | [z.B. 20h] | [z.B. DevOps] |
## Konferenzen & Meetups
- [z.B. WeAreDevelopers 2025, Berlin — Teilnehmer]
- [z.B. React Meetup Frankfurt — regelmäßiger Teilnehmer seit 2023]
## Geplante Weiterbildungen
- [z.B. Kubernetes CKA Zertifizierung — geplant Q2 2026]
- [z.B. Rust Grundlagen — in Arbeit]
+35
View File
@@ -0,0 +1,35 @@
# Zielstellen-Profil
## Ideale Stellenbezeichnungen
- [z.B. Senior Fullstack Developer]
- [z.B. Lead Software Engineer]
- [z.B. Backend Developer (Python/Node.js)]
## Must-Have Kriterien
- [z.B. Remote-Option (mind. 80%)]
- [z.B. Moderner Tech-Stack (kein COBOL/SAP)]
- [z.B. Agile Arbeitsweise]
- [z.B. Unbefristeter Vertrag]
## Nice-to-Have Kriterien
- [z.B. Startup-/Scale-up-Kultur]
- [z.B. Open-Source-Beteiligung]
- [z.B. Weiterbildungsbudget]
- [z.B. 4-Tage-Woche / flexible Arbeitszeiten]
## Relevante Keywords in Stellenausschreibungen
- [z.B. React, TypeScript, Node.js, Python, PostgreSQL]
- [z.B. Docker, Kubernetes, CI/CD, AWS/GCP]
- [z.B. REST API, Microservices, Event-Driven]
- [z.B. Agile, Scrum, Kanban]
## Ausschlusskriterien (Red Flags)
- [z.B. "Rockstar Developer" / toxische Kultur-Signale]
- [z.B. Keine Remote-Option]
- [z.B. Veralteter Tech-Stack ohne Modernisierungsplan]
- [z.B. Unbezahlte Überstunden erwartet]
## Unternehmensgröße
- **Bevorzugt:** [z.B. 20200 Mitarbeiter / Scale-up]
- **Auch ok:** [z.B. Konzern mit autonomen Teams]
- **Weniger passend:** [z.B. Einzelkämpfer-Rolle in Kleinstunternehmen]