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:
@@ -0,0 +1 @@
|
||||
{"specId": "f4477224-78a1-4ce4-a5fc-689c61620b81", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,798 @@
|
||||
# Design: LLM-Volltext-Matching als zweites Verfahren
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Design beschreibt die Einführung eines zweiten Matching-Verfahrens neben dem bestehenden Score-basierten Matching: einen **LLM-basierten Volltext-Vergleich** (`llm_fulltext`), der Kapazitäten und Aufgaben anhand von ganzen Profiltexten bewertet und jedes Ergebnis direkt einer der bestehenden Kategorien (`Top`, `Good`, `Partial`, `Low`, `Irrelevant`) zuordnet. Es gibt keine numerischen Scores mehr im neuen Modus, dafür eine Begründung (Rationale) pro Treffer.
|
||||
|
||||
Das neue Verfahren erweitert den Datenraum um:
|
||||
|
||||
- die Capacity-Beschreibung (`teamlandkarte_v_capacities_latest.description`)
|
||||
- die Capacity-Zertifikate (`teamlandkarte_v_capacity_certificates_latest.description`)
|
||||
- die Capacity-Referenzen (`teamlandkarte_v_capacity_references_latest.projects`)
|
||||
- den Partner-Namen je Referenz aus `teamlandkarte_v_partners_latest.name`, verknüpft über `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id`
|
||||
|
||||
Eine Capacity_Reference besteht damit aus dem Tupel (`partner_name`, `projects`); `partner_name` kann leer sein, wenn `partner_id` `NULL` ist oder der Join keinen Treffer liefert.
|
||||
|
||||
Auf Aufgabenseite werden die bestehenden Felder (`title`, `description`, `skills`) genutzt.
|
||||
|
||||
Der Nutzer wählt das Verfahren über den neuen Tool-Parameter `matching_method` (`score` | `llm_fulltext`); der Standardwert ist konfigurierbar (`config.toml: matching.default_method`). Beide Verfahren teilen sich Suchcache, Pagination, Filtertools und Bestätigungs-Workflow.
|
||||
|
||||
## Architektur
|
||||
|
||||
### Komponenten-Überblick (nachher)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[MCP Tool: find_matching_capacities] -->|matching_method| R{Routing}
|
||||
B[MCP Tool: find_matching_tasks] -->|matching_method| R
|
||||
R -->|score| M[Matcher BM25+LLM Role]
|
||||
R -->|llm_fulltext| F[LLM_Fulltext_Matcher]
|
||||
|
||||
F --> P1[Profile Builder<br/>Capacity_Profile + Task_Profile]
|
||||
F --> CL[AzureOpenAIClient.chat_completion]
|
||||
F --> SC[(SearchCache<br/>category + rationale)]
|
||||
|
||||
P1 --> DB[(TrinoClient)]
|
||||
DB --> V1[teamlandkarte_v_capacities_latest.description]
|
||||
DB --> V2[teamlandkarte_v_capacity_certificates_latest]
|
||||
DB --> V3[teamlandkarte_v_capacity_references_latest]
|
||||
V3 -->|partner_id = id| V4[teamlandkarte_v_partners_latest.name]
|
||||
|
||||
M --> SC
|
||||
```
|
||||
|
||||
Der `LLM_Fulltext_Matcher` ist eine neue Komponente in der Business-Logic-Layer und wird beim Server-Start instanziiert. Er greift auf den bestehenden `AzureOpenAIClient`, den `DBClient` und den `SearchCache` zu. Der bestehende `Matcher` bleibt unverändert; die Auswahl erfolgt im MCP-Tool.
|
||||
|
||||
### Runtime: find_matching_capacities (LLM-Volltext)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Server as MCP_Server
|
||||
participant FM as LLM_Fulltext_Matcher
|
||||
participant DB as TrinoClient
|
||||
participant LLM as AzureOpenAIClient
|
||||
|
||||
Client->>Server: find_matching_capacities(role_name, competences, dates, matching_method="llm_fulltext")
|
||||
Server->>Server: Validate matching_method, confirmation gate
|
||||
Server->>DB: get_all_capacities_with_competences()
|
||||
Server->>Server: Vorfilter (Verfügbarkeit) wie bei score
|
||||
Server->>FM: match_capacities(task_profile, filtered_capacities)
|
||||
FM->>DB: batch_get_capacity_descriptions(ids)
|
||||
FM->>DB: batch_get_capacity_certificates(ids)
|
||||
FM->>DB: batch_get_capacity_references(ids)
|
||||
FM->>FM: build Task_Profile + Capacity_Profile pro Kandidat
|
||||
loop pro Kapazität
|
||||
FM->>LLM: chat_completion(system_prompt, user_prompt)
|
||||
LLM-->>FM: {"category": "...", "rationale": "..."}
|
||||
FM->>FM: validate(category) sonst Irrelevant + Hinweis
|
||||
end
|
||||
FM-->>Server: {by_category, errors}
|
||||
Server->>SC: store_search(results=payload mit matching_method)
|
||||
Server-->>Client: Markdown (Summary + Begründungs-Spalte)
|
||||
```
|
||||
|
||||
### Runtime: find_matching_tasks (LLM-Volltext)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Server as MCP_Server
|
||||
participant FM as LLM_Fulltext_Matcher
|
||||
participant DB as TrinoClient
|
||||
participant LLM as AzureOpenAIClient
|
||||
|
||||
Client->>Server: find_matching_tasks(capacity_id, matching_method="llm_fulltext")
|
||||
Server->>DB: get_capacity_by_id(capacity_id)
|
||||
Server->>DB: get_open_tasks(limit=0)
|
||||
Server->>FM: match_tasks(capacity_profile, tasks)
|
||||
FM->>DB: get_capacity_description(capacity_id)
|
||||
FM->>DB: get_capacity_certificates(capacity_id)
|
||||
FM->>DB: get_capacity_references(capacity_id)
|
||||
FM->>FM: build Capacity_Profile + Task_Profile pro Aufgabe
|
||||
loop pro Aufgabe
|
||||
FM->>LLM: chat_completion(system_prompt, user_prompt)
|
||||
LLM-->>FM: {"category": "...", "rationale": "..."}
|
||||
end
|
||||
FM-->>Server: {by_category, errors}
|
||||
Server->>SC: store_search(results)
|
||||
Server-->>Client: Markdown (Summary + Begründungs-Spalte)
|
||||
```
|
||||
|
||||
## Komponenten und Schnittstellen
|
||||
|
||||
### 1. DBClient (`database/types.py`) – neue Methoden
|
||||
|
||||
Eine Capacity_Reference wird als strukturierter Eintrag mit den Feldern `partner_name` und `projects` modelliert. `partner_name` kann eine leere Zeichenkette sein (NULL `partner_id` oder Join-Mismatch, vgl. Anforderung 2.8); `projects` enthält den Inhalt der Spalte `projects`.
|
||||
|
||||
```python
|
||||
class CapacityReferenceRow(TypedDict):
|
||||
partner_name: str # leer, wenn partner_id NULL ist oder kein Partner gefunden wurde
|
||||
projects: str
|
||||
|
||||
|
||||
class DBClient(Protocol):
|
||||
# ... bestehende Methoden ...
|
||||
|
||||
def get_capacity_description(self, capacity_id: int | str) -> str | None:
|
||||
"""Return description from teamlandkarte_v_capacities_latest."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_capacity_certificates(self, capacity_id: int | str) -> list[str]:
|
||||
"""Return certificate descriptions joined via capacity_id."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_capacity_references(
|
||||
self, capacity_id: int | str
|
||||
) -> list[CapacityReferenceRow]:
|
||||
"""Return reference entries joined via capacity_id.
|
||||
|
||||
Each entry contains the project text (`projects`) and the partner
|
||||
name from `teamlandkarte_v_partners_latest` (joined via
|
||||
`partner_id = id`). `partner_name` may be an empty string if
|
||||
`partner_id` is NULL or no matching partner exists.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def batch_get_capacity_descriptions(
|
||||
self, capacity_ids: list[int | str]
|
||||
) -> dict[str, str | None]:
|
||||
"""Batch variant: one SELECT for many capacity_ids."""
|
||||
raise NotImplementedError
|
||||
|
||||
def batch_get_capacity_certificates(
|
||||
self, capacity_ids: list[int | str]
|
||||
) -> dict[str, list[str]]:
|
||||
"""Batch variant: one SELECT, grouped per capacity_id."""
|
||||
raise NotImplementedError
|
||||
|
||||
def batch_get_capacity_references(
|
||||
self, capacity_ids: list[int | str]
|
||||
) -> dict[str, list[CapacityReferenceRow]]:
|
||||
"""Batch variant: one SELECT with LEFT JOIN on partners,
|
||||
grouped per capacity_id. `partner_name` may be empty per entry."""
|
||||
raise NotImplementedError
|
||||
```
|
||||
|
||||
Schlüssel der Batch-Rückgaben sind die `capacity_id` als String, damit die Aufrufer unabhängig vom Quelltyp (`int`/`str`) deterministisch zugreifen können.
|
||||
|
||||
### 2. TrinoClient (`database/trino_client.py`)
|
||||
|
||||
Alle neuen Methoden nutzen `_ensure_select_only`, den Pool und `_retry`. Beispiel für die Batch-Variante:
|
||||
|
||||
```python
|
||||
def batch_get_capacity_descriptions(
|
||||
self, capacity_ids: list[int | str]
|
||||
) -> dict[str, str | None]:
|
||||
if not capacity_ids:
|
||||
return {}
|
||||
placeholders = ", ".join(["?"] * len(capacity_ids))
|
||||
query = (
|
||||
"SELECT capacity_id, description "
|
||||
"FROM teamlandkarte_v_capacities_latest "
|
||||
f"WHERE capacity_id IN ({placeholders})"
|
||||
)
|
||||
_ensure_select_only(query)
|
||||
params = [str(c) for c in capacity_ids]
|
||||
|
||||
def _run():
|
||||
with self._cursor() as cur:
|
||||
cur.execute(query, params)
|
||||
return cur.fetchall()
|
||||
|
||||
rows = self._retry(_run)
|
||||
out: dict[str, str | None] = {str(c): None for c in capacity_ids}
|
||||
for row in rows:
|
||||
cap_id = str(row[0])
|
||||
desc = row[1]
|
||||
out[cap_id] = (desc.strip() if isinstance(desc, str) and desc.strip() else None)
|
||||
return out
|
||||
```
|
||||
|
||||
`batch_get_capacity_certificates` ist analog aufgebaut, gruppiert n:1 (`defaultdict(list)`), filtert leere Strings und liefert stabile, fehlende IDs als leere Liste zurück.
|
||||
|
||||
`batch_get_capacity_references` führt zusätzlich einen `LEFT JOIN` auf `teamlandkarte_v_partners_latest` aus, damit der Partner-Name in derselben Abfrage zurückgegeben wird (Anforderung 2.4: keine zusätzliche SQL-Abfrage für den Partner-Join):
|
||||
|
||||
```python
|
||||
def batch_get_capacity_references(
|
||||
self, capacity_ids: list[int | str]
|
||||
) -> dict[str, list[CapacityReferenceRow]]:
|
||||
if not capacity_ids:
|
||||
return {}
|
||||
placeholders = ", ".join(["?"] * len(capacity_ids))
|
||||
query = (
|
||||
"SELECT r.capacity_id, r.projects, COALESCE(p.name, '') AS partner_name "
|
||||
"FROM teamlandkarte_v_capacity_references_latest r "
|
||||
"LEFT JOIN teamlandkarte_v_partners_latest p ON r.partner_id = p.id "
|
||||
f"WHERE r.capacity_id IN ({placeholders})"
|
||||
)
|
||||
_ensure_select_only(query)
|
||||
params = [str(c) for c in capacity_ids]
|
||||
|
||||
def _run():
|
||||
with self._cursor() as cur:
|
||||
cur.execute(query, params)
|
||||
return cur.fetchall()
|
||||
|
||||
rows = self._retry(_run)
|
||||
out: dict[str, list[CapacityReferenceRow]] = {str(c): [] for c in capacity_ids}
|
||||
for row in rows:
|
||||
cap_id = str(row[0])
|
||||
projects = row[1]
|
||||
partner_name = row[2] or "" # NULL/COALESCE → ""
|
||||
if not (isinstance(projects, str) and projects.strip()):
|
||||
continue
|
||||
out.setdefault(cap_id, []).append(
|
||||
{"partner_name": str(partner_name), "projects": projects.strip()}
|
||||
)
|
||||
return out
|
||||
```
|
||||
|
||||
Hinweise:
|
||||
|
||||
- `COALESCE(p.name, '')` deckt sowohl NULL `partner_id` (kein Join-Match) als auch existierende Partner ohne Namen ab und garantiert einen leeren String statt `None` (Anforderung 2.8).
|
||||
- Der LEFT JOIN ist Bestandteil derselben Referenz-Abfrage; es entsteht keine zusätzliche SQL-Abfrage. Damit bleibt das Drei-Abfragen-Limit pro Quelle (Beschreibung, Zertifikate, Referenzen) erhalten (Anforderung 2.4).
|
||||
|
||||
Alle Methoden führen genau **eine** SQL-Abfrage pro Quelle aus (Anforderung 2.4) und nutzen Parameter-Bindung gegen Injection.
|
||||
|
||||
### 3. Datenmodelle: Capacity_Profile und Task_Profile
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class CapacityReferenceEntry:
|
||||
"""Strukturierter Referenz-Eintrag im CapacityProfile.
|
||||
|
||||
`partner_name` darf leer sein (NULL `partner_id` oder Join-Mismatch);
|
||||
in diesem Fall wird die Referenz dennoch im Profil geführt und
|
||||
ausschließlich `projects` in der Serialisierung dargestellt.
|
||||
"""
|
||||
|
||||
partner_name: str # leer, wenn nicht zuordenbar
|
||||
projects: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapacityProfile:
|
||||
id: str
|
||||
owner_name: str
|
||||
role_name: str
|
||||
competences: list[str]
|
||||
description: str # leer wenn None/leer in DB
|
||||
references: list[CapacityReferenceEntry]
|
||||
certificates: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskProfile:
|
||||
id: str
|
||||
title: str
|
||||
description: str
|
||||
skills: list[str] # gesuchte Kompetenzen
|
||||
```
|
||||
|
||||
Beide Profile haben deterministische Serialisierungen (siehe `serialize`). Leere Felder erzeugen leere Zeichenkette/leere Liste, das Profil wird nie verworfen (Anforderung 3.2/4.2).
|
||||
|
||||
### 4. LLM_Fulltext_Matcher (`matching/llm_fulltext_matcher.py`)
|
||||
|
||||
```python
|
||||
class LlmFulltextMatcher:
|
||||
"""LLM-based full-text matching between capacities and tasks."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
db: DBClient,
|
||||
client: AzureOpenAIClient,
|
||||
rationale_max_chars: int = 280,
|
||||
) -> None:
|
||||
self._db = db
|
||||
self._client = client
|
||||
self._rationale_max_chars = rationale_max_chars
|
||||
|
||||
async def match_capacities(
|
||||
self,
|
||||
*,
|
||||
task_profile: TaskProfile,
|
||||
capacities: list[Capacity],
|
||||
) -> "LlmFulltextResult": ...
|
||||
|
||||
async def match_tasks(
|
||||
self,
|
||||
*,
|
||||
capacity_profile: CapacityProfile,
|
||||
tasks: list[Task],
|
||||
) -> "LlmFulltextResult": ...
|
||||
```
|
||||
|
||||
Ergebnistyp:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class LlmFulltextItem:
|
||||
item_id: str # capacity_id oder task_id
|
||||
category: str # Top|Good|Partial|Low|Irrelevant
|
||||
rationale: str # ungekürzt, von LLM
|
||||
raw: dict # ursprüngliches Item-Payload für Cache (asdict(Capacity)/Task)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LlmFulltextError:
|
||||
item_id: str
|
||||
error: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LlmFulltextResult:
|
||||
by_category: dict[str, list[LlmFulltextItem]]
|
||||
errors: list[LlmFulltextError]
|
||||
```
|
||||
|
||||
#### Profil-Serialisierung (deterministisch)
|
||||
|
||||
```python
|
||||
def _format_reference(entry: CapacityReferenceEntry) -> str:
|
||||
"""Format a single reference deterministically.
|
||||
|
||||
- `Partner: <name> – Projekte: <projects>` wenn partner_name nicht leer
|
||||
- `Projekte: <projects>` wenn partner_name leer (kein Platzhalter)
|
||||
"""
|
||||
projects = entry.projects.strip()
|
||||
if entry.partner_name:
|
||||
return f"Partner: {entry.partner_name} – Projekte: {projects}"
|
||||
return f"Projekte: {projects}"
|
||||
|
||||
|
||||
def serialize_capacity_profile(p: CapacityProfile) -> str:
|
||||
refs = [_format_reference(r) for r in p.references]
|
||||
return "\n".join([
|
||||
f"Rolle: {p.role_name}",
|
||||
"Kompetenzen: " + ", ".join(p.competences),
|
||||
f"Beschreibung: {p.description}",
|
||||
"Referenzen:" + ("\n- " + "\n- ".join(refs) if refs else " (keine)"),
|
||||
"Zertifikate:" + ("\n- " + "\n- ".join(p.certificates) if p.certificates else " (keine)"),
|
||||
])
|
||||
|
||||
|
||||
def serialize_task_profile(p: TaskProfile) -> str:
|
||||
return "\n".join([
|
||||
f"Titel: {p.title}",
|
||||
f"Beschreibung: {p.description}",
|
||||
"Gesuchte Kompetenzen: " + ", ".join(p.skills),
|
||||
])
|
||||
```
|
||||
|
||||
Die Reihenfolge der Felder ist über alle Profile konstant, und die Reihenfolge der Referenzen entspricht der Reihenfolge aus dem DBClient (DB-stabil sortiert), sodass auch `partner_name` deterministisch erscheint (Anforderung 3.4 / 3.7 / 4.4). Ist `partner_name` leer, entfällt das Partner-Token in der Ausgabe; die Referenz selbst bleibt erhalten (Anforderung 3.4 / 3.6).
|
||||
|
||||
#### LLM-Prompt-Design
|
||||
|
||||
System-Prompt (deutschsprachig, deterministisch):
|
||||
|
||||
```text
|
||||
Du bist ein erfahrener Personal- und Skill-Matcher der DB Systel.
|
||||
Du erhältst ein Aufgabenprofil und ein Kapazitätsprofil.
|
||||
Bewerte, wie gut die Kapazität zur Aufgabe passt, und wähle GENAU EINE Kategorie aus:
|
||||
- Top: passt fachlich und in den Kompetenzen praktisch vollständig
|
||||
- Good: passt gut, mit kleinen Lücken
|
||||
- Partial: passt teilweise, mehrere relevante Lücken
|
||||
- Low: schwacher Bezug, nur einzelne Berührungspunkte
|
||||
- Irrelevant: kein erkennbarer fachlicher Bezug
|
||||
|
||||
Begründe deine Wahl in 1-2 prägnanten deutschen Sätzen
|
||||
(maximal ~280 Zeichen, keine Aufzählungspunkte, keine Zeilenumbrüche).
|
||||
Antworte AUSSCHLIESSLICH als gültiges JSON-Objekt mit den Feldern:
|
||||
{"category": "<Top|Good|Partial|Low|Irrelevant>", "rationale": "<Begründung>"}
|
||||
```
|
||||
|
||||
User-Prompt (Beispiel Aufgabe→Kapazität):
|
||||
|
||||
```text
|
||||
=== Aufgabe ===
|
||||
<serialize_task_profile(...)>
|
||||
|
||||
=== Kapazität ===
|
||||
ID: <capacity.id>
|
||||
Owner: <capacity.owner_name>
|
||||
<serialize_capacity_profile(...)>
|
||||
```
|
||||
|
||||
Die Antwort wird über `chat_completion(system, user)` (bereits mit `response_format=json_object`) angefordert und mit `json.loads` geparst.
|
||||
|
||||
#### Kategorie-Normalisierung
|
||||
|
||||
```python
|
||||
_ALLOWED = ("Top", "Good", "Partial", "Low", "Irrelevant")
|
||||
_ALIAS = {x.lower(): x for x in _ALLOWED}
|
||||
|
||||
def normalize_category(value: str | None) -> tuple[str, bool]:
|
||||
"""Return (category, is_valid). Invalid → ("Irrelevant", False)."""
|
||||
if not isinstance(value, str):
|
||||
return "Irrelevant", False
|
||||
norm = _ALIAS.get(value.strip().lower())
|
||||
if norm is None:
|
||||
return "Irrelevant", False
|
||||
return norm, True
|
||||
```
|
||||
|
||||
Bei ungültiger Kategorie wird das Item nach `Irrelevant` einsortiert und in der Rationale wird angehängt: `"[Hinweis: ungültige LLM-Kategorie: <wert>]"` (Anforderung 5.6 / 6.6).
|
||||
|
||||
#### Sortierung
|
||||
|
||||
Innerhalb jeder Kategorie werden die Items deterministisch nach `item_id` aufsteigend (lexikographisch als String) sortiert (Anforderung 5.8 / 6.8).
|
||||
|
||||
### 5. MCP_Server – erweiterte Tool-Signaturen
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def find_matching_capacities(
|
||||
role_name: str,
|
||||
competences: list[str],
|
||||
date_start: Optional[str] = None,
|
||||
date_end: Optional[str] = None,
|
||||
matching_method: Optional[str] = None,
|
||||
) -> str: ...
|
||||
|
||||
@mcp.tool()
|
||||
async def find_matching_tasks(
|
||||
capacity_id: int | str,
|
||||
matching_method: Optional[str] = None,
|
||||
) -> str: ...
|
||||
```
|
||||
|
||||
Validierung von `matching_method`:
|
||||
|
||||
```python
|
||||
_ALLOWED_METHODS = ("score", "llm_fulltext")
|
||||
|
||||
def _resolve_method(value: Optional[str]) -> str:
|
||||
if value is None:
|
||||
return cfg.matching.default_method
|
||||
norm = str(value).strip().lower()
|
||||
if norm not in _ALLOWED_METHODS:
|
||||
raise ValueError(
|
||||
f"Invalid matching_method: {value!r}. "
|
||||
f"Allowed: {', '.join(_ALLOWED_METHODS)}"
|
||||
)
|
||||
return norm
|
||||
```
|
||||
|
||||
Bei ungültigem Wert gibt das Tool eine Fehlermeldung zurück und führt keine Suche aus (Anforderung 1.5).
|
||||
|
||||
### 6. Persistenz im SearchCache
|
||||
|
||||
Das Ergebnis-Payload behält den bestehenden Aufbau (`search_type`, `reference`, `summary`, `by_category`), wird aber pro Modus unterschiedlich befüllt:
|
||||
|
||||
```python
|
||||
results_payload = {
|
||||
"search_type": "capacity_search", # oder "task_search"
|
||||
"matching_method": "llm_fulltext", # NEU – im Score-Modus "score"
|
||||
"reference": {...},
|
||||
"summary": {...}, # Counter pro Kategorie
|
||||
"by_category": {
|
||||
"Top": [
|
||||
{
|
||||
**asdict(capacity), # bzw. Task-Felder
|
||||
"category": "Top",
|
||||
"rationale": "<unverkürzt>",
|
||||
# KEIN role_score / competence_score / overall_score
|
||||
},
|
||||
...
|
||||
],
|
||||
...
|
||||
},
|
||||
"errors": [ # nur im LLM-Modus, sonst weglassen
|
||||
{"item_id": "...", "error": "..."}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Im Score-Modus bleibt das bisherige Schema (mit Score-Feldern, ohne `rationale`/`errors`) unverändert. Damit ist das Schema rückwärtskompatibel: bestehende Filter- und Pagination-Tools lesen `by_category` weiterhin korrekt.
|
||||
|
||||
### 7. Ausgabe-Tabellen
|
||||
|
||||
#### Score-Modus (unverändert)
|
||||
|
||||
Spalten: `ID | Owner | Role | Competences | Availability | Role Score | Competence Score | Overall Score | Category`.
|
||||
|
||||
#### LLM-Volltext-Modus
|
||||
|
||||
`find_matching_capacities`:
|
||||
|
||||
`ID | Owner | Role | Competences | Availability | Category | Begründung`
|
||||
|
||||
`find_matching_tasks`:
|
||||
|
||||
`task_id | Title | Required Competences | Availability | Category | Begründung`
|
||||
|
||||
Eine Hilfsfunktion kürzt Rationales konsistent:
|
||||
|
||||
```python
|
||||
def _format_rationale_for_table(rationale: str, max_chars: int = 280) -> str:
|
||||
text = (rationale or "").replace("|", "/").replace("\r", " ").replace("\n", " ")
|
||||
text = " ".join(text.split()) # collapse whitespace
|
||||
if len(text) > max_chars:
|
||||
text = text[: max_chars - 1].rstrip() + "…"
|
||||
return text
|
||||
```
|
||||
|
||||
- Pipes (`|`) werden zu `/`, Zeilenumbrüche zu Leerzeichen ersetzt (Anforderung 8.4).
|
||||
- Bei > 280 Zeichen wird gekürzt und mit `…` abgeschlossen (Anforderung 8.5).
|
||||
- Die ungekürzte Rationale steht im SearchCache (Anforderung 8.6).
|
||||
|
||||
Die Summary-Tabelle bleibt in beiden Modi gleich (Counter je Kategorie).
|
||||
|
||||
Im META-JSON jeder Tool-Antwort wird `matching_method` zusätzlich aufgenommen (Anforderung 1.6 / 9.5):
|
||||
|
||||
```json
|
||||
{"search_id": "...", "filter_id": null, "default_category": "Top", "matching_method": "llm_fulltext"}
|
||||
```
|
||||
|
||||
### 8. Filter- und Pagination-Tools
|
||||
|
||||
#### get_results_by_category
|
||||
|
||||
Erkennt das Modus-Schema am Feld `matching_method` im SearchEntry und rendert entweder die Score-Tabelle (bisheriges Verhalten) oder die LLM-Tabelle mit `Begründung`-Spalte. Das Routing kapselt eine neue Hilfsfunktion `_format_results_table(items, *, search_type, matching_method, ref_start, ref_end)`.
|
||||
|
||||
#### filter_search_results
|
||||
|
||||
Erkennt den Modus ebenfalls am persistierten `matching_method`. Im LLM-Volltext-Modus:
|
||||
|
||||
- Bestehende Filter (Rollen-, Kompetenz-, Verfügbarkeits-, Aufgaben-Text-/Kompetenzfilter) bleiben aktiv (Anforderung 9.3).
|
||||
- `min_similarity` wird ignoriert; in `Applied Filters` erscheint eine Zeile `min_similarity (ignored: not applicable in llm_fulltext mode)` (Anforderung 9.4).
|
||||
- Die Zwischensortierung erfolgt im LLM-Modus stabil nach `(category_rank, item_id)` statt nach `overall_score`.
|
||||
|
||||
### 9. Konfiguration (`config.py`, `config.toml`)
|
||||
|
||||
Neue Felder in `MatchingConfig`:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class MatchingConfig:
|
||||
# ... bestehende Felder ...
|
||||
default_method: str = "score"
|
||||
```
|
||||
|
||||
Validierung in `load_config`:
|
||||
|
||||
```python
|
||||
allowed = {"score", "llm_fulltext"}
|
||||
default_method = (raw.get("matching", {}).get("default_method") or "score").strip().lower()
|
||||
if default_method not in allowed:
|
||||
raise ConfigError(
|
||||
f"matching.default_method must be one of {sorted(allowed)}, "
|
||||
f"got: {default_method!r}"
|
||||
)
|
||||
```
|
||||
|
||||
`config.toml`:
|
||||
|
||||
```toml
|
||||
[matching]
|
||||
# Default method for new searches when callers do not pass matching_method.
|
||||
# Allowed: "score" (BM25 + LLM role similarity) or "llm_fulltext"
|
||||
# (LLM-based full-text matching with rationale).
|
||||
default_method = "score"
|
||||
```
|
||||
|
||||
Beim Start wird die Validierung als `ConfigError` (fail-fast) geworfen (Anforderung 12.4).
|
||||
|
||||
### 10. Agenten- und Dokumentationsanpassungen
|
||||
|
||||
- `.github/agents/teamlandkarte_agent.md` und `.kiro/agents/teamlandkarte.md`:
|
||||
- Beschreibung beider Verfahren (`score`, `llm_fulltext`).
|
||||
- Pflicht-Frage "Welches Verfahren soll verwendet werden?" vor `find_matching_capacities`/`find_matching_tasks`, falls nicht aus dem Verlauf bekannt.
|
||||
- Hinweis: Im LLM-Volltext-Modus keine numerischen Scores; stattdessen Spalte `Begründung`.
|
||||
- Bestätigungs-Workflow (`show_pending_requirements` → `confirm_requirements`) bleibt für beide Verfahren identisch.
|
||||
- `docs/architecture.md`:
|
||||
- Neue Komponente `LLM_Fulltext_Matcher` im Business-Logic-Diagramm und in der Komponentenbeschreibung.
|
||||
- Erweiterung der Schema-Verifikation um `teamlandkarte_v_capacities_latest.description`, `teamlandkarte_v_capacity_certificates_latest.{capacity_id, description}`, `teamlandkarte_v_capacity_references_latest.{capacity_id, partner_id, projects}` sowie `teamlandkarte_v_partners_latest.{id, name}` einschließlich der Join-Beziehung `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id`.
|
||||
- Tool-Surface-Tabelle mit neuem Parameter `matching_method`.
|
||||
- Runtime-View für beide Suchrichtungen ergänzt um den LLM-Volltext-Pfad.
|
||||
- `README.md`:
|
||||
- Quick Start: Verfahrenswahl per Tool-Parameter; Default-Konfiguration in `[matching].default_method`.
|
||||
- Hinweis "keine Score-Spalten im LLM-Modus, dafür `Begründung`".
|
||||
- Zusätzliche Datenbank-Views aufgelistet, einschließlich `teamlandkarte_v_partners_latest` mit Hinweis auf den LEFT JOIN über `partner_id` in der Referenz-Abfrage.
|
||||
|
||||
## Datenmodelle
|
||||
|
||||
### Übersicht der zusätzlichen DB-Felder
|
||||
|
||||
| Quelle | Spalte | Genutzt für |
|
||||
|---|---|---|
|
||||
| `teamlandkarte_v_capacities_latest` | `description` | `CapacityProfile.description` |
|
||||
| `teamlandkarte_v_capacity_certificates_latest` | `capacity_id`, `description` | `CapacityProfile.certificates` |
|
||||
| `teamlandkarte_v_capacity_references_latest` | `capacity_id`, `partner_id`, `projects` | `CapacityProfile.references[].projects` (Join-Schlüssel: `partner_id`) |
|
||||
| `teamlandkarte_v_partners_latest` | `id`, `name` | Partner_Name in `CapacityProfile.references[].partner_name` (LEFT JOIN über `partner_id = id`) |
|
||||
|
||||
### LLM-Antwortschema
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "Top|Good|Partial|Low|Irrelevant",
|
||||
"rationale": "Kurzbegründung in 1-2 Sätzen."
|
||||
}
|
||||
```
|
||||
|
||||
### Persistierte Item-Struktur (LLM-Modus)
|
||||
|
||||
Im LLM-Modus enthält ein gespeichertes Item dieselben Identitäts- und Verfügbarkeitsfelder wie im Score-Modus, plus `category` und `rationale`. Falls Referenzen für nachgelagerte Anzeige zusätzlich am Item gepuffert werden sollen, werden sie als Liste strukturierter Einträge mit den Feldern `partner_name` und `projects` abgelegt (gleiche Form wie in `CapacityProfile.references`); `partner_name` darf leer sein.
|
||||
|
||||
```python
|
||||
# capacity_search
|
||||
{
|
||||
"id": 12345,
|
||||
"owner_name": "...",
|
||||
"role_name": "...",
|
||||
"begin_date": "2025-03-01",
|
||||
"end_date": "2025-12-31",
|
||||
"competences": ["..."],
|
||||
# optional, falls Referenzen mitgepuffert werden:
|
||||
# "references": [{"partner_name": "...", "projects": "..."}, ...],
|
||||
"category": "Top",
|
||||
"rationale": "Volltext-Begründung des LLM ...",
|
||||
}
|
||||
|
||||
# task_search
|
||||
{
|
||||
"task_id": "00T...",
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"skills": ["..."],
|
||||
"required_competences": [],
|
||||
"start_date": "2025-04-01",
|
||||
"end_date": "2025-09-30",
|
||||
"category": "Good",
|
||||
"rationale": "Volltext-Begründung des LLM ...",
|
||||
}
|
||||
```
|
||||
|
||||
## 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: Profil-Serialisierung ist deterministisch und feldvollständig
|
||||
|
||||
*For any* `CapacityProfile` (bzw. `TaskProfile`) zwei wiederholte Aufrufe von `serialize_capacity_profile` (bzw. `serialize_task_profile`) liefern denselben String, und der String enthält jede Feldüberschrift (`Rolle:`, `Kompetenzen:`, `Beschreibung:`, `Referenzen:`, `Zertifikate:` bzw. `Titel:`, `Beschreibung:`, `Gesuchte Kompetenzen:`) in einer fixen Reihenfolge. Innerhalb des Abschnitts `Referenzen:` erscheinen die Einträge in derselben Reihenfolge wie in `references`, und für jeden Eintrag mit nicht-leerem `partner_name` ist der Partner-Name in der serialisierten Darstellung deterministisch enthalten.
|
||||
|
||||
**Validates: Requirements 3.3, 3.4, 3.6, 3.7, 4.3, 4.4**
|
||||
|
||||
### Property 2: Leere/None-Felder verwerfen das Profil nicht
|
||||
|
||||
*For any* `Capacity` (bzw. `Task`), bei dem ein Teil der Felder `None`, leerer String oder leere Liste ist, liefert der Profil-Builder ein `CapacityProfile`/`TaskProfile`, dessen leere Felder als leerer String bzw. leere Liste erscheinen, und dessen Serialisierung weiterhin alle Feldüberschriften enthält. Dies gilt insbesondere auch für Capacity_References mit leerem `partner_name`: Die Referenz wird nicht verworfen, sondern in `references` aufgenommen; lediglich das Partner-Token entfällt in der Serialisierung.
|
||||
|
||||
**Validates: Requirements 3.2, 3.4, 4.2**
|
||||
|
||||
### Property 2b: Referenzen mit leerem Partner-Name behalten projects, ohne Partner-Token
|
||||
|
||||
*For any* Liste von `CapacityReferenceEntry`-Werten, in der ein Teil der Einträge `partner_name == ""` hat, ist die serialisierte `Referenzen:`-Sektion so beschaffen, dass (a) die Anzahl der ausgegebenen Referenz-Zeilen gleich der Anzahl der Einträge mit nicht-leerem `projects` ist, (b) jede Zeile zu einem Eintrag mit leerem `partner_name` mit `Projekte:` beginnt und keinen Token `Partner:` enthält, und (c) jede Zeile zu einem Eintrag mit nicht-leerem `partner_name` sowohl `Partner: <name>` als auch `Projekte: <projects>` enthält.
|
||||
|
||||
**Validates: Requirements 3.4, 3.6, 2.8**
|
||||
|
||||
### Property 3: Kategorienormalisierung bildet auf erlaubte Menge ab
|
||||
|
||||
*For any* String-Eingabe gibt `normalize_category` ein Tupel `(category, is_valid)` zurück, bei dem `category` immer in `{"Top","Good","Partial","Low","Irrelevant"}` liegt; `is_valid` ist genau dann `True`, wenn die getrimmte, lower-case Eingabe einer dieser Kategorien (case-insensitive) entspricht.
|
||||
|
||||
**Validates: Requirements 5.3, 5.6, 6.3, 6.6**
|
||||
|
||||
### Property 4: Ungültige LLM-Kategorie wird auf Irrelevant gemappt
|
||||
|
||||
*For any* LLM-Antwort `{"category": X, "rationale": R}`, bei der `X` nicht in der erlaubten Menge liegt, wird das Item in der Kategorie `Irrelevant` einsortiert, und seine gespeicherte `rationale` enthält den ursprünglichen `R` sowie einen Hinweis auf die ungültige LLM-Antwort.
|
||||
|
||||
**Validates: Requirements 5.6, 6.6**
|
||||
|
||||
### Property 5: LLM-Fehler erscheinen in der Fehlerliste, nicht als Ergebnis
|
||||
|
||||
*For any* Liste von Kapazitäten (bzw. Aufgaben), bei denen der LLM-Aufruf für eine Teilmenge `S` fehlschlägt, ist jedes Item aus `S` in `result.errors` enthalten und kommt in keiner Kategorie von `result.by_category` vor; alle restlichen Items befinden sich in genau einer Kategorie.
|
||||
|
||||
**Validates: Requirements 5.7, 6.7**
|
||||
|
||||
### Property 6: Ergebnisse sind innerhalb jeder Kategorie deterministisch sortiert
|
||||
|
||||
*For any* `LlmFulltextResult` ist innerhalb jeder Kategorie die Liste der `item_id`-Werte streng aufsteigend (lexikographisch) sortiert; Permutationen der Eingabeliste verändern die Ausgabe-Reihenfolge nicht.
|
||||
|
||||
**Validates: Requirements 5.8, 6.8**
|
||||
|
||||
### Property 7: Tabellen-Rationale ist gültiges Markdown und längenbegrenzt
|
||||
|
||||
*For any* String `R`, hat `_format_rationale_for_table(R)` höchstens 280 Zeichen, enthält weder `|` noch Zeilenumbrüche, und ist genau dann mit `…` abgeschlossen, wenn die normalisierte Eingabe länger als 280 Zeichen war.
|
||||
|
||||
**Validates: Requirements 8.4, 8.5**
|
||||
|
||||
### Property 8: Ungekürzte Rationale wird persistiert
|
||||
|
||||
*For any* erfolgreich kategorisiertes Item ist die im SearchCache gespeicherte `rationale` exakt der vom LLM gelieferte (oder durch ungültige-Kategorie-Hinweis ergänzte) String, unabhängig von der für die Tabellendarstellung verwendeten gekürzten Form.
|
||||
|
||||
**Validates: Requirements 8.6**
|
||||
|
||||
### Property 9: matching_method-Validierung lehnt unbekannte Werte ab
|
||||
|
||||
*For any* Eingabe `matching_method`, die nach `strip().lower()` weder `"score"` noch `"llm_fulltext"` ist, gibt das MCP-Tool eine Fehlermeldung zurück, in der beide erlaubten Werte vorkommen, und führt weder DB- noch LLM-Aufrufe aus.
|
||||
|
||||
**Validates: Requirements 1.5**
|
||||
|
||||
### Property 10: Score-Modus ist abwärtskompatibel
|
||||
|
||||
*For any* Aufruf von `find_matching_capacities` bzw. `find_matching_tasks` ohne den Parameter `matching_method` (oder mit `"score"`) ist das im SearchCache persistierte Ergebnis-Payload schemagleich zum bisherigen Score-Payload (Felder `role_score`, `competence_score`, `overall_score` pro Item; kein `rationale`-Feld).
|
||||
|
||||
**Validates: Requirements 1.2, 1.3, 7.3**
|
||||
|
||||
### Property 11: META enthält das verwendete Verfahren
|
||||
|
||||
*For any* erfolgreichen Tool-Aufruf von `find_matching_capacities`/`find_matching_tasks` enthält das META-JSON in der Antwort einen Schlüssel `matching_method`, dessen Wert genau dem verwendeten Verfahren entspricht (`"score"` oder `"llm_fulltext"`).
|
||||
|
||||
**Validates: Requirements 1.6, 9.5**
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Szenario | Verhalten |
|
||||
|---|---|
|
||||
| `matching_method` ungültig | Tool gibt Fehlermeldung mit erlaubten Werten zurück, keine DB-/LLM-Aufrufe (Anforderung 1.5) |
|
||||
| `matching.default_method` ungültig | `ConfigError` beim Server-Start (fail-fast) |
|
||||
| LLM-Antwort kein gültiges JSON | Item landet in `errors` mit Meldung `"invalid JSON: <excerpt>"` |
|
||||
| LLM-Antwort enthält `category` außerhalb der Menge | Item in Kategorie `Irrelevant`; Rationale erhält Hinweis `[Hinweis: ungültige LLM-Kategorie: <wert>]` |
|
||||
| LLM-Aufruf wirft `AzureAPIError` / Timeout | Item landet in `errors` mit Meldung der Exception-Klasse + erstem Satz; kein Eintrag in `by_category` |
|
||||
| `description`/`certificates`/`references` in DB leer | Profil wird mit leerem String/leerer Liste gebaut, niemals verworfen (Anforderung 3.2/4.2) |
|
||||
| Capacity ohne Datenbank-Eintrag in der Batch-Antwort | Wird als `description=None` / `certificates=[]` / `references=[]` interpretiert |
|
||||
| `min_similarity` im Filter angewandt im LLM-Modus | Filter wird ignoriert; Eintrag in `Applied Filters` mit Hinweis (Anforderung 9.4) |
|
||||
| Rationale enthält `|` oder Zeilenumbruch | Wird vor Tabellenausgabe ersetzt; ungekürzte Original-Rationale bleibt im SearchCache |
|
||||
| Rationale länger als 280 Zeichen | In Tabellenausgabe gekürzt mit `…`; ungekürzt im SearchCache |
|
||||
|
||||
Die Fehlerliste wird im Tool-Output nach der Ergebnistabelle als zusätzlicher Markdown-Block (`## Errors`) ausgegeben, sofern nicht leer. Der MCP-Tool-Aufruf selbst schlägt nicht fehl, solange wenigstens ein Item kategorisiert werden konnte oder die Fehlerliste vollständig ist – damit ist das Verfahren robust gegen Einzelausfälle.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Property-Based Tests
|
||||
|
||||
Bibliothek: **Hypothesis**, mindestens 100 Iterationen pro Property. Jeder PBT-Test wird mit einem Kommentar getaggt:
|
||||
|
||||
```python
|
||||
# Feature: llm-fulltext-matching, Property {N}: {title}
|
||||
```
|
||||
|
||||
| Property | Test-Ansatz | Generatoren |
|
||||
|---|---|---|
|
||||
| 1: Profil-Serialisierung deterministisch | Generiere `CapacityProfile`/`TaskProfile` (inkl. `CapacityReferenceEntry` mit/ohne `partner_name`), rufe Serializer zweimal auf, prüfe Gleichheit + Vorkommen aller Feldüberschriften und Partner-Namen | `st.builds(...)` mit `st.text`/`st.lists(st.builds(CapacityReferenceEntry, ...))` |
|
||||
| 2: Leere Felder verwerfen Profil nicht | Generiere `Capacity`/`Task` mit zufällig leeren Feldern (inkl. Referenzen mit leerem `partner_name`), baue Profil, prüfe Vollständigkeit | Custom Capacity/Task strategy mit `st.one_of(st.none(), st.text())` und Referenz-Strategie mit `partner_name=st.one_of(st.just(""), st.text())` |
|
||||
| 2b: Leerer Partner-Name → kein Partner-Token, projects bleibt | Generiere Referenz-Listen mit gemischtem `partner_name`, serialisiere, prüfe Zeilenanzahl, Präfixe und Token-Vorkommen | `st.lists(st.builds(CapacityReferenceEntry, partner_name=st.one_of(st.just(""), st.text(min_size=1)), projects=st.text(min_size=1)))` |
|
||||
| 3: Kategorienormalisierung im Wertebereich | Generiere zufällige Strings (inkl. Aliase), prüfe Output ∈ erlaubte Menge | `st.text()` und `st.sampled_from([...alias variants...])` |
|
||||
| 4: Ungültige Kategorie → Irrelevant | Mocke LLM mit zufälliger ungültiger Kategorie, prüfe Item in `Irrelevant` und Hinweis in Rationale | `st.text().filter(lambda x: x.strip().lower() not in {"top",...})` |
|
||||
| 5: LLM-Fehler in errors | Mocke LLM, das per Bool-Strategie eine Exception wirft, prüfe Trennung errors / by_category | `st.lists(st.booleans())` |
|
||||
| 6: Sortierung deterministisch | Generiere Items, permutiere Eingabe, prüfe gleiche `by_category`-Reihenfolge | `st.permutations(...)` |
|
||||
| 7: Tabellen-Rationale-Format | Generiere Strings inkl. `|`, `\n`, sehr lang, prüfe Längen- und Zeichen-Constraints | `st.text(alphabet=st.characters(blacklist_categories=()))` |
|
||||
| 8: Ungekürzte Rationale persistiert | Generiere Rationale > 280 Zeichen, prüfe Cache-Eintrag == Original | `st.text(min_size=300)` |
|
||||
| 9: matching_method Validierung | Generiere zufällige Strings, mocke DB+LLM, prüfe Fehlerpfad ohne Aufrufe | `st.text()` |
|
||||
| 10: Score-Modus abwärtskompatibel | Vergleiche persistiertes Payload-Schema vor/nach Patch (snapshot-frei: Feldmenge je Item) | bestehende Capacity-Strategie |
|
||||
| 11: META-Schlüssel | Aus Tool-Output META-JSON parsen und prüfen | bestehende Capacity-Strategie |
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- **DBClient (Trino)**: Mock-Cursor-Tests für `get_capacity_description`, `get_capacity_certificates`, `get_capacity_references` und ihre Batch-Varianten. Verifiziert: genau eine SQL-Abfrage je Methode, korrekte SELECT-Only-Guard, Gruppierung n:1, Default für fehlende IDs. Für die Referenz-Methoden zusätzlich:
|
||||
- LEFT JOIN auf `teamlandkarte_v_partners_latest` ist Bestandteil derselben SQL-Abfrage (kein zusätzlicher SQL-Roundtrip; Anforderung 2.4).
|
||||
- Mock-Cursor liefert drei Spalten (`capacity_id`, `projects`, `partner_name`); Rückgabe enthält `CapacityReferenceRow`-Einträge mit korrektem Partner-Namen.
|
||||
- Fall NULL `partner_id` bzw. fehlender Partner: `partner_name` ist leerer String (`COALESCE`), Referenz bleibt mit `projects` erhalten (Anforderung 2.8).
|
||||
- **LlmFulltextMatcher**: Beispiel-basierte Tests mit gemocktem `AzureOpenAIClient`:
|
||||
- Erfolgsfall (gültige Kategorie + Rationale)
|
||||
- Ungültiges JSON
|
||||
- Gültiges JSON mit unbekannter Kategorie
|
||||
- Exception aus `chat_completion` (`AzureAPIError`)
|
||||
- Vermischung mehrerer Kapazitäten/Aufgaben (Reihenfolge, Sortierung)
|
||||
- **MCP-Tools** (`find_matching_capacities`, `find_matching_tasks`):
|
||||
- `matching_method=None` → Default greift, Schema entspricht Score-Modus.
|
||||
- `matching_method="llm_fulltext"` → keine Score-Spalten, `Begründung`-Spalte vorhanden, `META` enthält `matching_method`.
|
||||
- `matching_method="bogus"` → Fehlermeldung; keine DB/LLM-Aufrufe (über Mocks verifiziert).
|
||||
- **`get_results_by_category` / `filter_search_results`**:
|
||||
- Im LLM-Modus rendern sie die `Begründung`-Spalte und ignorieren `min_similarity` mit Hinweis.
|
||||
- Im Score-Modus bleibt das Verhalten unverändert (Regression).
|
||||
- **Konfiguration**: Test, dass `matching.default_method = "irgendwas"` einen `ConfigError` beim Laden wirft, und dass das Weglassen den Default `"score"` ergibt.
|
||||
|
||||
### Integrationstests
|
||||
|
||||
- End-to-End-Lauf für beide Suchrichtungen mit gemocktem LLM-Client und gemocktem `DBClient`:
|
||||
- Verfügbarkeitsfilter ist im LLM-Modus identisch zum Score-Modus.
|
||||
- SearchCache enthält `matching_method`, `rationale` (ungekürzt), `errors`-Liste.
|
||||
- Tabellen-Snapshot-Test (deterministisch über stabile Mock-Antworten), der die Kopfzeilen `... | Category | Begründung` für `find_matching_capacities`/`find_matching_tasks` im LLM-Modus festschreibt.
|
||||
|
||||
### Bewusst nicht getestet
|
||||
|
||||
- Inhaltliche Qualität der LLM-Begründungen (subjektiv).
|
||||
- Konkrete Wahl der Kategorie durch das LLM für reale Inhalte (modellabhängig, nicht deterministisch).
|
||||
- Performance/Latenz der LLM-Aufrufe (kein Unit-Test-Scope).
|
||||
@@ -0,0 +1,197 @@
|
||||
# Anforderungsdokument
|
||||
|
||||
## Einleitung
|
||||
|
||||
Dieses Dokument beschreibt die Anforderungen für die Einführung eines zweiten Matching-Verfahrens in der Teamlandkarte: einen **LLM-basierten Volltext-Vergleich** zwischen Aufgaben und Kapazitäten. Das bestehende Score-basierte Verfahren (Rolle + Kompetenzen, BM25/RRF + LLM-Rollen-Similarity) bleibt unverändert verfügbar. Der Nutzer wählt pro Suche das Verfahren aus.
|
||||
|
||||
Das neue Verfahren bezieht zusätzliche Felder aus der Datenbank ein (Beschreibung, Referenzen, Zertifikate auf Kapazitätsseite; Titel, Beschreibung und gesuchte Kompetenzen auf Aufgabenseite), berechnet kein numerisches Scoring mehr und ordnet jede Kapazität bzw. Aufgabe direkt einer der bestehenden Kategorien zu. Zusätzlich liefert das LLM für jeden Fall eine Kurzbegründung (1–2 Sätze).
|
||||
|
||||
## Glossar
|
||||
|
||||
- **MCP_Server**: Der Teamlandkarte MCP-Server (Modul `mcp_server.py`), der die MCP-Tools für Matching, Suche und Datenanzeige bereitstellt.
|
||||
- **DBClient**: Protokollklasse aus `database/types.py`, die alle Datenbankzugriffe abstrahiert.
|
||||
- **TrinoClient**: Konkrete `DBClient`-Implementierung (`database/trino_client.py`) für Trino/Presto.
|
||||
- **Matcher**: Bestehende, Score-basierte Matching-Komponente in `matching/matcher.py`.
|
||||
- **LLM_Fulltext_Matcher**: Neues Modul, das den LLM-basierten Volltext-Vergleich durchführt und Kategorien direkt zuweist.
|
||||
- **AzureOpenAIClient**: Wrapper für Azure-OpenAI-Chat-Completions in `azure/openai_client.py`.
|
||||
- **LLM**: Large Language Model (Azure OpenAI Chat Completion).
|
||||
- **Capacity**: Frozen Dataclass `Capacity` in `models.py` (Kapazitätseintrag eines Mitarbeitenden).
|
||||
- **Task**: Frozen Dataclass `Task` in `models.py` (veröffentlichte Aufgabe).
|
||||
- **Capacity_Profile**: Aggregiertes Volltext-Profil einer Kapazität, bestehend aus Rolle, Kompetenzen, Beschreibung, Referenzen und Zertifikaten.
|
||||
- **Task_Profile**: Aggregiertes Volltext-Profil einer Aufgabe, bestehend aus Titel, Beschreibung und gesuchten Kompetenzen.
|
||||
- **Matching_Method**: Auswahlwert für das verwendete Verfahren. Erlaubte Werte: `score` (bisheriges Score-basiertes Matching) und `llm_fulltext` (neues LLM-Volltext-Matching).
|
||||
- **Kategorie**: Eine der bestehenden Ergebniskategorien `Top`, `Good`, `Partial`, `Low`, `Irrelevant`.
|
||||
- **Rationale**: Vom LLM erzeugte Kurzbegründung (1–2 Sätze) für die zugewiesene Kategorie.
|
||||
- **find_matching_capacities**: MCP-Tool für die Suchrichtung Aufgabe→Kapazität.
|
||||
- **find_matching_tasks**: MCP-Tool für die Suchrichtung Kapazität→Aufgabe.
|
||||
- **Teamlandkarte_Agent**: GitHub-Copilot-Agent in `.github/agents/teamlandkarte_agent.md` (sowie das Pendant in `.kiro/agents/teamlandkarte.md`) inklusive seiner Skills/Workflows.
|
||||
- **Architecture_Doc**: `docs/architecture.md`.
|
||||
- **Readme**: `README.md` im Repository-Root.
|
||||
- **Capacity_Description**: Inhalt der Spalte `description` in `teamlandkarte_v_capacities_latest`.
|
||||
- **Capacity_Certificate**: Eintrag aus `teamlandkarte_v_capacity_certificates_latest` (Feld `description`, Join via `capacity_id`, 1:n).
|
||||
- **Capacity_Reference**: Eintrag aus `teamlandkarte_v_capacity_references_latest` (Spalte `projects`, Join via `capacity_id`, 1:n) inklusive des zugehörigen Partner_Name aus `teamlandkarte_v_partners_latest`.
|
||||
- **Partner**: Eintrag aus `teamlandkarte_v_partners_latest`. Eine Capacity_Reference verweist über die Spalte `partner_id` auf einen Partner; die Verknüpfung erfolgt über `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id`.
|
||||
- **Partner_Name**: Wert der Spalte `name` aus `teamlandkarte_v_partners_latest`, der einer Capacity_Reference über `partner_id` zugeordnet ist. Ist `partner_id` `NULL` oder existiert kein passender Partner, gilt der Partner_Name als leere Zeichenkette.
|
||||
|
||||
## Anforderungen
|
||||
|
||||
### Anforderung 1: Auswahl des Matching-Verfahrens
|
||||
|
||||
**User Story:** Als Nutzer möchte ich pro Suchanfrage zwischen dem bisherigen Score-basierten Matching und dem neuen LLM-basierten Volltext-Matching wählen können, damit ich je nach Situation das passende Verfahren einsetzen kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL akzeptieren einen Parameter `matching_method` mit den erlaubten Werten `score` und `llm_fulltext` in den Tools `find_matching_capacities` und `find_matching_tasks`.
|
||||
2. WHEN `matching_method` nicht übergeben wird, THE MCP_Server SHALL den Standardwert `score` verwenden, sodass das bestehende Verhalten unverändert bleibt.
|
||||
3. WHEN `matching_method = "score"` übergeben wird, THE MCP_Server SHALL das bestehende Score-basierte Matching ausführen.
|
||||
4. WHEN `matching_method = "llm_fulltext"` übergeben wird, THE MCP_Server SHALL das neue LLM-basierte Volltext-Matching über den LLM_Fulltext_Matcher ausführen.
|
||||
5. IF ein ungültiger Wert für `matching_method` übergeben wird, THEN THE MCP_Server SHALL eine Fehlermeldung zurückgeben, die die erlaubten Werte (`score`, `llm_fulltext`) auflistet, und die Suche nicht ausführen.
|
||||
6. THE MCP_Server SHALL den verwendeten Wert von `matching_method` im Antwort-`META`-JSON sowie in der angezeigten Suchkonfiguration ausweisen.
|
||||
|
||||
### Anforderung 2: Erweiterte Datenabfrage für Kapazitäten
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass für das LLM-Volltext-Matching die Kapazitäts-Beschreibung, alle Zertifikate und alle Referenzen aus der Datenbank verfügbar sind, damit das LLM ein vollständiges Profil bewerten kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE DBClient SHALL eine Methode bereitstellen, die für eine gegebene `capacity_id` die Capacity_Description aus `teamlandkarte_v_capacities_latest` (Spalte `description`) zurückgibt.
|
||||
2. THE DBClient SHALL eine Methode bereitstellen, die für eine gegebene `capacity_id` alle zugeordneten Capacity_Certificate-Beschreibungen aus `teamlandkarte_v_capacity_certificates_latest` (Feld `description`, Join über `capacity_id`) als Liste von Strings zurückgibt.
|
||||
3. THE DBClient SHALL eine Methode bereitstellen, die für eine gegebene `capacity_id` alle zugeordneten Capacity_Reference-Einträge aus `teamlandkarte_v_capacity_references_latest` (Spalte `projects`, Join über `capacity_id`) inklusive des zugehörigen Partner_Name aus `teamlandkarte_v_partners_latest` (Join `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id`, Spalte `name`) als Liste strukturierter Einträge mit den Feldern `projects` und `partner_name` zurückgibt.
|
||||
4. WHEN ein LLM-Volltext-Matching für mehrere Kapazitäten ausgeführt wird, THE DBClient SHALL eine Batch-Variante bereitstellen, die Beschreibungen, Zertifikate und Referenzen (inklusive Partner_Name über den Join auf `teamlandkarte_v_partners_latest`) für eine Liste von `capacity_id`-Werten in höchstens drei SQL-Abfragen lädt (eine pro Quelle); der Partner-Join SHALL Bestandteil derselben Referenz-Abfrage sein und keine zusätzliche SQL-Abfrage erzeugen.
|
||||
5. WHEN für eine Kapazität keine Beschreibung in der Datenbank vorhanden ist (NULL oder leer), THE DBClient SHALL für die Capacity_Description den Wert `None` zurückgeben.
|
||||
6. WHEN für eine Kapazität keine Zertifikate vorhanden sind, THE DBClient SHALL eine leere Liste für Capacity_Certificate zurückgeben.
|
||||
7. WHEN für eine Kapazität keine Referenzen vorhanden sind, THE DBClient SHALL eine leere Liste für Capacity_Reference zurückgeben.
|
||||
8. IF die `partner_id` einer Capacity_Reference `NULL` ist oder der Join auf `teamlandkarte_v_partners_latest` keinen Treffer liefert, THEN THE DBClient SHALL den Partner_Name dieser Capacity_Reference als leere Zeichenkette zurückgeben und die Referenz dennoch mit dem Feld `projects` in der Ergebnisliste belassen.
|
||||
9. THE TrinoClient SHALL alle neuen SQL-Abfragen ausschließlich als `SELECT`-Statements ausführen und die bestehende Read-Only-Guard `_ensure_select_only` verwenden.
|
||||
10. THE TrinoClient SHALL die neuen Abfragen über die bestehende Connection-Pool-Infrastruktur und die Retry-Logik (`_retry`) ausführen.
|
||||
|
||||
### Anforderung 3: Aufbau des Capacity_Profile
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System aus den Datenbankfeldern ein konsistentes Volltext-Profil pro Kapazität erzeugt, damit das LLM eine einheitliche Eingabe erhält.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE LLM_Fulltext_Matcher SHALL pro Kapazität ein Capacity_Profile bilden, das die folgenden Felder enthält: `id`, `owner_name`, `role_name`, `competences`, `description`, `references` und `certificates`.
|
||||
2. THE LLM_Fulltext_Matcher SHALL jedes Element der Liste `references` im Capacity_Profile als strukturierten Eintrag mit den Feldern `partner_name` und `projects` führen, sodass beide Bestandteile einer Capacity_Reference erhalten bleiben.
|
||||
3. WHEN ein Feld in der Datenbank leer oder `None` ist, THE LLM_Fulltext_Matcher SHALL das entsprechende Feld im Capacity_Profile mit einer leeren Zeichenkette bzw. einer leeren Liste belegen, ohne das gesamte Profil zu verwerfen.
|
||||
4. WHEN der Partner_Name einer Capacity_Reference leer ist, THE LLM_Fulltext_Matcher SHALL die Referenz dennoch in `references` aufnehmen und ausschließlich das Feld `projects` in die serialisierte Darstellung übernehmen, ohne einen Platzhaltertext für den Partner einzufügen.
|
||||
5. THE LLM_Fulltext_Matcher SHALL das Capacity_Profile in einer für das LLM lesbaren, deterministischen Textstruktur serialisieren, in der jedes Feld klar mit einer Überschrift gekennzeichnet ist (z. B. `Rolle:`, `Kompetenzen:`, `Beschreibung:`, `Referenzen:`, `Zertifikate:`).
|
||||
6. THE LLM_Fulltext_Matcher SHALL jeden Eintrag im Abschnitt `Referenzen:` so darstellen, dass sowohl Partner_Name als auch Projekte für das LLM sichtbar sind (z. B. im Format `Partner: <partner_name> – Projekte: <projects>` oder als gleichwertige strukturierte Darstellung mit benannten Feldern).
|
||||
7. THE LLM_Fulltext_Matcher SHALL die Reihenfolge der Felder in der serialisierten Darstellung über alle Kapazitäten konstant halten, sodass die LLM-Eingabe deterministisch ist.
|
||||
|
||||
### Anforderung 4: Aufbau des Task_Profile
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System aus den Datenbankfeldern ein konsistentes Volltext-Profil pro Aufgabe erzeugt, damit das LLM eine einheitliche Eingabe erhält.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE LLM_Fulltext_Matcher SHALL pro Aufgabe ein Task_Profile bilden, das die folgenden Felder enthält: `id`, `title`, `description` und `skills` (gesuchte Kompetenzen).
|
||||
2. WHEN ein Feld in der Datenbank leer oder `None` ist, THE LLM_Fulltext_Matcher SHALL das entsprechende Feld im Task_Profile mit einer leeren Zeichenkette bzw. einer leeren Liste belegen.
|
||||
3. THE LLM_Fulltext_Matcher SHALL das Task_Profile in einer für das LLM lesbaren, deterministischen Textstruktur serialisieren, in der jedes Feld klar mit einer Überschrift gekennzeichnet ist (z. B. `Titel:`, `Beschreibung:`, `Gesuchte Kompetenzen:`).
|
||||
4. THE LLM_Fulltext_Matcher SHALL die Reihenfolge der Felder in der serialisierten Darstellung über alle Aufgaben konstant halten.
|
||||
|
||||
### Anforderung 5: LLM-Volltext-Matching für die Richtung Aufgabe→Kapazität
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass `find_matching_capacities` mit `matching_method = "llm_fulltext"` einen LLM-basierten Volltext-Vergleich zwischen einem Task_Profile und allen Capacity_Profile-Einträgen durchführt, damit ich Kapazitäten über die rein lexikalische Kompetenzbetrachtung hinaus bewerten lassen kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `find_matching_capacities` mit `matching_method = "llm_fulltext"` aufgerufen wird, THE LLM_Fulltext_Matcher SHALL für jede gefilterte Kapazität (gleicher Vorfilter wie beim Score-Matching, z. B. Verfügbarkeitsfilter) einen LLM-Vergleich zwischen Task_Profile und Capacity_Profile durchführen.
|
||||
2. WHEN `find_matching_capacities` mit `matching_method = "llm_fulltext"` aufgerufen wird, THE MCP_Server SHALL als Eingabe das aktuell bestätigte Anforderungs-Set (`role_name`, `competences`, optionale Beschreibung, Zeitraum) sowie ggf. die zugrunde liegende Aufgabe verwenden, um das Task_Profile zu bilden.
|
||||
3. THE LLM_Fulltext_Matcher SHALL pro Kapazität genau eine Kategorie aus der Menge `Top`, `Good`, `Partial`, `Low`, `Irrelevant` zurückgeben.
|
||||
4. THE LLM_Fulltext_Matcher SHALL pro Kapazität eine Rationale mit 1 bis 2 Sätzen zurückgeben, die die Zuweisung in die jeweilige Kategorie erläutert.
|
||||
5. THE LLM_Fulltext_Matcher SHALL die LLM-Antwort als strukturiertes JSON pro Kapazität anfordern und parsen (Felder: `category`, `rationale`).
|
||||
6. IF das LLM für eine Kapazität eine Kategorie zurückgibt, die nicht in der erlaubten Menge liegt, THEN THE LLM_Fulltext_Matcher SHALL diese Kapazität der Kategorie `Irrelevant` zuordnen und die Rationale durch einen Hinweis auf die ungültige LLM-Antwort ergänzen.
|
||||
7. IF der LLM-Aufruf für eine Kapazität fehlschlägt, THEN THE LLM_Fulltext_Matcher SHALL diese Kapazität in einer separaten Fehlerliste ausweisen und sie nicht als reguläres Ergebnis kategorisieren.
|
||||
8. THE LLM_Fulltext_Matcher SHALL die Ergebnisse nach Kategorie gruppieren und innerhalb jeder Kategorie eine deterministische Sortierreihenfolge anwenden (Sortierung primär nach Kategorie, sekundär nach `capacity_id` aufsteigend).
|
||||
|
||||
### Anforderung 6: LLM-Volltext-Matching für die Richtung Kapazität→Aufgabe
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass `find_matching_tasks` mit `matching_method = "llm_fulltext"` einen LLM-basierten Volltext-Vergleich zwischen einem Capacity_Profile und allen Task_Profile-Einträgen durchführt, damit ich auch in dieser Suchrichtung das neue Verfahren nutzen kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `find_matching_tasks` mit `matching_method = "llm_fulltext"` aufgerufen wird, THE LLM_Fulltext_Matcher SHALL für jede offene Aufgabe einen LLM-Vergleich zwischen Capacity_Profile und Task_Profile durchführen.
|
||||
2. WHEN `find_matching_tasks` mit `matching_method = "llm_fulltext"` aufgerufen wird, THE MCP_Server SHALL für die angegebene `capacity_id` Beschreibung, Zertifikate und Referenzen aus der Datenbank laden und in das Capacity_Profile einbeziehen.
|
||||
3. THE LLM_Fulltext_Matcher SHALL pro Aufgabe genau eine Kategorie aus der Menge `Top`, `Good`, `Partial`, `Low`, `Irrelevant` zurückgeben.
|
||||
4. THE LLM_Fulltext_Matcher SHALL pro Aufgabe eine Rationale mit 1 bis 2 Sätzen zurückgeben.
|
||||
5. THE LLM_Fulltext_Matcher SHALL die LLM-Antwort als strukturiertes JSON pro Aufgabe anfordern und parsen (Felder: `category`, `rationale`).
|
||||
6. IF das LLM für eine Aufgabe eine Kategorie zurückgibt, die nicht in der erlaubten Menge liegt, THEN THE LLM_Fulltext_Matcher SHALL diese Aufgabe der Kategorie `Irrelevant` zuordnen und die Rationale durch einen Hinweis auf die ungültige LLM-Antwort ergänzen.
|
||||
7. IF der LLM-Aufruf für eine Aufgabe fehlschlägt, THEN THE LLM_Fulltext_Matcher SHALL diese Aufgabe in einer separaten Fehlerliste ausweisen und sie nicht als reguläres Ergebnis kategorisieren.
|
||||
8. THE LLM_Fulltext_Matcher SHALL die Ergebnisse nach Kategorie gruppieren und innerhalb jeder Kategorie eine deterministische Sortierreihenfolge anwenden (Sortierung primär nach Kategorie, sekundär nach `task_id` aufsteigend).
|
||||
|
||||
### Anforderung 7: Direkte Kategorisierung ohne mathematisches Scoring
|
||||
|
||||
**User Story:** Als Nutzer möchte ich beim LLM-Volltext-Matching keine numerischen Score-Spalten mehr sehen, sondern ausschließlich die vom LLM zugewiesene Kategorie, damit das neue Verfahren als rein qualitative Bewertung erkennbar ist.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `matching_method = "llm_fulltext"` verwendet wird, THE MCP_Server SHALL in den Ergebnistabellen keine Spalten `Role Score`, `Competence Score` oder `Overall Score` ausgeben.
|
||||
2. WHEN `matching_method = "llm_fulltext"` verwendet wird, THE MCP_Server SHALL für jeden Treffer ausschließlich die LLM-zugewiesene Kategorie als Bewertungsfeld ausweisen.
|
||||
3. WHEN `matching_method = "score"` verwendet wird, THE MCP_Server SHALL die bestehenden Score-Spalten unverändert ausgeben.
|
||||
4. THE LLM_Fulltext_Matcher SHALL für jedes Ergebnis ein Datenfeld `category` (String) und ein Datenfeld `rationale` (String) im gespeicherten Suchergebnis (`SearchCache`) hinterlegen, ohne numerische Scores zu schreiben.
|
||||
5. THE MCP_Server SHALL die Summary-Tabelle im LLM-Volltext-Modus weiterhin als Zähler je Kategorie (`Top`, `Good`, `Partial`, `Low`, `Irrelevant`) ausgeben.
|
||||
|
||||
### Anforderung 8: Begründungsspalte (Rationale) in der Ausgabe
|
||||
|
||||
**User Story:** Als Nutzer möchte ich in der Ergebnistabelle des LLM-Volltext-Matchings eine zusätzliche Spalte sehen, die in 1–2 Sätzen erläutert, warum eine Kapazität bzw. Aufgabe in der jeweiligen Kategorie gelandet ist, damit ich die Entscheidung des LLM nachvollziehen kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `matching_method = "llm_fulltext"` verwendet wird, THE MCP_Server SHALL die Ergebnistabellen für `find_matching_capacities` und `find_matching_tasks` um eine Spalte `Begründung` (Rationale) erweitern.
|
||||
2. THE MCP_Server SHALL die Spalte `Begründung` direkt rechts neben der Spalte `Category` einfügen.
|
||||
3. THE MCP_Server SHALL pro Zeile genau die vom LLM zurückgegebene Rationale (1–2 Sätze) anzeigen.
|
||||
4. WHEN die Rationale Zeilenumbrüche oder Pipe-Zeichen enthält, THE MCP_Server SHALL diese so escapen oder ersetzen, dass die Markdown-Tabelle gültig bleibt.
|
||||
5. WHEN die Rationale länger als 280 Zeichen ist, THE MCP_Server SHALL die Rationale auf 280 Zeichen kürzen und ein abschließendes Auslassungszeichen (`…`) anhängen, damit die Tabellendarstellung lesbar bleibt.
|
||||
6. THE MCP_Server SHALL die ungekürzte Rationale im persistierten Suchergebnis (`SearchCache`) speichern, sodass nachgelagerte Tools (`get_results_by_category`, `filter_search_results`) den vollständigen Text ausgeben können.
|
||||
|
||||
### Anforderung 9: Kompatibilität mit Refinement- und Pagination-Tools
|
||||
|
||||
**User Story:** Als Nutzer möchte ich auch beim LLM-Volltext-Matching durch Kategorien blättern und Filter anwenden können, damit der bestehende Such-Workflow konsistent bleibt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `matching_method = "llm_fulltext"` verwendet wird, THE MCP_Server SHALL ein gültiges `search_id` zurückgeben, das mit `get_results_by_category` und `filter_search_results` verwendet werden kann.
|
||||
2. WHEN `get_results_by_category` ein Suchergebnis aus dem LLM-Volltext-Modus paginiert, THE MCP_Server SHALL die Ergebnistabelle ohne Score-Spalten und mit der Spalte `Begründung` ausgeben.
|
||||
3. WHEN `filter_search_results` ein Suchergebnis aus dem LLM-Volltext-Modus filtert, THE MCP_Server SHALL die Filterung ausschließlich auf nicht-Score-basierten Filtern (Rollenfilter, Kompetenzfilter, Verfügbarkeitsfilter, Aufgaben-Textfilter, Aufgaben-Kompetenzfilter) durchführen.
|
||||
4. IF ein Score-bezogener Filter (z. B. `min_similarity`) auf ein LLM-Volltext-Suchergebnis angewendet wird, THEN THE MCP_Server SHALL den Filter ignorieren und in der `Applied Filters`-Tabelle einen Hinweis aufnehmen, dass der Filter im LLM-Volltext-Modus nicht wirksam ist.
|
||||
5. THE MCP_Server SHALL im `META`-JSON des Suchergebnisses das verwendete Verfahren als `matching_method` ausweisen, damit Folgewerkzeuge das Schema korrekt interpretieren können.
|
||||
|
||||
### Anforderung 10: Anpassung der Copilot-Agent-Konfiguration
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass sowohl der GitHub-Copilot-Agent `teamlandkarte_agent` als auch der Kiro-Pendant-Agent das neue Matching-Verfahren kennen und mich aktiv nach dem gewünschten Verfahren fragen, damit das neue Feature über die Agenten nutzbar ist.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE Teamlandkarte_Agent SHALL in seiner Konfigurationsdatei (`.github/agents/teamlandkarte_agent.md`) und im Pendant `.kiro/agents/teamlandkarte.md` die Existenz und den Zweck der beiden Verfahren `score` und `llm_fulltext` dokumentieren.
|
||||
2. WHEN der Nutzer eine Suche nach passenden Kapazitäten oder Aufgaben startet, THE Teamlandkarte_Agent SHALL den Nutzer explizit nach dem gewünschten `matching_method` (Score-basiert oder LLM-Volltext) fragen, sofern dieses nicht bereits aus dem Verlauf hervorgeht.
|
||||
3. THE Teamlandkarte_Agent SHALL die Skills/Workflows so erweitern, dass `find_matching_capacities` und `find_matching_tasks` mit dem zusätzlichen Parameter `matching_method` aufgerufen werden.
|
||||
4. THE Teamlandkarte_Agent SHALL die Rolle der Spalte `Begründung` im Output dokumentieren und in den Hinweisen erwähnen, dass im LLM-Volltext-Modus keine numerischen Scores erscheinen.
|
||||
5. THE Teamlandkarte_Agent SHALL den bestehenden Bestätigungs-Workflow (`show_pending_requirements`, `confirm_requirements`) beibehalten und für beide Verfahren gleich anwenden.
|
||||
|
||||
### Anforderung 11: Aktualisierung von Architektur- und README-Dokumentation
|
||||
|
||||
**User Story:** Als Entwickler oder Onboardee möchte ich, dass `architecture.md` und `README.md` das neue Matching-Verfahren beschreiben, damit ich Architektur und Nutzung des Systems korrekt verstehe.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE Architecture_Doc SHALL einen Abschnitt enthalten, der den LLM_Fulltext_Matcher als Komponente innerhalb der Business-Logic-Layer beschreibt, einschließlich seiner Eingaben, Ausgaben und externen Abhängigkeiten (Azure OpenAI Chat Completion).
|
||||
2. THE Architecture_Doc SHALL die zusätzlichen Datenquellen (`teamlandkarte_v_capacities_latest.description`, `teamlandkarte_v_capacity_certificates_latest`, `teamlandkarte_v_capacity_references_latest`, `teamlandkarte_v_partners_latest`) im Datenmodell- und Schema-Verifikationsabschnitt aufführen.
|
||||
3. THE Architecture_Doc SHALL die Verknüpfung zwischen `teamlandkarte_v_capacity_references_latest.partner_id` und `teamlandkarte_v_partners_latest.id` sowie die Übernahme der Spalte `name` als Partner_Name in das Capacity_Profile dokumentieren.
|
||||
4. THE Architecture_Doc SHALL den neuen Parameter `matching_method` und seine Wertebereiche im Tool-Surface-Abschnitt für `find_matching_capacities` und `find_matching_tasks` dokumentieren.
|
||||
5. THE Architecture_Doc SHALL den Runtime-View für beide Suchrichtungen um den LLM-Volltext-Pfad ergänzen.
|
||||
6. THE Readme SHALL im Quick-Start- und Usage-Abschnitt erklären, wie der Nutzer zwischen `score` und `llm_fulltext` wählt.
|
||||
7. THE Readme SHALL beschreiben, dass im LLM-Volltext-Modus keine numerischen Scores ausgegeben werden und stattdessen eine Spalte `Begründung` erscheint.
|
||||
8. THE Readme SHALL die zusätzlichen Datenbank-Views aufführen, die der Server im LLM-Volltext-Modus liest, einschließlich `teamlandkarte_v_partners_latest` und der Verknüpfung zu Capacity_Reference über `partner_id`.
|
||||
|
||||
### Anforderung 12: Anpassung weiterer Skripte und Tools
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass alle relevanten Hilfsskripte und MCP-Tools mit dem neuen Verfahren konsistent zusammenarbeiten, damit es keine Inkonsistenzen zwischen Server, Agent und Skripten gibt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL den Parameter `matching_method` in allen Docstrings der betroffenen Tools (`find_matching_capacities`, `find_matching_tasks`, ggf. `filter_search_results`, `get_results_by_category`) dokumentieren.
|
||||
2. THE MCP_Server SHALL die Konfigurationsdatei `config.toml` um einen optionalen Schlüssel `matching.default_method` erweitern, der den Standardwert für `matching_method` beim Server-Start festlegt.
|
||||
3. WHEN `matching.default_method` in `config.toml` nicht gesetzt ist, THE MCP_Server SHALL den Default-Wert `score` verwenden.
|
||||
4. IF `matching.default_method` einen anderen Wert als `score` oder `llm_fulltext` enthält, THEN THE MCP_Server SHALL beim Start einen `ConfigError` mit beschreibender Meldung werfen.
|
||||
5. THE MCP_Server SHALL alle bestehenden Tests so erweitern oder ergänzen, dass sowohl der Modus `score` als auch der Modus `llm_fulltext` (mit gemocktem LLM) abgedeckt sind.
|
||||
@@ -0,0 +1,384 @@
|
||||
# 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: <partner_name> – Projekte: <projects>`; bei leerem `partner_name` → `Projekte: <projects>` (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: <name>` als auch `Projekte: <projects>`
|
||||
- **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: <excerpt>"`
|
||||
- Bei ungültiger Kategorie → Item nach `Irrelevant` mit Hinweis `[Hinweis: ungültige LLM-Kategorie: <wert>]` 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.
|
||||
Reference in New Issue
Block a user