# Implementation Plan: LLM-Volltext-Matching als zweites Verfahren ## Overview Inkrementelle Einführung des neuen `llm_fulltext`-Matching-Verfahrens neben dem bestehenden `score`-Verfahren. Die Reihenfolge stellt sicher, dass keine Zwischenstände kaputt sind: zuerst Datenbankschicht (DBClient/TrinoClient) erweitern, dann Datenmodelle (`CapacityProfile`/`TaskProfile`) und der `LlmFulltextMatcher`, anschließend Konfiguration und MCP-Tool-Integration, am Ende Cache-/Tabellenausgabe sowie Anpassungen an `get_results_by_category`/`filter_search_results` und Dokumentation. ## Tasks - [x] 1. Datenbankschicht für Capacity-Volltext-Felder erweitern - [x] 1.1 Neue Methoden im `DBClient`-Protokoll deklarieren (`database/types.py`) - `CapacityReferenceRow` als `TypedDict` mit Feldern `partner_name: str` und `projects: str` im Protokoll-Modul (bzw. unter `database/types.py`) deklarieren; `partner_name` darf leer sein (NULL `partner_id` oder Join-Mismatch, Anforderung 2.8) - `get_capacity_description(capacity_id) -> str | None` - `get_capacity_certificates(capacity_id) -> list[str]` - `get_capacity_references(capacity_id) -> list[CapacityReferenceRow]` - `batch_get_capacity_descriptions(capacity_ids) -> dict[str, str | None]` - `batch_get_capacity_certificates(capacity_ids) -> dict[str, list[str]]` - `batch_get_capacity_references(capacity_ids) -> dict[str, list[CapacityReferenceRow]]` - Docstrings gemäß Design (Quelle, Join-Verhalten inkl. Partner-Join `partner_id = id`, Rückgabetyp bei leerer Eingabe, leerer `partner_name` bei NULL/Mismatch) - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.8_ - [x] 1.2 Einzel-Methoden im `TrinoClient` implementieren (`database/trino_client.py`) - SELECT-Only-Statements mit `_ensure_select_only`, Connection-Pool und `_retry` - `get_capacity_description`: NULL/leerer String → `None` - `get_capacity_certificates`: Join über `capacity_id`, leere Strings filtern, Rückgabe `[]` wenn keine Treffer - `get_capacity_references`: zusätzlich `LEFT JOIN teamlandkarte_v_partners_latest p ON r.partner_id = p.id` und `COALESCE(p.name, '') AS partner_name` in derselben Abfrage; Rückgabe als `CapacityReferenceRow`-Liste mit `partner_name` und `projects` - NULL `partner_id` bzw. Join-Mismatch → leerer `partner_name`, Referenz bleibt mit `projects` erhalten (Anforderung 2.8) - Parameter-Bindung gegen SQL-Injection - _Requirements: 2.1, 2.2, 2.3, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10_ - [x] 1.3 Batch-Methoden im `TrinoClient` implementieren - Genau **eine** SQL-Abfrage pro Quelle (Anforderung 2.4); der Partner-LEFT-JOIN ist Bestandteil derselben Referenz-Abfrage und erzeugt keinen zusätzlichen Roundtrip - `batch_get_capacity_references`: `SELECT r.capacity_id, r.projects, COALESCE(p.name, '') AS partner_name ... LEFT JOIN teamlandkarte_v_partners_latest p ON r.partner_id = p.id` - Schlüssel der Rückgaben sind `str(capacity_id)` (deterministisch) - Fehlende IDs als `None` (Beschreibungen) bzw. `[]` (Listen) vorbelegen - Gruppierung n:1 via `defaultdict(list)`, leere `projects`-Strings filtern; leerer `partner_name` (NULL/Mismatch) führt nicht zum Verwerfen der Referenz - Frühe Rückkehr bei leerer Eingabeliste (`{}`) - _Requirements: 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10_ - [x] 1.4 Unit-Tests für die neuen DB-Methoden mit Mock-Cursor* - Genau eine SQL-Abfrage je Methode wird abgesetzt; LEFT JOIN auf `teamlandkarte_v_partners_latest` ist Bestandteil derselben Referenz-Abfrage (kein zusätzlicher Roundtrip) - SELECT-Only-Guard wird angewendet - Korrekte Gruppierung n:1 in den Batch-Varianten - Mock-Cursor liefert für `references` drei Spalten (`capacity_id`, `projects`, `partner_name`); Test inkl. Fall NULL `partner_id` → leerer `partner_name`, Referenz bleibt erhalten - Defaults für fehlende IDs (None / []) - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8_ - [x] 2. Datenmodelle `CapacityProfile` und `TaskProfile` - [x] 2.1 Neue frozen Dataclasses anlegen (`matching/profiles.py`) - `CapacityReferenceEntry` als `@dataclass(frozen=True)` mit Feldern `partner_name: str` und `projects: str` (`partner_name` darf leer sein, vgl. Anforderung 2.8 / 3.2) - `CapacityProfile` mit Feldern `id`, `owner_name`, `role_name`, `competences`, `description`, `references: list[CapacityReferenceEntry]`, `certificates` - `TaskProfile` mit Feldern `id`, `title`, `description`, `skills` - Beide `@dataclass(frozen=True)` und nur primitive Felder bzw. `list[str]` / `list[CapacityReferenceEntry]` - _Requirements: 3.1, 3.2, 4.1_ - [x] 2.2 Profil-Builder implementieren (`matching/profiles.py`) - `build_capacity_profile(capacity, *, description, certificates, references) -> CapacityProfile` - Konvertiere `CapacityReferenceRow`-Einträge aus dem DBClient in `CapacityReferenceEntry`-Instanzen; ein leerer `partner_name` führt nicht zum Verwerfen der Referenz (Anforderung 3.4) - `build_task_profile(task) -> TaskProfile` (alternativ `build_task_profile_from_requirements(...)` für Suchrichtung Aufgabe→Kapazität) - Leere/`None`-Felder → leerer String bzw. leere Liste, Profil wird nie verworfen - Reihenfolge der Felder ist über alle Profile konstant - _Requirements: 3.1, 3.2, 3.4, 4.1, 4.2_ - [x] 2.3 Deterministische Profil-Serialisierung implementieren - `serialize_capacity_profile(profile) -> str` mit fixer Reihenfolge `Rolle:`, `Kompetenzen:`, `Beschreibung:`, `Referenzen:`, `Zertifikate:` - `_format_reference(entry: CapacityReferenceEntry) -> str`: bei nicht-leerem `partner_name` → `Partner: – Projekte: `; bei leerem `partner_name` → `Projekte: ` (kein Platzhalter, kein `Partner:`-Token) - `serialize_task_profile(profile) -> str` mit fixer Reihenfolge `Titel:`, `Beschreibung:`, `Gesuchte Kompetenzen:` - Leere Listen werden als `(keine)` ausgegeben, alle Feldüberschriften erscheinen immer; Reihenfolge der Felder und Referenzen bleibt deterministisch - _Requirements: 3.3, 3.4, 3.6, 3.7, 4.3, 4.4_ - [x] 2.4 Property-Test für deterministische Serialisierung schreiben - **Property 1: Profil-Serialisierung ist deterministisch und feldvollständig** - Hypothesis mit `@settings(max_examples=100)`, `st.builds(...)` für Profile inkl. `CapacityReferenceEntry` mit/ohne `partner_name` - Zwei Aufrufe liefern identischen String; alle Feldüberschriften enthalten; Partner-Name erscheint deterministisch in der serialisierten Ausgabe, sofern nicht leer - **Validates: Requirements 3.3, 3.4, 3.6, 3.7, 4.3, 4.4** - [x] 2.5 Property-Test für Profil-Builder mit leeren Feldern schreiben - **Property 2: Leere/None-Felder verwerfen das Profil nicht** - Capacity-/Task-Strategien mit `st.one_of(st.none(), st.text())`; Referenzen-Strategie mit gemischtem `partner_name` (`st.one_of(st.just(""), st.text(min_size=1))`) - Profil enthält leere Strings/Listen; Serialisierung enthält weiterhin alle Überschriften; auch Referenzen mit leerem `partner_name` werden nicht verworfen - **Validates: Requirements 3.2, 3.4, 4.2** - [x] 2.6 Property-Test für Referenzen mit leerem Partner-Name schreiben - **Property 2b: Referenzen mit leerem Partner_Name behalten projects, ohne Partner-Token** - Hypothesis-Generator mischt leere und nicht-leere `partner_name`-Werte: `st.lists(st.builds(CapacityReferenceEntry, partner_name=st.one_of(st.just(""), st.text(min_size=1)), projects=st.text(min_size=1)))` - Prüft (a) die Anzahl der Referenz-Zeilen entspricht der Anzahl der Einträge mit nicht-leerem `projects`, (b) jede Zeile zu einem Eintrag mit leerem `partner_name` beginnt mit `Projekte:` und enthält keinen `Partner:`-Token, (c) jede Zeile zu einem Eintrag mit nicht-leerem `partner_name` enthält sowohl `Partner: ` als auch `Projekte: ` - **Validates: Requirements 2.8, 3.4, 3.6** - [x] 3. `LlmFulltextMatcher`-Komponente bauen - [x] 3.1 Modul-Skelett `matching/llm_fulltext_matcher.py` anlegen - Konstanten `_ALLOWED_CATEGORIES = ("Top", "Good", "Partial", "Low", "Irrelevant")` und `_ALIAS` - Dataclasses `LlmFulltextItem`, `LlmFulltextError`, `LlmFulltextResult` - Klasse `LlmFulltextMatcher` mit Konstruktor `(*, db, client, rationale_max_chars=280)` - _Requirements: 5.1, 5.3, 6.1, 6.3, 7.4_ - [x] 3.2 Kategorie-Normalisierung implementieren - `normalize_category(value) -> tuple[str, bool]` - Trim + lowercase, Mapping über `_ALIAS`, ungültige Werte → `("Irrelevant", False)` - Nicht-String-Eingaben → `("Irrelevant", False)` - _Requirements: 5.3, 5.6, 6.3, 6.6_ - [x] 3.3 Property-Test für Kategorie-Normalisierung schreiben - **Property 3: Kategorienormalisierung bildet auf erlaubte Menge ab** - Hypothesis mit `st.text()` und Aliase via `st.sampled_from` - Output immer in erlaubter Menge; `is_valid` korrekt - **Validates: Requirements 5.3, 5.6, 6.3, 6.6** - [x] 3.4 LLM-Aufruf und Antwort-Parsing implementieren - System-Prompt gemäß Design (deutschsprachig, deterministisch, JSON-only) - User-Prompt baut auf `serialize_task_profile`/`serialize_capacity_profile` auf - `chat_completion(system, user, response_format=json_object)` aufrufen - JSON parsen, Felder `category` und `rationale` extrahieren - Bei `JSONDecodeError` → `LlmFulltextError` mit Meldung `"invalid JSON: "` - Bei ungültiger Kategorie → Item nach `Irrelevant` mit Hinweis `[Hinweis: ungültige LLM-Kategorie: ]` an Rationale anhängen - Bei `AzureAPIError`/Timeout/sonstiger Exception → `LlmFulltextError` mit Klassenname + erstem Satz - _Requirements: 5.4, 5.5, 5.6, 5.7, 6.4, 6.5, 6.6, 6.7_ - [x] 3.5 `match_capacities` implementieren - Eingabe: `task_profile`, `capacities` (bereits vorgefiltert) - Capacity-IDs sammeln, `batch_get_capacity_descriptions/certificates/references` aufrufen - Pro Kapazität `CapacityProfile` bauen und LLM-Aufruf durchführen - Erfolgreiche Items in `by_category` einsortieren, fehlerhafte in `errors` - Innerhalb jeder Kategorie deterministisch nach `item_id` aufsteigend (lexikographisch) sortieren - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.7, 5.8_ - [x] 3.6 `match_tasks` implementieren - Eingabe: `capacity_profile`, `tasks` - Pro Aufgabe `TaskProfile` bauen und LLM-Aufruf durchführen - Sortierung primär nach Kategorie, sekundär nach `task_id` aufsteigend - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.7, 6.8_ - [x] 3.7 Property-Test für ungültige LLM-Kategorie schreiben - **Property 4: Ungültige LLM-Kategorie wird auf Irrelevant gemappt** - Mock-LLM gibt zufällige ungültige Kategorie zurück - Item landet in `Irrelevant`, Rationale enthält Originaltext + Hinweis - **Validates: Requirements 5.6, 6.6** - [x] 3.8 Property-Test für LLM-Fehler-Trennung schreiben - **Property 5: LLM-Fehler erscheinen in der Fehlerliste, nicht als Ergebnis** - Hypothesis-Strategy `st.lists(st.booleans())` als Fehlermaske - Items aus Fehlermaske erscheinen ausschließlich in `result.errors` - **Validates: Requirements 5.7, 6.7** - [x] 3.9 Property-Test für deterministische Sortierung schreiben - **Property 6: Ergebnisse sind innerhalb jeder Kategorie deterministisch sortiert** - Permutationen der Eingabeliste erzeugen identische `by_category`-Reihenfolge - **Validates: Requirements 5.8, 6.8** - [x] 3.10 Unit-Tests für `LlmFulltextMatcher` mit gemocktem `AzureOpenAIClient`* - Erfolgsfall (gültige Kategorie + Rationale) - Ungültiges JSON - Gültiges JSON mit unbekannter Kategorie - `chat_completion` wirft `AzureAPIError` - Vermischung mehrerer Items: Reihenfolge und Sortierung korrekt - _Requirements: 5.4, 5.5, 5.6, 5.7, 5.8, 6.4, 6.5, 6.6, 6.7, 6.8_ - [x] 4. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 5. Konfiguration `matching.default_method` einführen - [x] 5.1 `MatchingConfig` um Feld `default_method: str = "score"` erweitern (`config.py`) - Validierung in `load_config`: Wert muss `"score"` oder `"llm_fulltext"` sein - Bei ungültigem Wert `ConfigError` mit beschreibender Meldung werfen - Fehlender Schlüssel → Default `"score"` - _Requirements: 12.2, 12.3, 12.4_ - [x] 5.2 `config.toml` und `config.toml.example` aktualisieren - Neuer Abschnitt `[matching].default_method = "score"` mit Kommentar zu erlaubten Werten - _Requirements: 12.2, 12.3_ - [x] 5.3 Unit-Tests für Konfigurations-Validierung* - `default_method` fehlt → Default `"score"` - `default_method = "llm_fulltext"` → übernommen - `default_method = "irgendwas"` → `ConfigError` - _Requirements: 12.3, 12.4_ - [x] 6. MCP-Tool-Integration für `matching_method` - [x] 6.1 Hilfsfunktion `_resolve_matching_method` im `mcp_server.py` implementieren - `None` → `cfg.matching.default_method` - Trim + lowercase, gegen `("score", "llm_fulltext")` validieren - Bei ungültigem Wert `ValueError` mit beiden erlaubten Werten in der Meldung werfen - _Requirements: 1.1, 1.2, 1.5, 12.3_ - [x] 6.2 `find_matching_capacities` um `matching_method` erweitern - Neuer Parameter `matching_method: Optional[str] = None` - Validierung **vor** DB-/LLM-Aufrufen, bei Fehler frühe Markdown-Fehlermeldung mit erlaubten Werten zurückgeben - Routing: `"score"` → bestehender `Matcher`-Pfad; `"llm_fulltext"` → `LlmFulltextMatcher.match_capacities` - Im LLM-Modus Vorfilter (z. B. Verfügbarkeit) identisch zum Score-Modus anwenden - Task_Profile aus aktuell bestätigtem Anforderungs-Set bauen - _Requirements: 1.1, 1.3, 1.4, 1.5, 5.1, 5.2_ - [x] 6.3 `find_matching_tasks` um `matching_method` erweitern - Neuer Parameter `matching_method: Optional[str] = None` - Routing analog 6.2 - Im LLM-Modus für die `capacity_id` Beschreibung, Zertifikate und Referenzen aus DB laden und ins `CapacityProfile` einbeziehen - _Requirements: 1.1, 1.3, 1.4, 1.5, 6.1, 6.2_ - [x] 6.4 Docstrings für betroffene MCP-Tools aktualisieren - `find_matching_capacities`, `find_matching_tasks`: Parameter `matching_method` mit erlaubten Werten dokumentieren - Hinweis auf Unterschiede in Ausgabe-Schema (keine Score-Spalten im LLM-Modus, `Begründung`-Spalte) - _Requirements: 12.1_ - [x] 6.5 Property-Test für `matching_method`-Validierung schreiben - **Property 9: matching_method-Validierung lehnt unbekannte Werte ab** - Hypothesis-Strategy `st.text()` - Bei ungültiger Eingabe enthält die Fehlermeldung beide erlaubten Werte; DB- und LLM-Mocks werden nicht aufgerufen - **Validates: Requirements 1.5** - [x] 6.6 Unit-Tests für Routing in beiden MCP-Tools - `matching_method=None` → Default greift, Schema entspricht Score-Modus - `matching_method="llm_fulltext"` → `LlmFulltextMatcher` wird aufgerufen, Score-Pfad nicht - `matching_method="bogus"` → Fehlermeldung, weder DB noch LLM werden aufgerufen - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_ - [x] 7. SearchCache-Payload und META-JSON - [x] 7.1 Persistenz im SearchCache an LLM-Modus anpassen - `matching_method` als Schlüssel im Payload aufnehmen (auch im Score-Modus) - Im LLM-Modus pro Item Felder `category` und `rationale` (ungekürzt) speichern, **keine** `role_score`/`competence_score`/`overall_score` - Optionale `errors`-Liste mit `{"item_id", "error"}` im LLM-Modus - Im Score-Modus bisheriges Schema unverändert (keine `rationale`/`errors`) - _Requirements: 7.1, 7.4, 8.6, 9.5_ - [x] 7.2 META-JSON in Tool-Antworten um `matching_method` erweitern - In beiden Modi setzen, sodass Folgewerkzeuge das Schema korrekt interpretieren - _Requirements: 1.6, 9.5_ - [x] 7.3 Property-Test für persistierte ungekürzte Rationale schreiben - **Property 8: Ungekürzte Rationale wird persistiert** - Hypothesis `st.text(min_size=300)`, Cache-Eintrag muss exakt der LLM-Antwort entsprechen - **Validates: Requirements 8.6** - [x] 7.4 Property-Test für META-`matching_method` schreiben - **Property 11: META enthält das verwendete Verfahren** - Aus Tool-Output META-JSON parsen und Wert prüfen - **Validates: Requirements 1.6, 9.5** - [x] 7.5 Property-Test für Score-Modus-Abwärtskompatibilität schreiben - **Property 10: Score-Modus ist abwärtskompatibel** - Tool-Aufruf ohne `matching_method` bzw. mit `"score"`: Persistiertes Payload-Schema entspricht weiter dem bisherigen (Felder `role_score`, `competence_score`, `overall_score`; kein `rationale`) - **Validates: Requirements 1.2, 1.3, 7.3** - [x] 8. Tabellenausgabe mit `Begründung`-Spalte und Rationale-Formatierung - [x] 8.1 Hilfsfunktion `_format_rationale_for_table` implementieren - Pipes (`|`) → `/`, Carriage Return / Linefeed → Leerzeichen - Whitespace zusammenfalten - Bei > 280 Zeichen kürzen und mit `…` abschließen - _Requirements: 8.4, 8.5_ - [x] 8.2 Ausgabe-Tabellen für `find_matching_capacities` (LLM-Modus) anpassen - Spalten: `ID | Owner | Role | Competences | Availability | Category | Begründung` - **Keine** Spalten `Role Score`, `Competence Score`, `Overall Score` - Pro Zeile gekürzte Rationale via `_format_rationale_for_table` - Leere Ergebnistabelle weiterhin korrekt rendern (Header-Konsistenz) - _Requirements: 7.1, 7.2, 8.1, 8.2, 8.3, 8.4, 8.5_ - [x] 8.3 Ausgabe-Tabellen für `find_matching_tasks` (LLM-Modus) anpassen - Spalten: `task_id | Title | Required Competences | Availability | Category | Begründung` - Verhalten analog 8.2 - _Requirements: 7.1, 7.2, 8.1, 8.2, 8.3, 8.4, 8.5_ - [x] 8.4 Summary-Tabelle und Errors-Block einbauen - Summary bleibt in beiden Modi identisch (Counter je Kategorie) - Wenn `errors` nicht leer: zusätzlicher Markdown-Block `## Errors` nach der Ergebnistabelle - _Requirements: 5.7, 6.7, 7.5_ - [x] 8.5 Score-Modus-Tabelle unverändert lassen (Regression) - Bestehende Spalten `Role Score`, `Competence Score`, `Overall Score`, `Category` bleiben - Keine `Begründung`-Spalte - _Requirements: 7.3_ - [x] 8.6 Property-Test für `_format_rationale_for_table` schreiben - **Property 7: Tabellen-Rationale ist gültiges Markdown und längenbegrenzt** - Hypothesis erzeugt Strings inkl. `|`, `\n`, sehr lang - Output ≤ 280 Zeichen, weder `|` noch Zeilenumbrüche, `…`-Suffix genau dann, wenn normalisierte Eingabe > 280 Zeichen war - **Validates: Requirements 8.4, 8.5** - [x] 9. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 10. Anpassungen für `get_results_by_category` - [x] 10.1 Hilfsfunktion `_format_results_table(items, *, search_type, matching_method, ref_start, ref_end)` einführen - Routing nach `matching_method`: Score-Tabelle (bisher) oder LLM-Tabelle mit `Begründung`-Spalte - Wiederverwendbar in `find_matching_capacities`, `find_matching_tasks` und `get_results_by_category` - _Requirements: 7.1, 7.2, 8.1, 8.2, 9.2_ - [x] 10.2 `get_results_by_category` an LLM-Modus anpassen - `matching_method` aus persistiertem SearchEntry lesen - Im LLM-Modus Tabelle ohne Score-Spalten und mit `Begründung`-Spalte rendern - Im Score-Modus unverändertes Verhalten - META-JSON enthält `matching_method` - _Requirements: 9.1, 9.2, 9.5_ - [x] 10.3 Docstring für `get_results_by_category` aktualisieren - Hinweis auf modusabhängige Spalten (`Begründung` im LLM-Modus) - _Requirements: 12.1_ - [x] 11. Anpassungen für `filter_search_results` - [x] 11.1 Modus-Erkennung über persistiertes `matching_method` - Im LLM-Modus bestehende Filter (Rollen-, Kompetenz-, Verfügbarkeits-, Aufgaben-Text-/Kompetenzfilter) weiterhin anwenden - Score-bezogenen Filter `min_similarity` ignorieren und in `Applied Filters` einen Hinweis aufnehmen (`min_similarity (ignored: not applicable in llm_fulltext mode)`) - _Requirements: 9.3, 9.4_ - [x] 11.2 Sortierung in `filter_search_results` modusabhängig - Score-Modus: bestehende Sortierung nach `overall_score` - LLM-Modus: stabile Sortierung nach `(category_rank, item_id)` - _Requirements: 5.8, 6.8, 9.3_ - [x] 11.3 Tabellen-Rendering in `filter_search_results` an `_format_results_table` anbinden - Verwendung der neuen Hilfsfunktion (siehe 10.1) für konsistente Spalten - META-JSON enthält `matching_method` - _Requirements: 9.2, 9.5_ - [x] 11.4 Docstring für `filter_search_results` aktualisieren - Hinweis, dass `min_similarity` im LLM-Modus ignoriert wird - _Requirements: 12.1_ - [x] 11.5 Unit-Tests für `get_results_by_category` und `filter_search_results` in beiden Modi - LLM-Modus: `Begründung`-Spalte vorhanden, `min_similarity` ignoriert mit Hinweis - Score-Modus: bestehendes Verhalten unverändert (Regression) - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5_ - [x] 12. Server-Wiring und Integrationstests - [x] 12.1 `LlmFulltextMatcher` in `build_server` instanziieren - Konstruktor mit `db`, `client` (`AzureOpenAIClient`) verdrahten - Komponente an MCP-Tool-Handler weiterreichen - _Requirements: 1.4, 5.1, 6.1_ - [x] 12.2 End-to-End-Integrationstests mit gemocktem LLM-Client und gemocktem `DBClient` - `find_matching_capacities` und `find_matching_tasks` mit `matching_method="llm_fulltext"` - Verfügbarkeitsfilter im LLM-Modus identisch zum Score-Modus - SearchCache enthält `matching_method`, ungekürzte `rationale`, `errors`-Liste - _Requirements: 5.1, 5.2, 6.1, 6.2, 7.4, 8.6, 9.5_ - [x] 12.3 Tabellen-Snapshot-Test für Header in beiden Modi - Score-Modus-Header unverändert - LLM-Modus-Header endet auf `... | Category | Begründung` - _Requirements: 7.1, 8.1, 8.2_ - [x] 13. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 14. Dokumentation aktualisieren - [x] 14.1 `docs/architecture.md` erweitern - `LLM_Fulltext_Matcher` als Komponente in der Business-Logic-Layer beschreiben (Eingaben, Ausgaben, externe Abhängigkeit Azure OpenAI Chat Completion) - Zusätzliche Datenquellen (`teamlandkarte_v_capacities_latest.description`, `teamlandkarte_v_capacity_certificates_latest`, `teamlandkarte_v_capacity_references_latest`, `teamlandkarte_v_partners_latest.{id, name}`) im Schema-Verifikationsabschnitt aufführen - Join `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id` und Übernahme der Spalte `name` als Partner_Name dokumentieren - Tool-Surface-Tabelle um Parameter `matching_method` (Wertebereich `score`/`llm_fulltext`) ergänzen - Runtime-View für beide Suchrichtungen um den LLM-Volltext-Pfad erweitern - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_ - [x] 14.2 `README.md` erweitern - Quick-Start- und Usage-Abschnitt: Auswahl zwischen `score` und `llm_fulltext` per Tool-Parameter - Hinweis: Im LLM-Volltext-Modus keine numerischen Scores; stattdessen Spalte `Begründung` - Zusätzliche Datenbank-Views auflisten, die der Server im LLM-Volltext-Modus liest, einschließlich `teamlandkarte_v_partners_latest` mit Hinweis auf den LEFT JOIN über `partner_id` in der Referenz-Abfrage - Default-Konfiguration `[matching].default_method` - _Requirements: 11.6, 11.7, 11.8_ - [x] 14.3 `.github/agents/teamlandkarte_agent.md` aktualisieren - Beschreibung beider Verfahren `score` und `llm_fulltext` (Existenz und Zweck) - Pflicht-Frage nach `matching_method` vor `find_matching_capacities`/`find_matching_tasks`, sofern nicht aus dem Verlauf bekannt - Skills/Workflows: Aufruf der Tools mit zusätzlichem Parameter `matching_method` - Rolle der Spalte `Begründung` und Hinweis, dass im LLM-Modus keine numerischen Scores erscheinen - Bestehender Bestätigungs-Workflow (`show_pending_requirements`, `confirm_requirements`) bleibt für beide Verfahren gleich - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_ - [x] 14.4 `.kiro/agents/teamlandkarte.md` analog zu 14.3 aktualisieren - Inhaltliche Gleichheit zum GitHub-Pendant sicherstellen - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_ - [x] 14.5 Beispiele und Mini-Walkthroughs in README/Architektur ergänzen - Beispielausgabe einer LLM-Volltext-Tabelle inkl. Spalte `Begründung` - Beispielhafte META-JSON-Ausgabe mit `matching_method` - _Requirements: 11.5, 11.6_ - [x] 15. Final checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. ## Notes - Tasks markiert mit `*` sind optional und können für einen schnelleren MVP übersprungen werden. - Property-Based Tests verwenden Hypothesis mit `@settings(max_examples=100)` (mindestens 100 Iterationen pro Property) und werden als `# Feature: llm-fulltext-matching, Property {N}: {title}` getaggt. - Unit- und Integrationstests verwenden `pytest` (Run-once, kein Watch-Modus); der `AzureOpenAIClient` wird stets gemockt. - Tasks referenzieren explizit Anforderungen aus `requirements.md` zur lückenlosen Nachverfolgbarkeit. - Die Implementierungssprache ist Python (bestehende Codebase). - Score-Modus bleibt vollständig abwärtskompatibel; nur additive Erweiterungen am Payload und am META-JSON.