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.
54 KiB
Designdokument: Team-Profil-Matching
Übersicht
Dieses Designdokument beschreibt die Erweiterung des Teamlandkarte MCP-Servers um eine
zweite Profilart: das Team-Profil. Heute kann das System nur Aufgaben gegen
Kapazitätsprofile matchen (Capacity aus
teamlandkarte_v_capacities_latest und Folge-Views). Künftig wählt der Nutzer pro
Suchanfrage über einen Profile_Type (capacity | team), ob er gegen Personen
oder gegen Teams matcht.
Das neue Feature führt parallel zu den bestehenden Tools find_matching_capacities,
list_free_capacities und get_capacity_details drei neue MCP-Tools ein:
find_matching_teams– Aufgabe → Team-Matching, mit den gleichenmatching_method-Werten (score,llm_fulltext).list_teams– Listet die ersten N Teams als Tabelle.get_team_details– Zeigt ein Team-Profil als Tabelle plus Beschreibungstexte und Listen.
Auf Datenebene werden vier neue Views in database/types.py::DBClient und
database/trino_client.py::TrinoClient ergänzt
(teamlandkarte_v_teams_latest, teamlandkarte_v_teammeter_organizational_units_latest,
teamlandkarte_v_teammeter_team_competences_latest,
teamlandkarte_v_team_references_latest). Die bestehende
Read-Only-Guard _ensure_select_only und die Retry-Infrastruktur (_retry) werden
unverändert wiederverwendet. Ein neues Frozen-Dataclass-Trio
(Team, TeamCompetence, TeamReference) ergänzt Capacity in models.py. Die
LLM-Eingabe für Teams wird über einen neuen TeamProfile-Builder und einen neuen
serialize_team_profile-Serializer in matching/profiles.py erzeugt, der die in den
Anforderungen festgelegten deutschen Überschriften (Teamname:, Schwerpunkt:,
Über uns:, Leistungen:, Interessen:, Kompetenzen:, Referenzen:) verwendet.
Das Score-basierte Matching (Matcher) wird so verallgemeinert, dass es Teams mit
focus_name als Rollen-Stellvertreter und gewichteten Top-Kompetenzen
(matching.team.top_competency_weight, Default 1.5) bewerten kann, ohne den
bestehenden Capacity-Pfad zu verändern. Das LLM-Volltext-Matching
(LlmFulltextMatcher) erhält eine neue match_teams-Methode, die das gleiche
JSON-Schema ({"category", "rationale"}) und die gleiche Fehler- und
Sortier-Semantik wie match_capacities/match_tasks verwendet.
Verfügbarkeitsfilter (availability_date_start, availability_date_end,
is_fully_available) sind für Team-Suchen fachlich nicht definiert und werden
ignoriert, aber in der Applied Filters-Tabelle als nicht wirksam markiert. Der
SearchCache-Eintrag erhält ein neues Feld search_type = "team_search" (parallel zu
capacity_search und task_search), sodass filter_search_results und
get_results_by_category die Team-spezifische Spaltenkonfiguration auswählen können.
Beim Serverstart prüft die bestehende Schema-Verifikation
(database/schema_verifier.py) zusätzlich die vier neuen Views inklusive ihrer
relevanten Spalten. Die Konfiguration in config.toml wird um den optionalen Block
[matching.team] mit dem Schlüssel top_competency_weight (Default 1.5)
erweitert; ungültige Werte führen zu einem ConfigError beim Start.
Die wichtigsten Designentscheidungen sind:
- Wiederverwendung statt Parallelaufbau:
find_matching_teamsreusesMatcher/LlmFulltextMatcherüber schmale Adapter, statt eigene Score-/LLM-Pipelines aufzubauen. Damit bleibt die Kategorisierungs- und Konfidenzlogik identisch. - Determinismus:
Teamist frozen,TeamProfilewird deterministisch serialisiert (feste Feldreihenfolge, stabile(top desc, name asc)-Sortierung der Kompetenzen,(partner_name asc, projects asc)-Sortierung der Referenzen). Das ist Grundlage für die Round-Trip-/Idempotenz-Eigenschaft (Anforderung 13) und für stabile LLM-Eingaben. - Keine Änderung des Capacity-Pfads: Bestehende Tools, Tests, Caches und Tabellenformate für Kapazitäten bleiben unverändert. Neue Logik wird ausschließlich additiv eingeführt.
- Saubere Trennung der Such-Typen:
search_typeim persistierten Cache-Eintrag ist die Single Source of Truth für die nachgelagerte Renderlogik. Bestehende Cache-Einträge (ohnesearch_type) bleiben kompatibel und werden weiterhin alscapacity_searchinterpretiert (existierender Fallback).
Architektur
High-Level-Datenfluss
flowchart LR
User[Nutzer]
Agent[Teamlandkarte_Agent]
MCP[MCP_Server\nmcp_server.py]
DB[DBClient / TrinoClient]
SC[SearchCache]
M[Matcher\nScore]
LFM[LlmFulltextMatcher]
LLM[Azure OpenAI]
Trino[(Trino / Open Data Lake)]
User -- "natürliche Sprache" --> Agent
Agent -- "find_matching_teams\nmatching_method" --> MCP
MCP -- "Team-Stammdaten + Kompetenzen + Referenzen" --> DB
DB -- "SELECT (read-only)" --> Trino
MCP -- "score" --> M
MCP -- "llm_fulltext" --> LFM
LFM -- "chat completion (JSON)" --> LLM
MCP -- "store_search\nsearch_type=team_search" --> SC
SC --> MCP
MCP -- "Markdown-Tabelle\n+ search_id + META" --> Agent
Agent --> User
Schichtenmodell
| Schicht | Bestehende Komponenten | Neue / erweiterte Komponenten |
|---|---|---|
| Tool-Surface (MCP) | find_matching_capacities, list_free_capacities, get_capacity_details, find_matching_tasks, filter_search_results, get_results_by_category |
find_matching_teams, list_teams, get_team_details; _format_results_table erweitert um search_type="team_search"; filter_search_results/get_results_by_category erweitert um Team-Spalten und Filter-Ignore-Hinweise |
| Matching | Matcher (Score), LlmFulltextMatcher (LLM-JSON), SimilarityEngine, Bm25Index |
Matcher.match_teams(...) (oder Adapter _score_teams(...)) für Top-Kompetenz-Gewichtung; LlmFulltextMatcher.match_teams(...) für Team-Profile |
| Profile/Serialisierung | CapacityProfile, TaskProfile, serialize_capacity_profile, serialize_task_profile, build_capacity_profile |
TeamProfile, TeamCompetenceEntry, TeamReferenceEntry, build_team_profile, serialize_team_profile |
| Models | Capacity, Task, Requirements, ScoredCapacity |
Team, TeamCompetence, TeamReference, ScoredTeam |
| DB | DBClient Protocol + TrinoClient, _ensure_select_only, _retry, ConnectionPool |
Neue Methoden: get_all_teams, get_team_by_id, get_team_competences, batch_get_team_competences, get_team_references, batch_get_team_references |
| Cache | QueryCache[list[Capacity]], SearchCache |
QueryCache[list[Team]] (parallele Cache-Keys: all_teams); SearchCache mit search_type="team_search" |
| Konfiguration | MatchingConfig, MatchingThresholds |
Neuer TeamMatchingConfig (top_competency_weight: float = 1.5); MatchingConfig.team: TeamMatchingConfig |
| Schema-Verifikation | verify_required_columns |
Erweiterte schema_expected-Map um die vier neuen Views |
| Dokumentation/Agenten | docs/architecture.md, README.md, .github/agents/teamlandkarte_agent.md, .kiro/agents/teamlandkarte.md |
Erweiterung um Profile_Type, Team-Tools, neue Views, fehlende Verfügbarkeitsfilter |
Runtime-View: Aufgabe → Team (score)
sequenceDiagram
participant U as User/Agent
participant S as MCP_Server
participant DB as DBClient
participant Q as QueryCache
participant M as Matcher (Score)
participant SC as SearchCache
U->>S: find_matching_teams(role_name, competences, matching_method="score")
S->>S: _resolve_matching_method() / _validate_requirements_minimum()
S->>S: _require_confirmed_or_auto(req)
S->>Q: get_or_fetch("all_teams", db.get_all_teams_with_competences_and_references)
Q-->>DB: get_all_teams + batch_get_team_competences + batch_get_team_references
DB-->>Q: list[Team]
Q-->>S: list[Team]
S->>M: match_teams(teams, requirements, top_competency_weight)
M-->>S: MatchResult[ScoredTeam] mit by_category
S->>SC: store_search(search_type="team_search", matching_method="score", results)
SC-->>S: search_id
S-->>U: Markdown (Summary + Top Results) + SEARCH_ID + META
Runtime-View: Aufgabe → Team (llm_fulltext)
sequenceDiagram
participant U as User/Agent
participant S as MCP_Server
participant DB as DBClient
participant LFM as LlmFulltextMatcher
participant LLM as Azure OpenAI
participant SC as SearchCache
U->>S: find_matching_teams(role_name, competences, matching_method="llm_fulltext")
S->>S: _resolve_matching_method() / Confirm-Gate
S->>DB: get_all_teams_with_competences_and_references()
DB-->>S: list[Team]
S->>S: build_task_profile_from_requirements(req)
S->>LFM: match_teams(task_profile, teams)
par per Team (max_concurrency=cfg.azure_openai.max_concurrency)
LFM->>LFM: build_team_profile + serialize_team_profile
LFM->>LLM: chat completion (system + user)
LLM-->>LFM: JSON {"category", "rationale"}
LFM->>LFM: normalize_category() / Fehlerbehandlung
end
LFM-->>S: LlmFulltextResult{by_category, errors}
S->>SC: store_search(search_type="team_search", matching_method="llm_fulltext")
SC-->>S: search_id
S-->>U: Markdown + SEARCH_ID + META + ggf. ## Errors
Schema-Verifikation beim Start
Die schema_expected-Map in mcp_server.build_server wird additiv erweitert:
schema_expected = {
# bestehend ...
"teamlandkarte_v_teams_latest": {
"team_id", "ouid", "about_us", "offerings", "interests", "focus_name",
},
"teamlandkarte_v_teammeter_organizational_units_latest": {
"id", "name", # Spaltenname für Team_Name siehe Anforderung 12.2
},
"teamlandkarte_v_teammeter_team_competences_latest": {
"ouid", "competence_id", "top_competency",
},
"teamlandkarte_v_team_references_latest": {
"ouid", "partner_id", "projects",
},
}
Die exakten Spaltennamen für name in ..._organizational_units_latest werden bei
Implementierung gegen die Live-DB abgeglichen; das Feld in der expected-Map ist die
Single Source of Truth für die Verifikation und wird beim Start gegen
get_table_columns(...) geprüft.
Komponenten und Schnittstellen
models.py – neue Frozen Dataclasses
Parallel zu Capacity werden drei neue, immutable Datentypen ergänzt:
@dataclass(frozen=True)
class TeamCompetence:
"""Eine Team-Kompetenz mit aufgelöstem Namen und Top-Markierung."""
name: str
top_competency: bool
@dataclass(frozen=True)
class TeamReference:
"""Eine Team-Referenz mit aufgelöstem Partner-Namen und Projekttext."""
partner_name: str # leer, wenn partner_id NULL oder Join leer
projects: str # bereits getrimmt; nie leer (Filter im DBClient)
@dataclass(frozen=True)
class Team:
"""Aggregiertes Team-Stammdatum aus dem Data Lake."""
team_id: str
ouid: str
team_name: str
focus_name: str
about_us: str
offerings: str
interests: str
competences: list[TeamCompetence] = field(default_factory=list)
references: list[TeamReference] = field(default_factory=list)
@dataclass(frozen=True)
class ScoredTeam:
"""Team plus Matching-Scores und Diagnose-Listen (nur Score-Modus)."""
team: Team
competence_score: float
role_score: float
overall_score: float
category: str
matched_competences: list[str] = field(default_factory=list)
missing_competences: list[str] = field(default_factory=list)
Begründung: Frozen Dataclasses sind im Codebase Standard (Capacity,
ScoredCapacity, Task). Die Felder spiegeln die Anforderungen 5.1 und 6.1 wider.
competences/references werden bewusst als Listen typisierter Records geführt,
damit der Serializer und der Score-Matcher direkt top_competency bzw.
partner_name lesen können, ohne Dict-Konventionen.
database/types.py – DBClient-Erweiterungen
Es werden sechs neue Methoden ergänzt, die das gleiche Konventions-Set wie die
Capacity-Pendants verwenden (TypedDict für DB-Rohzeilen, Batch-Variante mit dict
über alle Eingabe-IDs, deterministische Sortierung in der DB-Schicht):
class TeamCompetenceRow(TypedDict):
"""Eine Zeile aus teamlandkarte_v_teammeter_team_competences_latest
inkl. aufgelöstem Kompetenz-Namen.
`top_competency` wird in der DB-Schicht aus NULL → False normalisiert.
"""
name: str
top_competency: bool
class TeamReferenceRow(TypedDict):
"""Eine Zeile aus teamlandkarte_v_team_references_latest inkl. Partner-Namen
aus dem LEFT JOIN auf teamlandkarte_v_partners_latest.
`partner_name` ist leer, wenn `partner_id` NULL ist oder kein Join-Treffer
existiert. Whitespace-only `projects` werden in der DB-Schicht gefiltert.
"""
partner_name: str
projects: str
class DBClient(Protocol):
# ... bestehende Methoden ...
def get_all_teams(self) -> list[Team]:
"""Liefert alle Teams (INNER JOIN auf organizational_units_latest
für Team_Name) inklusive ihrer Kompetenzen und Referenzen.
Sammelt Stammdaten + Team_Name in einer Query, ruft dann
batch_get_team_competences + batch_get_team_references mit den
OUIDs der gefundenen Teams auf und kombiniert die Ergebnisse zu
`Team`-Instanzen.
Reihenfolge: Teams nach team_name aufsteigend (deterministisch).
Empty about_us/offerings/interests/focus_name werden als "" geliefert.
"""
def get_team_by_id(self, team_id: str) -> Team | None:
"""Einzelnes Team über team_id (oder ouid) inkl. Kompetenzen + Referenzen.
Liefert None, wenn weder team_id noch ouid einem Team entspricht
(oder der INNER JOIN keinen Eintrag in
teamlandkarte_v_teammeter_organizational_units_latest findet).
"""
def get_team_competences(self, ouid: str) -> list[TeamCompetenceRow]:
"""1:n Kompetenzen für eine ouid mit aufgelöstem Namen.
Reihenfolge: top_competency=True zuerst, dann name aufsteigend.
Einträge ohne auflösbaren competence_id-Namen werden gefiltert.
"""
def batch_get_team_competences(
self, ouids: list[str]
) -> dict[str, list[TeamCompetenceRow]]:
"""Batch-Variante für mehrere OUIDs in einer einzigen SQL-Query.
Jede angefragte ouid ist im Ergebnis vorhanden (leere Liste, falls keine
Kompetenzen).
"""
def get_team_references(self, ouid: str) -> list[TeamReferenceRow]:
"""1:n Referenzen für eine ouid mit Partner-Name (LEFT JOIN partners).
Reihenfolge: partner_name aufsteigend, dann projects aufsteigend.
Einträge mit leerem/whitespace-only `projects` werden gefiltert.
Einträge ohne Partner werden mit partner_name="" beibehalten.
"""
def batch_get_team_references(
self, ouids: list[str]
) -> dict[str, list[TeamReferenceRow]]:
"""Batch-Variante mit einem einzigen SELECT (LEFT JOIN partners
ist Teil derselben Query, kein separater Roundtrip)."""
Begründung: Die Schnittstelle spiegelt die existierenden
get_capacity_references / batch_get_capacity_references-Pendants wider, damit der
LLM-Fulltext-Matcher konsistent gegen DBClient programmieren kann. Die
deterministische Sortierung erfüllt Anforderungen 3.7 und 4.7 und ist Grundlage für
die Determinismus-Eigenschaft der Profil-Serialisierung (Anforderung 13).
database/trino_client.py – konkrete SQL-Implementierung
Die SQL-Statements folgen dem bestehenden Stil (?-Platzhalter, COALESCE für
NULL-Defaults, _ensure_select_only, _retry, log_query,
with self._cursor() as cur). Drei zentrale Queries:
1. get_all_teams – Stammdaten + Teamname (INNER JOIN, Anforderung 2):
SELECT
t.team_id,
t.ouid,
ou.name AS team_name,
COALESCE(t.focus_name, '') AS focus_name,
COALESCE(t.about_us, '') AS about_us,
COALESCE(t.offerings, '') AS offerings,
COALESCE(t.interests, '') AS interests
FROM teamlandkarte_v_teams_latest t
INNER JOIN teamlandkarte_v_teammeter_organizational_units_latest ou
ON t.team_id = ou.id
ORDER BY ou.name ASC, t.team_id ASC
Die Spalte für den Teamnamen (name) wird in der Schema-Verifikation geprüft;
falls der Live-DB-Spaltenname abweicht, wird sie an genau einer Stelle in
schema_expected und der SQL-Query angepasst.
2. batch_get_team_competences (Anforderung 3):
SELECT
tc.ouid,
c.name AS competence_name,
COALESCE(tc.top_competency, FALSE) AS top_competency
FROM teamlandkarte_v_teammeter_team_competences_latest tc
JOIN teamlandkarte_v_competences_latest c
ON tc.competence_id = c.id
WHERE tc.ouid IN (?, ?, ..., ?)
ORDER BY tc.ouid ASC,
CASE WHEN COALESCE(tc.top_competency, FALSE) THEN 0 ELSE 1 END ASC,
c.name ASC
Der Join auf teamlandkarte_v_competences_latest ist analog zum bestehenden
Capacity-Pfad und filtert implizit Einträge ohne auflösbaren Namen
(Anforderung 3.6).
3. batch_get_team_references (Anforderung 4):
SELECT
r.ouid,
r.projects,
COALESCE(p.name, '') AS partner_name
FROM teamlandkarte_v_team_references_latest r
LEFT JOIN teamlandkarte_v_partners_latest p
ON r.partner_id = p.id
WHERE r.ouid IN (?, ?, ..., ?)
ORDER BY r.ouid ASC, partner_name ASC, r.projects ASC
Die Whitespace-Filterung von projects erfolgt nach dem Fetch in Python
(projects.strip() und Skip bei Leerstring), exakt wie in
batch_get_capacity_references.
get_all_teams ist intern wie folgt strukturiert (vgl.
get_all_capacities_with_competences):
- Stammdaten + Team_Name laden.
- Einmal
batch_get_team_competences(ouids), einmalbatch_get_team_references(ouids). - In Python zu
Team-Instanzen aggregieren, Reihenfolge der inneren Listen aus demORDER BYder DB-Query.
matching/profiles.py – TeamProfile und Serialisierung
Drei neue Strukturen werden ergänzt, parallel zu CapacityProfile:
@dataclass(frozen=True)
class TeamCompetenceEntry:
name: str
top_competency: bool
@dataclass(frozen=True)
class TeamReferenceEntry:
partner_name: str # darf "" sein
projects: str
@dataclass(frozen=True)
class TeamProfile:
id: str # team_id (Stringform)
ouid: str
team_name: str
focus_name: str
about_us: str
offerings: str
interests: str
competences: list[TeamCompetenceEntry]
references: list[TeamReferenceEntry]
def build_team_profile(team: Team) -> TeamProfile: ...
def serialize_team_profile(profile: TeamProfile) -> str: ...
serialize_team_profile produziert eine deterministische, mehrzeilige Darstellung
mit fester Feld-Reihenfolge und festen deutschen Überschriften. Empty Werte werden
nicht weggelassen, sondern als (keine) bzw. leerer String nach der Überschrift
gerendert (analog zu serialize_capacity_profile):
Teamname: <team_name>
Schwerpunkt: <focus_name>
Über uns: <about_us>
Leistungen: <offerings>
Interessen: <interests>
Kompetenzen:
- <name1> (Top)
- <name2>
- ...
Referenzen:
- Partner: <partner_name> – Projekte: <projects>
- Projekte: <projects> (wenn partner_name leer)
- ...
Regeln:
- Top-Kompetenzen werden mit Suffix
(Top)markiert (Anforderung 5.5). - Bei leerem
partner_nameentfällt derPartner:-Token komplett, es wird kein Platzhalter eingefügt (Anforderung 5.3 / 5.6). - Empty Listen werden als
Kompetenzen: (keine)bzw.Referenzen: (keine)auf einer Zeile dargestellt (analogserialize_capacity_profile). - Die Reihenfolge der Listen entspricht exakt der vom DBClient gelieferten Reihenfolge (Anforderung 13.4); der Serializer sortiert nicht erneut.
matching/matcher.py – Score-Matching für Teams
Der bestehende Matcher wird um einen Team-Pfad ergänzt. Da Capacity und Team
unterschiedliche Felder haben (role_name vs. focus_name,
competences: list[str] vs. competences: list[TeamCompetence]) und das
Top-Kompetenz-Gewicht nur bei Teams gilt, gibt es zwei Optionen:
Variante A (gewählt): Eigene match_teams-Methode am Matcher.
class Matcher:
async def match_teams(
self,
teams: list[Team],
requirements: Requirements,
*,
top_competency_weight: float,
) -> "TeamMatchResult":
...
Vorteile:
- Klare Trennung der Pfade, kein versehentlicher Drift im Capacity-Pfad.
- Der Top-Kompetenz-Gewichtungsfaktor bleibt explizit am Aufruf, statt über interne Konfigflags zu fließen.
Algorithmus (Anforderung 6):
- Kompetenz-Liste erzeugen: Für jedes Team eine Score-Eingabeliste der Form
[name1, name2, ...]austeam.competences(Reihenfolge wie inTeam). Die Top-Markierung wird nicht in den Namen gemischt, sondern in einem parallelen Top-Set festgehalten. - BM25/RRF-Similarity wie bisher gegen die
requirements.competencesrechnen (SimilarityEngine.compute_competence_similarity). Pro Required-Kompetenz wird das beste Match (best_match) zurückgeliefert. - Top-Kompetenz-Gewichtung: Pro Required-Kompetenz wird der Roh-Score
smittop_competency_weight(Default 1.5) multipliziert, wennbest_matchzu einer Top-Kompetenz des Teams gehört, sonst mit1.0. Anschließend wird auf[0, 1]geclampt:Die Mittelung über die Required-Kompetenzen ergibt dieweighted = min(1.0, s * (top_weight if best_match in top_set else 1.0))competence_score. - Role Score über
SimilarityEngine.compute_role_similarity( requirements.role_name, team.focus_name)(Anforderung 6.3). Dadurch wirdfocus_nameals Rollen-Stellvertreter für Teams genutzt und nutzt den bestehenden symmetrischen LLM-Cache. - Overall Score über das bestehende
compute_overall(...)ausmatching/scorer.py(gleiche Gewichtecompetence_weight/role_weightund gleiche Thresholds). Die Kategorisierung erfolgt übercategorize(...)und liefertTop|Good|Partial|Low|Irrelevant. - Verfügbarkeitsfilter werden ignoriert (Anforderung 6.7): Der Pfad ruft
availability_overlaps(...)nicht auf.
Ausgabe ist ein neues TeamMatchResult-Pendant zu MatchResult:
@dataclass
class TeamMatchResult:
scored: list[ScoredTeam]
by_category: dict[str, list[ScoredTeam]]
Matcher.summary_counts(...) wird so generalisiert, dass es sowohl
dict[str, list[ScoredCapacity]] als auch dict[str, list[ScoredTeam]] akzeptiert
(strukturell identisch, nur len-basiert).
matching/llm_fulltext_matcher.py – match_teams
Der LlmFulltextMatcher erhält parallel zu match_capacities und match_tasks eine
neue match_teams-Methode:
class LlmFulltextMatcher:
async def match_teams(
self,
*,
task_profile: TaskProfile,
teams: list[Team],
) -> "LlmFulltextResult":
...
Implementierungsdetails:
-
Es wird kein zusätzlicher DB-Roundtrip benötigt, da
Teambereitscompetencesundreferencesmitführt (im Gegensatz zuCapacity, wodescription/certificates/referencesvia Batch-Methoden nachgeladen werden). -
Pro Team wird
build_team_profile + serialize_team_profileaufgerufen, der User-Prompt wird wie in_build_user_prompt_capacity_for_taskstrukturiert:=== Aufgabe === <serialize_task_profile(task_profile)> === Team === ID: <team_id> <serialize_team_profile(team_profile)> -
System-Prompt: dasselbe Schema wie für Kapazitäten (
Top/Good/Partial/Low/Irrelevant, JSON-Antwort{"category": ..., "rationale": ...}). Da der Bewertungsmaßstab inhaltlich identisch ist (Passung Aufgabe ↔ Profil), wird der bestehende_SYSTEM_PROMPTmit einer minimalen Wortwahl-Anpassung (Kapazitätsprofil→Profil) verwendet, ohne neuen Prompt einzuführen, der die Kategoriesemantik ändert. Die Implementierung kann bei Bedarf einen_SYSTEM_PROMPT_TEAMeinführen, falls A/B-Tests dies nahelegen. -
Concurrency-Steuerung über die existierende
asyncio.Semaphore-Logik (max_concurrency=cfg.azure_openai.max_concurrency). -
Fehlerbehandlung: identisch zu
match_capacities; ungültige Kategorien werden übernormalize_categoryaufIrrelevantgemappt mit Hinweis-Suffix in der Rationale (Anforderung 7.5). LLM-Fehler landen inerrors-Liste und nicht imby_category-Bucket (Anforderung 7.6). -
Sortierung: pro Kategorie nach
item_idaufsteigend (team_id), wie in den Capacity- und Task-Pfaden bereits erfolgt (Anforderung 7.7).
mcp_server.py – neue Tools
find_matching_teams
Signatur (parallel zu find_matching_capacities):
@mcp.tool()
async def find_matching_teams(
role_name: str,
competences: list[str],
matching_method: str = "",
) -> str:
...
Wesentliche Schritte:
_resolve_matching_method(matching_method)(Anforderung 1.5/1.6).role_name-Normalisierung wie im Capacity-Pfad ("Beliebige Rolle"als Fallback).competencesmuss non-empty sein (_validate_requirements_minimum).- Confirm-Gate über
_require_confirmed_or_auto(req). - Teams aus
_get_teams_cached()holen (QueryCache[list[Team]]). - Score-Pfad:
matcher.match_teams(teams, req, top_competency_weight=cfg.matching.team.top_competency_weight). - LLM-Pfad:
task_profile = build_task_profile_from_requirements(req)→llm_fulltext_matcher.match_teams(task_profile=task_profile, teams=teams). - Persistenz im
SearchCachemitresults_payload["search_type"] = "team_search"undmatching_method = ...(Anforderung 8.2 / 8.3). - Ausgabe:
SEARCH_ID-Header + Summary-Tabelle + Top-Kategorie-Tabelle, exakt wie infind_matching_capacities. Tabellenformat siehe nächster Abschnitt.
Tabellenformat für Team-Suchen
_format_results_table wird um search_type="team_search" erweitert:
| Modus | Spalten |
|---|---|
team_search × score |
Team Name, Schwerpunkt, Top-Kompetenzen, Role Score, Competence Score, Overall Score, Category |
team_search × llm_fulltext |
Team Name, Schwerpunkt, Top-Kompetenzen, Category, Begründung |
Top-Kompetenzen ist die kommaseparierte Liste aller competences mit
top_competency=True (Anforderung 6.6/7.8). Die Begründung-Spalte verwendet
denselben Truncation-Helper _format_rationale_for_table.
Der Helper _team_to_row(team_dict, *, matching_method) extrahiert aus dem
persistierten dict-Eintrag (siehe _coerce_team weiter unten) die nötigen Felder
und nutzt team["category"] bzw. die Score-Felder.
list_teams und get_team_details
@mcp.tool()
def list_teams(limit: int = 20) -> str:
"""Markdown-Tabelle: Team Id | Team Name | Schwerpunkt
| Anzahl Kompetenzen | Anzahl Referenzen.
Nutzt _get_teams_cached() und schneidet auf `limit` ab. Empty-Fallback
wie bei list_free_capacities.
"""
@mcp.tool()
def get_team_details(team_id: str) -> str:
"""Markdown-Tabelle für ein Team plus Beschreibungssektionen.
Layout (analog get_capacity_details):
| Team Id | Team Name | Schwerpunkt | Anzahl Kompetenzen | Anzahl Referenzen |
## Über uns
...
## Leistungen
...
## Interessen
...
## Kompetenzen
- <name> (Top)
- <name>
- ...
## Referenzen
- **<partner_name>**: <projects> (wenn partner_name vorhanden)
- <projects> (wenn partner_name leer)
## Next steps
Call find_matching_teams(role_name=..., competences=[...])
to find matching tasks for this team's profile.
"""
Validierung: Wenn db_client.get_team_by_id(team_id) None liefert, wird
f"Team not found: {team_id}" zurückgegeben (Anforderung 9.3, parallel zu
get_capacity_details).
_coerce_team
Analog zu _coerce_capacity werden persistierte SearchCache-Einträge zurück in
Team-Instanzen rehydratisiert (für Tabellen-Rendering nach get_results_by_category
oder filter_search_results):
def _coerce_team(item: Any) -> Team:
if isinstance(item, Team):
return item
if isinstance(item, dict):
d = item["team"] if isinstance(item.get("team"), dict) else item
comps_raw = d.get("competences") or []
return Team(
team_id=str(d["team_id"]),
ouid=str(d["ouid"]),
team_name=str(d.get("team_name") or ""),
focus_name=str(d.get("focus_name") or ""),
about_us=str(d.get("about_us") or ""),
offerings=str(d.get("offerings") or ""),
interests=str(d.get("interests") or ""),
competences=[
TeamCompetence(
name=str(c["name"]),
top_competency=bool(c.get("top_competency", False)),
)
for c in comps_raw
],
references=[
TeamReference(
partner_name=str(r.get("partner_name") or ""),
projects=str(r["projects"]),
)
for r in (d.get("references") or [])
],
)
raise TypeError(f"Unsupported team payload type: {type(item)}")
filter_search_results und get_results_by_category
filter_search_results (Anforderung 8.5/8.6) wird so erweitert, dass für
search_type == "team_search":
role_filtermatcht gegenteam.focus_name(stattcap.role_name).competence_filtermatcht gegen die Liste der Kompetenz-Namen ([c["name"] for c in item["competences"]]).- Ein neuer optionaler Filter
top_competency_only: bool = Falsewird im Tool nicht eingeführt; stattdessen kanncompetence_filterauf Top-Kompetenzen beschränkt werden, indem das TabellenfeldTop-Kompetenzengenutzt wird. Eine echte Top-Filter-Funktion wird über die existierendecompetence_filter-Logik realisiert: Wenn ein Filterwert mit Suffix(Top)versehen ist, wird das Suffix entfernt und nur Top-Kompetenzen werden geprüft. Detail-Verfeinerung erfolgt in Tasks; das Tool-Surface bleibt unverändert. availability_date_*undis_fully_availablewerden ignoriert; die Werte fließen in dieApplied Filters-Tabelle mit Suffix"(ignored: not applicable for team_search)"(Anforderung 8.6 / 6.7).task_*-Filter (task_text_filter,task_competence_filter) sind weiterhin nur fürtask_searchaktiv und werden fürteam_searchignoriert (kein zusätzlicher Hinweis nötig, da bereits heute so dokumentiert).
get_results_by_category benötigt nur eine kleine Erweiterung in
_format_results_table, da der Branch bereits über search_type und
matching_method routet. Das LLM-Mode-Sortierkey für team_search ist
(category_rank, item.get("team_id") or ""), parallel zur bestehenden
Capacity-Logik.
Konfigurationserweiterung
config.py wird um TeamMatchingConfig und das Feld MatchingConfig.team
erweitert. Außerdem wird in load_config das neue TOML-Sub-Table
[matching.team] geparst:
@dataclass(frozen=True)
class TeamMatchingConfig:
top_competency_weight: float = 1.5
@dataclass(frozen=True)
class MatchingConfig:
# ... bestehende Felder ...
team: TeamMatchingConfig = TeamMatchingConfig()
Validierung in load_config (Anforderung 12.6):
team_raw = matching_raw.get("team", {}) or {}
top_w_raw = team_raw.get("top_competency_weight", 1.5)
try:
top_w = float(top_w_raw)
except (TypeError, ValueError) as exc:
raise ConfigError(
"matching.team.top_competency_weight must be a number, "
f"got {top_w_raw!r}"
) from exc
if top_w < 1.0:
raise ConfigError(
"matching.team.top_competency_weight must be >= 1.0, "
f"got {top_w}"
)
config.toml.example wird um den neuen Abschnitt erweitert:
[matching.team]
# Gewichtungsfaktor für Top-Kompetenzen im Score-basierten Team-Matching.
# Default: 1.5. Muss numerisch und >= 1.0 sein.
top_competency_weight = 1.5
Dokumentations- und Agenten-Updates
Die folgenden Dateien werden im Rahmen der Implementierungstasks aktualisiert (Anforderung 10/11):
docs/architecture.md: Neuer Unterabschnitt "Team Profile" im Datenmodell-Kapitel, Datenquellen-Tabelle wird um vier Views ergänzt, Tool-Surface-Tabelle umfind_matching_teams/list_teams/get_team_details, Runtime-View für Aufgabe → Team in beiden Modi.README.md: Quick-Start- und Usage-Abschnitt umProfile_Type-Wahl, neue Views, fehlende Verfügbarkeitsfilter und neue Spalten ergänzen..github/agents/teamlandkarte_agent.mdund.kiro/agents/teamlandkarte.md:Profile_Type-Wertecapacity/teammit Bedeutung dokumentieren.- Ablauf-Frage "Profile_Type" zur initialen Captures-Phase ergänzen.
- Hinweis: Verfügbarkeitsfilter wirken in
team-Suchen nicht, Ergebnisspalten weichen ab. - Erwähnung von
find_matching_teams,list_teams,get_team_details. - Bestätigungs-Workflow bleibt für beide Profile_Type-Werte gleich.
Datenmodelle
Profile_Type-Enum (logisch)
Profile_Type ist bewusst kein enum.Enum im Code, sondern eine String-Konstante in
mcp_server.py (parallel zu _ALLOWED_MATCHING_METHODS):
_ALLOWED_PROFILE_TYPES: tuple[str, ...] = ("capacity", "team")
Begründung: MCP-Tools übergeben Strings, und der Profile_Type ergibt sich implizit aus
dem Tool-Namen (find_matching_capacities → capacity, find_matching_teams →
team). Ein expliziter Parameter würde redundant zur Tool-Identität werden. Im
persistierten Suchergebnis wird das Feld als search_type ∈ {"capacity_search","task_search","team_search"} geführt.
Team und Folge-Records
| Feld | Typ | Quelle | Bemerkungen |
|---|---|---|---|
team_id |
str |
teamlandkarte_v_teams_latest.team_id |
Stringform für SearchCache-Persistenz |
ouid |
str |
teamlandkarte_v_teams_latest.ouid |
Join-Schlüssel für Kompetenzen + Referenzen |
team_name |
str |
teamlandkarte_v_teammeter_organizational_units_latest.<name> |
INNER JOIN über team_id = id |
focus_name |
str |
teamlandkarte_v_teams_latest.focus_name |
NULL → "", dient als Rollen-Stellvertreter |
about_us |
str |
teamlandkarte_v_teams_latest.about_us |
NULL → "" |
offerings |
str |
teamlandkarte_v_teams_latest.offerings |
NULL → "" |
interests |
str |
teamlandkarte_v_teams_latest.interests |
NULL → "" |
competences |
list[TeamCompetence] |
teamlandkarte_v_teammeter_team_competences_latest |
Sortiert: Top zuerst, dann Name |
references |
list[TeamReference] |
teamlandkarte_v_team_references_latest |
Sortiert: partner_name, dann projects |
TeamCompetence: (name: str, top_competency: bool) mit
top_competency aus COALESCE(top_competency, FALSE) (Anforderung 3.5).
TeamReference: (partner_name: str, projects: str) – partner_name darf ""
sein, projects ist non-empty (whitespace-only wird in der DB-Schicht gefiltert,
Anforderung 4.6).
Persistiertes Such-Ergebnis (SearchCache)
{
"search_type": "team_search",
"matching_method": "score", // oder "llm_fulltext"
"reference": {
"availability_date_start": null, // immer null bei team_search
"availability_date_end": null
},
"summary": {
"Top": 0, "Good": 0, "Partial": 0, "Low": 0, "Irrelevant": 0
},
"by_category": {
"Top": [
{
"team_id": "...",
"ouid": "...",
"team_name": "...",
"focus_name": "...",
"about_us": "...",
"offerings": "...",
"interests": "...",
"competences": [
{"name": "Python", "top_competency": true},
{"name": "FastAPI", "top_competency": false}
],
"references": [
{"partner_name": "DB Cargo", "projects": "..."}
],
// Score-Modus:
"competence_score": 0.81,
"role_score": 0.5,
"overall_score": 0.74,
// LLM-Modus:
"category": "Good",
"rationale": "..."
}
],
"...": []
},
"errors": [] // nur llm_fulltext
}
asdict(team) erzeugt diese Struktur weitgehend automatisch; lediglich die
verschachtelten TeamCompetence/TeamReference-Dataclasses werden ebenfalls über
asdict rekursiv flach gemacht. Der Server-Code stellt sicher, dass
category/Score-Felder über dict | asdict(team)-Merge ergänzt werden, exakt wie im
Capacity-Pfad.
META-JSON in der Tool-Antwort
{
"search_id": "<uuid>",
"filter_id": null,
"default_category": "Top",
"matching_method": "score",
"search_type": "team_search" // neu für find_matching_teams
}
Begründung: search_type wird zusätzlich zu matching_method ausgegeben, damit der
Agent in seiner UI klar zwischen Capacity- und Team-Suchen unterscheiden kann
(Anforderung 1.4 / 8.3).
Korrektheits-Eigenschaften
Eine Eigenschaft ist ein Verhalten, das für alle gültigen Ausführungen des Systems gelten muss – eine formale Aussage darüber, was die Software tun soll. Eigenschaften überbrücken die Lücke zwischen menschenlesbaren Anforderungen und maschinell prüfbaren Korrektheits-Garantien. Jede Eigenschaft enthält einen expliziten "für alle"-Quantifikator und referenziert die Anforderungen, die sie validiert.
Die folgenden Eigenschaften ergeben sich aus der Vorprüfung der EARS-Akzeptanzkriterien
und wurden via Reflexion auf logisch eigenständige Aussagen reduziert. Akzeptanzkriterien,
die als reine Beispiele klassifiziert wurden (Tool-Existenz, Markdown-Doku-Strings,
spezifische Fehlermeldungen für ungültige team_id), werden über klassische Unit-Tests
abgedeckt und sind nicht als Properties aufgeführt.
Property 1: matching_method-Validierung in find_matching_teams
Für jeden String m, der nicht (case-insensitiv getrimmt) in
{"score", "llm_fulltext"} liegt und nicht leer/None ist, gibt
find_matching_teams(role_name, competences, matching_method=m) eine
Fehlermeldung zurück, die sowohl 'score' als auch 'llm_fulltext' als
erlaubte Werte enthält, und führt keinen DB- oder LLM-Aufruf aus.
Validates: Requirements 1.5, 1.6
Property 2: SELECT-Only-Eigenschaft aller neuen Trino-Queries
Für alle Aufrufe von TrinoClient.get_all_teams, get_team_by_id,
get_team_competences, batch_get_team_competences, get_team_references
und batch_get_team_references (mit beliebigen gültigen Eingaben) ist die
ausgeführte SQL-Anfrage ein SELECT- oder WITH-Statement, sodass
_ensure_select_only(sql) ohne Fehler durchläuft.
Validates: Requirements 2.6
Property 3: Stammdaten-Konsistenz und INNER-JOIN-Filter
Für jede Stub-DB-Antwort (Listen von Teams- und
OrganizationalUnits-Zeilen mit beliebigen NULL/Empty-Verteilungen) gilt:
get_all_teams()
- enthält ausschließlich Teams, deren
team_idin der OrganizationalUnits-Liste überideinen Treffer hat (Anforderung 2.3), - liefert für jede der Spalten
about_us,offerings,interests,focus_nameeinen leeren String, wenn die QuelleNULLoder leer ist, und niemalsNone(Anforderung 2.4), - setzt
Team.team_nameauf den Namen aus der OU-Zeile, der demteam_id-Match entspricht (Anforderung 2.2), - liefert für jede gefundene
team_idein konsistentes Ergebnis mitget_team_by_id(team_id)(Anforderung 2.5).
Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5
Property 4: Kompetenz-Batch ist konsistent zur Einzel-Variante
Für jede Liste von OUIDs ouids und jede Stub-DB-Antwort gilt:
batch_get_team_competences(ouids)[ouid] ist für jedes ouid ∈ ouids
gleich get_team_competences(ouid). Außerdem gilt für jeden Eintrag der
Ergebnisliste:
top_competencyistFalse, wenn die QuellzeileNULLenthielt (Anforderung 3.5),- Einträge ohne auflösbaren Kompetenz-Namen sind nicht enthalten (Anforderung 3.6),
- die Reihenfolge ist
(top_competency desc, name asc)und damit deterministisch (Anforderung 3.7), - für
ouid-Werte ohne Kompetenzen ist die Liste leer (Anforderung 3.4).
Validates: Requirements 3.1, 3.3, 3.4, 3.5, 3.6, 3.7
Property 5: Referenz-Batch ist konsistent zur Einzel-Variante
Für jede Liste von OUIDs ouids und jede Stub-DB-Antwort gilt:
batch_get_team_references(ouids)[ouid] ist für jedes ouid ∈ ouids
gleich get_team_references(ouid). Außerdem gilt für jeden Eintrag:
- Bei
partner_id IS NULLoder leerem Partner-Join istpartner_name == "", der Eintrag bleibt aber in der Liste erhalten (Anforderung 4.5), - Einträge mit leerem oder ausschließlich Whitespace gefülltem
projectssind nicht enthalten (Anforderung 4.6), - die Reihenfolge ist
(partner_name asc, projects asc)und damit deterministisch (Anforderung 4.7), - für
ouid-Werte ohne Referenzen ist die Liste leer (Anforderung 4.4).
Zusätzlich ruft die Batch-Methode genau einen cur.execute(...)-Call
gegen teamlandkarte_v_team_references_latest ab (Anforderung 4.3).
Validates: Requirements 4.1, 4.3, 4.4, 4.5, 4.6, 4.7
Property 6: Determinismus und Vollständigkeit der Team-Profil-Serialisierung
Für alle TeamProfile-Instanzen p (inklusive solcher mit allen
Pflichtfeldern leer, leeren Listen und Referenzen mit leerem
partner_name) gilt:
serialize_team_profile(p) == serialize_team_profile(p)(Determinismus, Anforderung 13.1).serialize_team_profile(build_team_profile(team)) == serialize_team_profile(build_team_profile(team))für jedesTeam(Idempotenz der Profil-Bildung, Anforderung 13.2).- Die Ausgabe enthält jede der sieben Überschriften
Teamname:,Schwerpunkt:,Über uns:,Leistungen:,Interessen:,Kompetenzen:,Referenzen:mindestens einmal in genau dieser Reihenfolge (Anforderungen 5.4, 5.7, 13.3). - Für jede Kompetenz mit
top_competency=Trueenthält die Ausgabe das Marker-Suffix(Top)direkt am Kompetenz-Namen (Anforderung 5.5). - Für jede Referenz mit
partner_name != ""enthält die Ausgabe sowohlpartner_nameals auchprojectsals Substring (Anforderung 5.6); für Referenzen mitpartner_name == ""enthält die zugehörige Zeile keinPartner:-Token (Anforderung 5.3). - Die Reihenfolge der Listen-Elemente in der Serialisierung entspricht
exakt der Reihenfolge in
profile.competencesbzw.profile.references(Anforderung 13.4).
Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 13.1, 13.2, 13.3, 13.4
Property 7: Top-Kompetenz-Monotonie im Score-Matching
Für alle Requirements, alle Listen cs von Kompetenz-Namen und alle
Konfigurationswerte top_weight ≥ 1.0 gilt:
Matcher.match_teams([T_top], req, top_competency_weight=top_weight) ergibt
für T_top einen competence_score, der größer oder gleich dem
competence_score von T_plain ist, wobei
T_plainein Team mitcompetences=[TeamCompetence(c, top_competency=False) for c in cs]ist,T_topein Team mit identischercompetences-Liste, jedochtop_competency=Truefür alle Einträge ist.
Außerdem ist bei top_weight == 1.0 die Differenz exakt 0.0 (Anforderung
6.4: Top-Markierung ohne Gewicht-Effekt).
Validates: Requirements 6.4
Property 8: Score-Pfad liefert gültige Score- und Kategorie-Werte
Für jede Liste teams: list[Team] und alle gültigen Requirements req
liefert Matcher.match_teams(teams, req, top_competency_weight=cfg.matching.team.top_competency_weight) ein
TeamMatchResult, in dem für jedes ScoredTeam gilt:
0.0 <= competence_score <= 1.0,0.0 <= role_score <= 1.0,0.0 <= overall_score <= 1.0,category ∈ {"Top","Good","Partial","Low","Irrelevant"},categoryist konsistent mitcategorize(overall_score, cfg.matching)(gleiche Schwellwerte wie für Kapazitäten),- die Verfügbarkeitsfelder von
req(date_start,date_end) ändern weder die Liste noch die Scores (Anforderung 6.7).
Validates: Requirements 6.1, 6.2, 6.3, 6.5, 6.7
Property 9: LLM-Pfad ist eine vollständige Partition mit gültigen Kategorien
Für jede Liste teams: list[Team], jedes task_profile: TaskProfile und
jede deterministische LLM-Maske
(mask: list[bool] mit len(mask) == len(teams), wobei True einen
LLM-Fehler simuliert) liefert
LlmFulltextMatcher.match_teams(task_profile=..., teams=teams) ein
LlmFulltextResult, für das gilt:
sum(len(items) for items in result.by_category.values()) + len(result.errors) == len(teams)(Vollständigkeit, Anforderung 7.1, 7.6).- Alle
team_id-Werte überby_categoryunderrorssind paarweise eindeutig (kein Team verschwindet oder dupliziert sich). - Jede
LlmFulltextItem.category ∈ {"Top","Good","Partial","Low","Irrelevant"}(Anforderung 7.2). - Innerhalb jeder Kategorie sind die Items aufsteigend nach
item_id(team_id) sortiert (Anforderung 7.7). - Bei erfolgreichem LLM-Aufruf ist
rationaleein nicht-leerer String; bei fehlerhaftem LLM-Aufruf erscheint das Team inerrorsund nicht inby_category(Anforderung 7.3, 7.6).
Validates: Requirements 7.1, 7.2, 7.3, 7.6, 7.7
Property 10: Ungültige LLM-Kategorien fallen auf Irrelevant zurück
Für jeden String c, der (nach normalize_category) nicht in
{"Top","Good","Partial","Low","Irrelevant"} liegt, und jedes
LLM-Antwort-JSON {"category": c, "rationale": r} mit beliebigem r gilt:
das resultierende LlmFulltextItem hat category == "Irrelevant" und
rationale enthält einen Hinweis-Text auf die ungültige LLM-Antwort.
Validates: Requirements 7.5
Property 11: META und SearchCache markieren Team-Suchen korrekt
Für jede erfolgreiche find_matching_teams-Antwort gilt:
- Die Ausgabe enthält eine Zeile
SEARCH_ID=<uuid>mit<uuid>als gültigem UUID-String (Anforderung 8.1). - Die Ausgabe enthält eine Zeile
META=<json>mit<json>["search_type"] == "team_search"und<json>["matching_method"] ∈ {"score","llm_fulltext"}(Anforderungen 1.4, 8.3). - Der zugehörige
SearchCache-Eintrag hatentry.results["search_type"] == "team_search"undentry.results["matching_method"] == <json>["matching_method"](Anforderung 8.2). get_results_by_category(search_id, ...)liefert fürteam_searcheine Tabelle, die je nachmatching_methoddie in_format_results_tabledefinierten Team-Header enthält (Team Name,Schwerpunkt,Top-Kompetenzen, ... Anforderung 8.4).
Validates: Requirements 1.4, 8.1, 8.2, 8.3, 8.4
Property 12: Verfügbarkeitsfilter werden in Team-Suchen ignoriert
Für jeden filter_search_results-Aufruf auf einen team_search-Cache-Eintrag
mit beliebigen Werten für availability_date_start,
availability_date_end und is_fully_available gilt:
- Die Menge der gefilterten Items ist die gleiche wie bei einem Aufruf
ohne Verfügbarkeitsfilter (gleiche
role_filter/competence_filter). - Die Ausgabe enthält in der
Applied Filters-Tabelle für jeden Verfügbarkeitswert einen Hinweis, der die Zeichenkette"team_search"enthält und kennzeichnet, dass der Filter nicht wirksam ist (Anforderungen 6.7, 8.5, 8.6).
Validates: Requirements 6.7, 8.5, 8.6
Property 13: Konfig-Validierung von top_competency_weight
Für jeden Wert v, der entweder
- ein Float
≥ 1.0ist (positiv):load_config(...)liefert eineAppConfigmitcfg.matching.team.top_competency_weight == float(v), oder - nicht-numerisch oder
< 1.0(negativ):load_config(...)wirft einenConfigError, dessen Meldung sowohl den Schlüsselnamenmatching.team.top_competency_weightals auch den fehlerhaften Wert enthält.
Wenn der Schlüssel im TOML fehlt, ist top_competency_weight == 1.5.
Validates: Requirements 12.5, 12.6
Property 14: Schema-Verifikation deckt fehlende Spalten in den vier neuen Views auf
Für jede Mock-DB-Antwort, die in einer der vier Views
(teamlandkarte_v_teams_latest,
teamlandkarte_v_teammeter_organizational_units_latest,
teamlandkarte_v_teammeter_team_competences_latest,
teamlandkarte_v_team_references_latest) eine erwartete Spalte weglässt,
liefert verify_required_columns(...) für die in mcp_server.build_server
konfigurierte schema_expected-Erweiterung einen SchemaIssue mit dem
betroffenen Tabellennamen und enthält die fehlende Spalte in message.
Wenn alle erwarteten Spalten vorhanden sind, ist das Issue-Set für diese
vier Views leer.
Validates: Requirements 12.1, 12.2, 12.3, 12.4
Fehlerbehandlung
| Fehlerquelle | Verhalten | Referenz |
|---|---|---|
matching_method ungültig |
find_matching_teams gibt String mit Fehlermeldung + erlaubten Werten zurück, ohne DB-/LLM-Aufruf. |
1.5/1.6, Property 1 |
competences leer / nur Whitespace |
_validate_requirements_minimum wirft ValueError, der vom Tool als Fehlermeldung formatiert wird (parallel zum Capacity-Pfad). |
1.5 (Defaults wie Capacities) |
Bestätigung fehlt (require_confirmation=true) |
Tool gibt Hinweis zurück und ruft Matching nicht auf. | 1.5 |
team_id unbekannt |
get_team_details liefert f"Team not found: {team_id}", kein Stack-Trace. |
9.3 |
| INNER JOIN ohne Treffer | Team wird nicht in get_all_teams aufgenommen; get_team_by_id liefert None. |
2.3 / 9.3 |
competence_id nicht auflösbar |
Eintrag wird in der DB-Schicht herausgefiltert. | 3.6 |
partner_id NULL / Join leer |
partner_name = "", Referenz bleibt erhalten. |
4.5 |
projects leer / Whitespace |
Eintrag wird ausgeschlossen. | 4.6 |
| LLM-JSON nicht parsebar | LlmFulltextError(item_id=team_id, error="invalid JSON: ...") in errors-Liste; Team nicht in by_category. |
7.4, 7.6 |
LLM-Fehler (AzureAPIError, generische Exception) |
LlmFulltextError(error=type+message) in errors-Liste. |
7.6 |
| Ungültige LLM-Kategorie | category = "Irrelevant", Hinweis im rationale-Suffix. |
7.5 |
Ungültiger top_competency_weight (TOML) |
ConfigError beim Start. |
12.6 |
| Schema-Spalte fehlt | verify_required_columns erzeugt SchemaIssue; build_server wirft RuntimeError("Database schema verification failed: ...") und der Server startet nicht. |
12.1-12.4 |
Unbekannte/abgelaufene search_id |
filter_search_results und get_results_by_category geben STATUS=unknown_or_expired zurück (existierender Pfad). |
8.1 |
Sicherheitsrelevant ist insbesondere die Read-Only-Eigenschaft aller neuen Queries.
Die zentrale Stelle dafür ist die bestehende _ensure_select_only-Guard, die in
allen sechs neuen TrinoClient-Methoden vor dem cur.execute(...)-Aufruf greift.
Teststrategie
Dualer Ansatz
Die Tests verwenden den im Projekt etablierten dualen Ansatz aus
Unit-Tests (für konkrete Beispiele und Edge Cases) und Property-Based
Tests (für universell quantifizierte Eigenschaften). Das eingesetzte
PBT-Framework ist Hypothesis, parallel zu den bestehenden
tests/test_*_pbt.py-Modulen. Property-Based-Tests werden nicht von
Hand reimplementiert.
Konfiguration
- Mindestens
max_examples=100pro Property-Test (analog zu existierenden PBT-Tests). deadline=Nonefür Tests mit asyncio-Loop.- Jeder PBT-Test trägt einen Tag-Kommentar im Format:
# Feature: team-profile-matching, Property <N>: <kurze Beschreibung> - Jede in Korrektheits-Eigenschaften definierte Property wird durch genau einen Property-Based Test implementiert.
Unit-Test-Schwerpunkte
Unit-Tests decken die als yes - example markierten Akzeptanzkriterien ab und
ergänzen die Properties:
Profile_Type-Konstante existiert und enthält"capacity"und"team"(Anforderung 1.1).- Tools
find_matching_teams,list_teams,get_team_detailssind überFastMCPregistriert (Anforderungen 1.3, 9.1, 9.2). find_matching_capacities-Regression: Snapshot-Test, dass Outputs für einen festen Input nach dem Refactoring unverändert bleiben (Anforderung 1.2).- Markdown-Outputs von
list_teamsundget_team_detailsenthalten die geforderten Header und Sektionen (Anforderungen 9.1, 9.2, 9.4). - Fehler-Beispiel:
get_team_details("unknown")liefert eine Fehlermeldung, dieunknownenthält (Anforderung 9.3). - Doku-Snapshot-Tests:
docs/architecture.md,README.md,.kiro/agents/teamlandkarte.md,.github/agents/teamlandkarte_agent.mdenthalten die definierten Stichworte (Profile_Type,find_matching_teams,team_search, …) (Anforderungen 10.1-10.6, 11.1-11.9).
Property-Based-Tests (Übersicht)
| Test-Datei (vorgeschlagen) | Property | Hypothesis-Strategie |
|---|---|---|
test_team_matching_method_validation_pbt.py |
1 | st.text() ohne whitelisted Werte |
test_team_trino_select_only_pbt.py |
2 | Stub-Cursor, der _ensure_select_only jeden generierten SQL-String prüfen lässt |
test_team_master_data_pbt.py |
3 | Strategy für Listen aus Teams- und OU-Stub-Zeilen mit beliebigen NULL-Verteilungen |
test_team_competences_batch_pbt.py |
4 | Strategy aus Mengen von OUIDs + zufälligen Top/Name-Verteilungen |
test_team_references_batch_pbt.py |
5 | Strategy für Referenzzeilen mit/ohne Partner und Whitespace-projects |
test_team_profile_serialization_pbt.py |
6 | Strategy für TeamProfile analog zu _capacity_profile in test_profile_serialization_pbt.py |
test_team_score_top_weight_pbt.py |
7 | Strategy für Kompetenz-Listen + top_weight ∈ [1.0, 5.0] |
test_team_score_pipeline_pbt.py |
8 | Strategy für Team-Listen mit injizierter Stub-SimilarityEngine |
test_team_llm_fulltext_partition_pbt.py |
9 | Strategy: mask: list[bool] analog zu test_batch_completeness_pbt.py |
test_team_llm_invalid_category_pbt.py |
10 | Strategy: zufällige Strings + JSON-Wrapper |
test_team_meta_search_type_pbt.py |
11 | Strategy: zufällige Inputs + Snapshot-Parser für META-JSON |
test_team_filter_ignores_availability_pbt.py |
12 | Strategy: zufällige Datumswerte + is_fully_available |
test_team_config_top_weight_pbt.py |
13 | Strategy: zufällige numerische und non-numerische Werte |
test_team_schema_verification_pbt.py |
14 | Strategy: Powerset der erwarteten Spalten pro View, jeweils minus eine Spalte |
Stubs und Fakes
- DB-Stub: Ein In-Memory-
DBClient-Double (analog zu_StubDbintests/test_batch_completeness_pbt.py) liefert deterministische Team-/Kompetenz-/Referenzlisten und zählt SQL-Aufrufe für die Batch-Konsistenz-Property. - LLM-Fake: Maskenbasiertes Fake (
_MaskLlm-Pattern aus existierenden Tests) erzeugt deterministischechat_completion-Antworten plus einstreuendeAzureAPIErrors. - SimilarityEngine-Stub: Liefert deterministische BM25-Scores für die Top-Kompetenz-Monotonie-Property; verhindert echte LLM-Aufrufe in den Score-Pfad-Tests.
Integrationstests
Bestehende Integrationstests (test_integration_matching.py,
test_llm_fulltext_integration.py) werden um team_search-Pendants
ergänzt: ein Smoke-Test pro matching_method, der mit einem
gemockten Trino-Cursor und einem gemockten Azure-Client den vollen Pfad
durchläuft (find_matching_teams → SearchCache → get_results_by_category
→ filter_search_results).