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:
+11
@@ -1,5 +1,16 @@
|
||||
# Umgebung
|
||||
.env
|
||||
.env.job_matching
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Embedding-Modell Cache (wird automatisch heruntergeladen)
|
||||
.cache/
|
||||
|
||||
# Ausgefüllte persönliche Dokumente nicht ins Repo
|
||||
templates/ausgefuellt/
|
||||
|
||||
@@ -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.0–1.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
|
||||
cd ~/Projekte/rag-ingestion
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Umgebungsvariablen konfigurieren:
|
||||
|
||||
```bash
|
||||
brew install tesseract tesseract-lang # nur für ingest.py (OCR)
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,29 @@
|
||||
# Karriereziele
|
||||
|
||||
## Kurzfristig (nächste 1–2 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 (3–5 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]
|
||||
@@ -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
|
||||
@@ -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?]
|
||||
@@ -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.000–85.000 EUR brutto/Jahr]
|
||||
- **Freiberuflich:** [z.B. 90–110 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]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Referenzen
|
||||
|
||||
## Referenz 1
|
||||
- **Name:** [Vor- und Nachname]
|
||||
- **Position:** [z.B. Teamleiter Softwareentwicklung]
|
||||
- **Unternehmen:** [Name]
|
||||
- **Verhältnis:** [z.B. Direkter Vorgesetzter, 2021–2026]
|
||||
- **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 | 1–3 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] | [1–3 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] | [1–3 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]
|
||||
@@ -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. 20–200 Mitarbeiter / Scale-up]
|
||||
- **Auch ok:** [z.B. Konzern mit autonomen Teams]
|
||||
- **Weniger passend:** [z.B. Einzelkämpfer-Rolle in Kleinstunternehmen]
|
||||
Reference in New Issue
Block a user