Migrate all repos into monorepo context folders

Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1 @@
{"specId": "6c39340f-10af-426d-9318-c96cb2f7ad6d", "workflowType": "requirements-first", "specType": "feature"}
@@ -0,0 +1,677 @@
# Design: Entfernung Embedding-basierter Similarity Umstellung auf BM25 + LLM
## Übersicht
Dieses Design beschreibt die vollständige Entfernung der Embedding-Infrastruktur aus dem Teamlandkarte-MCP-System. Das System wird von einem hybriden Embedding/BM25-Ansatz auf eine reine BM25 + LLM-Architektur umgestellt:
- **Kompetenz-Matching**: Ausschließlich BM25+RRF (bereits implementiert, nur Conditional-Logik entfernen)
- **Rollen-Similarity**: LLM Chat Completion (neu, ersetzt Embedding-Cosine-Similarity)
- **Rollen-Inferenz**: LLM Chat Completion (neu, ersetzt Embedding-basierte Nearest-Neighbor-Suche)
- **AutoTagger**: Bleibt unverändert (bereits LLM-basiert)
Entfernt werden: `EmbeddingCache`, `_embed`, `prefetch_embeddings`, `_per_skill_similarity`, `_aggregate_similarity`, Embedding-API-Methoden im `AzureOpenAIClient`, alle Embedding-Konfigurationsfelder, und der Startup-Preload.
## Architektur
### Vorher (Hybrid)
```mermaid
graph TD
A[MCP Server Startup] --> B[Preload Embeddings]
B --> C[VocabularyCache: Role + Competence Embeddings]
B --> D[Task Embeddings]
E[Matching Request] --> F{use_bm25_search?}
F -->|true| G[BM25+RRF Competence Similarity]
F -->|false| H[Embedding Cosine Competence Similarity]
E --> I[Embedding Cosine Role Similarity]
J[infer_primary_role] --> K[Task Embedding → Cosine vs Role Vocab]
```
### Nachher (BM25 + LLM)
```mermaid
graph TD
A[MCP Server Startup] --> B[DB Schema Verification]
A --> C[AzureOpenAIClient: nur Chat Completion]
E[Matching Request] --> G[BM25+RRF Competence Similarity]
E --> I[LLM Role Similarity mit In-Run Cache]
J[infer_primary_role] --> K[LLM Chat Completion: Task Text → Role Selection]
```
### Datenfluss Matching (Nachher)
```mermaid
sequenceDiagram
participant Client
participant Matcher
participant SimilarityEngine
participant BM25
participant LLM as AzureOpenAIClient (Chat)
Client->>Matcher: match(capacities, requirements)
Matcher->>Matcher: Filter by availability
Matcher->>BM25: Build global index (all candidate competences)
loop Per Candidate
Matcher->>SimilarityEngine: compute_competence_similarity(required, candidate, global_index)
SimilarityEngine->>BM25: rank per required competence
SimilarityEngine-->>Matcher: {req → {score, best_match, rationale}}
Matcher->>SimilarityEngine: compute_role_similarity(required_role, candidate_role)
SimilarityEngine->>SimilarityEngine: Check in-run cache
alt Cache Miss
SimilarityEngine->>LLM: chat_completion(role_similarity_prompt)
LLM-->>SimilarityEngine: {"similarity": 0.85}
SimilarityEngine->>SimilarityEngine: Cache result
end
SimilarityEngine-->>Matcher: float score
end
Matcher-->>Client: MatchResult
```
## Komponenten und Schnittstellen
### 1. SimilarityEngine (refactored)
**Datei:** `src/teamlandkarte_mcp/matching/similarity.py`
**Entfernte Methoden:**
- `prefetch_embeddings`
- `_embed`
- `get_embedding_for_cache_key`
- `get_embeddings_for_cache_keys`
- `_aggregate_similarity`
- `_per_skill_similarity`
**Entfernte Properties:**
- `embedding_model`
- `embedding_dimensions`
- `use_bm25_search`
**Entfernte Constructor-Parameter:**
- `cache` (EmbeddingCache)
- `embedding_model`
- `embedding_dimensions`
- `strategy`
- `use_bm25_search`
**Neuer Constructor:**
```python
class SimilarityEngine:
"""Compute similarity scores using BM25+RRF and LLM."""
def __init__(
self,
*,
client: AzureOpenAIClient,
cost_tracker: CostTracker | None = None,
use_auto_tagging: bool = False,
auto_tagger: AutoTagger | None = None,
) -> None:
self._client = client
self._cost_tracker = cost_tracker
self._use_auto_tagging = use_auto_tagging
self._auto_tagger = auto_tagger
# Per-run cache for LLM role similarity: (role_a, role_b) → score
self._role_similarity_cache: dict[tuple[str, str], float] = {}
```
**Geänderte Methode `compute_competence_similarity`:**
```python
async def compute_competence_similarity(
self,
required: list[str],
candidate: list[str],
global_index: Bm25Index | None = None,
) -> dict[str, dict[str, object]]:
"""BM25+RRF competence similarity (einziger Pfad)."""
working = list(candidate)
if self._use_auto_tagging and self._auto_tagger is not None:
working = await self._auto_tagger.expand_competences(required, candidate)
return self._bm25_rrf_similarity(required, working, global_index=global_index)
```
**Neue Methode `compute_role_similarity` (LLM-basiert):**
```python
_ROLE_SIMILARITY_SYSTEM_PROMPT = (
"You are a job-role similarity expert. Given two role names, "
"determine their semantic similarity on a scale from 0.0 to 1.0. "
"0.0 means completely unrelated roles, 1.0 means identical or "
"interchangeable roles. Consider synonyms, hierarchy, and domain overlap. "
'Respond with a JSON object: {"similarity": <float>}'
)
async def compute_role_similarity(
self,
required_role: str | None,
candidate_role: str | None,
) -> float:
"""LLM-basierte Rollen-Similarity."""
if self._is_bad_role(required_role) or self._is_bad_role(candidate_role):
return 0.0
req_norm = str(required_role).strip().lower()
cand_norm = str(candidate_role).strip().lower()
if req_norm == cand_norm:
return 1.0
# Symmetrischer Cache-Key
cache_key = tuple(sorted((req_norm, cand_norm)))
if cache_key in self._role_similarity_cache:
return self._role_similarity_cache[cache_key]
try:
raw = await self._client.chat_completion(
system=_ROLE_SIMILARITY_SYSTEM_PROMPT,
user=f"Role A: {required_role}\nRole B: {candidate_role}",
)
payload = json.loads(raw)
score = float(payload.get("similarity", 0.0))
score = max(0.0, min(1.0, score))
except Exception:
score = 0.0
self._role_similarity_cache[cache_key] = score
return score
```
**Neue Methode `clear_role_cache`:**
```python
def clear_role_cache(self) -> None:
"""Cache zwischen Matching-Runs leeren."""
self._role_similarity_cache.clear()
```
### 2. VocabularyCache (refactored)
**Datei:** `src/teamlandkarte_mcp/matching/vocabulary.py`
**Entfernte Methoden:**
- `preload`
- `_preload_role_vocab`
- `_preload_competence_vocab`
- `ensure_task_embedding`
- `infer_primary_role` (alte Signatur mit `task_embedding`)
- `infer_competences`
**Entfernte Properties:**
- `roles`
- `competences`
**Neuer Constructor:**
```python
class VocabularyCache:
"""LLM-basierte Rollen-Inferenz aus Task-Text."""
def __init__(
self,
*,
db: DBClient,
client: AzureOpenAIClient,
) -> None:
self._db = db
self._client = client
```
**Neue Methode `infer_primary_role` (LLM-basiert):**
```python
_ROLE_INFERENCE_SYSTEM_PROMPT = (
"You are a role classification expert. Given a task description and a list "
"of available roles, select the single most appropriate role for the task. "
"You MUST select exactly one role from the provided list. "
"Respond with a JSON object: "
'{"role": "<selected role name>", "confidence": <float 0.0-1.0>}'
)
async def infer_primary_role(
self,
*,
task_text: str,
) -> tuple[str, float] | None:
"""Inferiere die passendste Rolle für einen Task-Text via LLM."""
role_names = self._db.get_all_role_names()
if not role_names:
return None
text = (task_text or "").strip()
if not text:
return None
user_prompt = (
f"Task: {text}\n\n"
f"Available roles: {json.dumps(list(role_names), ensure_ascii=False)}"
)
try:
raw = await self._client.chat_completion(
system=_ROLE_INFERENCE_SYSTEM_PROMPT,
user=user_prompt,
)
payload = json.loads(raw)
role = str(payload.get("role", "")).strip()
confidence = float(payload.get("confidence", 0.0))
confidence = max(0.0, min(1.0, confidence))
# Validierung: Rolle muss in der DB-Liste existieren
if role not in set(role_names):
return None
return role, confidence
except Exception:
return None
```
### 3. AzureOpenAIClient (vereinfacht)
**Datei:** `src/teamlandkarte_mcp/azure/openai_client.py`
**Entfernte Methoden:**
- `embeddings`
- `get_embeddings_batch`
**Entfernte Constructor-Parameter:**
- `embedding_api_key`
- `embedding_deployment`
- `embedding_batch_size`
**Neuer Constructor:**
```python
class AzureOpenAIClient:
"""Azure OpenAI Chat Completion Client."""
def __init__(
self,
*,
endpoint: str,
api_version: str,
chat_deployment: str,
llm_api_key: str,
timeout_s: float = 30.0,
max_retries: int = 5,
cost_tracker: CostTracker | None = None,
) -> None:
self._chat_deployment = chat_deployment
self._max_retries = max_retries
self._timeout_s = timeout_s
self._cost_tracker = cost_tracker
self._chat = AsyncAzureOpenAI(
api_key=llm_api_key,
azure_endpoint=endpoint,
api_version=api_version,
)
```
Die `chat_completion`-Methode bleibt unverändert, außer dass die Guard-Clause für fehlende Konfiguration entfällt (Chat ist jetzt immer konfiguriert).
### 4. Matcher (minimal geändert)
**Datei:** `src/teamlandkarte_mcp/matching/matcher.py`
**Änderungen:**
- Entfernung der `if self._sim.use_bm25_search`-Bedingung beim BM25-Index-Aufbau
- Der globale BM25-Index wird **immer** gebaut (unconditional)
```python
# Vorher:
global_bm25_index: Bm25Index | None = None
if self._sim.use_bm25_search and filtered:
global_corpus = list(...)
global_bm25_index = Bm25Index(corpus=global_corpus)
# Nachher:
global_bm25_index: Bm25Index | None = None
if filtered:
global_corpus = list(
{c for cap in filtered for c in cap.competences if c.strip()}
)
global_bm25_index = Bm25Index(corpus=global_corpus)
```
### 5. MCP Server (vereinfacht)
**Datei:** `src/teamlandkarte_mcp/mcp_server.py`
**Entfernte Elemente:**
- `_startup_preload_embeddings` Funktion
- `_deferred_preload` Funktion und `_preload_started` Flag
- `_ensure_preloaded` Funktion
- `EmbeddingCache`-Instanziierung
- `emb_cache`-Variable
- Alle `await _ensure_preloaded()`-Aufrufe in Tools
- Import von `EmbeddingCache`
- Import von `VocabularyCache` (wird direkt mit `AzureOpenAIClient` konstruiert)
**Geänderte Konstruktion:**
```python
# Vorher: Embedding-Client + separater LLM-Client
azure_client = AzureOpenAIClient(
endpoint=..., embedding_api_key=..., embedding_deployment=..., ...
)
# Nachher: Nur noch ein LLM-Client
azure_client = AzureOpenAIClient(
endpoint=cfg.azure_openai.endpoint,
api_version=cfg.azure_openai.api_version,
chat_deployment=cfg.azure_openai.chat_deployment,
llm_api_key=cfg.azure_openai.llm_api_key,
cost_tracker=cost_tracker,
)
similarity = SimilarityEngine(
client=azure_client,
cost_tracker=cost_tracker,
use_auto_tagging=cfg.similarity.use_auto_tagging,
auto_tagger=auto_tagger,
)
vocab_cache = VocabularyCache(
db=db_client,
client=azure_client,
)
```
**Geändertes `infer_primary_role`-Tool:**
```python
@mcp.tool()
async def infer_primary_role(
task_id: Optional[str] = None,
task_text: Optional[str] = None,
) -> str:
"""Infer the single closest role from either a DB task or free text."""
if bool(task_id) == bool(task_text):
return "Provide exactly one of task_id or task_text."
text = (task_text or "").strip()
if task_id:
_ensure_db()
task = db_client.get_task_by_id(task_id)
if task is None:
return f"Task not found or not published: {task_id}"
title = (task.title or "").strip()
desc = (task.description or "").strip()
text = (title + "\n\n" + desc).strip() if title else desc
if not text:
return "Task text is empty."
best = await vocab_cache.infer_primary_role(task_text=text)
if best is None:
rows = [["", ""]]
else:
role, score = best
rows = [[str(role), f"{float(score):.3f}"]]
return md_table(["Role", "Confidence"], rows)
```
**Entferntes Tool `validate_task_requirements`:**
Dieses Tool basiert vollständig auf Embedding-Inferenz (`infer_competences`, `ensure_task_embedding`). Es wird entfernt oder durch eine vereinfachte Version ersetzt, die nur DB-Felder anzeigt.
### 6. Config (vereinfacht)
**Datei:** `src/teamlandkarte_mcp/config.py`
**Entfernte Dataclasses:**
- `EmbeddingCacheConfig`
- `InferenceConfig`
**Geänderte Dataclasses:**
```python
@dataclass(frozen=True)
class AzureOpenAIConfig:
"""Azure OpenAI configuration (nur Chat Completion)."""
endpoint: str
api_version: str = "2024-02-15-preview"
chat_deployment: str = ""
llm_api_key: str = ""
show_costs_in_output: bool = False
@dataclass(frozen=True)
class SimilarityConfig:
"""Similarity engine configuration."""
use_auto_tagging: bool = False
@dataclass(frozen=True)
class MatchingConfig:
"""Matching weights and thresholds."""
competence_weight: float = 0.8
role_weight: float = 0.2
require_confirmation: bool = True
thresholds: MatchingThresholds = MatchingThresholds()
fuzzy: FuzzyConfig = FuzzyConfig()
# inference entfällt
@dataclass(frozen=True)
class AppConfig:
"""Top-level application configuration."""
database: DatabaseConfig
matching: MatchingConfig
cache: CacheConfig
azure_openai: AzureOpenAIConfig
similarity: SimilarityConfig
# embedding_cache entfällt
```
**Entfernte Felder aus `SimilarityConfig`:**
- `embedding_model`
- `embedding_dimensions`
- `strategy`
- `use_bm25_search`
**Entfernte Felder aus `AzureOpenAIConfig`:**
- `embedding_deployment`
- `embedding_batch_size`
**Entfernte Validierungen in `load_config`:**
- `AZURE_OPENAI_EMBEDDING_API_KEY` Prüfung
- `embedding_dimensions == 3072` Prüfung
- `strategy` Validierung
- `inference` Parsing
**Neue Validierung:**
- `AZURE_OPENAI_LLM_API_KEY` ist jetzt immer erforderlich (nicht nur bei auto_tagging)
- `chat_deployment` ist jetzt erforderlich
### 7. Entfernte Dateien
| Datei | Grund |
|-------|-------|
| `src/teamlandkarte_mcp/cache/embedding_cache.py` | Keine Embeddings mehr |
| Zugehörige Tests für EmbeddingCache | Keine Embeddings mehr |
### 8. config.toml Änderungen
**Entfernte Sektionen:**
- `[embedding_cache]` komplett
**Entfernte Felder aus `[matching.similarity]`:**
- `embedding_model`
- `embedding_dimensions`
- `strategy`
- `use_bm25_search`
**Entfernte Felder aus `[azure_openai]`:**
- `embedding_deployment`
- `embedding_batch_size`
**Entfernte Felder aus `[matching]`:**
- `[matching.inference]` komplett
**Neue Pflichtfelder in `[azure_openai]`:**
- `chat_deployment` (bereits vorhanden als optionales Feld, wird Pflicht)
**Neue Umgebungsvariable (Pflicht):**
- `AZURE_OPENAI_LLM_API_KEY` (ersetzt `AZURE_OPENAI_EMBEDDING_API_KEY`)
**Entfernte Umgebungsvariable:**
- `AZURE_OPENAI_EMBEDDING_API_KEY`
## Datenmodelle
### LLM Role Similarity Request/Response
```json
// System Prompt: _ROLE_SIMILARITY_SYSTEM_PROMPT
// User Message:
"Role A: Software Engineer\nRole B: Backend Developer"
// Expected Response:
{"similarity": 0.75}
```
### LLM Role Inference Request/Response
```json
// System Prompt: _ROLE_INFERENCE_SYSTEM_PROMPT
// User Message:
"Task: Implementierung einer REST-API für Benutzerverwaltung\n\nAvailable roles: [\"Backend Developer\", \"Frontend Developer\", \"DevOps Engineer\", \"Data Engineer\"]"
// Expected Response:
{"role": "Backend Developer", "confidence": 0.92}
```
### In-Run Role Similarity Cache
```python
# Symmetrischer Cache innerhalb eines Matching-Runs
# Key: tuple(sorted((role_a_lower, role_b_lower)))
# Value: float (0.0 - 1.0)
_role_similarity_cache: dict[tuple[str, str], float] = {}
```
Der Cache wird pro `SimilarityEngine`-Instanz gehalten und lebt für die Dauer des Server-Prozesses. Da Rollennamen stabil sind (aus der DB), ist kein TTL nötig.
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: BM25 Zero-Score für fehlenden Token-Overlap
*For any* set of required competences and candidate competences where no candidate shares any token with a required competence, `compute_competence_similarity` shall return a score of 0.0 for that required competence.
**Validates: Requirements 1.1**
### Property 2: Matcher baut BM25-Index bedingungslos
*For any* non-empty list of filtered candidates with competences, the Matcher shall always build a global BM25 index and use it for competence scoring, producing results where candidates with no token overlap receive score 0.0.
**Validates: Requirements 2.6**
### Property 3: infer_primary_role Ausgabe-Validität
*For any* non-empty task text and non-empty role list from the database, if `infer_primary_role` returns a non-None result, the returned role name must be an element of the database role list and the confidence must be a float in [0.0, 1.0].
**Validates: Requirements 3.1, 3.4**
### Property 4: Graceful Degradation bei LLM-Fehler
*For any* LLM call that raises an exception, `compute_role_similarity` shall return 0.0 and `infer_primary_role` shall return None, without propagating the exception to the caller.
**Validates: Requirements 3.5, 5.6**
### Property 5: compute_role_similarity Wertebereich
*For any* two non-empty, non-"(unknown)" role names, `compute_role_similarity` shall return a float in the closed interval [0.0, 1.0]. For identical role names (case-insensitive), it shall return 1.0.
**Validates: Requirements 5.1**
### Property 6: Rollen-Similarity-Cache ist symmetrisch und idempotent
*For any* role pair (A, B), calling `compute_role_similarity(A, B)` and then `compute_role_similarity(B, A)` shall return the same score, and the second call shall not invoke the LLM (cache hit). Repeated calls with the same pair shall always return the same cached value.
**Validates: Requirements 5.8**
## Error Handling
### LLM-Fehler in compute_role_similarity
- Bei jeder Exception (Timeout, API-Fehler, JSON-Parse-Fehler) wird `0.0` zurückgegeben
- Fehler wird geloggt (LOGGER.warning)
- Kein Eintrag im Cache für fehlgeschlagene Aufrufe (Retry bei nächstem Aufruf möglich)
### LLM-Fehler in infer_primary_role
- Bei jeder Exception wird `None` zurückgegeben
- Fehler wird geloggt (LOGGER.warning)
- Caller (MCP-Tool) zeigt leere Tabelle an
### Ungültige LLM-Antworten
- **Role Similarity**: Wenn `similarity` nicht im JSON oder nicht parsebar → 0.0
- **Role Inference**: Wenn `role` nicht in der DB-Liste → None
- **Role Inference**: Wenn `confidence` nicht parsebar → 0.0 (aber Rolle wird trotzdem zurückgegeben wenn valide)
### Leere/Ungültige Eingaben
- `compute_role_similarity` mit None/leer/"(unknown)" → 0.0 (kein LLM-Aufruf)
- `infer_primary_role` mit leerem Text → None (kein LLM-Aufruf)
- `infer_primary_role` mit leerer Rollenliste aus DB → None (kein LLM-Aufruf)
- `compute_competence_similarity` mit leerer Required-Liste → leeres Dict
- `compute_competence_similarity` mit leerer Candidate-Liste → alle Scores 0.0
### Konfigurationsfehler
- Fehlende `AZURE_OPENAI_LLM_API_KEY``ConfigError` beim Laden (fail-fast)
- Fehlender `chat_deployment``ConfigError` beim Laden (fail-fast)
## Testing Strategy
### Property-Based Tests (fast-check / Hypothesis)
Bibliothek: **Hypothesis** (Python PBT-Standard)
Konfiguration: Mindestens 100 Iterationen pro Property-Test.
Jeder Property-Test wird mit einem Kommentar getaggt:
```
# Feature: remove-embedding-competence-similarity, Property {N}: {title}
```
| Property | Test-Ansatz | Generator |
|----------|-------------|-----------|
| 1: BM25 Zero-Score | Generiere disjunkte Token-Sets für required/candidate, prüfe Score == 0.0 | `st.lists(st.text(alphabet=st.characters(whitelist_categories=('L',)), min_size=3))` |
| 2: Matcher BM25 unconditional | Generiere Capacities + Requirements, prüfe dass Ergebnis BM25-Charakteristik hat (0.0 bei no-overlap) | Custom Capacity/Requirements strategies |
| 3: infer_primary_role Validität | Generiere Task-Texte + Role-Listen, mocke LLM mit zufälliger valider Antwort, prüfe Output-Constraints | `st.text(min_size=1)`, `st.lists(st.text(min_size=1), min_size=1)` |
| 4: Graceful Degradation | Generiere zufällige Exceptions, prüfe dass 0.0/None zurückkommt | `st.sampled_from([TimeoutError, RuntimeError, ValueError, json.JSONDecodeError])` |
| 5: Role Similarity Wertebereich | Generiere Rollenpaare, mocke LLM mit zufälligem Score, prüfe [0.0, 1.0] und Identitäts-Case | `st.text(min_size=1, max_size=50)` |
| 6: Cache Symmetrie | Generiere Rollenpaare, rufe in beiden Reihenfolgen auf, prüfe gleichen Score + nur 1 LLM-Call | `st.text(min_size=1, max_size=50)` |
### Unit Tests
Unit Tests fokussieren auf:
- **Spezifische Beispiele**: Bekannte Rollenpaare (z.B. "Backend Developer" vs "Software Engineer") mit gemocktem LLM
- **Edge Cases**: Leere Strings, None-Werte, "(unknown)", Whitespace-only
- **Integration**: Config-Loading ohne Embedding-Felder, Server-Startup ohne Preload
- **Regressions**: Sicherstellen dass BM25+RRF-Ergebnisse identisch zum bisherigen `use_bm25_search=True`-Pfad sind
### Integrationstests
- End-to-End Matching-Run mit gemocktem LLM-Client
- Config-Loading aus minimaler TOML-Datei (ohne Embedding-Sektionen)
- Server-Startup ohne `AZURE_OPENAI_EMBEDDING_API_KEY` (darf nicht mehr geprüft werden)
### Nicht getestet (bewusst ausgelassen)
- Prompt-Qualität (subjektiv, erfordert manuelles Review)
- LLM-Antwort-Genauigkeit (abhängig vom Modell, nicht deterministisch)
- Startup-Performance-Verbesserung (Benchmark, kein Unit-Test)
@@ -0,0 +1,111 @@
# Anforderungsdokument
## Einleitung
Dieses Dokument beschreibt die Anforderungen für die Entfernung der Embedding-basierten Kompetenz-Similarity zugunsten von BM25 als einzigem Kompetenz-Matching-Verfahren, die Umstellung der Rollen-Inferenz und Rollen-Similarity auf einen LLM-Ansatz sowie die vollständige Entfernung der Embedding-Infrastruktur. Das System wird vollständig BM25 + LLM-basiert.
## Glossar
- **SimilarityEngine**: Modul in `matching/similarity.py`, das Ähnlichkeitsberechnungen für Kompetenzen und Rollen durchführt.
- **VocabularyCache**: Modul in `matching/vocabulary.py`, das Vokabular-Embeddings vorhält und Inferenz-Funktionen bereitstellt.
- **BM25**: Probabilistisches Ranking-Verfahren für Textähnlichkeit basierend auf Termfrequenz und inverser Dokumentfrequenz.
- **RRF**: Reciprocal Rank Fusion Verfahren zur Kombination mehrerer Rankings.
- **AutoTagger**: LLM-basiertes Modul zur Erweiterung von Kompetenzlisten vor dem BM25-Scoring.
- **AzureOpenAIClient**: Client-Wrapper für Azure OpenAI API-Aufrufe (Embeddings und Chat Completions).
- **SimilarityConfig**: Konfigurationsklasse für die Similarity-Engine in `config.py`.
- **infer_primary_role**: Funktion, die einer Aufgabe die passendste Rolle aus der Datenbank zuordnet.
- **LLM**: Large Language Model hier Azure OpenAI Chat Completion.
- **Preload**: Eageres Vorladen von Embeddings beim Server-Start.
- **EmbeddingCache**: Cache-Modul für gespeicherte Embedding-Vektoren.
- **compute_role_similarity**: Funktion in der SimilarityEngine, die die semantische Ähnlichkeit zwischen einer geforderten Rolle und einer Kandidaten-Rolle berechnet.
## Anforderungen
### Anforderung 1: Entfernung der Embedding-basierten Kompetenz-Similarity
**User Story:** Als Entwickler möchte ich, dass BM25+RRF das einzige Verfahren für Kompetenz-Matching ist, damit die Codebasis vereinfacht wird und keine Embedding-Kosten für Kompetenz-Vergleiche anfallen.
#### Akzeptanzkriterien
1. THE SimilarityEngine SHALL use BM25+RRF as the sole method for computing competence similarity scores.
2. WHEN compute_competence_similarity is called, THE SimilarityEngine SHALL execute the BM25+RRF path without checking a strategy flag or use_bm25_search parameter.
3. THE SimilarityEngine SHALL no longer contain the `_per_skill_similarity` method for embedding-based per-skill competence matching.
4. THE SimilarityEngine SHALL no longer contain the `_aggregate_similarity` method for embedding-based aggregate competence matching.
5. THE SimilarityEngine SHALL no longer accept a `strategy` parameter in its constructor for competence similarity strategy selection.
6. THE SimilarityEngine SHALL no longer accept a `use_bm25_search` parameter in its constructor.
7. THE SimilarityEngine SHALL remove the `prefetch_embeddings` and `_embed` methods, since role similarity also switches to LLM and no embedding use cases remain.
### Anforderung 2: Entfernung des Konfigurationsparameters use_bm25_search
**User Story:** Als Entwickler möchte ich, dass der Parameter `use_bm25_search` aus der Konfiguration entfernt wird, da BM25 nun immer aktiv ist und der Parameter redundant geworden ist.
#### Akzeptanzkriterien
1. THE SimilarityConfig SHALL no longer contain the field `use_bm25_search`.
2. THE SimilarityConfig SHALL no longer contain the field `strategy` (da nur noch BM25 verwendet wird).
3. WHEN the configuration is loaded, THE Config-Loader SHALL not read or validate `use_bm25_search` from the TOML file.
4. WHEN the configuration is loaded, THE Config-Loader SHALL not read or validate `matching.similarity.strategy` from the TOML file.
5. THE SimilarityEngine SHALL no longer expose a `use_bm25_search` property.
6. THE Matcher SHALL build the global BM25 index unconditionally for all filtered candidates without checking a `use_bm25_search` flag.
### Anforderung 3: LLM-basierte Rollen-Inferenz
**User Story:** Als Entwickler möchte ich, dass `infer_primary_role` einen LLM-Ansatz (Chat Completion) verwendet anstelle von Embedding-Cosine-Similarity, damit die Rollenzuordnung kontextbezogener und genauer erfolgt.
#### Akzeptanzkriterien
1. WHEN infer_primary_role is called, THE VocabularyCache SHALL use an LLM chat completion to determine the best matching role for a given task text.
2. WHEN infer_primary_role is called, THE VocabularyCache SHALL provide the list of all available role names from the database as context to the LLM.
3. WHEN infer_primary_role is called, THE VocabularyCache SHALL provide the task text (title and/or description) as input to the LLM.
4. THE VocabularyCache SHALL return the role name and a confidence score from the LLM response.
5. IF the LLM call fails, THEN THE VocabularyCache SHALL return None rather than raising an unhandled exception.
6. THE VocabularyCache SHALL no longer require a task embedding as input parameter for infer_primary_role.
7. THE VocabularyCache SHALL accept the task text directly as input parameter for infer_primary_role.
8. WHEN infer_primary_role is called, THE VocabularyCache SHALL instruct the LLM to select exactly one role from the provided list and return a structured JSON response.
9. THE infer_primary_role tool in mcp_server.py SHALL call the new LLM-based infer_primary_role without first generating a task embedding.
### Anforderung 4: Entfernung des Embedding-Preloads offener Tasks
**User Story:** Als Entwickler möchte ich, dass Embeddings offener Tasks nicht mehr beim Server-Start vorgeladen werden und auch nicht mehr on-demand erzeugt werden, da keine Embedding-basierte Verarbeitung mehr stattfindet.
#### Akzeptanzkriterien
1. THE MCP-Server SHALL no longer preload embeddings for open tasks during startup.
2. THE `_startup_preload_embeddings` function SHALL be removed entirely.
3. THE VocabularyCache SHALL no longer provide an `ensure_task_embedding` method, since task embeddings are not needed in the BM25 + LLM architecture.
4. THE MCP-Server SHALL no longer preload role or competence vocabulary embeddings at startup, since all similarity computations now use BM25 or LLM.
5. THE startup time of the MCP-Server SHALL be reduced by eliminating all embedding preload loops.
### Anforderung 5: Umstellung der Rollen-Similarity im Matcher auf LLM
**User Story:** Als Entwickler möchte ich, dass die Rollen-Similarity im Matcher (`compute_role_similarity`) ebenfalls einen LLM-Ansatz (Chat Completion) nutzt anstelle von Embedding-Cosine-Similarity, damit das gesamte System ohne Embeddings auskommt und konsistent BM25 + LLM-basiert ist.
#### Akzeptanzkriterien
1. WHEN compute_role_similarity is called, THE SimilarityEngine SHALL use an LLM chat completion to determine the semantic similarity between the required role and the candidate role.
2. WHEN compute_role_similarity is called, THE SimilarityEngine SHALL provide both role names to the LLM and request a numeric similarity score between 0.0 and 1.0.
3. THE SimilarityEngine SHALL instruct the LLM to return a structured JSON response containing the similarity score.
4. THE SimilarityEngine SHALL no longer use embedding cosine similarity for role comparisons.
5. THE SimilarityEngine SHALL no longer call `_embed` or `prefetch_embeddings` for role similarity computation.
6. IF the LLM call fails, THEN THE SimilarityEngine SHALL return a default similarity score of 0.0 rather than raising an unhandled exception.
7. THE VocabularyCache SHALL no longer preload role vocabulary embeddings at startup, since they are not needed for LLM-based role similarity.
8. THE SimilarityEngine SHALL cache LLM-based role similarity results for identical role pairs within a matching run to avoid redundant API calls.
### Anforderung 6: Vollständige Entfernung der Embedding-Infrastruktur
**User Story:** Als Entwickler möchte ich, dass die gesamte Embedding-Infrastruktur entfernt wird, da weder Kompetenz-Matching noch Rollen-Similarity noch Rollen-Inferenz Embeddings benötigen und das System vollständig BM25 + LLM-basiert ist.
#### Akzeptanzkriterien
1. THE SimilarityEngine SHALL remove the `prefetch_embeddings` method entirely.
2. THE SimilarityEngine SHALL remove the `_embed` method entirely.
3. THE VocabularyCache SHALL remove `_preload_competence_vocab` entirely.
4. THE VocabularyCache SHALL remove `_preload_role_vocab` (or equivalent role embedding preload logic) entirely.
5. THE VocabularyCache SHALL remove `infer_competences` if embedding-based competence inference is no longer used.
6. THE EmbeddingCache module SHALL be removed entirely, since no code path requires cached embeddings.
7. THE AzureOpenAIClient SHALL remove the embedding API method (e.g. `get_embeddings` or equivalent batch embedding call), retaining only chat completion methods.
8. THE config.py SHALL remove embedding-related configuration fields (e.g. `embedding_model`, `embedding_dimensions`, embedding batch size settings).
9. THE config.py SHALL remove the `InferenceConfig` dataclass if `max_competences` and `min_similarity` are no longer used.
10. THE SimilarityConfig SHALL remove any fields related to embedding thresholds or embedding model selection.
11. THE config.toml SHALL remove embedding-related configuration entries.
12. THE MCP-Server SHALL remove the `_startup_preload_embeddings` function entirely if no embedding preloads remain.
@@ -0,0 +1,178 @@
# Implementation Plan: Entfernung Embedding-basierter Similarity Umstellung auf BM25 + LLM
## Overview
Incremental removal of all embedding infrastructure and replacement with BM25 + LLM. Tasks are ordered to avoid breaking the system mid-way: first add new LLM-based methods, then remove embedding code paths, then clean up config and delete dead modules.
## Tasks
- [x] 1. Simplify AzureOpenAIClient to remove embedding methods
- [x] 1.1 Remove `embeddings` and `get_embeddings_batch` methods from `AzureOpenAIClient`
- Remove the `self._emb` AsyncAzureOpenAI client instance
- Remove constructor parameters: `embedding_api_key`, `embedding_deployment`, `embedding_batch_size`
- Keep only `chat_completion` method and its supporting `self._chat` client
- Make `self._chat` the primary client (no longer optional/conditional)
- Remove the guard clause in `chat_completion` that checks for missing config
- _Requirements: 6.7_
- [x] 1.2 Update `AzureOpenAIClient` constructor signature
- New required params: `endpoint`, `api_version`, `chat_deployment`, `llm_api_key`
- Keep optional: `timeout_s`, `max_retries`, `cost_tracker`
- Remove cost tracker calls for embedding requests (`log_embedding_request`, `log_embedding_batch_request`)
- _Requirements: 6.7_
- [-] 2. Refactor SimilarityEngine to remove embedding methods and add LLM role similarity
- [x] 2.1 Remove embedding-related methods and properties from `SimilarityEngine`
- Remove methods: `prefetch_embeddings`, `_embed`, `get_embedding_for_cache_key`, `get_embeddings_for_cache_keys`, `_aggregate_similarity`, `_per_skill_similarity`
- Remove properties: `embedding_model`, `embedding_dimensions`, `use_bm25_search`
- Remove constructor parameters: `cache`, `embedding_model`, `embedding_dimensions`, `strategy`, `use_bm25_search`
- Remove module-level helpers: `_cache_key`, `cosine_similarity`
- Update constructor to accept only: `client`, `cost_tracker`, `use_auto_tagging`, `auto_tagger`
- Add `_role_similarity_cache: dict[tuple[str, str], float]` to constructor
- _Requirements: 1.3, 1.4, 1.5, 1.6, 1.7, 6.1, 6.2_
- [x] 2.2 Simplify `compute_competence_similarity` to always use BM25+RRF
- Remove any strategy/conditional branching
- Always call `_bm25_rrf_similarity` directly (with optional auto-tagging expansion)
- _Requirements: 1.1, 1.2_
- [x] 2.3 Implement LLM-based `compute_role_similarity`
- Replace embedding cosine similarity with LLM chat completion
- Add `_ROLE_SIMILARITY_SYSTEM_PROMPT` constant
- Implement symmetric cache key: `tuple(sorted((role_a_lower, role_b_lower)))`
- Return 0.0 for bad/empty/None/"(unknown)" roles without LLM call
- Return 1.0 for identical roles (case-insensitive) without LLM call
- Parse JSON response, clamp score to [0.0, 1.0]
- On any exception: return 0.0, do not cache failed results
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.8_
- [x] 2.4 Add `clear_role_cache` method
- Clears `_role_similarity_cache` dict
- _Requirements: 5.8_
- [x] 2.5 Write property test for BM25 zero-score on disjoint tokens
- **Property 1: BM25 Zero-Score für fehlenden Token-Overlap**
- **Validates: Requirements 1.1**
- [x] 2.6 Write property test for role similarity value range and identity
- **Property 5: compute_role_similarity Wertebereich**
- **Validates: Requirements 5.1**
- [x] 2.7 Write property test for role similarity cache symmetry
- **Property 6: Rollen-Similarity-Cache ist symmetrisch und idempotent**
- **Validates: Requirements 5.8**
- [-] 3. Refactor VocabularyCache to use LLM-based role inference
- [x] 3.1 Rewrite `VocabularyCache` class
- Remove all embedding-related methods: `preload`, `_preload_role_vocab`, `_preload_competence_vocab`, `ensure_task_embedding`, `infer_competences`
- Remove old `infer_primary_role` (embedding-based)
- Remove properties: `roles`, `competences`
- Remove constructor dependencies on `SimilarityEngine` and `EmbeddingCache`
- New constructor accepts only: `db: DBClient`, `client: AzureOpenAIClient`
- Remove module-level helpers: `role_vocab_cache_key`, `competence_vocab_cache_key`, `task_id_cache_key`, `VocabItem`
- _Requirements: 4.3, 6.3, 6.4, 6.5_
- [x] 3.2 Implement new LLM-based `infer_primary_role`
- Accept `task_text: str` keyword argument (no more `task_embedding`)
- Fetch all role names from DB via `self._db.get_all_role_names()`
- Build LLM prompt with task text and available roles list
- Parse JSON response: `{"role": "...", "confidence": 0.0-1.0}`
- Validate returned role exists in DB role list
- Return `None` on empty text, empty role list, or any exception
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8_
- [x] 3.3 Write property test for infer_primary_role output validity
- **Property 3: infer_primary_role Ausgabe-Validität**
- **Validates: Requirements 3.1, 3.4**
- [x] 3.4 Write property test for graceful degradation on LLM failure
- **Property 4: Graceful Degradation bei LLM-Fehler**
- **Validates: Requirements 3.5, 5.6**
- [x] 4. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
- [-] 5. Update Matcher to unconditionally build BM25 index
- [x] 5.1 Remove `use_bm25_search` conditional in `Matcher.match`
- Remove `if self._sim.use_bm25_search` guard around BM25 index construction
- Always build global BM25 index when `filtered` is non-empty
- _Requirements: 2.6_
- [x] 5.2 Write property test for unconditional BM25 index building
- **Property 2: Matcher baut BM25-Index bedingungslos**
- **Validates: Requirements 2.6**
- [x] 6. Update MCP Server to remove embedding preload and wire new components
- [x] 6.1 Remove embedding preload infrastructure from `mcp_server.py`
- Remove `_startup_preload_embeddings` function
- Remove `_deferred_preload` function and `_preload_started` flag
- Remove `_ensure_preloaded` function
- Remove all `await _ensure_preloaded()` calls in tool handlers
- Remove `EmbeddingCache` import and instantiation
- _Requirements: 4.1, 4.2, 4.4, 4.5, 6.12_
- [x] 6.2 Update component wiring in `build_server`
- Construct `AzureOpenAIClient` with new simplified signature (no embedding params)
- Construct `SimilarityEngine` with new signature (client, cost_tracker, use_auto_tagging, auto_tagger)
- Construct `VocabularyCache` with new signature (db, client)
- _Requirements: 6.7_
- [x] 6.3 Update `infer_primary_role` tool handler
- Remove embedding generation step
- Call `vocab_cache.infer_primary_role(task_text=text)` directly with task text
- _Requirements: 3.9_
- [x] 6.4 Remove or simplify `validate_task_requirements` tool if it depends on embeddings
- Remove embedding-based `infer_competences` usage
- Either remove the tool entirely or replace with a DB-field-only version
- _Requirements: 6.5_
- [x] 7. Simplify config.py and config.toml
- [x] 7.1 Remove embedding-related dataclasses and fields from `config.py`
- Delete `EmbeddingCacheConfig` dataclass
- Delete `InferenceConfig` dataclass
- Remove from `AzureOpenAIConfig`: `embedding_deployment`, `embedding_batch_size`
- Remove from `SimilarityConfig`: `embedding_model`, `embedding_dimensions`, `strategy`, `use_bm25_search`
- Remove `inference` field from `MatchingConfig`
- Remove `embedding_cache` field from `AppConfig`
- _Requirements: 2.1, 2.2, 6.8, 6.9, 6.10_
- [x] 7.2 Update `load_config` / `_parse_azure_openai` validation
- Remove `AZURE_OPENAI_EMBEDDING_API_KEY` environment variable check
- Make `AZURE_OPENAI_LLM_API_KEY` always required
- Make `chat_deployment` required
- Remove `embedding_dimensions == 3072` validation
- Remove `strategy` validation
- Remove `[matching.inference]` parsing
- Remove `[embedding_cache]` parsing
- _Requirements: 2.3, 2.4_
- [x] 7.3 Update `config.toml` and `config.toml.example`
- Remove `[embedding_cache]` section
- Remove from `[matching.similarity]`: `embedding_model`, `embedding_dimensions`, `strategy`, `use_bm25_search`
- Remove from `[azure_openai]`: `embedding_deployment`, `embedding_batch_size`
- Remove `[matching.inference]` section
- _Requirements: 6.11_
- [x] 8. Delete EmbeddingCache module and related tests
- [x] 8.1 Delete `src/teamlandkarte_mcp/cache/embedding_cache.py`
- _Requirements: 6.6_
- [x] 8.2 Remove or update tests that reference embedding functionality
- Delete tests for `EmbeddingCache`
- Update tests for `SimilarityEngine` to use new constructor
- Update tests for `VocabularyCache` to use new constructor
- Update tests for `AzureOpenAIClient` to use new constructor
- Remove any test fixtures that create embedding mocks
- _Requirements: 6.6_
- [x] 9. Final checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- Order ensures no broken intermediate states: new LLM methods added before old embedding paths removed