# LLM-Kompetenz-Inferenz Bugfix Design ## Übersicht Die Kompetenz-Inferenz in `validate_task_requirements`, `extract_requirements` und `find_matching_tasks` (Score-Modus) ist defekt, weil der alte Embedding-Ansatz entfernt wurde, ohne einen Ersatz zu implementieren. Die Variable `inferred_comps` ist stets eine leere Liste `[]`. Der Fix implementiert eine neue Methode `infer_competences` in der bestehenden `VocabularyCache`-Klasse, analog zum bereits funktionierenden `infer_primary_role`. Diese Methode nutzt den `AzureOpenAIClient.chat_completion`-Aufruf, um aus der vollständigen Kompetenzliste (`get_all_competence_names()`) bis zu 10 passende Kompetenzen mit Konfidenzwerten auszuwählen. ## Glossar - **Bug_Condition (C)**: Der Zustand, in dem ein Task-Text vorhanden ist UND Kompetenzen in der DB existieren, aber `inferred_comps` trotzdem `[]` zurückgibt - **Property (P)**: Das gewünschte Verhalten – eine nicht-leere Liste von bis zu 10 `(competence_name, confidence)`-Tupeln, wobei jeder Name in `get_all_competence_names()` enthalten ist - **Preservation**: Die bestehende Rollen-Inferenz (`infer_primary_role`), DB-Skills-Anzeige und LLM-Fulltext-Matching bleiben unverändert - **VocabularyCache**: Klasse in `src/teamlandkarte_mcp/matching/vocabulary.py`, die LLM-basierte Inferenz kapselt - **AzureOpenAIClient**: Client in `src/teamlandkarte_mcp/azure/openai_client.py` mit `chat_completion(system, user) -> str` - **inferred_comps**: Die lokale Variable in den betroffenen Tools, die aktuell immer `[]` ist ## Bug-Details ### Fault Condition Der Bug manifestiert sich, wenn ein Tool (`validate_task_requirements`, `extract_requirements`, `find_matching_tasks` im Score-Modus) einen nicht-leeren Task-Text verarbeitet und Kompetenzen in der DB vorhanden sind. Die Kompetenz-Inferenz liefert stets eine leere Liste, weil kein LLM-Aufruf stattfindet. **Formale Spezifikation:** ``` FUNCTION isBugCondition(input) INPUT: input of type {task_text: str, competence_names: list[str]} OUTPUT: boolean RETURN input.task_text.strip() != "" AND len(input.competence_names) > 0 AND infer_competences(input.task_text) == [] END FUNCTION ``` ### Beispiele - `validate_task_requirements("task-123")` mit Task-Titel "Python Backend Entwicklung" und 50 Kompetenzen in der DB → Erwartung: bis zu 10 Kompetenzen mit Konfidenz; Aktuell: leere Tabelle - `extract_requirements("Wir brauchen einen React-Entwickler mit TypeScript-Erfahrung")` mit Kompetenzen ["React", "TypeScript", "Angular", ...] in der DB → Erwartung: ["React", "TypeScript", ...] mit Konfidenz; Aktuell: `inferred_competences = []` - `find_matching_tasks(capacity_id=1)` im Score-Modus → Erwartung: `inferred_comp_names` enthält LLM-inferierte Kompetenzen für das Scoring; Aktuell: nur DB-Skills werden verwendet - Leerer Task-Text → Erwartung: leere Liste (kein Fehler) – dieses Verhalten ist korrekt und bleibt erhalten ## Erwartetes Verhalten ### Preservation Requirements **Unverändertes Verhalten:** - `infer_primary_role` muss weiterhin genau eine Rolle mit Konfidenz zurückgeben - DB-Skills eines Tasks müssen weiterhin korrekt in der Ausgabe angezeigt werden - LLM-Fulltext-Matching (`find_matching_capacities` / `find_matching_tasks` im `llm_fulltext`-Modus) bleibt unverändert - Fehlerbehandlung bei LLM-Aufruf-Fehlern für Rollen-Inferenz bleibt unverändert (`None` zurückgeben) - Die `AzureOpenAIClient`-Schnittstelle wird nicht verändert **Scope:** Alle Eingaben, die KEINEN nicht-leeren Task-Text mit vorhandenen Kompetenzen in der DB kombinieren, sind vom Fix nicht betroffen: - Leerer Task-Text → weiterhin leere Liste - Keine Kompetenzen in der DB → weiterhin leere Liste - Mausklick-/UI-Interaktionen → nicht betroffen (MCP-Server) - LLM-Fulltext-Modus → verwendet eigene Logik, nicht betroffen ## Hypothesierte Ursache Basierend auf der Bug-Analyse sind die Ursachen klar identifiziert: 1. **Fehlende Implementierung**: Die alte `infer_competences`-Methode (Embedding-basiert) wurde entfernt. An den drei Stellen im Code steht nur noch `inferred_comps: list[tuple[str, float]] = []` bzw. `inferred_competences: list[tuple[str, float]] = []` ohne jeglichen LLM-Aufruf. 2. **Kein System-Prompt für Kompetenz-Inferenz**: Im Gegensatz zu `_ROLE_INFERENCE_SYSTEM_PROMPT` existiert kein entsprechender Prompt für Kompetenz-Inferenz. 3. **Keine Methode in VocabularyCache**: Die Klasse hat nur `infer_primary_role`, aber keine `infer_competences`-Methode. 4. **Keine Integration in die Tools**: Selbst wenn eine Methode existieren würde, fehlt der `await`-Aufruf an den drei betroffenen Stellen. ## Correctness Properties Property 1: Fault Condition - Kompetenz-Inferenz liefert Ergebnisse _For any_ input where der Task-Text nicht leer ist UND mindestens eine Kompetenz in der DB existiert (isBugCondition returns true), SHALL die fixierte `infer_competences`-Methode eine nicht-leere Liste von bis zu 10 Tupeln `(competence_name, confidence)` zurückgeben, wobei jeder `competence_name` in `get_all_competence_names()` enthalten ist und `confidence` im Bereich [0.0, 1.0] liegt. **Validates: Requirements 2.1, 2.2, 2.3** Property 2: Preservation - Rollen-Inferenz und DB-Skills unverändert _For any_ input (unabhängig davon ob die Bug-Condition gilt oder nicht), SHALL die fixierte Codebasis das gleiche Ergebnis für `infer_primary_role` und die DB-Skills-Anzeige produzieren wie der originale Code, und das LLM-Fulltext-Matching bleibt unverändert. **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5** ## Fix-Implementierung ### Erforderliche Änderungen **Datei**: `src/teamlandkarte_mcp/matching/vocabulary.py` **Änderung 1: System-Prompt hinzufügen** - Neuer Modul-Level-Konstante `_COMPETENCE_INFERENCE_SYSTEM_PROMPT` analog zu `_ROLE_INFERENCE_SYSTEM_PROMPT` - Prompt instruiert das LLM, aus einer gegebenen Kompetenzliste bis zu 10 passende Kompetenzen für einen Task-Text auszuwählen - Antwortformat: `{"competences": [{"name": "", "confidence": }]}` **Änderung 2: Neue Methode `infer_competences` in `VocabularyCache`** - Signatur: `async def infer_competences(self, *, task_text: str, max_competences: int = 10) -> list[tuple[str, float]]` - Holt Kompetenzliste via `self._db.get_all_competence_names()` - Bei leerer Liste oder leerem Text: `[]` zurückgeben - LLM-Aufruf via `self._client.chat_completion(system, user)` - JSON-Parsing der Antwort, Validierung gegen DB-Kompetenzliste - Bei Fehler: leere Liste zurückgeben + Warning loggen --- **Datei**: `src/teamlandkarte_mcp/mcp_server.py` **Änderung 3: `validate_task_requirements` – LLM-Aufruf integrieren** - Ersetze `inferred_comps: list[tuple[str, float]] = []` durch: ```python inferred_comps = await vocab_cache.infer_competences(task_text=task_text) ``` **Änderung 4: `extract_requirements` – LLM-Aufruf integrieren** - Ersetze `inferred_competences: list[tuple[str, float]] = []` durch: ```python inferred_competences = await vocab_cache.infer_competences(task_text=desc) ``` **Änderung 5: `find_matching_tasks` (Score-Modus) – LLM-Aufruf integrieren** - Ersetze den Block `inferred_comp_names = [str(x) for x in (getattr(t, "skills", None) or []) if x]` durch: ```python inferred_comp_tuples = await vocab_cache.infer_competences(task_text=full_text) inferred_comp_names = [name for name, _conf in inferred_comp_tuples] if not inferred_comp_names: inferred_comp_names = [str(x) for x in (getattr(t, "skills", None) or []) if x] ``` ## Testing-Strategie ### Validierungsansatz Die Testing-Strategie folgt einem zweiphasigen Ansatz: Zuerst Counterexamples auf dem unfixierten Code aufdecken, dann den Fix verifizieren und Preservation sicherstellen. ### Exploratory Fault Condition Checking **Ziel**: Counterexamples aufdecken, die den Bug VOR der Implementierung des Fixes demonstrieren. Root-Cause-Analyse bestätigen oder widerlegen. **Testplan**: Tests schreiben, die `infer_competences` (bzw. die betroffenen Tools) mit nicht-leerem Task-Text und vorhandenen Kompetenzen aufrufen. Auf dem unfixierten Code beobachten, dass stets `[]` zurückkommt. **Testfälle**: 1. **validate_task_requirements mit gültigem Task**: Aufruf mit Task-ID, der einen beschriebenen Task hat (wird auf unfixiertem Code leere Kompetenz-Tabelle liefern) 2. **extract_requirements mit Freitext**: Aufruf mit beschreibendem Text (wird auf unfixiertem Code `inferred_competences = []` liefern) 3. **find_matching_tasks im Score-Modus**: Aufruf mit Capacity-ID (wird auf unfixiertem Code nur DB-Skills verwenden, keine LLM-Inferenz) 4. **Leerer Task-Text**: Aufruf mit leerem Text (soll auch nach Fix `[]` liefern – Baseline) **Erwartete Counterexamples**: - `inferred_comps` ist immer `[]`, unabhängig vom Task-Text - Ursache: Kein LLM-Aufruf, keine `infer_competences`-Methode vorhanden ### Fix Checking **Ziel**: Verifizieren, dass für alle Eingaben, bei denen die Bug-Condition gilt, die fixierte Funktion das erwartete Verhalten produziert. **Pseudocode:** ``` FOR ALL input WHERE isBugCondition(input) DO result := infer_competences_fixed(input.task_text) ASSERT len(result) > 0 ASSERT len(result) <= 10 FOR EACH (name, confidence) IN result DO ASSERT name IN get_all_competence_names() ASSERT 0.0 <= confidence <= 1.0 END FOR END FOR ``` ### Preservation Checking **Ziel**: Verifizieren, dass für alle Eingaben, bei denen die Bug-Condition NICHT gilt, die fixierte Funktion das gleiche Ergebnis wie die originale Funktion produziert. **Pseudocode:** ``` FOR ALL input WHERE NOT isBugCondition(input) DO ASSERT infer_competences_fixed(input.task_text) == [] END FOR FOR ALL input DO ASSERT infer_primary_role_fixed(input) == infer_primary_role_original(input) END FOR ``` **Testing-Ansatz**: Property-Based Testing wird für Preservation Checking empfohlen, weil: - Es automatisch viele Testfälle über den Eingabebereich generiert - Es Randfälle findet, die manuelle Unit-Tests übersehen könnten - Es starke Garantien bietet, dass Verhalten für alle nicht-buggy Eingaben unverändert bleibt **Testplan**: Verhalten auf unfixiertem Code zuerst beobachten (leere Ergebnisse, funktionierende Rollen-Inferenz), dann Property-Based Tests schreiben, die dieses Verhalten nach dem Fix verifizieren. **Testfälle**: 1. **Rollen-Inferenz Preservation**: Verifizieren, dass `infer_primary_role` nach dem Fix identische Ergebnisse liefert 2. **DB-Skills Preservation**: Verifizieren, dass DB-Skills weiterhin korrekt angezeigt werden 3. **Leerer Text Preservation**: Verifizieren, dass leerer Task-Text weiterhin `[]` liefert 4. **LLM-Fulltext Preservation**: Verifizieren, dass der LLM-Fulltext-Modus nicht beeinflusst wird ### Unit Tests - Test `infer_competences` mit gemocktem LLM-Client: gültige JSON-Antwort → korrekte Tupel-Liste - Test `infer_competences` mit leerem Task-Text → `[]` - Test `infer_competences` mit leerer Kompetenzliste in DB → `[]` - Test `infer_competences` bei LLM-Fehler (Exception) → `[]` + Warning geloggt - Test `infer_competences` bei ungültiger JSON-Antwort → `[]` - Test `infer_competences` bei Kompetenz-Namen die nicht in DB sind → werden herausgefiltert - Test `validate_task_requirements` liefert nicht-leere Kompetenz-Tabelle - Test `extract_requirements` liefert nicht-leere `inferred_competences` ### Property-Based Tests - Generiere zufällige Task-Texte und Kompetenzlisten; verifiziere, dass Ergebnisse stets Subset der DB-Kompetenzen sind - Generiere zufällige Eingaben; verifiziere, dass Konfidenzwerte immer in [0.0, 1.0] liegen - Generiere zufällige Eingaben; verifiziere, dass maximal 10 Kompetenzen zurückgegeben werden - Generiere zufällige Eingaben; verifiziere, dass `infer_primary_role` unverändert funktioniert (Preservation) ### Integration Tests - End-to-End Test: `validate_task_requirements` mit echtem (gemocktem) LLM-Client zeigt Kompetenzen - End-to-End Test: `extract_requirements` → `find_matching_capacities` Pipeline mit inferierten Kompetenzen - End-to-End Test: `find_matching_tasks` im Score-Modus nutzt inferierte Kompetenzen für besseres Scoring