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": "036a2f60-80b4-4a0b-8cac-7201dd154bed", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,163 @@
|
||||
# Design Document: Capacity Details Enrichment
|
||||
|
||||
## Overview
|
||||
|
||||
This feature enriches the existing `get_capacity_details` MCP tool to display additional information about a capacity: its description, references (partner + projects), and certifications. The data is already available in the database and exposed through existing `DBClient` methods (`get_capacity_description`, `get_capacity_references`, `get_capacity_certificates`). The change is purely in the tool's output formatting layer.
|
||||
|
||||
The tool currently returns a Markdown table with basic capacity fields (ID, Owner, Role, Competences, Availability) followed by a "Next steps" section. After this enhancement, it will include three additional sections between the table and the next steps: Beschreibung, Referenzen, and Zertifizierungen.
|
||||
|
||||
## Architecture
|
||||
|
||||
The change is localized to the `get_capacity_details` tool function inside `build_server()` in `src/teamlandkarte_mcp/mcp_server.py`. No new modules, classes, or external dependencies are needed.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as MCP Client
|
||||
participant Tool as get_capacity_details
|
||||
participant DB as DBClient
|
||||
|
||||
Client->>Tool: call(capacity_id)
|
||||
Tool->>DB: get_capacity_by_id(capacity_id)
|
||||
DB-->>Tool: Capacity | None
|
||||
Tool->>DB: get_capacity_description(capacity_id)
|
||||
DB-->>Tool: str | None
|
||||
Tool->>DB: get_capacity_references(capacity_id)
|
||||
DB-->>Tool: list[CapacityReferenceRow]
|
||||
Tool->>DB: get_capacity_certificates(capacity_id)
|
||||
DB-->>Tool: list[str]
|
||||
Tool-->>Client: Formatted Markdown string
|
||||
```
|
||||
|
||||
### Design Decisions
|
||||
|
||||
1. **Sequential DB calls** – The three additional queries are simple key lookups on indexed views. Parallelizing them would add complexity (async conversion of the tool) for negligible latency gain. Keep the tool synchronous.
|
||||
2. **Formatting inline** – The formatting logic is simple string concatenation. No need for a separate formatter class.
|
||||
3. **Empty-state handling** – Each section shows a "(keine)" placeholder when data is absent, keeping the output structure predictable for LLM consumers.
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### Modified Component: `get_capacity_details` tool
|
||||
|
||||
**Current signature** (unchanged):
|
||||
```python
|
||||
def get_capacity_details(capacity_id: int | str) -> str:
|
||||
```
|
||||
|
||||
**New internal calls added:**
|
||||
```python
|
||||
description: str | None = db_client.get_capacity_description(capacity_id)
|
||||
references: list[CapacityReferenceRow] = db_client.get_capacity_references(capacity_id)
|
||||
certificates: list[str] = db_client.get_capacity_certificates(capacity_id)
|
||||
```
|
||||
|
||||
**Output format** (Markdown string):
|
||||
```
|
||||
| ID | Owner | Role | Competences | Availability |
|
||||
| ... |
|
||||
|
||||
## Beschreibung
|
||||
|
||||
<description text or "(keine)">
|
||||
|
||||
## Referenzen
|
||||
|
||||
- **Partner A**: Projekt X, Projekt Y
|
||||
- Projekt Z (no partner)
|
||||
|
||||
*or* Referenzen: (keine)
|
||||
|
||||
## Zertifizierungen
|
||||
|
||||
- Zertifikat 1
|
||||
- Zertifikat 2
|
||||
|
||||
*or* Zertifizierungen: (keine)
|
||||
|
||||
## Next steps
|
||||
|
||||
Call find_matching_tasks(capacity_id=...) to see matching open tasks.
|
||||
```
|
||||
|
||||
### Existing Interfaces Used (no changes)
|
||||
|
||||
| Method | Returns | Source |
|
||||
|--------|---------|--------|
|
||||
| `DBClient.get_capacity_description(capacity_id)` | `str \| None` | `teamlandkarte_v_capacities_latest.description` |
|
||||
| `DBClient.get_capacity_references(capacity_id)` | `list[CapacityReferenceRow]` | `teamlandkarte_v_capacity_references_latest` joined with partners |
|
||||
| `DBClient.get_capacity_certificates(capacity_id)` | `list[str]` | `teamlandkarte_v_capacity_certificates_latest.description` |
|
||||
|
||||
## Data Models
|
||||
|
||||
### CapacityReferenceRow (existing, unchanged)
|
||||
|
||||
```python
|
||||
class CapacityReferenceRow(TypedDict):
|
||||
partner_name: str # May be empty string when partner_id is NULL
|
||||
projects: str # Project text from the references view
|
||||
```
|
||||
|
||||
No new data models are introduced.
|
||||
|
||||
|
||||
## 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: All enrichment data is fetched for any capacity
|
||||
|
||||
*For any* valid capacity ID that resolves to an existing capacity, the tool SHALL invoke `get_capacity_description`, `get_capacity_references`, and `get_capacity_certificates` with that capacity ID.
|
||||
|
||||
**Validates: Requirements 1.1, 2.1, 3.1**
|
||||
|
||||
### Property 2: Non-empty data appears in output
|
||||
|
||||
*For any* capacity with a non-empty description, a non-empty list of references, and a non-empty list of certificates, the formatted output SHALL contain the description text, every reference's projects text, and every certificate string.
|
||||
|
||||
**Validates: Requirements 1.2, 2.2, 3.2**
|
||||
|
||||
### Property 3: Section ordering is fixed
|
||||
|
||||
*For any* capacity (regardless of which fields are empty or populated), the output string SHALL contain the section markers in the order: capacity table first, then "Beschreibung", then "Referenzen", then "Zertifizierungen", then "Next steps" — and each section SHALL be separated by at least one blank line.
|
||||
|
||||
**Validates: Requirements 4.1, 4.2**
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Behaviour |
|
||||
|----------|-----------|
|
||||
| `get_capacity_by_id` returns `None` | Return `"Capacity not found: {capacity_id}"` (existing behaviour, unchanged) |
|
||||
| `get_capacity_description` returns `None` | Display `"Beschreibung: (keine)"` |
|
||||
| `get_capacity_references` returns `[]` | Display `"Referenzen: (keine)"` |
|
||||
| `get_capacity_certificates` returns `[]` | Display `"Zertifizierungen: (keine)"` |
|
||||
| `get_capacity_references` returns a row with empty `partner_name` | Display only the `projects` text (no bold partner prefix) |
|
||||
| Any DB call raises an exception | Let it propagate (existing error handling in the MCP framework catches and reports it) |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Test `get_capacity_details` with a mock DB client returning known data for all three new fields → verify output contains expected sections and content.
|
||||
- Test empty-state: description=None, references=[], certificates=[] → verify "(keine)" placeholders appear.
|
||||
- Test edge case: reference with empty `partner_name` → verify only projects text is shown.
|
||||
- Test that capacity-not-found still returns the error message unchanged.
|
||||
|
||||
### Property-Based Tests
|
||||
|
||||
Library: **Hypothesis** (Python)
|
||||
|
||||
Each property test runs a minimum of 100 iterations.
|
||||
|
||||
- **Property 1 test**: Generate random capacity IDs and mock DB responses. Verify all three DB methods are called with the correct ID.
|
||||
- Tag: `Feature: capacity-details-enrichment, Property 1: All enrichment data is fetched for any capacity`
|
||||
|
||||
- **Property 2 test**: Generate random non-empty descriptions (text strategy), random lists of `CapacityReferenceRow` dicts with non-empty `projects` and `partner_name`, and random lists of non-empty certificate strings. Call the formatting logic and assert all generated data appears in the output.
|
||||
- Tag: `Feature: capacity-details-enrichment, Property 2: Non-empty data appears in output`
|
||||
|
||||
- **Property 3 test**: Generate random combinations of present/absent data (description: str|None, references: list of 0-5 items, certificates: list of 0-5 items). Call the formatting logic and assert section headers appear in the correct order with blank-line separation.
|
||||
- Tag: `Feature: capacity-details-enrichment, Property 3: Section ordering is fixed`
|
||||
|
||||
### Test Configuration
|
||||
|
||||
- Property-based testing library: `hypothesis` (already available in the project's test dependencies)
|
||||
- Minimum iterations: 100 per property (`@settings(max_examples=100)`)
|
||||
- Each test tagged with a comment referencing the design property
|
||||
@@ -0,0 +1,57 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
Das MCP-Tool `get_capacity_details` zeigt aktuell nur eine Tabelle mit ID, Owner, Rolle, Kompetenzen und Verfügbarkeit an. Es fehlen die Beschreibung (Description), Referenzen und Zertifizierungen einer Kapazität. Diese Informationen sind bereits in der Datenbank vorhanden und über die DB-Client-Methoden `get_capacity_description`, `get_capacity_references` und `get_capacity_certificates` abrufbar. Das Tool soll erweitert werden, um diese zusätzlichen Felder anzuzeigen.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **MCP_Server**: Der Teamlandkarte MCP Server, der Tools für Kapazitäts- und Aufgabenabgleich bereitstellt
|
||||
- **get_capacity_details_Tool**: Das MCP-Tool, das Detailinformationen zu einer einzelnen Kapazität anzeigt
|
||||
- **DB_Client**: Die Datenbankzugriffsschicht, die Kapazitätsdaten aus der Trino-Datenbank liest
|
||||
- **Capacity**: Ein Eintrag, der die verfügbare Kapazität einer Person beschreibt (ID, Owner, Rolle, Kompetenzen, Verfügbarkeit)
|
||||
- **Description**: Freitext-Beschreibung einer Kapazität aus `teamlandkarte_v_capacities_latest.description`
|
||||
- **Reference**: Ein Referenzeintrag bestehend aus Partnername und Projekten aus `teamlandkarte_v_capacity_references_latest`
|
||||
- **Certificate**: Eine Zertifizierungsbeschreibung aus `teamlandkarte_v_capacity_certificates_latest`
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Beschreibung anzeigen
|
||||
|
||||
**User Story:** Als Nutzer möchte ich die Beschreibung einer Kapazität im Tool `get_capacity_details` sehen, damit ich ein vollständigeres Bild der Kapazität erhalte.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a capacity is retrieved by `get_capacity_details`, THE get_capacity_details_Tool SHALL fetch the description via `DB_Client.get_capacity_description`
|
||||
2. WHEN the description is non-empty, THE get_capacity_details_Tool SHALL display the description in a dedicated section labeled "Beschreibung" below the capacity table
|
||||
3. WHEN the description is empty or not available, THE get_capacity_details_Tool SHALL display "Beschreibung: (keine)" in the description section
|
||||
|
||||
### Requirement 2: Referenzen anzeigen
|
||||
|
||||
**User Story:** Als Nutzer möchte ich die Referenzen einer Kapazität im Tool `get_capacity_details` sehen, damit ich die bisherigen Projekterfahrungen der Person einschätzen kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a capacity is retrieved by `get_capacity_details`, THE get_capacity_details_Tool SHALL fetch the references via `DB_Client.get_capacity_references`
|
||||
2. WHEN references exist, THE get_capacity_details_Tool SHALL display each reference as a bullet point in a section labeled "Referenzen", including partner name and projects
|
||||
3. WHEN a reference has an empty partner name, THE get_capacity_details_Tool SHALL display only the projects for that reference entry
|
||||
4. WHEN no references exist, THE get_capacity_details_Tool SHALL display "Referenzen: (keine)"
|
||||
|
||||
### Requirement 3: Zertifizierungen anzeigen
|
||||
|
||||
**User Story:** Als Nutzer möchte ich die Zertifizierungen einer Kapazität im Tool `get_capacity_details` sehen, damit ich die formalen Qualifikationen der Person erkennen kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a capacity is retrieved by `get_capacity_details`, THE get_capacity_details_Tool SHALL fetch the certificates via `DB_Client.get_capacity_certificates`
|
||||
2. WHEN certificates exist, THE get_capacity_details_Tool SHALL display each certificate as a bullet point in a section labeled "Zertifizierungen"
|
||||
3. WHEN no certificates exist, THE get_capacity_details_Tool SHALL display "Zertifizierungen: (keine)"
|
||||
|
||||
### Requirement 4: Darstellungsreihenfolge
|
||||
|
||||
**User Story:** Als Nutzer möchte ich eine konsistente und übersichtliche Darstellung aller Kapazitätsdetails, damit ich die Informationen schnell erfassen kann.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE get_capacity_details_Tool SHALL display sections in the following fixed order: Capacity-Tabelle, Beschreibung, Referenzen, Zertifizierungen, Next Steps
|
||||
2. THE get_capacity_details_Tool SHALL separate each section with a blank line for Lesbarkeit
|
||||
@@ -0,0 +1,86 @@
|
||||
# Implementation Plan: Capacity Details Enrichment
|
||||
|
||||
## Overview
|
||||
|
||||
Extend the `get_capacity_details` tool in `src/teamlandkarte_mcp/mcp_server.py` to fetch and display description, references, and certificates for a capacity. The change is localized to the tool function with inline formatting. All DB client methods already exist.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Extend `get_capacity_details` with enrichment data fetching and formatting
|
||||
- [x] 1.1 Add DB calls for description, references, and certificates
|
||||
- After the existing `get_capacity_by_id` call, add sequential calls to `db_client.get_capacity_description(capacity_id)`, `db_client.get_capacity_references(capacity_id)`, and `db_client.get_capacity_certificates(capacity_id)`
|
||||
- _Requirements: 1.1, 2.1, 3.1_
|
||||
|
||||
- [x] 1.2 Format the Beschreibung section
|
||||
- If description is non-empty, render `## Beschreibung\n\n<text>`
|
||||
- If description is None or empty, render `## Beschreibung\n\nBeschreibung: (keine)`
|
||||
- _Requirements: 1.2, 1.3_
|
||||
|
||||
- [x] 1.3 Format the Referenzen section
|
||||
- If references exist, render `## Referenzen` followed by bullet points: `- **partner_name**: projects` for each reference
|
||||
- If a reference has an empty `partner_name`, render only `- projects` (no bold partner prefix)
|
||||
- If no references exist, render `## Referenzen\n\nReferenzen: (keine)`
|
||||
- _Requirements: 2.2, 2.3, 2.4_
|
||||
|
||||
- [x] 1.4 Format the Zertifizierungen section
|
||||
- If certificates exist, render `## Zertifizierungen` followed by bullet points: `- certificate` for each entry
|
||||
- If no certificates exist, render `## Zertifizierungen\n\nZertifizierungen: (keine)`
|
||||
- _Requirements: 3.2, 3.3_
|
||||
|
||||
- [x] 1.5 Assemble output in fixed section order
|
||||
- Combine sections in order: capacity table, Beschreibung, Referenzen, Zertifizierungen, Next Steps
|
||||
- Separate each section with a blank line
|
||||
- _Requirements: 4.1, 4.2_
|
||||
|
||||
- [x] 2. Write unit tests for the enriched output
|
||||
- [x] 2.1 Test full data scenario
|
||||
- Mock DB client to return a known description, list of references (with and without partner_name), and list of certificates
|
||||
- Assert output contains all expected section headers, content, and correct ordering
|
||||
- Create test file `tests/test_capacity_details_enrichment.py`
|
||||
- _Requirements: 1.2, 2.2, 2.3, 3.2, 4.1_
|
||||
|
||||
- [x] 2.2 Test empty-state scenario
|
||||
- Mock DB client to return None description, empty references list, empty certificates list
|
||||
- Assert output contains "(keine)" placeholders for all three sections
|
||||
- _Requirements: 1.3, 2.4, 3.3_
|
||||
|
||||
- [x] 2.3 Test capacity-not-found unchanged
|
||||
- Mock `get_capacity_by_id` to return None
|
||||
- Assert the tool still returns the existing error message without calling enrichment methods
|
||||
- _Requirements: (error handling, no regression)_
|
||||
|
||||
- [x] 3. Checkpoint
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 4. Property-based tests with Hypothesis
|
||||
- [x] 4.1 Write property test: All enrichment data is fetched
|
||||
- **Property 1: All enrichment data is fetched for any capacity**
|
||||
- Generate random capacity IDs; mock DB to return a capacity. Verify `get_capacity_description`, `get_capacity_references`, and `get_capacity_certificates` are each called exactly once with the correct ID.
|
||||
- Create test file `tests/test_capacity_details_enrichment_pbt.py`
|
||||
- **Validates: Requirements 1.1, 2.1, 3.1**
|
||||
|
||||
- [x] 4.2 Write property test: Non-empty data appears in output
|
||||
- **Property 2: Non-empty data appears in output**
|
||||
- Generate random non-empty descriptions (text strategy), random lists of `CapacityReferenceRow` dicts with non-empty `projects` and `partner_name`, and random lists of non-empty certificate strings. Assert all generated data appears in the formatted output.
|
||||
- **Validates: Requirements 1.2, 2.2, 3.2**
|
||||
|
||||
- [x] 4.3 Write property test: Section ordering is fixed
|
||||
- **Property 3: Section ordering is fixed**
|
||||
- Generate random combinations of present/absent data (description: str|None, references: 0-5 items, certificates: 0-5 items). Assert section headers appear in the correct order with blank-line separation.
|
||||
- **Validates: Requirements 4.1, 4.2**
|
||||
|
||||
- [x] 5. Final checkpoint
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 6. Update agent skill documentation
|
||||
- [x] 6.1 Update `.github/skills/capacity-browsing/SKILL.md`
|
||||
- Change the `get_capacity_details` description to mention description, references, and certifications alongside role, competences, and availability window
|
||||
- [x] 6.2 Update `.kiro/agents/teamlandkarte.md`
|
||||
- Change the `get_capacity_details` description to mention that it shows description, references, and certifications alongside the basic profile
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- The implementation language is Python (matching the existing codebase and design)
|
||||
- All DB client methods (`get_capacity_description`, `get_capacity_references`, `get_capacity_certificates`) already exist — no data layer changes needed
|
||||
- Property tests use Hypothesis with `@settings(max_examples=100)`
|
||||
@@ -0,0 +1 @@
|
||||
{"specId": "036a2f60-80b4-4a0b-8cac-7201dd154bed", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,287 @@
|
||||
# Design: LLM Batch-Matching (Concurrent Requests)
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das bestehende `LlmFulltextMatcher`-Modul führt LLM-Aufrufe sequenziell aus – jeder Kandidat wartet auf die Antwort des vorherigen. Bei 30 Kandidaten mit je ~2s Latenz ergibt das ~60s Gesamtlaufzeit.
|
||||
|
||||
Die Lösung ersetzt die sequenzielle `for`-Schleife durch `asyncio.gather()` mit einem `asyncio.Semaphore` zur Begrenzung der Parallelität. Die öffentliche Schnittstelle (`match_capacities`, `match_tasks`) bleibt unverändert. Die Concurrency wird über `config.toml` konfigurierbar gemacht.
|
||||
|
||||
**Erwarteter Effekt:** Bei `max_concurrency=5` und 30 Kandidaten sinkt die Laufzeit von ~60s auf ~12s (6 Batches × 2s statt 30 × 2s).
|
||||
|
||||
## Architektur
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[MCP Server / Aufrufer] -->|match_capacities / match_tasks| B[LlmFulltextMatcher]
|
||||
B -->|asyncio.gather + Semaphore| C[_categorize_one Task 1]
|
||||
B -->|asyncio.gather + Semaphore| D[_categorize_one Task 2]
|
||||
B -->|asyncio.gather + Semaphore| E[_categorize_one Task N]
|
||||
C --> F[AzureOpenAIClient.chat_completion]
|
||||
D --> F
|
||||
E --> F
|
||||
F --> G[Azure OpenAI API]
|
||||
```
|
||||
|
||||
Die Architektur bleibt flach: Es wird kein neues Modul oder neue Klasse eingeführt. Die Änderung betrifft ausschließlich die interne Ablaufsteuerung in `LlmFulltextMatcher` und die Konfigurationsschicht.
|
||||
|
||||
### Designentscheidungen
|
||||
|
||||
1. **asyncio.Semaphore statt Thread-Pool:** Das Projekt ist bereits vollständig async (FastMCP, AsyncAzureOpenAI). Ein Semaphore ist der idiomatische Mechanismus zur Begrenzung von I/O-Concurrency in asyncio.
|
||||
|
||||
2. **Kein separates Batch-Modul:** Die Änderung ist minimal und lokal. Ein eigenes `batch_executor.py` wäre Over-Engineering für eine ~20-Zeilen-Änderung.
|
||||
|
||||
3. **Semaphore im Matcher, nicht im Client:** Der `AzureOpenAIClient` bleibt unverändert. Die Concurrency-Steuerung liegt beim Aufrufer (Matcher), da verschiedene Aufrufer unterschiedliche Limits haben könnten.
|
||||
|
||||
4. **Validierung bei Config-Load:** Ungültige `max_concurrency`-Werte (< 1 oder > 20) werden beim Start abgefangen, nicht erst beim ersten Matching-Aufruf.
|
||||
|
||||
## Komponenten und Schnittstellen
|
||||
|
||||
### 1. `AzureOpenAIConfig` (config.py)
|
||||
|
||||
Neues Feld:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class AzureOpenAIConfig:
|
||||
# ... bestehende Felder ...
|
||||
max_concurrency: int = 5
|
||||
```
|
||||
|
||||
### 2. `_parse_azure_openai` (config.py)
|
||||
|
||||
Erweiterte Parsing-Logik:
|
||||
|
||||
```python
|
||||
def _parse_azure_openai(cfg: dict) -> AzureOpenAIConfig:
|
||||
max_concurrency = int(cfg.get("max_concurrency", 5))
|
||||
if max_concurrency < 1:
|
||||
raise ConfigError(
|
||||
"azure_openai.max_concurrency must be >= 1, "
|
||||
f"got {max_concurrency}"
|
||||
)
|
||||
if max_concurrency > 20:
|
||||
raise ConfigError(
|
||||
"azure_openai.max_concurrency must be <= 20, "
|
||||
f"got {max_concurrency}"
|
||||
)
|
||||
return AzureOpenAIConfig(
|
||||
# ... bestehende Felder ...
|
||||
max_concurrency=max_concurrency,
|
||||
)
|
||||
```
|
||||
|
||||
### 3. `LlmFulltextMatcher` (matching/llm_fulltext_matcher.py)
|
||||
|
||||
Geänderte Konstruktor-Signatur:
|
||||
|
||||
```python
|
||||
class LlmFulltextMatcher:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
db: DBClient,
|
||||
client: AzureOpenAIClient,
|
||||
rationale_max_chars: int = 280,
|
||||
max_concurrency: int = 5, # NEU
|
||||
) -> None:
|
||||
self._db = db
|
||||
self._client = client
|
||||
self._rationale_max_chars = rationale_max_chars
|
||||
self._semaphore = asyncio.Semaphore(max_concurrency)
|
||||
self._max_concurrency = max_concurrency
|
||||
```
|
||||
|
||||
Neue interne Methode:
|
||||
|
||||
```python
|
||||
async def _categorize_one_throttled(
|
||||
self,
|
||||
*,
|
||||
item_id: str,
|
||||
user_prompt: str,
|
||||
raw: dict,
|
||||
) -> tuple[LlmFulltextItem | None, LlmFulltextError | None]:
|
||||
"""Wrapper um _categorize_one mit Semaphore-Begrenzung."""
|
||||
async with self._semaphore:
|
||||
return await self._categorize_one(
|
||||
item_id=item_id,
|
||||
user_prompt=user_prompt,
|
||||
raw=raw,
|
||||
)
|
||||
```
|
||||
|
||||
Geänderte `match_capacities` / `match_tasks` (Kernänderung):
|
||||
|
||||
```python
|
||||
async def match_capacities(self, *, task_profile, capacities) -> LlmFulltextResult:
|
||||
# ... Profil-Aufbau wie bisher ...
|
||||
|
||||
LOGGER.info(
|
||||
"Batch-Matching gestartet: %d Kandidaten, max_concurrency=%d",
|
||||
len(capacities), self._max_concurrency,
|
||||
)
|
||||
start_time = time.monotonic()
|
||||
|
||||
tasks = [
|
||||
self._categorize_one_throttled(
|
||||
item_id=cap_id,
|
||||
user_prompt=user_prompt,
|
||||
raw=raw,
|
||||
)
|
||||
for cap_id, user_prompt, raw in prepared
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
elapsed = time.monotonic() - start_time
|
||||
|
||||
# Ergebnisse zuordnen
|
||||
for item, error in results:
|
||||
if item is not None:
|
||||
by_category[item.category].append(item)
|
||||
elif error is not None:
|
||||
errors.append(error)
|
||||
|
||||
LOGGER.info(
|
||||
"Batch-Matching abgeschlossen: %.1fs, %d kategorisiert, %d Fehler",
|
||||
elapsed, sum(len(v) for v in by_category.values()), len(errors),
|
||||
)
|
||||
# ... Sortierung wie bisher ...
|
||||
```
|
||||
|
||||
### 4. MCP Server (mcp_server.py)
|
||||
|
||||
Übergabe des neuen Parameters bei Instanziierung:
|
||||
|
||||
```python
|
||||
llm_fulltext_matcher = LlmFulltextMatcher(
|
||||
db=db_client,
|
||||
client=azure_client,
|
||||
max_concurrency=cfg.azure_openai.max_concurrency, # NEU
|
||||
)
|
||||
```
|
||||
|
||||
### 5. config.toml
|
||||
|
||||
Neuer optionaler Schlüssel:
|
||||
|
||||
```toml
|
||||
[azure_openai]
|
||||
# ... bestehende Schlüssel ...
|
||||
|
||||
# Maximale Anzahl paralleler LLM-Anfragen (1-20, Standard: 5).
|
||||
# Höhere Werte beschleunigen das Matching, können aber Rate-Limits auslösen.
|
||||
# max_concurrency = 5
|
||||
```
|
||||
|
||||
## Datenmodelle
|
||||
|
||||
Keine neuen Datenmodelle erforderlich. Die bestehenden Strukturen bleiben unverändert:
|
||||
|
||||
- `LlmFulltextResult` (Rückgabetyp) – unverändert
|
||||
- `LlmFulltextItem` – unverändert
|
||||
- `LlmFulltextError` – unverändert
|
||||
- `AzureOpenAIConfig` – erweitert um `max_concurrency: int = 5`
|
||||
|
||||
Die Erweiterung von `AzureOpenAIConfig` ist abwärtskompatibel (Standardwert vorhanden).
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
_Eine Property ist eine Eigenschaft oder ein Verhalten, das über alle gültigen Ausführungen eines Systems hinweg gelten muss – im Wesentlichen eine formale Aussage darüber, was das System tun soll. Properties bilden die Brücke zwischen menschenlesbaren Spezifikationen und maschinell verifizierbaren Korrektheitsgarantien._
|
||||
|
||||
### Property 1: Vollständigkeit der Ergebnisse (Partition)
|
||||
|
||||
_Für jede_ Liste von Kandidaten (Capacities oder Tasks) und jede Konfiguration von `max_concurrency`, muss die Summe aller Items in `by_category` plus die Anzahl der Einträge in `errors` exakt der Anzahl der Eingabe-Kandidaten entsprechen. Kein Kandidat darf verloren gehen oder doppelt erscheinen.
|
||||
|
||||
**Validates: Requirements 1.1, 1.4, 3.2, 4.2, 4.3**
|
||||
|
||||
### Property 2: Concurrency-Begrenzung
|
||||
|
||||
_Für jede_ Anzahl von Kandidaten und jeden gültigen `max_concurrency`-Wert, darf zu keinem Zeitpunkt die Anzahl gleichzeitig laufender LLM-Aufrufe den konfigurierten `max_concurrency`-Wert überschreiten.
|
||||
|
||||
**Validates: Requirements 1.2**
|
||||
|
||||
### Property 3: Deterministische Sortierung
|
||||
|
||||
_Für jede_ Liste von Kandidaten und jede Zuordnung von Kategorien, muss das Ergebnis innerhalb jeder Kategorie aufsteigend nach `item_id` (lexikographisch) sortiert sein, und die Fehlerliste muss ebenfalls nach `item_id` sortiert sein.
|
||||
|
||||
**Validates: Requirements 3.1**
|
||||
|
||||
### Property 4: Eingabereihenfolge-Unabhängigkeit
|
||||
|
||||
_Für jede_ Permutation der Eingabe-Kandidatenliste muss das Ergebnis (`by_category` und `errors`) identisch sein – die Reihenfolge der Eingabe hat keinen Einfluss auf die Ausgabe.
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 5: Korrekte Zuordnung (Response-Mapping)
|
||||
|
||||
_Für jeden_ Kandidaten in der Eingabeliste muss die zugehörige LLM-Antwort exakt dem richtigen Kandidaten zugeordnet werden. Wenn der Mock für Kandidat X die Kategorie "Top" zurückgibt, muss das Item mit `item_id=X` in `by_category["Top"]` erscheinen.
|
||||
|
||||
**Validates: Requirements 3.4**
|
||||
|
||||
### Property 6: Config-Validierung
|
||||
|
||||
_Für jeden_ Integer-Wert `n`: Das Parsen von `azure_openai.max_concurrency = n` muss genau dann erfolgreich sein, wenn `1 <= n <= 20`. Für `n < 1` oder `n > 20` muss ein `ConfigError` geworfen werden.
|
||||
|
||||
**Validates: Requirements 2.1, 2.3, 2.4**
|
||||
|
||||
### Property 7: Logging-Konsistenz
|
||||
|
||||
_Für jede_ Ausführung von `match_capacities` oder `match_tasks` mit mindestens einem Kandidaten müssen die INFO-Log-Nachrichten (Start und Ende) die korrekte Kandidatenanzahl, das konfigurierte Concurrency-Limit, die Anzahl erfolgreicher Kategorisierungen und die Anzahl der Fehler enthalten. Die Summe von Erfolgen und Fehlern im Log muss der Eingabeanzahl entsprechen.
|
||||
|
||||
**Validates: Requirements 6.1, 6.2**
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
| Fehlerszenario | Verhalten | Auswirkung auf andere Kandidaten |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Einzelner LLM-Aufruf schlägt fehl (nach Retries) | Kandidat wird in `errors`-Liste aufgenommen | Keine – andere Kandidaten laufen unabhängig weiter |
|
||||
| Alle LLM-Aufrufe schlagen fehl | Leere `by_category`, vollständige `errors`-Liste | Kein Exception-Wurf, normaler Return |
|
||||
| `max_concurrency` ungültig (< 1 oder > 20) | `ConfigError` beim Server-Start | Server startet nicht |
|
||||
| `asyncio.TimeoutError` in einem Aufruf | Wird vom bestehenden Retry-Mechanismus in `AzureOpenAIClient` behandelt | Keine |
|
||||
| HTTP 429 (Rate Limit) | Exponentielles Backoff im `AzureOpenAIClient` (bestehendes Verhalten) | Keine direkte; Semaphore hält Slot belegt bis Retry abgeschlossen |
|
||||
|
||||
### Fehler-Isolation
|
||||
|
||||
Die Verwendung von `asyncio.gather(*tasks)` (ohne `return_exceptions=True`) in Kombination mit der bestehenden try/except-Logik in `_categorize_one` stellt sicher, dass:
|
||||
|
||||
- Jeder Task seine eigenen Exceptions fängt und als `LlmFulltextError` zurückgibt
|
||||
- Kein einzelner Fehler die gesamte `gather`-Operation abbricht
|
||||
- Die Semaphore auch im Fehlerfall korrekt freigegeben wird (async context manager)
|
||||
|
||||
## Teststrategie
|
||||
|
||||
### Property-Based Tests (pytest + hypothesis)
|
||||
|
||||
Die Property-Tests verwenden die Bibliothek **hypothesis** (bereits im Python-Ökosystem etabliert). Jeder Test wird mit mindestens 100 Iterationen konfiguriert.
|
||||
|
||||
| Property | Testansatz | Generator |
|
||||
| ------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| P1: Vollständigkeit | Generiere zufällige Kandidatenlisten mit zufälligem Mix aus Erfolg/Fehler-Mocks | `st.lists(st.builds(Capacity, ...))` |
|
||||
| P2: Concurrency-Begrenzung | Mock-Client mit Counter für gleichzeitige Aufrufe; prüfe `max_concurrent <= max_concurrency` | `st.integers(min_value=1, max_value=20)` für concurrency |
|
||||
| P3: Deterministische Sortierung | Generiere Ergebnisse mit zufälligen Kategorien; prüfe Sortierung | `st.lists(st.sampled_from(categories))` |
|
||||
| P4: Eingabereihenfolge-Unabhängigkeit | Generiere Liste, permutiere, vergleiche Ergebnisse | `st.permutations(candidates)` |
|
||||
| P5: Korrekte Zuordnung | Mock gibt item_id-spezifische Kategorien zurück; prüfe Mapping | `st.dictionaries(st.text(), st.sampled_from(categories))` |
|
||||
| P6: Config-Validierung | Generiere Integers im Bereich [-100, 100]; prüfe Erfolg/Fehler | `st.integers(min_value=-100, max_value=100)` |
|
||||
| P7: Logging-Konsistenz | Capture Logs; prüfe Zahlen gegen tatsächliche Ergebnisse | `st.lists(st.builds(Capacity, ...))` |
|
||||
|
||||
Jeder Test wird mit einem Kommentar getaggt:
|
||||
|
||||
```python
|
||||
# Feature: llm-batch-matching, Property 1: Vollständigkeit der Ergebnisse
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit Tests ergänzen die Property-Tests für spezifische Szenarien:
|
||||
|
||||
- **Leere Eingabe:** `match_capacities(capacities=[])` gibt sofort leeres Ergebnis zurück
|
||||
- **Alle Fehler:** Wenn alle LLM-Aufrufe fehlschlagen, keine Exception, vollständige Fehlerliste
|
||||
- **Default-Wert:** `LlmFulltextMatcher()` ohne `max_concurrency` verwendet 5
|
||||
- **Config-Parsing:** Fehlender `max_concurrency`-Schlüssel ergibt Standardwert 5
|
||||
- **Integration:** End-to-End-Test mit gemocktem `AzureOpenAIClient` und 10 Kandidaten
|
||||
|
||||
### Testinfrastruktur
|
||||
|
||||
- **Mock-Client:** Ein `FakeAzureOpenAIClient` der konfigurierbare Antworten (Erfolg/Fehler/Delay) pro `item_id` liefert
|
||||
- **Concurrency-Tracker:** Ein Wrapper der die maximale Anzahl gleichzeitiger Aufrufe misst (via `asyncio.Lock` + Counter)
|
||||
- **Log-Capture:** pytest `caplog` Fixture für Log-Assertions
|
||||
@@ -0,0 +1,101 @@
|
||||
# Anforderungsdokument
|
||||
|
||||
## Einleitung
|
||||
|
||||
Das bestehende LLM-Volltext-Matching (`llm_fulltext`) führt für jede Kapazität bzw. Aufgabe einen separaten, sequenziellen LLM-Aufruf an die Azure OpenAI API durch. Bei einer größeren Anzahl von Kandidaten (z. B. 20–50 Kapazitäten) führt dies zu inakzeptablen Wartezeiten, da jeder Aufruf einzeln auf die API-Antwort wartet.
|
||||
|
||||
Dieses Dokument beschreibt die Anforderungen für die Einführung eines **Batching-Mechanismus**, der mehrere LLM-Aufrufe parallel (concurrent) an die Azure OpenAI API sendet, um die Gesamtlaufzeit des Matchings signifikant zu reduzieren. Es wird dabei die asynchrone Parallelisierung (concurrent requests) genutzt, nicht die Azure Batch API (die für Offline-Verarbeitung gedacht ist und keine Echtzeit-Antworten liefert).
|
||||
|
||||
## Glossar
|
||||
|
||||
- **LLM_Fulltext_Matcher**: Bestehendes Modul (`matching/llm_fulltext_matcher.py`), 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`.
|
||||
- **Batch**: Eine Gruppe von LLM-Anfragen, die gleichzeitig (concurrent) an die Azure OpenAI API gesendet werden.
|
||||
- **Batch_Size**: Maximale Anzahl gleichzeitig laufender LLM-Anfragen innerhalb eines Batches.
|
||||
- **Concurrency_Limit**: Obergrenze für die Anzahl paralleler HTTP-Anfragen an die Azure OpenAI API, um Rate-Limits nicht zu überschreiten.
|
||||
- **Rate_Limit**: Von Azure OpenAI auferlegte Begrenzung der Anfragen pro Zeiteinheit (Requests per Minute / Tokens per Minute).
|
||||
- **Semaphore**: Synchronisationsmechanismus zur Begrenzung der gleichzeitigen Zugriffe auf eine Ressource.
|
||||
- **MCP_Server**: Der Teamlandkarte MCP-Server (`mcp_server.py`).
|
||||
- **Capacity**: Frozen Dataclass `Capacity` in `models.py`.
|
||||
- **Task**: Frozen Dataclass `Task` in `models.py`.
|
||||
- **Capacity_Profile**: Aggregiertes Volltext-Profil einer Kapazität.
|
||||
- **Task_Profile**: Aggregiertes Volltext-Profil einer Aufgabe.
|
||||
|
||||
## Anforderungen
|
||||
|
||||
### Anforderung 1: Parallele LLM-Aufrufe mit konfigurierbarer Concurrency
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass das LLM-Volltext-Matching mehrere Kapazitäten bzw. Aufgaben gleichzeitig bewertet, damit die Gesamtwartezeit bei vielen Kandidaten deutlich sinkt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `match_capacities` oder `match_tasks` mit einer Liste von Kandidaten aufgerufen wird, THE LLM_Fulltext_Matcher SHALL alle LLM-Aufrufe für die Kandidaten concurrent (nicht sequenziell) ausführen, begrenzt durch das konfigurierte Concurrency_Limit.
|
||||
2. THE LLM_Fulltext_Matcher SHALL ein Concurrency_Limit verwenden, das die maximale Anzahl gleichzeitig laufender LLM-Anfragen auf einen konfigurierbaren Wert begrenzt.
|
||||
3. THE LLM_Fulltext_Matcher SHALL als Standard-Concurrency_Limit den Wert 5 verwenden, wenn kein anderer Wert konfiguriert ist.
|
||||
4. WHEN das Concurrency_Limit erreicht ist, THE LLM_Fulltext_Matcher SHALL weitere LLM-Anfragen zurückhalten, bis ein laufender Aufruf abgeschlossen ist, ohne Anfragen zu verwerfen.
|
||||
5. THE LLM_Fulltext_Matcher SHALL die Concurrency-Begrenzung über einen asyncio-Semaphore implementieren, sodass die Event-Loop nicht blockiert wird.
|
||||
|
||||
### Anforderung 2: Konfiguration des Concurrency-Limits
|
||||
|
||||
**User Story:** Als Entwickler möchte ich das Concurrency-Limit über die Konfigurationsdatei anpassen können, damit ich es an die Rate-Limits meines Azure-OpenAI-Deployments anpassen kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL in `config.toml` unter `[azure_openai]` einen optionalen Schlüssel `max_concurrency` akzeptieren, der das Concurrency_Limit für parallele LLM-Aufrufe festlegt.
|
||||
2. WHEN `azure_openai.max_concurrency` nicht in `config.toml` gesetzt ist, THE MCP_Server SHALL den Standardwert 5 verwenden.
|
||||
3. IF `azure_openai.max_concurrency` auf einen Wert kleiner als 1 gesetzt ist, THEN THE MCP_Server SHALL beim Start einen `ConfigError` mit beschreibender Meldung werfen.
|
||||
4. IF `azure_openai.max_concurrency` auf einen Wert größer als 20 gesetzt ist, THEN THE MCP_Server SHALL beim Start einen `ConfigError` mit beschreibender Meldung werfen, da ein zu hoher Wert Rate-Limit-Fehler provoziert.
|
||||
5. THE AzureOpenAIConfig SHALL ein Feld `max_concurrency` vom Typ `int` mit Standardwert 5 enthalten.
|
||||
|
||||
### Anforderung 3: Ergebniskonsistenz bei paralleler Verarbeitung
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass die Ergebnisse des parallelen Matchings identisch zu denen des sequenziellen Matchings sind, damit die Umstellung auf Batching keine funktionalen Unterschiede verursacht.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE LLM_Fulltext_Matcher SHALL nach Abschluss aller parallelen LLM-Aufrufe die Ergebnisse in derselben deterministischen Sortierreihenfolge liefern wie bisher (primär nach Kategorie, sekundär nach `item_id` aufsteigend).
|
||||
2. THE LLM_Fulltext_Matcher SHALL bei paralleler Verarbeitung dieselbe Fehlerbehandlung anwenden wie bei sequenzieller Verarbeitung: fehlgeschlagene Aufrufe erscheinen in der Fehlerliste, erfolgreiche in `by_category`.
|
||||
3. THE LLM_Fulltext_Matcher SHALL sicherstellen, dass die Reihenfolge der Eingabe-Kandidaten keinen Einfluss auf die Sortierung der Ausgabe hat.
|
||||
4. THE LLM_Fulltext_Matcher SHALL bei paralleler Verarbeitung keine Race-Conditions bei der Zuordnung von LLM-Antworten zu Kandidaten aufweisen; jede Antwort wird exakt dem zugehörigen Kandidaten zugeordnet.
|
||||
|
||||
### Anforderung 4: Fehlerbehandlung bei Rate-Limit-Überschreitung
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System bei Rate-Limit-Fehlern der Azure OpenAI API robust reagiert, damit einzelne 429-Fehler nicht den gesamten Matching-Lauf abbrechen.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN ein LLM-Aufruf innerhalb eines Batches mit einem HTTP-429-Fehler (Rate Limit Exceeded) fehlschlägt, THE AzureOpenAIClient SHALL den Aufruf nach einer exponentiellen Backoff-Pause erneut versuchen (bestehendes Retry-Verhalten).
|
||||
2. WHEN ein LLM-Aufruf nach Ausschöpfung aller Retries endgültig fehlschlägt, THE LLM_Fulltext_Matcher SHALL diesen Kandidaten in der Fehlerliste ausweisen, ohne die parallele Verarbeitung der übrigen Kandidaten zu beeinflussen.
|
||||
3. THE LLM_Fulltext_Matcher SHALL sicherstellen, dass ein Fehler bei einem einzelnen Kandidaten nicht zum Abbruch oder zur Verzögerung der Verarbeitung anderer Kandidaten führt.
|
||||
4. IF alle LLM-Aufrufe eines Batches fehlschlagen, THEN THE LLM_Fulltext_Matcher SHALL ein Ergebnis mit leeren Kategorien und einer vollständigen Fehlerliste zurückgeben, ohne eine Exception zu werfen.
|
||||
|
||||
### Anforderung 5: Beibehaltung der bestehenden Schnittstelle
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass die öffentliche Schnittstelle des LLM_Fulltext_Matcher unverändert bleibt, damit bestehende Aufrufer (MCP_Server, Tests) ohne Anpassung weiterhin funktionieren.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE LLM_Fulltext_Matcher SHALL die Signaturen von `match_capacities` und `match_tasks` unverändert beibehalten (gleiche Parameter, gleicher Rückgabetyp `LlmFulltextResult`).
|
||||
2. THE LLM_Fulltext_Matcher SHALL den Rückgabetyp `LlmFulltextResult` (mit `by_category` und `errors`) unverändert beibehalten.
|
||||
3. WHEN der LLM_Fulltext_Matcher mit einer leeren Kandidatenliste aufgerufen wird, THE LLM_Fulltext_Matcher SHALL sofort ein leeres Ergebnis zurückgeben, ohne LLM-Aufrufe zu starten.
|
||||
4. THE LLM_Fulltext_Matcher SHALL das neue `max_concurrency`-Feld als optionalen Konstruktor-Parameter akzeptieren, mit Standardwert 5.
|
||||
|
||||
### Anforderung 6: Logging und Beobachtbarkeit
|
||||
|
||||
**User Story:** Als Entwickler möchte ich nachvollziehen können, wie viele LLM-Aufrufe parallel laufen und wie lange das Batching insgesamt dauert, damit ich Performance-Probleme diagnostizieren kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN ein Batch-Matching gestartet wird, THE LLM_Fulltext_Matcher SHALL eine Log-Nachricht auf Level INFO ausgeben, die die Anzahl der Kandidaten und das konfigurierte Concurrency_Limit enthält.
|
||||
2. WHEN ein Batch-Matching abgeschlossen ist, THE LLM_Fulltext_Matcher SHALL eine Log-Nachricht auf Level INFO ausgeben, die die Gesamtdauer, die Anzahl erfolgreicher Kategorisierungen und die Anzahl der Fehler enthält.
|
||||
3. WHEN ein einzelner LLM-Aufruf innerhalb des Batches fehlschlägt, THE LLM_Fulltext_Matcher SHALL eine Log-Nachricht auf Level WARNING ausgeben, die die `item_id` und den Fehlergrund enthält.
|
||||
|
||||
### Anforderung 7: Abwärtskompatibilität mit Score-Modus
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass das Batching ausschließlich den LLM-Volltext-Modus betrifft und der Score-Modus unverändert bleibt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `matching_method = "score"` verwendet wird, THE MCP_Server SHALL das bestehende Score-basierte Matching ohne Änderungen ausführen.
|
||||
2. THE LLM_Fulltext_Matcher SHALL ausschließlich für den Modus `llm_fulltext` verwendet werden; der Score-Modus nutzt weiterhin den bestehenden `Matcher` und `SimilarityEngine`.
|
||||
3. THE MCP_Server SHALL keine neuen Abhängigkeiten oder Konfigurationsparameter einführen, die den Score-Modus beeinflussen.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Implementierungsplan: LLM Batch-Matching (Concurrent Requests)
|
||||
|
||||
## Übersicht
|
||||
|
||||
Sequenzielle LLM-Aufrufe in `LlmFulltextMatcher` werden durch `asyncio.gather()` mit Semaphore-Begrenzung ersetzt. Die Konfiguration wird um `max_concurrency` erweitert. Die öffentliche Schnittstelle bleibt unverändert.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Konfiguration erweitern
|
||||
- [x] 1.1 `AzureOpenAIConfig` um Feld `max_concurrency: int = 5` erweitern
|
||||
- In `src/teamlandkarte_mcp/config.py` das frozen Dataclass `AzureOpenAIConfig` um das Feld ergänzen
|
||||
- _Anforderungen: 2.5_
|
||||
- [x] 1.2 Validierung in `_parse_azure_openai` hinzufügen
|
||||
- `max_concurrency` aus Config lesen mit Default 5
|
||||
- `ConfigError` werfen wenn Wert < 1 oder > 20
|
||||
- Feld an `AzureOpenAIConfig`-Konstruktor übergeben
|
||||
- Auch im `load_config`-Rebuild-Block (`azure_openai = AzureOpenAIConfig(...)`) das neue Feld durchreichen
|
||||
- _Anforderungen: 2.1, 2.2, 2.3, 2.4_
|
||||
- [x] 1.3 `config.toml` um kommentierten `max_concurrency`-Schlüssel erweitern
|
||||
- Unter `[azure_openai]` einen Kommentar mit Erklärung und auskommentierten Default-Wert einfügen
|
||||
- _Anforderungen: 2.1_
|
||||
- [x] 1.4 Property-Test für Config-Validierung schreiben
|
||||
- **Property 6: Config-Validierung**
|
||||
- Teste mit hypothesis: `st.integers(min_value=-100, max_value=100)` – Erfolg genau dann wenn 1 <= n <= 20, sonst `ConfigError`
|
||||
- **Validiert: Anforderungen 2.1, 2.3, 2.4**
|
||||
|
||||
- [x] 2. `LlmFulltextMatcher` um Concurrency erweitern
|
||||
- [x] 2.1 Konstruktor um `max_concurrency`-Parameter und Semaphore erweitern
|
||||
- `max_concurrency: int = 5` als keyword-only Parameter hinzufügen
|
||||
- `self._semaphore = asyncio.Semaphore(max_concurrency)` im `__init__` anlegen
|
||||
- `self._max_concurrency = max_concurrency` speichern
|
||||
- `import asyncio` und `import time` ergänzen
|
||||
- _Anforderungen: 1.2, 1.5, 5.4_
|
||||
- [x] 2.2 Neue Methode `_categorize_one_throttled` implementieren
|
||||
- Async-Wrapper um `_categorize_one` der `async with self._semaphore:` verwendet
|
||||
- Gleiche Signatur wie `_categorize_one` (item_id, user_prompt, raw)
|
||||
- _Anforderungen: 1.2, 1.4, 1.5_
|
||||
- [x] 2.3 `match_capacities` auf `asyncio.gather` umstellen
|
||||
- Sequenzielle for-Schleife durch Liste von `_categorize_one_throttled`-Aufrufen ersetzen
|
||||
- `asyncio.gather(*tasks)` für parallele Ausführung verwenden
|
||||
- Ergebnisse iterieren und in `by_category` / `errors` einsortieren
|
||||
- Logging (INFO) vor und nach dem Batch mit Kandidatenanzahl, Concurrency, Dauer, Erfolge, Fehler
|
||||
- Bestehende Sortierung beibehalten
|
||||
- _Anforderungen: 1.1, 3.1, 3.2, 4.2, 4.3, 4.4, 5.1, 6.1, 6.2_
|
||||
- [x] 2.4 `match_tasks` auf `asyncio.gather` umstellen
|
||||
- Analog zu 2.3: sequenzielle Schleife durch gather + throttled ersetzen
|
||||
- Logging analog zu `match_capacities`
|
||||
- _Anforderungen: 1.1, 3.1, 3.2, 4.2, 4.3, 4.4, 5.1, 6.1, 6.2_
|
||||
|
||||
- [x] 3. MCP-Server Verdrahtung
|
||||
- [x] 3.1 `max_concurrency` an `LlmFulltextMatcher` übergeben
|
||||
- In `build_server()` bei der Instanziierung von `LlmFulltextMatcher` den Wert `cfg.azure_openai.max_concurrency` übergeben
|
||||
- _Anforderungen: 2.1, 5.4_
|
||||
|
||||
- [x] 4. Checkpoint
|
||||
- Sicherstellen dass alle bestehenden Tests weiterhin bestehen. Bei Fragen den Nutzer konsultieren.
|
||||
|
||||
- [x] 5. Property-Based Tests
|
||||
- [x] 5.1 Property-Test: Vollständigkeit der Ergebnisse (Partition)
|
||||
- **Property 1: Vollständigkeit der Ergebnisse**
|
||||
- Generiere zufällige Kandidatenlisten mit Mock-Client (Mix aus Erfolg/Fehler); prüfe `len(by_category items) + len(errors) == len(input)`
|
||||
- **Validiert: Anforderungen 1.1, 1.4, 3.2, 4.2, 4.3**
|
||||
- [x] 5.2 Property-Test: Concurrency-Begrenzung
|
||||
- **Property 2: Concurrency-Begrenzung**
|
||||
- Mock-Client mit asyncio-Counter für gleichzeitige Aufrufe; prüfe `max_concurrent <= max_concurrency` für verschiedene Werte
|
||||
- **Validiert: Anforderungen 1.2**
|
||||
- [x] 5.3 Property-Test: Deterministische Sortierung
|
||||
- **Property 3: Deterministische Sortierung**
|
||||
- Generiere Ergebnisse mit zufälligen Kategorien; prüfe dass jede Kategorie nach `item_id` aufsteigend sortiert ist
|
||||
- **Validiert: Anforderungen 3.1**
|
||||
- [x] 5.4 Property-Test: Eingabereihenfolge-Unabhängigkeit
|
||||
- **Property 4: Eingabereihenfolge-Unabhängigkeit**
|
||||
- Generiere Kandidatenliste, permutiere, führe Matching aus, vergleiche Ergebnisse auf Gleichheit
|
||||
- **Validiert: Anforderungen 3.3**
|
||||
- [x] 5.5 Property-Test: Korrekte Zuordnung (Response-Mapping)
|
||||
- **Property 5: Korrekte Zuordnung**
|
||||
- Mock gibt item_id-spezifische Kategorien zurück; prüfe dass jedes Item in der korrekten Kategorie landet
|
||||
- **Validiert: Anforderungen 3.4**
|
||||
- [x] 5.6 Property-Test: Logging-Konsistenz
|
||||
- **Property 7: Logging-Konsistenz**
|
||||
- Capture Logs mit `caplog`; prüfe dass Start- und End-Log die korrekte Kandidatenanzahl und Summe (Erfolge + Fehler) enthalten
|
||||
- **Validiert: Anforderungen 6.1, 6.2**
|
||||
|
||||
- [x] 6. Unit Tests
|
||||
- [x] 6.1 Unit Tests für Batch-Matching schreiben
|
||||
- Leere Eingabe: sofort leeres Ergebnis ohne LLM-Aufrufe
|
||||
- Alle Fehler: keine Exception, vollständige Fehlerliste
|
||||
- Default-Wert: ohne `max_concurrency` wird 5 verwendet
|
||||
- Einzelner Fehler beeinflusst andere Kandidaten nicht
|
||||
- _Anforderungen: 4.2, 4.3, 4.4, 5.3_
|
||||
|
||||
- [x] 7. Abschluss-Checkpoint
|
||||
- Sicherstellen dass alle Tests bestehen und die Schnittstelle abwärtskompatibel bleibt. Bei Fragen den Nutzer konsultieren.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Tasks mit `*` sind optional und können für ein schnelleres MVP übersprungen werden
|
||||
- Jeder Task referenziert spezifische Anforderungen für Nachvollziehbarkeit
|
||||
- Property-Tests verwenden `hypothesis` (pytest-Plugin)
|
||||
- Der Score-Modus bleibt vollständig unberührt (Anforderung 7)
|
||||
@@ -0,0 +1 @@
|
||||
{"specId": "218474d5-eca5-49e7-80ca-f7373f4397fb", "workflowType": "requirements-first", "specType": "bugfix"}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Bugfix Requirements Document
|
||||
|
||||
## Einleitung
|
||||
|
||||
Die Kompetenz-Inferenz im Tool `validate_task_requirements` (sowie in `extract_requirements` und `find_matching_tasks`) funktioniert nicht mehr. Der frühere Embedding-basierte Ansatz (`infer_competences`, `ensure_task_embedding`) wurde im Rahmen der Umstellung auf BM25+LLM entfernt, ohne dass ein Ersatz implementiert wurde. Die Variable `inferred_comps` ist daher immer eine leere Liste `[]`.
|
||||
|
||||
Es soll ein LLM-basierter Ansatz implementiert werden, der aus allen verfügbaren Kompetenzen (via `get_all_competence_names()`) bis zu 10 passende Kompetenzen für einen gegebenen Task-Text auswählt – analog zum bereits funktionierenden LLM-basierten `infer_primary_role`.
|
||||
|
||||
## Bug-Analyse
|
||||
|
||||
### Aktuelles Verhalten (Defekt)
|
||||
|
||||
1.1 WHEN `validate_task_requirements(task_id)` aufgerufen wird THEN liefert das System immer eine leere Kompetenz-Tabelle, da `inferred_comps` stets `[]` ist
|
||||
1.2 WHEN `extract_requirements(task_description)` aufgerufen wird THEN enthält das Ergebnis keine inferierten Kompetenzen (`inferred_competences` ist immer `[]`)
|
||||
1.3 WHEN `find_matching_tasks(capacity_id)` intern Kompetenzen inferieren soll THEN werden stattdessen nur die DB-Skills verwendet, da die Kompetenz-Inferenz deaktiviert ist
|
||||
|
||||
### Erwartetes Verhalten (Korrekt)
|
||||
|
||||
2.1 WHEN `validate_task_requirements(task_id)` aufgerufen wird THEN SHALL das System per LLM-Aufruf bis zu 10 passende Kompetenzen aus der vollständigen Kompetenzliste (`get_all_competence_names()`) inferieren und mit Konfidenzwerten in der Tabelle anzeigen
|
||||
2.2 WHEN `extract_requirements(task_description)` aufgerufen wird THEN SHALL das System per LLM-Aufruf bis zu 10 passende Kompetenzen inferieren und diese in den `requirements.competences` aufnehmen
|
||||
2.3 WHEN `find_matching_tasks(capacity_id)` intern Kompetenzen inferiert THEN SHALL das System per LLM-Aufruf bis zu 10 passende Kompetenzen inferieren und diese für das Scoring verwenden
|
||||
2.4 WHEN der Task-Text leer ist oder keine Kompetenzen in der DB vorhanden sind THEN SHALL das System eine leere Kompetenzliste zurückgeben, ohne einen Fehler zu werfen
|
||||
2.5 WHEN der LLM-Aufruf fehlschlägt (Timeout, API-Fehler) THEN SHALL das System eine leere Kompetenzliste zurückgeben und den Fehler loggen, ohne den gesamten Tool-Aufruf abzubrechen
|
||||
|
||||
### Unverändertes Verhalten (Regressionsprävention)
|
||||
|
||||
3.1 WHEN `validate_task_requirements(task_id)` aufgerufen wird THEN SHALL das System WEITERHIN die Rollen-Inferenz per LLM korrekt durchführen
|
||||
3.2 WHEN `validate_task_requirements(task_id)` aufgerufen wird THEN SHALL das System WEITERHIN die DB-Skills des Tasks korrekt anzeigen
|
||||
3.3 WHEN `find_matching_capacities(task_id)` aufgerufen wird THEN SHALL das System WEITERHIN das bestehende LLM-Fulltext-Matching unverändert verwenden
|
||||
3.4 WHEN der LLM-Aufruf für Rollen-Inferenz fehlschlägt THEN SHALL das System WEITERHIN `None` zurückgeben ohne Absturz
|
||||
3.5 WHEN `infer_primary_role` aufgerufen wird THEN SHALL das System WEITERHIN genau eine Rolle aus der Rollenliste auswählen
|
||||
@@ -0,0 +1,220 @@
|
||||
# LLM-Kompetenz-Inferenz Bugfix Design
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die Kompetenz-Inferenz in `validate_task_requirements`, `extract_requirements` und `find_matching_tasks` (Score-Modus) ist defekt, weil der alte Embedding-Ansatz entfernt wurde, ohne einen Ersatz zu implementieren. Die Variable `inferred_comps` ist stets eine leere Liste `[]`.
|
||||
|
||||
Der Fix implementiert eine neue Methode `infer_competences` in der bestehenden `VocabularyCache`-Klasse, analog zum bereits funktionierenden `infer_primary_role`. Diese Methode nutzt den `AzureOpenAIClient.chat_completion`-Aufruf, um aus der vollständigen Kompetenzliste (`get_all_competence_names()`) bis zu 10 passende Kompetenzen mit Konfidenzwerten auszuwählen.
|
||||
|
||||
## Glossar
|
||||
|
||||
- **Bug_Condition (C)**: Der Zustand, in dem ein Task-Text vorhanden ist UND Kompetenzen in der DB existieren, aber `inferred_comps` trotzdem `[]` zurückgibt
|
||||
- **Property (P)**: Das gewünschte Verhalten – eine nicht-leere Liste von bis zu 10 `(competence_name, confidence)`-Tupeln, wobei jeder Name in `get_all_competence_names()` enthalten ist
|
||||
- **Preservation**: Die bestehende Rollen-Inferenz (`infer_primary_role`), DB-Skills-Anzeige und LLM-Fulltext-Matching bleiben unverändert
|
||||
- **VocabularyCache**: Klasse in `src/teamlandkarte_mcp/matching/vocabulary.py`, die LLM-basierte Inferenz kapselt
|
||||
- **AzureOpenAIClient**: Client in `src/teamlandkarte_mcp/azure/openai_client.py` mit `chat_completion(system, user) -> str`
|
||||
- **inferred_comps**: Die lokale Variable in den betroffenen Tools, die aktuell immer `[]` ist
|
||||
|
||||
## Bug-Details
|
||||
|
||||
### Fault Condition
|
||||
|
||||
Der Bug manifestiert sich, wenn ein Tool (`validate_task_requirements`, `extract_requirements`, `find_matching_tasks` im Score-Modus) einen nicht-leeren Task-Text verarbeitet und Kompetenzen in der DB vorhanden sind. Die Kompetenz-Inferenz liefert stets eine leere Liste, weil kein LLM-Aufruf stattfindet.
|
||||
|
||||
**Formale Spezifikation:**
|
||||
```
|
||||
FUNCTION isBugCondition(input)
|
||||
INPUT: input of type {task_text: str, competence_names: list[str]}
|
||||
OUTPUT: boolean
|
||||
|
||||
RETURN input.task_text.strip() != ""
|
||||
AND len(input.competence_names) > 0
|
||||
AND infer_competences(input.task_text) == []
|
||||
END FUNCTION
|
||||
```
|
||||
|
||||
### Beispiele
|
||||
|
||||
- `validate_task_requirements("task-123")` mit Task-Titel "Python Backend Entwicklung" und 50 Kompetenzen in der DB → Erwartung: bis zu 10 Kompetenzen mit Konfidenz; Aktuell: leere Tabelle
|
||||
- `extract_requirements("Wir brauchen einen React-Entwickler mit TypeScript-Erfahrung")` mit Kompetenzen ["React", "TypeScript", "Angular", ...] in der DB → Erwartung: ["React", "TypeScript", ...] mit Konfidenz; Aktuell: `inferred_competences = []`
|
||||
- `find_matching_tasks(capacity_id=1)` im Score-Modus → Erwartung: `inferred_comp_names` enthält LLM-inferierte Kompetenzen für das Scoring; Aktuell: nur DB-Skills werden verwendet
|
||||
- Leerer Task-Text → Erwartung: leere Liste (kein Fehler) – dieses Verhalten ist korrekt und bleibt erhalten
|
||||
|
||||
## Erwartetes Verhalten
|
||||
|
||||
### Preservation Requirements
|
||||
|
||||
**Unverändertes Verhalten:**
|
||||
- `infer_primary_role` muss weiterhin genau eine Rolle mit Konfidenz zurückgeben
|
||||
- DB-Skills eines Tasks müssen weiterhin korrekt in der Ausgabe angezeigt werden
|
||||
- LLM-Fulltext-Matching (`find_matching_capacities` / `find_matching_tasks` im `llm_fulltext`-Modus) bleibt unverändert
|
||||
- Fehlerbehandlung bei LLM-Aufruf-Fehlern für Rollen-Inferenz bleibt unverändert (`None` zurückgeben)
|
||||
- Die `AzureOpenAIClient`-Schnittstelle wird nicht verändert
|
||||
|
||||
**Scope:**
|
||||
Alle Eingaben, die KEINEN nicht-leeren Task-Text mit vorhandenen Kompetenzen in der DB kombinieren, sind vom Fix nicht betroffen:
|
||||
- Leerer Task-Text → weiterhin leere Liste
|
||||
- Keine Kompetenzen in der DB → weiterhin leere Liste
|
||||
- Mausklick-/UI-Interaktionen → nicht betroffen (MCP-Server)
|
||||
- LLM-Fulltext-Modus → verwendet eigene Logik, nicht betroffen
|
||||
|
||||
## Hypothesierte Ursache
|
||||
|
||||
Basierend auf der Bug-Analyse sind die Ursachen klar identifiziert:
|
||||
|
||||
1. **Fehlende Implementierung**: Die alte `infer_competences`-Methode (Embedding-basiert) wurde entfernt. An den drei Stellen im Code steht nur noch `inferred_comps: list[tuple[str, float]] = []` bzw. `inferred_competences: list[tuple[str, float]] = []` ohne jeglichen LLM-Aufruf.
|
||||
|
||||
2. **Kein System-Prompt für Kompetenz-Inferenz**: Im Gegensatz zu `_ROLE_INFERENCE_SYSTEM_PROMPT` existiert kein entsprechender Prompt für Kompetenz-Inferenz.
|
||||
|
||||
3. **Keine Methode in VocabularyCache**: Die Klasse hat nur `infer_primary_role`, aber keine `infer_competences`-Methode.
|
||||
|
||||
4. **Keine Integration in die Tools**: Selbst wenn eine Methode existieren würde, fehlt der `await`-Aufruf an den drei betroffenen Stellen.
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
Property 1: Fault Condition - Kompetenz-Inferenz liefert Ergebnisse
|
||||
|
||||
_For any_ input where der Task-Text nicht leer ist UND mindestens eine Kompetenz in der DB existiert (isBugCondition returns true), SHALL die fixierte `infer_competences`-Methode eine nicht-leere Liste von bis zu 10 Tupeln `(competence_name, confidence)` zurückgeben, wobei jeder `competence_name` in `get_all_competence_names()` enthalten ist und `confidence` im Bereich [0.0, 1.0] liegt.
|
||||
|
||||
**Validates: Requirements 2.1, 2.2, 2.3**
|
||||
|
||||
Property 2: Preservation - Rollen-Inferenz und DB-Skills unverändert
|
||||
|
||||
_For any_ input (unabhängig davon ob die Bug-Condition gilt oder nicht), SHALL die fixierte Codebasis das gleiche Ergebnis für `infer_primary_role` und die DB-Skills-Anzeige produzieren wie der originale Code, und das LLM-Fulltext-Matching bleibt unverändert.
|
||||
|
||||
**Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
|
||||
|
||||
## Fix-Implementierung
|
||||
|
||||
### Erforderliche Änderungen
|
||||
|
||||
**Datei**: `src/teamlandkarte_mcp/matching/vocabulary.py`
|
||||
|
||||
**Änderung 1: System-Prompt hinzufügen**
|
||||
- Neuer Modul-Level-Konstante `_COMPETENCE_INFERENCE_SYSTEM_PROMPT` analog zu `_ROLE_INFERENCE_SYSTEM_PROMPT`
|
||||
- Prompt instruiert das LLM, aus einer gegebenen Kompetenzliste bis zu 10 passende Kompetenzen für einen Task-Text auszuwählen
|
||||
- Antwortformat: `{"competences": [{"name": "<name>", "confidence": <float>}]}`
|
||||
|
||||
**Änderung 2: Neue Methode `infer_competences` in `VocabularyCache`**
|
||||
- Signatur: `async def infer_competences(self, *, task_text: str, max_competences: int = 10) -> list[tuple[str, float]]`
|
||||
- Holt Kompetenzliste via `self._db.get_all_competence_names()`
|
||||
- Bei leerer Liste oder leerem Text: `[]` zurückgeben
|
||||
- LLM-Aufruf via `self._client.chat_completion(system, user)`
|
||||
- JSON-Parsing der Antwort, Validierung gegen DB-Kompetenzliste
|
||||
- Bei Fehler: leere Liste zurückgeben + Warning loggen
|
||||
|
||||
---
|
||||
|
||||
**Datei**: `src/teamlandkarte_mcp/mcp_server.py`
|
||||
|
||||
**Änderung 3: `validate_task_requirements` – LLM-Aufruf integrieren**
|
||||
- Ersetze `inferred_comps: list[tuple[str, float]] = []` durch:
|
||||
```python
|
||||
inferred_comps = await vocab_cache.infer_competences(task_text=task_text)
|
||||
```
|
||||
|
||||
**Änderung 4: `extract_requirements` – LLM-Aufruf integrieren**
|
||||
- Ersetze `inferred_competences: list[tuple[str, float]] = []` durch:
|
||||
```python
|
||||
inferred_competences = await vocab_cache.infer_competences(task_text=desc)
|
||||
```
|
||||
|
||||
**Änderung 5: `find_matching_tasks` (Score-Modus) – LLM-Aufruf integrieren**
|
||||
- Ersetze den Block `inferred_comp_names = [str(x) for x in (getattr(t, "skills", None) or []) if x]` durch:
|
||||
```python
|
||||
inferred_comp_tuples = await vocab_cache.infer_competences(task_text=full_text)
|
||||
inferred_comp_names = [name for name, _conf in inferred_comp_tuples]
|
||||
if not inferred_comp_names:
|
||||
inferred_comp_names = [str(x) for x in (getattr(t, "skills", None) or []) if x]
|
||||
```
|
||||
|
||||
## Testing-Strategie
|
||||
|
||||
### Validierungsansatz
|
||||
|
||||
Die Testing-Strategie folgt einem zweiphasigen Ansatz: Zuerst Counterexamples auf dem unfixierten Code aufdecken, dann den Fix verifizieren und Preservation sicherstellen.
|
||||
|
||||
### Exploratory Fault Condition Checking
|
||||
|
||||
**Ziel**: Counterexamples aufdecken, die den Bug VOR der Implementierung des Fixes demonstrieren. Root-Cause-Analyse bestätigen oder widerlegen.
|
||||
|
||||
**Testplan**: Tests schreiben, die `infer_competences` (bzw. die betroffenen Tools) mit nicht-leerem Task-Text und vorhandenen Kompetenzen aufrufen. Auf dem unfixierten Code beobachten, dass stets `[]` zurückkommt.
|
||||
|
||||
**Testfälle**:
|
||||
1. **validate_task_requirements mit gültigem Task**: Aufruf mit Task-ID, der einen beschriebenen Task hat (wird auf unfixiertem Code leere Kompetenz-Tabelle liefern)
|
||||
2. **extract_requirements mit Freitext**: Aufruf mit beschreibendem Text (wird auf unfixiertem Code `inferred_competences = []` liefern)
|
||||
3. **find_matching_tasks im Score-Modus**: Aufruf mit Capacity-ID (wird auf unfixiertem Code nur DB-Skills verwenden, keine LLM-Inferenz)
|
||||
4. **Leerer Task-Text**: Aufruf mit leerem Text (soll auch nach Fix `[]` liefern – Baseline)
|
||||
|
||||
**Erwartete Counterexamples**:
|
||||
- `inferred_comps` ist immer `[]`, unabhängig vom Task-Text
|
||||
- Ursache: Kein LLM-Aufruf, keine `infer_competences`-Methode vorhanden
|
||||
|
||||
### Fix Checking
|
||||
|
||||
**Ziel**: Verifizieren, dass für alle Eingaben, bei denen die Bug-Condition gilt, die fixierte Funktion das erwartete Verhalten produziert.
|
||||
|
||||
**Pseudocode:**
|
||||
```
|
||||
FOR ALL input WHERE isBugCondition(input) DO
|
||||
result := infer_competences_fixed(input.task_text)
|
||||
ASSERT len(result) > 0
|
||||
ASSERT len(result) <= 10
|
||||
FOR EACH (name, confidence) IN result DO
|
||||
ASSERT name IN get_all_competence_names()
|
||||
ASSERT 0.0 <= confidence <= 1.0
|
||||
END FOR
|
||||
END FOR
|
||||
```
|
||||
|
||||
### Preservation Checking
|
||||
|
||||
**Ziel**: Verifizieren, dass für alle Eingaben, bei denen die Bug-Condition NICHT gilt, die fixierte Funktion das gleiche Ergebnis wie die originale Funktion produziert.
|
||||
|
||||
**Pseudocode:**
|
||||
```
|
||||
FOR ALL input WHERE NOT isBugCondition(input) DO
|
||||
ASSERT infer_competences_fixed(input.task_text) == []
|
||||
END FOR
|
||||
|
||||
FOR ALL input DO
|
||||
ASSERT infer_primary_role_fixed(input) == infer_primary_role_original(input)
|
||||
END FOR
|
||||
```
|
||||
|
||||
**Testing-Ansatz**: Property-Based Testing wird für Preservation Checking empfohlen, weil:
|
||||
- Es automatisch viele Testfälle über den Eingabebereich generiert
|
||||
- Es Randfälle findet, die manuelle Unit-Tests übersehen könnten
|
||||
- Es starke Garantien bietet, dass Verhalten für alle nicht-buggy Eingaben unverändert bleibt
|
||||
|
||||
**Testplan**: Verhalten auf unfixiertem Code zuerst beobachten (leere Ergebnisse, funktionierende Rollen-Inferenz), dann Property-Based Tests schreiben, die dieses Verhalten nach dem Fix verifizieren.
|
||||
|
||||
**Testfälle**:
|
||||
1. **Rollen-Inferenz Preservation**: Verifizieren, dass `infer_primary_role` nach dem Fix identische Ergebnisse liefert
|
||||
2. **DB-Skills Preservation**: Verifizieren, dass DB-Skills weiterhin korrekt angezeigt werden
|
||||
3. **Leerer Text Preservation**: Verifizieren, dass leerer Task-Text weiterhin `[]` liefert
|
||||
4. **LLM-Fulltext Preservation**: Verifizieren, dass der LLM-Fulltext-Modus nicht beeinflusst wird
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Test `infer_competences` mit gemocktem LLM-Client: gültige JSON-Antwort → korrekte Tupel-Liste
|
||||
- Test `infer_competences` mit leerem Task-Text → `[]`
|
||||
- Test `infer_competences` mit leerer Kompetenzliste in DB → `[]`
|
||||
- Test `infer_competences` bei LLM-Fehler (Exception) → `[]` + Warning geloggt
|
||||
- Test `infer_competences` bei ungültiger JSON-Antwort → `[]`
|
||||
- Test `infer_competences` bei Kompetenz-Namen die nicht in DB sind → werden herausgefiltert
|
||||
- Test `validate_task_requirements` liefert nicht-leere Kompetenz-Tabelle
|
||||
- Test `extract_requirements` liefert nicht-leere `inferred_competences`
|
||||
|
||||
### Property-Based Tests
|
||||
|
||||
- Generiere zufällige Task-Texte und Kompetenzlisten; verifiziere, dass Ergebnisse stets Subset der DB-Kompetenzen sind
|
||||
- Generiere zufällige Eingaben; verifiziere, dass Konfidenzwerte immer in [0.0, 1.0] liegen
|
||||
- Generiere zufällige Eingaben; verifiziere, dass maximal 10 Kompetenzen zurückgegeben werden
|
||||
- Generiere zufällige Eingaben; verifiziere, dass `infer_primary_role` unverändert funktioniert (Preservation)
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- End-to-End Test: `validate_task_requirements` mit echtem (gemocktem) LLM-Client zeigt Kompetenzen
|
||||
- End-to-End Test: `extract_requirements` → `find_matching_capacities` Pipeline mit inferierten Kompetenzen
|
||||
- End-to-End Test: `find_matching_tasks` im Score-Modus nutzt inferierte Kompetenzen für besseres Scoring
|
||||
@@ -0,0 +1,120 @@
|
||||
# Implementation Plan: LLM-Kompetenz-Inferenz Bugfix
|
||||
|
||||
## Übersicht
|
||||
|
||||
Explorativer Bugfix-Workflow: Zuerst den Bug durch Tests bestätigen, dann Preservation sicherstellen, anschließend den Fix implementieren und validieren. Die `infer_competences`-Methode wird in `VocabularyCache` ergänzt und an drei Stellen im `mcp_server.py` integriert.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Bug-Condition Explorationstest schreiben
|
||||
- **Property 1: Fault Condition** - Kompetenz-Inferenz liefert stets leere Liste
|
||||
- **CRITICAL**: Dieser Test MUSS auf dem unfixierten Code FEHLSCHLAGEN – das Fehlschlagen bestätigt den Bug
|
||||
- **DO NOT** versuchen den Test oder den Code zu fixen wenn er fehlschlägt
|
||||
- **NOTE**: Dieser Test kodiert das erwartete Verhalten – er validiert den Fix wenn er nach der Implementierung besteht
|
||||
- **GOAL**: Counterexamples aufdecken, die demonstrieren dass der Bug existiert
|
||||
- **Scoped PBT Approach**: Property auf konkrete Fälle scopen: nicht-leerer Task-Text mit vorhandenen Kompetenzen in der DB
|
||||
- Test-Datei: `tests/test_competence_inference_fault_pbt.py`
|
||||
- Hypothesis-Strategie: `st.text(min_size=1)` für Task-Text, `st.lists(st.text(min_size=1), min_size=1, max_size=50)` für Kompetenzliste
|
||||
- Mock `AzureOpenAIClient.chat_completion` so dass er gültiges JSON mit Kompetenzen zurückgibt
|
||||
- Assertion: `infer_competences(task_text)` liefert eine nicht-leere Liste von bis zu 10 Tupeln `(name, confidence)` wobei jeder Name in der Kompetenzliste enthalten ist und `confidence` in [0.0, 1.0] liegt
|
||||
- Test auf UNFIXIERTEM Code ausführen – **ERWARTETES ERGEBNIS**: Test SCHLÄGT FEHL (bestätigt Bug)
|
||||
- Counterexamples dokumentieren (z.B. "infer_competences('Python Backend') gibt [] zurück statt Kompetenzen")
|
||||
- Task als abgeschlossen markieren wenn Test geschrieben, ausgeführt und Fehlschlag dokumentiert ist
|
||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3_
|
||||
|
||||
- [x] 2. Preservation Property-Tests schreiben (VOR der Fix-Implementierung)
|
||||
- **Property 2: Preservation** - Rollen-Inferenz und Leer-Eingaben unverändert
|
||||
- **IMPORTANT**: Observation-First-Methodik befolgen
|
||||
- Test-Datei: `tests/test_competence_inference_preservation_pbt.py`
|
||||
- Beobachten: `infer_primary_role` liefert auf unfixiertem Code weiterhin eine Rolle mit Konfidenz
|
||||
- Beobachten: Leerer Task-Text liefert auf unfixiertem Code `[]` (korrektes Verhalten)
|
||||
- Beobachten: Leere Kompetenzliste in DB liefert auf unfixiertem Code `[]` (korrektes Verhalten)
|
||||
- Property-Based Test 1: Für alle nicht-leeren Task-Texte liefert `infer_primary_role` weiterhin ein Tupel `(role_name, confidence)` oder `None` bei Fehler (aus Preservation Requirements)
|
||||
- Property-Based Test 2: Für alle leeren Task-Texte (`st.just("")` oder `st.from_regex(r'^\s*$')`) liefert `infer_competences` stets `[]`
|
||||
- Property-Based Test 3: Für leere Kompetenzliste in DB liefert `infer_competences` stets `[]`
|
||||
- Tests auf UNFIXIERTEM Code ausführen – **ERWARTETES ERGEBNIS**: Tests BESTEHEN (bestätigt Baseline-Verhalten)
|
||||
- Task als abgeschlossen markieren wenn Tests geschrieben, ausgeführt und bestanden auf unfixiertem Code
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
||||
|
||||
- [x] 3. Fix für LLM-Kompetenz-Inferenz implementieren
|
||||
|
||||
- [x] 3.1 System-Prompt `_COMPETENCE_INFERENCE_SYSTEM_PROMPT` in `vocabulary.py` hinzufügen
|
||||
- Modul-Level-Konstante analog zu `_ROLE_INFERENCE_SYSTEM_PROMPT`
|
||||
- Prompt instruiert das LLM, aus einer Kompetenzliste bis zu 10 passende Kompetenzen für einen Task-Text auszuwählen
|
||||
- Antwortformat: `{"competences": [{"name": "<name>", "confidence": <float>}]}`
|
||||
- _Bug_Condition: isBugCondition(input) where task_text.strip() != "" AND len(competence_names) > 0 AND infer_competences(task_text) == []_
|
||||
- _Expected_Behavior: Nicht-leere Liste von bis zu 10 (name, confidence)-Tupeln_
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
|
||||
- [x] 3.2 Methode `infer_competences` in `VocabularyCache` implementieren
|
||||
- Signatur: `async def infer_competences(self, *, task_text: str, max_competences: int = 10) -> list[tuple[str, float]]`
|
||||
- Kompetenzliste via `self._db.get_all_competence_names()` holen
|
||||
- Bei leerer Liste oder leerem Text: `[]` zurückgeben
|
||||
- LLM-Aufruf via `self._client.chat_completion(system, user)`
|
||||
- JSON-Parsing, Validierung gegen DB-Kompetenzliste (nur bekannte Namen übernehmen)
|
||||
- Bei Fehler (Exception, ungültiges JSON): leere Liste zurückgeben + Warning loggen
|
||||
- _Bug_Condition: isBugCondition(input) where task_text.strip() != "" AND len(competence_names) > 0_
|
||||
- _Expected_Behavior: expectedBehavior(result) = len(result) > 0 AND len(result) <= 10 AND all(name in competence_names for name, _ in result) AND all(0.0 <= conf <= 1.0 for _, conf in result)_
|
||||
- _Preservation: Leerer Text → [], Leere DB → [], Fehler → [] + Warning_
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
|
||||
|
||||
- [x] 3.3 LLM-Aufruf in `validate_task_requirements` integrieren
|
||||
- Ersetze `inferred_comps: list[tuple[str, float]] = []` durch `inferred_comps = await vocab_cache.infer_competences(task_text=task_text)`
|
||||
- _Bug_Condition: validate_task_requirements aufgerufen mit Task der beschriebenen Text hat_
|
||||
- _Expected_Behavior: inferred_comps enthält bis zu 10 Kompetenzen mit Konfidenz_
|
||||
- _Preservation: Rollen-Inferenz und DB-Skills bleiben unverändert_
|
||||
- _Requirements: 2.1, 3.1, 3.2_
|
||||
|
||||
- [x] 3.4 LLM-Aufruf in `extract_requirements` integrieren
|
||||
- Ersetze `inferred_competences: list[tuple[str, float]] = []` durch `inferred_competences = await vocab_cache.infer_competences(task_text=desc)`
|
||||
- _Bug_Condition: extract_requirements aufgerufen mit nicht-leerem Text_
|
||||
- _Expected_Behavior: inferred_competences enthält bis zu 10 Kompetenzen_
|
||||
- _Requirements: 2.2_
|
||||
|
||||
- [x] 3.5 LLM-Aufruf in `find_matching_tasks` (Score-Modus) integrieren
|
||||
- Ersetze den bestehenden `inferred_comp_names`-Block durch LLM-Inferenz mit Fallback auf DB-Skills
|
||||
- `inferred_comp_tuples = await vocab_cache.infer_competences(task_text=full_text)`
|
||||
- `inferred_comp_names = [name for name, _conf in inferred_comp_tuples]`
|
||||
- Fallback: `if not inferred_comp_names: inferred_comp_names = [str(x) for x in (getattr(t, "skills", None) or []) if x]`
|
||||
- _Bug_Condition: find_matching_tasks im Score-Modus mit Task der beschriebenen Text hat_
|
||||
- _Expected_Behavior: inferred_comp_names enthält LLM-inferierte Kompetenzen für Scoring_
|
||||
- _Preservation: LLM-Fulltext-Modus bleibt unverändert_
|
||||
- _Requirements: 2.3, 3.3_
|
||||
|
||||
- [x] 3.6 Unit-Tests für `infer_competences` mit gemocktem LLM-Client
|
||||
- Test gültige JSON-Antwort → korrekte Tupel-Liste
|
||||
- Test leerer Task-Text → `[]`
|
||||
- Test leere Kompetenzliste in DB → `[]`
|
||||
- Test LLM-Fehler (Exception) → `[]` + Warning geloggt
|
||||
- Test ungültige JSON-Antwort → `[]`
|
||||
- Test Kompetenz-Namen die nicht in DB sind → werden herausgefiltert
|
||||
- Test max_competences Begrenzung auf 10
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
|
||||
|
||||
- [x] 3.7 Bug-Condition Explorationstest erneut ausführen – Verifizieren dass er jetzt besteht
|
||||
- **Property 1: Expected Behavior** - Kompetenz-Inferenz liefert Ergebnisse
|
||||
- **IMPORTANT**: Den GLEICHEN Test aus Task 1 erneut ausführen – KEINEN neuen Test schreiben
|
||||
- Der Test aus Task 1 kodiert das erwartete Verhalten
|
||||
- Wenn dieser Test besteht, bestätigt das dass das erwartete Verhalten erfüllt ist
|
||||
- Bug-Condition Explorationstest aus Schritt 1 ausführen
|
||||
- **ERWARTETES ERGEBNIS**: Test BESTEHT (bestätigt Bug ist behoben)
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
|
||||
- [x] 3.8 Preservation-Tests erneut ausführen – Verifizieren dass sie weiterhin bestehen
|
||||
- **Property 2: Preservation** - Rollen-Inferenz und Leer-Eingaben unverändert
|
||||
- **IMPORTANT**: Die GLEICHEN Tests aus Task 2 erneut ausführen – KEINE neuen Tests schreiben
|
||||
- Preservation Property-Tests aus Schritt 2 ausführen
|
||||
- **ERWARTETES ERGEBNIS**: Tests BESTEHEN (bestätigt keine Regressionen)
|
||||
- Bestätigen dass alle Tests nach dem Fix weiterhin bestehen
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
||||
|
||||
- [x] 4. Checkpoint - Sicherstellen dass alle Tests bestehen
|
||||
- Alle Tests ausführen und sicherstellen dass sie bestehen, bei Fragen den User konsultieren.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Property-Based Tests verwenden Hypothesis mit `@settings(max_examples=100)`.
|
||||
- Unit- und Integrationstests verwenden `pytest` (Run-once, kein Watch-Modus); der `AzureOpenAIClient` wird stets gemockt.
|
||||
- Die Implementierungssprache ist Python (bestehende Codebase).
|
||||
- Tasks referenzieren explizit Anforderungen aus `bugfix.md` zur lückenlosen Nachverfolgbarkeit.
|
||||
- Der Fix ist additiv: bestehende Funktionalität (Rollen-Inferenz, DB-Skills, LLM-Fulltext-Matching) bleibt unverändert.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
{"specId": "6c39340f-10af-426d-9318-c96cb2f7ad6d", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,677 @@
|
||||
# Design: Entfernung Embedding-basierter Similarity – Umstellung auf BM25 + LLM
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Design beschreibt die vollständige Entfernung der Embedding-Infrastruktur aus dem Teamlandkarte-MCP-System. Das System wird von einem hybriden Embedding/BM25-Ansatz auf eine reine BM25 + LLM-Architektur umgestellt:
|
||||
|
||||
- **Kompetenz-Matching**: Ausschließlich BM25+RRF (bereits implementiert, nur Conditional-Logik entfernen)
|
||||
- **Rollen-Similarity**: LLM Chat Completion (neu, ersetzt Embedding-Cosine-Similarity)
|
||||
- **Rollen-Inferenz**: LLM Chat Completion (neu, ersetzt Embedding-basierte Nearest-Neighbor-Suche)
|
||||
- **AutoTagger**: Bleibt unverändert (bereits LLM-basiert)
|
||||
|
||||
Entfernt werden: `EmbeddingCache`, `_embed`, `prefetch_embeddings`, `_per_skill_similarity`, `_aggregate_similarity`, Embedding-API-Methoden im `AzureOpenAIClient`, alle Embedding-Konfigurationsfelder, und der Startup-Preload.
|
||||
|
||||
## Architektur
|
||||
|
||||
### Vorher (Hybrid)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[MCP Server Startup] --> B[Preload Embeddings]
|
||||
B --> C[VocabularyCache: Role + Competence Embeddings]
|
||||
B --> D[Task Embeddings]
|
||||
|
||||
E[Matching Request] --> F{use_bm25_search?}
|
||||
F -->|true| G[BM25+RRF Competence Similarity]
|
||||
F -->|false| H[Embedding Cosine Competence Similarity]
|
||||
|
||||
E --> I[Embedding Cosine Role Similarity]
|
||||
|
||||
J[infer_primary_role] --> K[Task Embedding → Cosine vs Role Vocab]
|
||||
```
|
||||
|
||||
### Nachher (BM25 + LLM)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[MCP Server Startup] --> B[DB Schema Verification]
|
||||
A --> C[AzureOpenAIClient: nur Chat Completion]
|
||||
|
||||
E[Matching Request] --> G[BM25+RRF Competence Similarity]
|
||||
E --> I[LLM Role Similarity mit In-Run Cache]
|
||||
|
||||
J[infer_primary_role] --> K[LLM Chat Completion: Task Text → Role Selection]
|
||||
```
|
||||
|
||||
### Datenfluss Matching (Nachher)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Matcher
|
||||
participant SimilarityEngine
|
||||
participant BM25
|
||||
participant LLM as AzureOpenAIClient (Chat)
|
||||
|
||||
Client->>Matcher: match(capacities, requirements)
|
||||
Matcher->>Matcher: Filter by availability
|
||||
Matcher->>BM25: Build global index (all candidate competences)
|
||||
|
||||
loop Per Candidate
|
||||
Matcher->>SimilarityEngine: compute_competence_similarity(required, candidate, global_index)
|
||||
SimilarityEngine->>BM25: rank per required competence
|
||||
SimilarityEngine-->>Matcher: {req → {score, best_match, rationale}}
|
||||
|
||||
Matcher->>SimilarityEngine: compute_role_similarity(required_role, candidate_role)
|
||||
SimilarityEngine->>SimilarityEngine: Check in-run cache
|
||||
alt Cache Miss
|
||||
SimilarityEngine->>LLM: chat_completion(role_similarity_prompt)
|
||||
LLM-->>SimilarityEngine: {"similarity": 0.85}
|
||||
SimilarityEngine->>SimilarityEngine: Cache result
|
||||
end
|
||||
SimilarityEngine-->>Matcher: float score
|
||||
end
|
||||
|
||||
Matcher-->>Client: MatchResult
|
||||
```
|
||||
|
||||
## Komponenten und Schnittstellen
|
||||
|
||||
### 1. SimilarityEngine (refactored)
|
||||
|
||||
**Datei:** `src/teamlandkarte_mcp/matching/similarity.py`
|
||||
|
||||
**Entfernte Methoden:**
|
||||
- `prefetch_embeddings`
|
||||
- `_embed`
|
||||
- `get_embedding_for_cache_key`
|
||||
- `get_embeddings_for_cache_keys`
|
||||
- `_aggregate_similarity`
|
||||
- `_per_skill_similarity`
|
||||
|
||||
**Entfernte Properties:**
|
||||
- `embedding_model`
|
||||
- `embedding_dimensions`
|
||||
- `use_bm25_search`
|
||||
|
||||
**Entfernte Constructor-Parameter:**
|
||||
- `cache` (EmbeddingCache)
|
||||
- `embedding_model`
|
||||
- `embedding_dimensions`
|
||||
- `strategy`
|
||||
- `use_bm25_search`
|
||||
|
||||
**Neuer Constructor:**
|
||||
|
||||
```python
|
||||
class SimilarityEngine:
|
||||
"""Compute similarity scores using BM25+RRF and LLM."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
client: AzureOpenAIClient,
|
||||
cost_tracker: CostTracker | None = None,
|
||||
use_auto_tagging: bool = False,
|
||||
auto_tagger: AutoTagger | None = None,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._cost_tracker = cost_tracker
|
||||
self._use_auto_tagging = use_auto_tagging
|
||||
self._auto_tagger = auto_tagger
|
||||
# Per-run cache for LLM role similarity: (role_a, role_b) → score
|
||||
self._role_similarity_cache: dict[tuple[str, str], float] = {}
|
||||
```
|
||||
|
||||
**Geänderte Methode `compute_competence_similarity`:**
|
||||
|
||||
```python
|
||||
async def compute_competence_similarity(
|
||||
self,
|
||||
required: list[str],
|
||||
candidate: list[str],
|
||||
global_index: Bm25Index | None = None,
|
||||
) -> dict[str, dict[str, object]]:
|
||||
"""BM25+RRF competence similarity (einziger Pfad)."""
|
||||
working = list(candidate)
|
||||
if self._use_auto_tagging and self._auto_tagger is not None:
|
||||
working = await self._auto_tagger.expand_competences(required, candidate)
|
||||
return self._bm25_rrf_similarity(required, working, global_index=global_index)
|
||||
```
|
||||
|
||||
**Neue Methode `compute_role_similarity` (LLM-basiert):**
|
||||
|
||||
```python
|
||||
_ROLE_SIMILARITY_SYSTEM_PROMPT = (
|
||||
"You are a job-role similarity expert. Given two role names, "
|
||||
"determine their semantic similarity on a scale from 0.0 to 1.0. "
|
||||
"0.0 means completely unrelated roles, 1.0 means identical or "
|
||||
"interchangeable roles. Consider synonyms, hierarchy, and domain overlap. "
|
||||
'Respond with a JSON object: {"similarity": <float>}'
|
||||
)
|
||||
|
||||
async def compute_role_similarity(
|
||||
self,
|
||||
required_role: str | None,
|
||||
candidate_role: str | None,
|
||||
) -> float:
|
||||
"""LLM-basierte Rollen-Similarity."""
|
||||
if self._is_bad_role(required_role) or self._is_bad_role(candidate_role):
|
||||
return 0.0
|
||||
|
||||
req_norm = str(required_role).strip().lower()
|
||||
cand_norm = str(candidate_role).strip().lower()
|
||||
|
||||
if req_norm == cand_norm:
|
||||
return 1.0
|
||||
|
||||
# Symmetrischer Cache-Key
|
||||
cache_key = tuple(sorted((req_norm, cand_norm)))
|
||||
if cache_key in self._role_similarity_cache:
|
||||
return self._role_similarity_cache[cache_key]
|
||||
|
||||
try:
|
||||
raw = await self._client.chat_completion(
|
||||
system=_ROLE_SIMILARITY_SYSTEM_PROMPT,
|
||||
user=f"Role A: {required_role}\nRole B: {candidate_role}",
|
||||
)
|
||||
payload = json.loads(raw)
|
||||
score = float(payload.get("similarity", 0.0))
|
||||
score = max(0.0, min(1.0, score))
|
||||
except Exception:
|
||||
score = 0.0
|
||||
|
||||
self._role_similarity_cache[cache_key] = score
|
||||
return score
|
||||
```
|
||||
|
||||
**Neue Methode `clear_role_cache`:**
|
||||
|
||||
```python
|
||||
def clear_role_cache(self) -> None:
|
||||
"""Cache zwischen Matching-Runs leeren."""
|
||||
self._role_similarity_cache.clear()
|
||||
```
|
||||
|
||||
### 2. VocabularyCache (refactored)
|
||||
|
||||
**Datei:** `src/teamlandkarte_mcp/matching/vocabulary.py`
|
||||
|
||||
**Entfernte Methoden:**
|
||||
- `preload`
|
||||
- `_preload_role_vocab`
|
||||
- `_preload_competence_vocab`
|
||||
- `ensure_task_embedding`
|
||||
- `infer_primary_role` (alte Signatur mit `task_embedding`)
|
||||
- `infer_competences`
|
||||
|
||||
**Entfernte Properties:**
|
||||
- `roles`
|
||||
- `competences`
|
||||
|
||||
**Neuer Constructor:**
|
||||
|
||||
```python
|
||||
class VocabularyCache:
|
||||
"""LLM-basierte Rollen-Inferenz aus Task-Text."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
db: DBClient,
|
||||
client: AzureOpenAIClient,
|
||||
) -> None:
|
||||
self._db = db
|
||||
self._client = client
|
||||
```
|
||||
|
||||
**Neue Methode `infer_primary_role` (LLM-basiert):**
|
||||
|
||||
```python
|
||||
_ROLE_INFERENCE_SYSTEM_PROMPT = (
|
||||
"You are a role classification expert. Given a task description and a list "
|
||||
"of available roles, select the single most appropriate role for the task. "
|
||||
"You MUST select exactly one role from the provided list. "
|
||||
"Respond with a JSON object: "
|
||||
'{"role": "<selected role name>", "confidence": <float 0.0-1.0>}'
|
||||
)
|
||||
|
||||
async def infer_primary_role(
|
||||
self,
|
||||
*,
|
||||
task_text: str,
|
||||
) -> tuple[str, float] | None:
|
||||
"""Inferiere die passendste Rolle für einen Task-Text via LLM."""
|
||||
role_names = self._db.get_all_role_names()
|
||||
if not role_names:
|
||||
return None
|
||||
|
||||
text = (task_text or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
user_prompt = (
|
||||
f"Task: {text}\n\n"
|
||||
f"Available roles: {json.dumps(list(role_names), ensure_ascii=False)}"
|
||||
)
|
||||
|
||||
try:
|
||||
raw = await self._client.chat_completion(
|
||||
system=_ROLE_INFERENCE_SYSTEM_PROMPT,
|
||||
user=user_prompt,
|
||||
)
|
||||
payload = json.loads(raw)
|
||||
role = str(payload.get("role", "")).strip()
|
||||
confidence = float(payload.get("confidence", 0.0))
|
||||
confidence = max(0.0, min(1.0, confidence))
|
||||
|
||||
# Validierung: Rolle muss in der DB-Liste existieren
|
||||
if role not in set(role_names):
|
||||
return None
|
||||
|
||||
return role, confidence
|
||||
except Exception:
|
||||
return None
|
||||
```
|
||||
|
||||
### 3. AzureOpenAIClient (vereinfacht)
|
||||
|
||||
**Datei:** `src/teamlandkarte_mcp/azure/openai_client.py`
|
||||
|
||||
**Entfernte Methoden:**
|
||||
- `embeddings`
|
||||
- `get_embeddings_batch`
|
||||
|
||||
**Entfernte Constructor-Parameter:**
|
||||
- `embedding_api_key`
|
||||
- `embedding_deployment`
|
||||
- `embedding_batch_size`
|
||||
|
||||
**Neuer Constructor:**
|
||||
|
||||
```python
|
||||
class AzureOpenAIClient:
|
||||
"""Azure OpenAI Chat Completion Client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
endpoint: str,
|
||||
api_version: str,
|
||||
chat_deployment: str,
|
||||
llm_api_key: str,
|
||||
timeout_s: float = 30.0,
|
||||
max_retries: int = 5,
|
||||
cost_tracker: CostTracker | None = None,
|
||||
) -> None:
|
||||
self._chat_deployment = chat_deployment
|
||||
self._max_retries = max_retries
|
||||
self._timeout_s = timeout_s
|
||||
self._cost_tracker = cost_tracker
|
||||
|
||||
self._chat = AsyncAzureOpenAI(
|
||||
api_key=llm_api_key,
|
||||
azure_endpoint=endpoint,
|
||||
api_version=api_version,
|
||||
)
|
||||
```
|
||||
|
||||
Die `chat_completion`-Methode bleibt unverändert, außer dass die Guard-Clause für fehlende Konfiguration entfällt (Chat ist jetzt immer konfiguriert).
|
||||
|
||||
### 4. Matcher (minimal geändert)
|
||||
|
||||
**Datei:** `src/teamlandkarte_mcp/matching/matcher.py`
|
||||
|
||||
**Änderungen:**
|
||||
- Entfernung der `if self._sim.use_bm25_search`-Bedingung beim BM25-Index-Aufbau
|
||||
- Der globale BM25-Index wird **immer** gebaut (unconditional)
|
||||
|
||||
```python
|
||||
# Vorher:
|
||||
global_bm25_index: Bm25Index | None = None
|
||||
if self._sim.use_bm25_search and filtered:
|
||||
global_corpus = list(...)
|
||||
global_bm25_index = Bm25Index(corpus=global_corpus)
|
||||
|
||||
# Nachher:
|
||||
global_bm25_index: Bm25Index | None = None
|
||||
if filtered:
|
||||
global_corpus = list(
|
||||
{c for cap in filtered for c in cap.competences if c.strip()}
|
||||
)
|
||||
global_bm25_index = Bm25Index(corpus=global_corpus)
|
||||
```
|
||||
|
||||
### 5. MCP Server (vereinfacht)
|
||||
|
||||
**Datei:** `src/teamlandkarte_mcp/mcp_server.py`
|
||||
|
||||
**Entfernte Elemente:**
|
||||
- `_startup_preload_embeddings` Funktion
|
||||
- `_deferred_preload` Funktion und `_preload_started` Flag
|
||||
- `_ensure_preloaded` Funktion
|
||||
- `EmbeddingCache`-Instanziierung
|
||||
- `emb_cache`-Variable
|
||||
- Alle `await _ensure_preloaded()`-Aufrufe in Tools
|
||||
- Import von `EmbeddingCache`
|
||||
- Import von `VocabularyCache` (wird direkt mit `AzureOpenAIClient` konstruiert)
|
||||
|
||||
**Geänderte Konstruktion:**
|
||||
|
||||
```python
|
||||
# Vorher: Embedding-Client + separater LLM-Client
|
||||
azure_client = AzureOpenAIClient(
|
||||
endpoint=..., embedding_api_key=..., embedding_deployment=..., ...
|
||||
)
|
||||
|
||||
# Nachher: Nur noch ein LLM-Client
|
||||
azure_client = AzureOpenAIClient(
|
||||
endpoint=cfg.azure_openai.endpoint,
|
||||
api_version=cfg.azure_openai.api_version,
|
||||
chat_deployment=cfg.azure_openai.chat_deployment,
|
||||
llm_api_key=cfg.azure_openai.llm_api_key,
|
||||
cost_tracker=cost_tracker,
|
||||
)
|
||||
|
||||
similarity = SimilarityEngine(
|
||||
client=azure_client,
|
||||
cost_tracker=cost_tracker,
|
||||
use_auto_tagging=cfg.similarity.use_auto_tagging,
|
||||
auto_tagger=auto_tagger,
|
||||
)
|
||||
|
||||
vocab_cache = VocabularyCache(
|
||||
db=db_client,
|
||||
client=azure_client,
|
||||
)
|
||||
```
|
||||
|
||||
**Geändertes `infer_primary_role`-Tool:**
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def infer_primary_role(
|
||||
task_id: Optional[str] = None,
|
||||
task_text: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Infer the single closest role from either a DB task or free text."""
|
||||
if bool(task_id) == bool(task_text):
|
||||
return "Provide exactly one of task_id or task_text."
|
||||
|
||||
text = (task_text or "").strip()
|
||||
if task_id:
|
||||
_ensure_db()
|
||||
task = db_client.get_task_by_id(task_id)
|
||||
if task is None:
|
||||
return f"Task not found or not published: {task_id}"
|
||||
title = (task.title or "").strip()
|
||||
desc = (task.description or "").strip()
|
||||
text = (title + "\n\n" + desc).strip() if title else desc
|
||||
|
||||
if not text:
|
||||
return "Task text is empty."
|
||||
|
||||
best = await vocab_cache.infer_primary_role(task_text=text)
|
||||
if best is None:
|
||||
rows = [["", ""]]
|
||||
else:
|
||||
role, score = best
|
||||
rows = [[str(role), f"{float(score):.3f}"]]
|
||||
|
||||
return md_table(["Role", "Confidence"], rows)
|
||||
```
|
||||
|
||||
**Entferntes Tool `validate_task_requirements`:**
|
||||
Dieses Tool basiert vollständig auf Embedding-Inferenz (`infer_competences`, `ensure_task_embedding`). Es wird entfernt oder durch eine vereinfachte Version ersetzt, die nur DB-Felder anzeigt.
|
||||
|
||||
### 6. Config (vereinfacht)
|
||||
|
||||
**Datei:** `src/teamlandkarte_mcp/config.py`
|
||||
|
||||
**Entfernte Dataclasses:**
|
||||
- `EmbeddingCacheConfig`
|
||||
- `InferenceConfig`
|
||||
|
||||
**Geänderte Dataclasses:**
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class AzureOpenAIConfig:
|
||||
"""Azure OpenAI configuration (nur Chat Completion)."""
|
||||
endpoint: str
|
||||
api_version: str = "2024-02-15-preview"
|
||||
chat_deployment: str = ""
|
||||
llm_api_key: str = ""
|
||||
show_costs_in_output: bool = False
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SimilarityConfig:
|
||||
"""Similarity engine configuration."""
|
||||
use_auto_tagging: bool = False
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MatchingConfig:
|
||||
"""Matching weights and thresholds."""
|
||||
competence_weight: float = 0.8
|
||||
role_weight: float = 0.2
|
||||
require_confirmation: bool = True
|
||||
thresholds: MatchingThresholds = MatchingThresholds()
|
||||
fuzzy: FuzzyConfig = FuzzyConfig()
|
||||
# inference entfällt
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AppConfig:
|
||||
"""Top-level application configuration."""
|
||||
database: DatabaseConfig
|
||||
matching: MatchingConfig
|
||||
cache: CacheConfig
|
||||
azure_openai: AzureOpenAIConfig
|
||||
similarity: SimilarityConfig
|
||||
# embedding_cache entfällt
|
||||
```
|
||||
|
||||
**Entfernte Felder aus `SimilarityConfig`:**
|
||||
- `embedding_model`
|
||||
- `embedding_dimensions`
|
||||
- `strategy`
|
||||
- `use_bm25_search`
|
||||
|
||||
**Entfernte Felder aus `AzureOpenAIConfig`:**
|
||||
- `embedding_deployment`
|
||||
- `embedding_batch_size`
|
||||
|
||||
**Entfernte Validierungen in `load_config`:**
|
||||
- `AZURE_OPENAI_EMBEDDING_API_KEY` Prüfung
|
||||
- `embedding_dimensions == 3072` Prüfung
|
||||
- `strategy` Validierung
|
||||
- `inference` Parsing
|
||||
|
||||
**Neue Validierung:**
|
||||
- `AZURE_OPENAI_LLM_API_KEY` ist jetzt immer erforderlich (nicht nur bei auto_tagging)
|
||||
- `chat_deployment` ist jetzt erforderlich
|
||||
|
||||
### 7. Entfernte Dateien
|
||||
|
||||
| Datei | Grund |
|
||||
|-------|-------|
|
||||
| `src/teamlandkarte_mcp/cache/embedding_cache.py` | Keine Embeddings mehr |
|
||||
| Zugehörige Tests für EmbeddingCache | Keine Embeddings mehr |
|
||||
|
||||
### 8. config.toml Änderungen
|
||||
|
||||
**Entfernte Sektionen:**
|
||||
- `[embedding_cache]` komplett
|
||||
|
||||
**Entfernte Felder aus `[matching.similarity]`:**
|
||||
- `embedding_model`
|
||||
- `embedding_dimensions`
|
||||
- `strategy`
|
||||
- `use_bm25_search`
|
||||
|
||||
**Entfernte Felder aus `[azure_openai]`:**
|
||||
- `embedding_deployment`
|
||||
- `embedding_batch_size`
|
||||
|
||||
**Entfernte Felder aus `[matching]`:**
|
||||
- `[matching.inference]` komplett
|
||||
|
||||
**Neue Pflichtfelder in `[azure_openai]`:**
|
||||
- `chat_deployment` (bereits vorhanden als optionales Feld, wird Pflicht)
|
||||
|
||||
**Neue Umgebungsvariable (Pflicht):**
|
||||
- `AZURE_OPENAI_LLM_API_KEY` (ersetzt `AZURE_OPENAI_EMBEDDING_API_KEY`)
|
||||
|
||||
**Entfernte Umgebungsvariable:**
|
||||
- `AZURE_OPENAI_EMBEDDING_API_KEY`
|
||||
|
||||
## Datenmodelle
|
||||
|
||||
### LLM Role Similarity Request/Response
|
||||
|
||||
```json
|
||||
// System Prompt: _ROLE_SIMILARITY_SYSTEM_PROMPT
|
||||
// User Message:
|
||||
"Role A: Software Engineer\nRole B: Backend Developer"
|
||||
|
||||
// Expected Response:
|
||||
{"similarity": 0.75}
|
||||
```
|
||||
|
||||
### LLM Role Inference Request/Response
|
||||
|
||||
```json
|
||||
// System Prompt: _ROLE_INFERENCE_SYSTEM_PROMPT
|
||||
// User Message:
|
||||
"Task: Implementierung einer REST-API für Benutzerverwaltung\n\nAvailable roles: [\"Backend Developer\", \"Frontend Developer\", \"DevOps Engineer\", \"Data Engineer\"]"
|
||||
|
||||
// Expected Response:
|
||||
{"role": "Backend Developer", "confidence": 0.92}
|
||||
```
|
||||
|
||||
### In-Run Role Similarity Cache
|
||||
|
||||
```python
|
||||
# Symmetrischer Cache innerhalb eines Matching-Runs
|
||||
# Key: tuple(sorted((role_a_lower, role_b_lower)))
|
||||
# Value: float (0.0 - 1.0)
|
||||
_role_similarity_cache: dict[tuple[str, str], float] = {}
|
||||
```
|
||||
|
||||
Der Cache wird pro `SimilarityEngine`-Instanz gehalten und lebt für die Dauer des Server-Prozesses. Da Rollennamen stabil sind (aus der DB), ist kein TTL nötig.
|
||||
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
### Property 1: BM25 Zero-Score für fehlenden Token-Overlap
|
||||
|
||||
*For any* set of required competences and candidate competences where no candidate shares any token with a required competence, `compute_competence_similarity` shall return a score of 0.0 for that required competence.
|
||||
|
||||
**Validates: Requirements 1.1**
|
||||
|
||||
### Property 2: Matcher baut BM25-Index bedingungslos
|
||||
|
||||
*For any* non-empty list of filtered candidates with competences, the Matcher shall always build a global BM25 index and use it for competence scoring, producing results where candidates with no token overlap receive score 0.0.
|
||||
|
||||
**Validates: Requirements 2.6**
|
||||
|
||||
### Property 3: infer_primary_role Ausgabe-Validität
|
||||
|
||||
*For any* non-empty task text and non-empty role list from the database, if `infer_primary_role` returns a non-None result, the returned role name must be an element of the database role list and the confidence must be a float in [0.0, 1.0].
|
||||
|
||||
**Validates: Requirements 3.1, 3.4**
|
||||
|
||||
### Property 4: Graceful Degradation bei LLM-Fehler
|
||||
|
||||
*For any* LLM call that raises an exception, `compute_role_similarity` shall return 0.0 and `infer_primary_role` shall return None, without propagating the exception to the caller.
|
||||
|
||||
**Validates: Requirements 3.5, 5.6**
|
||||
|
||||
### Property 5: compute_role_similarity Wertebereich
|
||||
|
||||
*For any* two non-empty, non-"(unknown)" role names, `compute_role_similarity` shall return a float in the closed interval [0.0, 1.0]. For identical role names (case-insensitive), it shall return 1.0.
|
||||
|
||||
**Validates: Requirements 5.1**
|
||||
|
||||
### Property 6: Rollen-Similarity-Cache ist symmetrisch und idempotent
|
||||
|
||||
*For any* role pair (A, B), calling `compute_role_similarity(A, B)` and then `compute_role_similarity(B, A)` shall return the same score, and the second call shall not invoke the LLM (cache hit). Repeated calls with the same pair shall always return the same cached value.
|
||||
|
||||
**Validates: Requirements 5.8**
|
||||
|
||||
## Error Handling
|
||||
|
||||
### LLM-Fehler in compute_role_similarity
|
||||
|
||||
- Bei jeder Exception (Timeout, API-Fehler, JSON-Parse-Fehler) wird `0.0` zurückgegeben
|
||||
- Fehler wird geloggt (LOGGER.warning)
|
||||
- Kein Eintrag im Cache für fehlgeschlagene Aufrufe (Retry bei nächstem Aufruf möglich)
|
||||
|
||||
### LLM-Fehler in infer_primary_role
|
||||
|
||||
- Bei jeder Exception wird `None` zurückgegeben
|
||||
- Fehler wird geloggt (LOGGER.warning)
|
||||
- Caller (MCP-Tool) zeigt leere Tabelle an
|
||||
|
||||
### Ungültige LLM-Antworten
|
||||
|
||||
- **Role Similarity**: Wenn `similarity` nicht im JSON oder nicht parsebar → 0.0
|
||||
- **Role Inference**: Wenn `role` nicht in der DB-Liste → None
|
||||
- **Role Inference**: Wenn `confidence` nicht parsebar → 0.0 (aber Rolle wird trotzdem zurückgegeben wenn valide)
|
||||
|
||||
### Leere/Ungültige Eingaben
|
||||
|
||||
- `compute_role_similarity` mit None/leer/"(unknown)" → 0.0 (kein LLM-Aufruf)
|
||||
- `infer_primary_role` mit leerem Text → None (kein LLM-Aufruf)
|
||||
- `infer_primary_role` mit leerer Rollenliste aus DB → None (kein LLM-Aufruf)
|
||||
- `compute_competence_similarity` mit leerer Required-Liste → leeres Dict
|
||||
- `compute_competence_similarity` mit leerer Candidate-Liste → alle Scores 0.0
|
||||
|
||||
### Konfigurationsfehler
|
||||
|
||||
- Fehlende `AZURE_OPENAI_LLM_API_KEY` → `ConfigError` beim Laden (fail-fast)
|
||||
- Fehlender `chat_deployment` → `ConfigError` beim Laden (fail-fast)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Property-Based Tests (fast-check / Hypothesis)
|
||||
|
||||
Bibliothek: **Hypothesis** (Python PBT-Standard)
|
||||
|
||||
Konfiguration: Mindestens 100 Iterationen pro Property-Test.
|
||||
|
||||
Jeder Property-Test wird mit einem Kommentar getaggt:
|
||||
```
|
||||
# Feature: remove-embedding-competence-similarity, Property {N}: {title}
|
||||
```
|
||||
|
||||
| Property | Test-Ansatz | Generator |
|
||||
|----------|-------------|-----------|
|
||||
| 1: BM25 Zero-Score | Generiere disjunkte Token-Sets für required/candidate, prüfe Score == 0.0 | `st.lists(st.text(alphabet=st.characters(whitelist_categories=('L',)), min_size=3))` |
|
||||
| 2: Matcher BM25 unconditional | Generiere Capacities + Requirements, prüfe dass Ergebnis BM25-Charakteristik hat (0.0 bei no-overlap) | Custom Capacity/Requirements strategies |
|
||||
| 3: infer_primary_role Validität | Generiere Task-Texte + Role-Listen, mocke LLM mit zufälliger valider Antwort, prüfe Output-Constraints | `st.text(min_size=1)`, `st.lists(st.text(min_size=1), min_size=1)` |
|
||||
| 4: Graceful Degradation | Generiere zufällige Exceptions, prüfe dass 0.0/None zurückkommt | `st.sampled_from([TimeoutError, RuntimeError, ValueError, json.JSONDecodeError])` |
|
||||
| 5: Role Similarity Wertebereich | Generiere Rollenpaare, mocke LLM mit zufälligem Score, prüfe [0.0, 1.0] und Identitäts-Case | `st.text(min_size=1, max_size=50)` |
|
||||
| 6: Cache Symmetrie | Generiere Rollenpaare, rufe in beiden Reihenfolgen auf, prüfe gleichen Score + nur 1 LLM-Call | `st.text(min_size=1, max_size=50)` |
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit Tests fokussieren auf:
|
||||
|
||||
- **Spezifische Beispiele**: Bekannte Rollenpaare (z.B. "Backend Developer" vs "Software Engineer") mit gemocktem LLM
|
||||
- **Edge Cases**: Leere Strings, None-Werte, "(unknown)", Whitespace-only
|
||||
- **Integration**: Config-Loading ohne Embedding-Felder, Server-Startup ohne Preload
|
||||
- **Regressions**: Sicherstellen dass BM25+RRF-Ergebnisse identisch zum bisherigen `use_bm25_search=True`-Pfad sind
|
||||
|
||||
### Integrationstests
|
||||
|
||||
- End-to-End Matching-Run mit gemocktem LLM-Client
|
||||
- Config-Loading aus minimaler TOML-Datei (ohne Embedding-Sektionen)
|
||||
- Server-Startup ohne `AZURE_OPENAI_EMBEDDING_API_KEY` (darf nicht mehr geprüft werden)
|
||||
|
||||
### Nicht getestet (bewusst ausgelassen)
|
||||
|
||||
- Prompt-Qualität (subjektiv, erfordert manuelles Review)
|
||||
- LLM-Antwort-Genauigkeit (abhängig vom Modell, nicht deterministisch)
|
||||
- Startup-Performance-Verbesserung (Benchmark, kein Unit-Test)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# Anforderungsdokument
|
||||
|
||||
## Einleitung
|
||||
|
||||
Dieses Dokument beschreibt die Anforderungen für die Entfernung der Embedding-basierten Kompetenz-Similarity zugunsten von BM25 als einzigem Kompetenz-Matching-Verfahren, die Umstellung der Rollen-Inferenz und Rollen-Similarity auf einen LLM-Ansatz sowie die vollständige Entfernung der Embedding-Infrastruktur. Das System wird vollständig BM25 + LLM-basiert.
|
||||
|
||||
## Glossar
|
||||
|
||||
- **SimilarityEngine**: Modul in `matching/similarity.py`, das Ähnlichkeitsberechnungen für Kompetenzen und Rollen durchführt.
|
||||
- **VocabularyCache**: Modul in `matching/vocabulary.py`, das Vokabular-Embeddings vorhält und Inferenz-Funktionen bereitstellt.
|
||||
- **BM25**: Probabilistisches Ranking-Verfahren für Textähnlichkeit basierend auf Termfrequenz und inverser Dokumentfrequenz.
|
||||
- **RRF**: Reciprocal Rank Fusion – Verfahren zur Kombination mehrerer Rankings.
|
||||
- **AutoTagger**: LLM-basiertes Modul zur Erweiterung von Kompetenzlisten vor dem BM25-Scoring.
|
||||
- **AzureOpenAIClient**: Client-Wrapper für Azure OpenAI API-Aufrufe (Embeddings und Chat Completions).
|
||||
- **SimilarityConfig**: Konfigurationsklasse für die Similarity-Engine in `config.py`.
|
||||
- **infer_primary_role**: Funktion, die einer Aufgabe die passendste Rolle aus der Datenbank zuordnet.
|
||||
- **LLM**: Large Language Model – hier Azure OpenAI Chat Completion.
|
||||
- **Preload**: Eageres Vorladen von Embeddings beim Server-Start.
|
||||
- **EmbeddingCache**: Cache-Modul für gespeicherte Embedding-Vektoren.
|
||||
- **compute_role_similarity**: Funktion in der SimilarityEngine, die die semantische Ähnlichkeit zwischen einer geforderten Rolle und einer Kandidaten-Rolle berechnet.
|
||||
|
||||
## Anforderungen
|
||||
|
||||
### Anforderung 1: Entfernung der Embedding-basierten Kompetenz-Similarity
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass BM25+RRF das einzige Verfahren für Kompetenz-Matching ist, damit die Codebasis vereinfacht wird und keine Embedding-Kosten für Kompetenz-Vergleiche anfallen.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE SimilarityEngine SHALL use BM25+RRF as the sole method for computing competence similarity scores.
|
||||
2. WHEN compute_competence_similarity is called, THE SimilarityEngine SHALL execute the BM25+RRF path without checking a strategy flag or use_bm25_search parameter.
|
||||
3. THE SimilarityEngine SHALL no longer contain the `_per_skill_similarity` method for embedding-based per-skill competence matching.
|
||||
4. THE SimilarityEngine SHALL no longer contain the `_aggregate_similarity` method for embedding-based aggregate competence matching.
|
||||
5. THE SimilarityEngine SHALL no longer accept a `strategy` parameter in its constructor for competence similarity strategy selection.
|
||||
6. THE SimilarityEngine SHALL no longer accept a `use_bm25_search` parameter in its constructor.
|
||||
7. THE SimilarityEngine SHALL remove the `prefetch_embeddings` and `_embed` methods, since role similarity also switches to LLM and no embedding use cases remain.
|
||||
|
||||
### Anforderung 2: Entfernung des Konfigurationsparameters use_bm25_search
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass der Parameter `use_bm25_search` aus der Konfiguration entfernt wird, da BM25 nun immer aktiv ist und der Parameter redundant geworden ist.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE SimilarityConfig SHALL no longer contain the field `use_bm25_search`.
|
||||
2. THE SimilarityConfig SHALL no longer contain the field `strategy` (da nur noch BM25 verwendet wird).
|
||||
3. WHEN the configuration is loaded, THE Config-Loader SHALL not read or validate `use_bm25_search` from the TOML file.
|
||||
4. WHEN the configuration is loaded, THE Config-Loader SHALL not read or validate `matching.similarity.strategy` from the TOML file.
|
||||
5. THE SimilarityEngine SHALL no longer expose a `use_bm25_search` property.
|
||||
6. THE Matcher SHALL build the global BM25 index unconditionally for all filtered candidates without checking a `use_bm25_search` flag.
|
||||
|
||||
### Anforderung 3: LLM-basierte Rollen-Inferenz
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass `infer_primary_role` einen LLM-Ansatz (Chat Completion) verwendet anstelle von Embedding-Cosine-Similarity, damit die Rollenzuordnung kontextbezogener und genauer erfolgt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN infer_primary_role is called, THE VocabularyCache SHALL use an LLM chat completion to determine the best matching role for a given task text.
|
||||
2. WHEN infer_primary_role is called, THE VocabularyCache SHALL provide the list of all available role names from the database as context to the LLM.
|
||||
3. WHEN infer_primary_role is called, THE VocabularyCache SHALL provide the task text (title and/or description) as input to the LLM.
|
||||
4. THE VocabularyCache SHALL return the role name and a confidence score from the LLM response.
|
||||
5. IF the LLM call fails, THEN THE VocabularyCache SHALL return None rather than raising an unhandled exception.
|
||||
6. THE VocabularyCache SHALL no longer require a task embedding as input parameter for infer_primary_role.
|
||||
7. THE VocabularyCache SHALL accept the task text directly as input parameter for infer_primary_role.
|
||||
8. WHEN infer_primary_role is called, THE VocabularyCache SHALL instruct the LLM to select exactly one role from the provided list and return a structured JSON response.
|
||||
9. THE infer_primary_role tool in mcp_server.py SHALL call the new LLM-based infer_primary_role without first generating a task embedding.
|
||||
|
||||
### Anforderung 4: Entfernung des Embedding-Preloads offener Tasks
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass Embeddings offener Tasks nicht mehr beim Server-Start vorgeladen werden und auch nicht mehr on-demand erzeugt werden, da keine Embedding-basierte Verarbeitung mehr stattfindet.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP-Server SHALL no longer preload embeddings for open tasks during startup.
|
||||
2. THE `_startup_preload_embeddings` function SHALL be removed entirely.
|
||||
3. THE VocabularyCache SHALL no longer provide an `ensure_task_embedding` method, since task embeddings are not needed in the BM25 + LLM architecture.
|
||||
4. THE MCP-Server SHALL no longer preload role or competence vocabulary embeddings at startup, since all similarity computations now use BM25 or LLM.
|
||||
5. THE startup time of the MCP-Server SHALL be reduced by eliminating all embedding preload loops.
|
||||
|
||||
### Anforderung 5: Umstellung der Rollen-Similarity im Matcher auf LLM
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass die Rollen-Similarity im Matcher (`compute_role_similarity`) ebenfalls einen LLM-Ansatz (Chat Completion) nutzt anstelle von Embedding-Cosine-Similarity, damit das gesamte System ohne Embeddings auskommt und konsistent BM25 + LLM-basiert ist.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN compute_role_similarity is called, THE SimilarityEngine SHALL use an LLM chat completion to determine the semantic similarity between the required role and the candidate role.
|
||||
2. WHEN compute_role_similarity is called, THE SimilarityEngine SHALL provide both role names to the LLM and request a numeric similarity score between 0.0 and 1.0.
|
||||
3. THE SimilarityEngine SHALL instruct the LLM to return a structured JSON response containing the similarity score.
|
||||
4. THE SimilarityEngine SHALL no longer use embedding cosine similarity for role comparisons.
|
||||
5. THE SimilarityEngine SHALL no longer call `_embed` or `prefetch_embeddings` for role similarity computation.
|
||||
6. IF the LLM call fails, THEN THE SimilarityEngine SHALL return a default similarity score of 0.0 rather than raising an unhandled exception.
|
||||
7. THE VocabularyCache SHALL no longer preload role vocabulary embeddings at startup, since they are not needed for LLM-based role similarity.
|
||||
8. THE SimilarityEngine SHALL cache LLM-based role similarity results for identical role pairs within a matching run to avoid redundant API calls.
|
||||
|
||||
### Anforderung 6: Vollständige Entfernung der Embedding-Infrastruktur
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass die gesamte Embedding-Infrastruktur entfernt wird, da weder Kompetenz-Matching noch Rollen-Similarity noch Rollen-Inferenz Embeddings benötigen und das System vollständig BM25 + LLM-basiert ist.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE SimilarityEngine SHALL remove the `prefetch_embeddings` method entirely.
|
||||
2. THE SimilarityEngine SHALL remove the `_embed` method entirely.
|
||||
3. THE VocabularyCache SHALL remove `_preload_competence_vocab` entirely.
|
||||
4. THE VocabularyCache SHALL remove `_preload_role_vocab` (or equivalent role embedding preload logic) entirely.
|
||||
5. THE VocabularyCache SHALL remove `infer_competences` if embedding-based competence inference is no longer used.
|
||||
6. THE EmbeddingCache module SHALL be removed entirely, since no code path requires cached embeddings.
|
||||
7. THE AzureOpenAIClient SHALL remove the embedding API method (e.g. `get_embeddings` or equivalent batch embedding call), retaining only chat completion methods.
|
||||
8. THE config.py SHALL remove embedding-related configuration fields (e.g. `embedding_model`, `embedding_dimensions`, embedding batch size settings).
|
||||
9. THE config.py SHALL remove the `InferenceConfig` dataclass if `max_competences` and `min_similarity` are no longer used.
|
||||
10. THE SimilarityConfig SHALL remove any fields related to embedding thresholds or embedding model selection.
|
||||
11. THE config.toml SHALL remove embedding-related configuration entries.
|
||||
12. THE MCP-Server SHALL remove the `_startup_preload_embeddings` function entirely if no embedding preloads remain.
|
||||
@@ -0,0 +1,178 @@
|
||||
# Implementation Plan: Entfernung Embedding-basierter Similarity – Umstellung auf BM25 + LLM
|
||||
|
||||
## Overview
|
||||
|
||||
Incremental removal of all embedding infrastructure and replacement with BM25 + LLM. Tasks are ordered to avoid breaking the system mid-way: first add new LLM-based methods, then remove embedding code paths, then clean up config and delete dead modules.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Simplify AzureOpenAIClient to remove embedding methods
|
||||
- [x] 1.1 Remove `embeddings` and `get_embeddings_batch` methods from `AzureOpenAIClient`
|
||||
- Remove the `self._emb` AsyncAzureOpenAI client instance
|
||||
- Remove constructor parameters: `embedding_api_key`, `embedding_deployment`, `embedding_batch_size`
|
||||
- Keep only `chat_completion` method and its supporting `self._chat` client
|
||||
- Make `self._chat` the primary client (no longer optional/conditional)
|
||||
- Remove the guard clause in `chat_completion` that checks for missing config
|
||||
- _Requirements: 6.7_
|
||||
|
||||
- [x] 1.2 Update `AzureOpenAIClient` constructor signature
|
||||
- New required params: `endpoint`, `api_version`, `chat_deployment`, `llm_api_key`
|
||||
- Keep optional: `timeout_s`, `max_retries`, `cost_tracker`
|
||||
- Remove cost tracker calls for embedding requests (`log_embedding_request`, `log_embedding_batch_request`)
|
||||
- _Requirements: 6.7_
|
||||
|
||||
- [-] 2. Refactor SimilarityEngine to remove embedding methods and add LLM role similarity
|
||||
- [x] 2.1 Remove embedding-related methods and properties from `SimilarityEngine`
|
||||
- Remove methods: `prefetch_embeddings`, `_embed`, `get_embedding_for_cache_key`, `get_embeddings_for_cache_keys`, `_aggregate_similarity`, `_per_skill_similarity`
|
||||
- Remove properties: `embedding_model`, `embedding_dimensions`, `use_bm25_search`
|
||||
- Remove constructor parameters: `cache`, `embedding_model`, `embedding_dimensions`, `strategy`, `use_bm25_search`
|
||||
- Remove module-level helpers: `_cache_key`, `cosine_similarity`
|
||||
- Update constructor to accept only: `client`, `cost_tracker`, `use_auto_tagging`, `auto_tagger`
|
||||
- Add `_role_similarity_cache: dict[tuple[str, str], float]` to constructor
|
||||
- _Requirements: 1.3, 1.4, 1.5, 1.6, 1.7, 6.1, 6.2_
|
||||
|
||||
- [x] 2.2 Simplify `compute_competence_similarity` to always use BM25+RRF
|
||||
- Remove any strategy/conditional branching
|
||||
- Always call `_bm25_rrf_similarity` directly (with optional auto-tagging expansion)
|
||||
- _Requirements: 1.1, 1.2_
|
||||
|
||||
- [x] 2.3 Implement LLM-based `compute_role_similarity`
|
||||
- Replace embedding cosine similarity with LLM chat completion
|
||||
- Add `_ROLE_SIMILARITY_SYSTEM_PROMPT` constant
|
||||
- Implement symmetric cache key: `tuple(sorted((role_a_lower, role_b_lower)))`
|
||||
- Return 0.0 for bad/empty/None/"(unknown)" roles without LLM call
|
||||
- Return 1.0 for identical roles (case-insensitive) without LLM call
|
||||
- Parse JSON response, clamp score to [0.0, 1.0]
|
||||
- On any exception: return 0.0, do not cache failed results
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.8_
|
||||
|
||||
- [x] 2.4 Add `clear_role_cache` method
|
||||
- Clears `_role_similarity_cache` dict
|
||||
- _Requirements: 5.8_
|
||||
|
||||
- [x] 2.5 Write property test for BM25 zero-score on disjoint tokens
|
||||
- **Property 1: BM25 Zero-Score für fehlenden Token-Overlap**
|
||||
- **Validates: Requirements 1.1**
|
||||
|
||||
- [x] 2.6 Write property test for role similarity value range and identity
|
||||
- **Property 5: compute_role_similarity Wertebereich**
|
||||
- **Validates: Requirements 5.1**
|
||||
|
||||
- [x] 2.7 Write property test for role similarity cache symmetry
|
||||
- **Property 6: Rollen-Similarity-Cache ist symmetrisch und idempotent**
|
||||
- **Validates: Requirements 5.8**
|
||||
|
||||
- [-] 3. Refactor VocabularyCache to use LLM-based role inference
|
||||
- [x] 3.1 Rewrite `VocabularyCache` class
|
||||
- Remove all embedding-related methods: `preload`, `_preload_role_vocab`, `_preload_competence_vocab`, `ensure_task_embedding`, `infer_competences`
|
||||
- Remove old `infer_primary_role` (embedding-based)
|
||||
- Remove properties: `roles`, `competences`
|
||||
- Remove constructor dependencies on `SimilarityEngine` and `EmbeddingCache`
|
||||
- New constructor accepts only: `db: DBClient`, `client: AzureOpenAIClient`
|
||||
- Remove module-level helpers: `role_vocab_cache_key`, `competence_vocab_cache_key`, `task_id_cache_key`, `VocabItem`
|
||||
- _Requirements: 4.3, 6.3, 6.4, 6.5_
|
||||
|
||||
- [x] 3.2 Implement new LLM-based `infer_primary_role`
|
||||
- Accept `task_text: str` keyword argument (no more `task_embedding`)
|
||||
- Fetch all role names from DB via `self._db.get_all_role_names()`
|
||||
- Build LLM prompt with task text and available roles list
|
||||
- Parse JSON response: `{"role": "...", "confidence": 0.0-1.0}`
|
||||
- Validate returned role exists in DB role list
|
||||
- Return `None` on empty text, empty role list, or any exception
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8_
|
||||
|
||||
- [x] 3.3 Write property test for infer_primary_role output validity
|
||||
- **Property 3: infer_primary_role Ausgabe-Validität**
|
||||
- **Validates: Requirements 3.1, 3.4**
|
||||
|
||||
- [x] 3.4 Write property test for graceful degradation on LLM failure
|
||||
- **Property 4: Graceful Degradation bei LLM-Fehler**
|
||||
- **Validates: Requirements 3.5, 5.6**
|
||||
|
||||
- [x] 4. Checkpoint - Ensure all tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [-] 5. Update Matcher to unconditionally build BM25 index
|
||||
- [x] 5.1 Remove `use_bm25_search` conditional in `Matcher.match`
|
||||
- Remove `if self._sim.use_bm25_search` guard around BM25 index construction
|
||||
- Always build global BM25 index when `filtered` is non-empty
|
||||
- _Requirements: 2.6_
|
||||
|
||||
- [x] 5.2 Write property test for unconditional BM25 index building
|
||||
- **Property 2: Matcher baut BM25-Index bedingungslos**
|
||||
- **Validates: Requirements 2.6**
|
||||
|
||||
- [x] 6. Update MCP Server to remove embedding preload and wire new components
|
||||
- [x] 6.1 Remove embedding preload infrastructure from `mcp_server.py`
|
||||
- Remove `_startup_preload_embeddings` function
|
||||
- Remove `_deferred_preload` function and `_preload_started` flag
|
||||
- Remove `_ensure_preloaded` function
|
||||
- Remove all `await _ensure_preloaded()` calls in tool handlers
|
||||
- Remove `EmbeddingCache` import and instantiation
|
||||
- _Requirements: 4.1, 4.2, 4.4, 4.5, 6.12_
|
||||
|
||||
- [x] 6.2 Update component wiring in `build_server`
|
||||
- Construct `AzureOpenAIClient` with new simplified signature (no embedding params)
|
||||
- Construct `SimilarityEngine` with new signature (client, cost_tracker, use_auto_tagging, auto_tagger)
|
||||
- Construct `VocabularyCache` with new signature (db, client)
|
||||
- _Requirements: 6.7_
|
||||
|
||||
- [x] 6.3 Update `infer_primary_role` tool handler
|
||||
- Remove embedding generation step
|
||||
- Call `vocab_cache.infer_primary_role(task_text=text)` directly with task text
|
||||
- _Requirements: 3.9_
|
||||
|
||||
- [x] 6.4 Remove or simplify `validate_task_requirements` tool if it depends on embeddings
|
||||
- Remove embedding-based `infer_competences` usage
|
||||
- Either remove the tool entirely or replace with a DB-field-only version
|
||||
- _Requirements: 6.5_
|
||||
|
||||
- [x] 7. Simplify config.py and config.toml
|
||||
- [x] 7.1 Remove embedding-related dataclasses and fields from `config.py`
|
||||
- Delete `EmbeddingCacheConfig` dataclass
|
||||
- Delete `InferenceConfig` dataclass
|
||||
- Remove from `AzureOpenAIConfig`: `embedding_deployment`, `embedding_batch_size`
|
||||
- Remove from `SimilarityConfig`: `embedding_model`, `embedding_dimensions`, `strategy`, `use_bm25_search`
|
||||
- Remove `inference` field from `MatchingConfig`
|
||||
- Remove `embedding_cache` field from `AppConfig`
|
||||
- _Requirements: 2.1, 2.2, 6.8, 6.9, 6.10_
|
||||
|
||||
- [x] 7.2 Update `load_config` / `_parse_azure_openai` validation
|
||||
- Remove `AZURE_OPENAI_EMBEDDING_API_KEY` environment variable check
|
||||
- Make `AZURE_OPENAI_LLM_API_KEY` always required
|
||||
- Make `chat_deployment` required
|
||||
- Remove `embedding_dimensions == 3072` validation
|
||||
- Remove `strategy` validation
|
||||
- Remove `[matching.inference]` parsing
|
||||
- Remove `[embedding_cache]` parsing
|
||||
- _Requirements: 2.3, 2.4_
|
||||
|
||||
- [x] 7.3 Update `config.toml` and `config.toml.example`
|
||||
- Remove `[embedding_cache]` section
|
||||
- Remove from `[matching.similarity]`: `embedding_model`, `embedding_dimensions`, `strategy`, `use_bm25_search`
|
||||
- Remove from `[azure_openai]`: `embedding_deployment`, `embedding_batch_size`
|
||||
- Remove `[matching.inference]` section
|
||||
- _Requirements: 6.11_
|
||||
|
||||
- [x] 8. Delete EmbeddingCache module and related tests
|
||||
- [x] 8.1 Delete `src/teamlandkarte_mcp/cache/embedding_cache.py`
|
||||
- _Requirements: 6.6_
|
||||
|
||||
- [x] 8.2 Remove or update tests that reference embedding functionality
|
||||
- Delete tests for `EmbeddingCache`
|
||||
- Update tests for `SimilarityEngine` to use new constructor
|
||||
- Update tests for `VocabularyCache` to use new constructor
|
||||
- Update tests for `AzureOpenAIClient` to use new constructor
|
||||
- Remove any test fixtures that create embedding mocks
|
||||
- _Requirements: 6.6_
|
||||
|
||||
- [x] 9. Final checkpoint - Ensure all tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- Each task references specific requirements for traceability
|
||||
- Checkpoints ensure incremental validation
|
||||
- Property tests validate universal correctness properties from the design document
|
||||
- Order ensures no broken intermediate states: new LLM methods added before old embedding paths removed
|
||||
@@ -0,0 +1 @@
|
||||
{"specId": "1a31d9bb-f189-4374-95b4-1bd117d009b6", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,222 @@
|
||||
# Design Document: Task Name Column
|
||||
|
||||
## Overview
|
||||
|
||||
This feature adds the `name__c` database column to the Task model and all task-related queries, enabling users to see and look up tasks by their short human-readable name. The changes span four layers:
|
||||
|
||||
1. **Model** – Add an optional `name` field to the `Task` dataclass.
|
||||
2. **Database** – Modify SQL queries in `TrinoClient` to SELECT `name__c`; add a new `get_task_by_name` method.
|
||||
3. **Protocol** – Extend the `DBClient` protocol with `get_task_by_name`.
|
||||
4. **MCP Tools** – Include the name in list/detail output; resolve name-based lookups in `get_task_details`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[MCP Tool: list_open_tasks] -->|calls| B[TrinoClient.get_open_tasks]
|
||||
C[MCP Tool: get_task_details] -->|calls| D[TrinoClient.get_task_by_id]
|
||||
C -->|calls| E[TrinoClient.get_task_by_name]
|
||||
B --> F[(Trino DB: name__c)]
|
||||
D --> F
|
||||
E --> F
|
||||
B --> G[Task dataclass with name field]
|
||||
D --> G
|
||||
E --> G
|
||||
```
|
||||
|
||||
The identifier resolution logic in `get_task_details` will attempt `get_task_by_id` first. If that returns `None`, it falls back to `get_task_by_name`. This keeps backward compatibility for callers passing IDs while enabling name-based lookup without a separate tool.
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. Task Dataclass (`models.py`)
|
||||
|
||||
Add a single field:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
id: str
|
||||
name: Optional[str] # NEW – maps to name__c
|
||||
title: str
|
||||
description: str
|
||||
start_date: Optional[date]
|
||||
end_date: Optional[date]
|
||||
created_date: datetime
|
||||
skills: list[str] = field(default_factory=list)
|
||||
```
|
||||
|
||||
The field is `Optional[str]` because the database column can be NULL.
|
||||
|
||||
### 2. DBClient Protocol (`database/types.py`)
|
||||
|
||||
Add one method:
|
||||
|
||||
```python
|
||||
def get_task_by_name(self, name: str) -> Task | None:
|
||||
"""Fetch one published task by its short name (name__c)."""
|
||||
raise NotImplementedError
|
||||
```
|
||||
|
||||
### 3. TrinoClient (`database/trino_client.py`)
|
||||
|
||||
**SQL changes** – Every SELECT that builds a `Task` must include `t.name__c`. Affected methods:
|
||||
- `get_open_tasks` – add `t.name__c` to both the unlimited and CTE-limited queries.
|
||||
- `get_task_by_id` – add `t.name__c` to the SELECT.
|
||||
|
||||
**New method** – `get_task_by_name`:
|
||||
|
||||
```python
|
||||
def get_task_by_name(self, name: str) -> Optional[Task]:
|
||||
"""Fetch a single published task by name__c (case-sensitive)."""
|
||||
query = """
|
||||
SELECT
|
||||
t.id,
|
||||
t.name__c,
|
||||
t.title__c,
|
||||
t.description__c,
|
||||
t.startdate__c,
|
||||
t.enddate__c,
|
||||
t.createddate,
|
||||
s.skillname__c
|
||||
FROM beschaffungstool_kmp_task_latest t
|
||||
LEFT JOIN beschaffungstool_kmp_skill_latest s
|
||||
ON t.id = s.task__c
|
||||
WHERE t.name__c = ? AND t.status__c = ?
|
||||
"""
|
||||
# Same row-assembly logic as get_task_by_id
|
||||
```
|
||||
|
||||
### 4. MCP Server (`mcp_server.py`)
|
||||
|
||||
**`list_open_tasks`** – Add a "Name" column to the output table (between task_id and Title).
|
||||
|
||||
**`get_task_details`** – Change the resolution logic:
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def get_task_details(task_id: str) -> str:
|
||||
# 1. Try by ID
|
||||
task = db_client.get_task_by_id(task_id)
|
||||
# 2. Fallback: try by name
|
||||
if task is None:
|
||||
task = db_client.get_task_by_name(task_id)
|
||||
# 3. Not found
|
||||
if task is None:
|
||||
return f"Task not found or not published: {task_id}"
|
||||
# ... rest unchanged, but include task.name in the summary table
|
||||
```
|
||||
|
||||
Add "Name" to the summary table header and row.
|
||||
|
||||
## Data Models
|
||||
|
||||
### Task Table Schema (relevant columns)
|
||||
|
||||
| Column | Type | Nullable | Maps to |
|
||||
|--------|------|----------|---------|
|
||||
| `id` | VARCHAR | No | `Task.id` |
|
||||
| `name__c` | VARCHAR | Yes | `Task.name` |
|
||||
| `title__c` | VARCHAR | Yes | `Task.title` |
|
||||
| `description__c` | VARCHAR | Yes | `Task.description` |
|
||||
| `startdate__c` | DATE | Yes | `Task.start_date` |
|
||||
| `enddate__c` | DATE | Yes | `Task.end_date` |
|
||||
| `createddate` | TIMESTAMP | No | `Task.created_date` |
|
||||
|
||||
### Task Dataclass (after change)
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
id: str
|
||||
name: Optional[str]
|
||||
title: str
|
||||
description: str
|
||||
start_date: Optional[date]
|
||||
end_date: Optional[date]
|
||||
created_date: datetime
|
||||
skills: list[str] = field(default_factory=list)
|
||||
```
|
||||
|
||||
|
||||
## 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: Task name field preserves database value
|
||||
|
||||
*For any* database row with a `name__c` value (including NULL), when the row is mapped to a `Task` object, `Task.name` must equal the original `name__c` value (with NULL mapped to `None`).
|
||||
|
||||
**Validates: Requirements 1.1, 1.2, 1.3, 1.4**
|
||||
|
||||
### Property 2: Task output contains name
|
||||
|
||||
*For any* `Task` with a non-None `name` field, the formatted output string (from both `list_open_tasks` and `get_task_details`) must contain that name value as a substring.
|
||||
|
||||
**Validates: Requirements 2.1, 2.2**
|
||||
|
||||
### Property 3: Name lookup returns correct task
|
||||
|
||||
*For any* task that exists in the database with a given `name__c` value, calling `get_task_by_name` with that exact name must return a `Task` whose `name` field equals the queried name.
|
||||
|
||||
**Validates: Requirements 3.2**
|
||||
|
||||
### Property 4: Name resolution equivalence
|
||||
|
||||
*For any* published task with a non-None name, calling `get_task_details` with the task's name must produce the same task data as calling `get_task_details` with the task's ID.
|
||||
|
||||
**Validates: Requirements 3.4**
|
||||
|
||||
### Property 5: Unknown identifier returns not-found
|
||||
|
||||
*For any* string that is neither a valid published task ID nor a valid published task name, `get_task_details` must return a not-found message.
|
||||
|
||||
**Validates: Requirements 3.3, 3.5**
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| `name__c` is NULL in DB | `Task.name` is set to `None`; output displays empty or omits the name cell |
|
||||
| `get_task_by_name` receives empty string | Returns `None` (no match) |
|
||||
| `get_task_details` identifier matches neither ID nor name | Returns `"Task not found or not published: {identifier}"` |
|
||||
| Multiple tasks share the same `name__c` (data quality issue) | `get_task_by_name` returns the first match (ORDER BY createddate DESC) |
|
||||
|
||||
The resolution order in `get_task_details` (ID first, then name) ensures that if a name happens to look like an ID, the ID lookup takes precedence. This avoids ambiguity.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Verify `Task` dataclass construction with `name=None` and `name="ABC-123"`.
|
||||
- Verify `list_open_tasks` output table includes a "Name" column.
|
||||
- Verify `get_task_details` output table includes the name value.
|
||||
- Verify `get_task_details` falls back to `get_task_by_name` when ID lookup returns None.
|
||||
- Verify `get_task_by_name` returns None for empty string input.
|
||||
|
||||
### Property-Based Tests
|
||||
|
||||
Use **Hypothesis** (Python PBT library) with a minimum of 100 iterations per property.
|
||||
|
||||
Each test must be tagged with a comment referencing the design property:
|
||||
|
||||
```python
|
||||
# Feature: task-name-column, Property 1: Task name field preserves database value
|
||||
```
|
||||
|
||||
**Property 1** – Generate random `(name__c_value | None)` values, construct Task objects via the row-mapping helper, assert `task.name == input_value`.
|
||||
|
||||
**Property 2** – Generate random Task objects with non-None names (using `st.text(min_size=1)`), format them through the output functions, assert the name string appears in the output.
|
||||
|
||||
**Property 3** – Generate a random set of tasks with unique names, mock the DB cursor to return those rows, call `get_task_by_name(name)`, assert the returned task's name matches.
|
||||
|
||||
**Property 4** – Generate a task with a non-None name, mock both `get_task_by_id` and `get_task_by_name` to return the same task, call `get_task_details` with the name and with the ID, assert both outputs are identical.
|
||||
|
||||
**Property 5** – Generate random strings, filter out any that match known task IDs or names in the mock data, call `get_task_details`, assert the output contains "not found".
|
||||
|
||||
### Test Configuration
|
||||
|
||||
```python
|
||||
from hypothesis import given, settings, strategies as st
|
||||
|
||||
@settings(max_examples=100)
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
The Teamlandkarte MCP server currently fetches tasks from the database without including the `name` column (a short identifier). Users need to reference tasks by this short name in addition to the full ID or title. This feature adds the `name` column to all task queries and enables task lookup by name.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **MCP_Server**: The Teamlandkarte MCP server that exposes tools for querying tasks and capacities.
|
||||
- **Task**: A published task record stored in the `beschaffungstool_kmp_task_latest` database table.
|
||||
- **Task_Name**: The `name__c` column in the task table, a short human-readable identifier for a task.
|
||||
- **DBClient**: The database client protocol that defines methods for fetching tasks and capacities.
|
||||
- **TrinoClient**: The concrete database client implementation that queries the Trino/Presto backend.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Include Task Name in Task Model
|
||||
|
||||
**User Story:** As a user, I want the task name (short ID) to be part of the task data, so that I can see and use it when browsing tasks.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Task model SHALL include a `name` field of type optional string.
|
||||
2. WHEN the TrinoClient fetches open tasks, THE TrinoClient SHALL select the `name__c` column from the task table and populate the Task `name` field.
|
||||
3. WHEN the TrinoClient fetches a task by ID, THE TrinoClient SHALL select the `name__c` column from the task table and populate the Task `name` field.
|
||||
4. WHEN the task name column value is NULL in the database, THE Task `name` field SHALL be set to None.
|
||||
|
||||
### Requirement 2: Display Task Name in Output
|
||||
|
||||
**User Story:** As a user, I want to see the task name in task listings and detail views, so that I can reference tasks by their short name.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the MCP_Server returns a list of open tasks, THE MCP_Server SHALL include the task name in the output for each task.
|
||||
2. WHEN the MCP_Server returns task details, THE MCP_Server SHALL include the task name in the summary table.
|
||||
|
||||
### Requirement 3: Look Up Task by Name
|
||||
|
||||
**User Story:** As a user, I want to retrieve task details by providing only the task name, so that I do not need to remember the full ID.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE DBClient protocol SHALL define a `get_task_by_name` method that accepts a task name string and returns a Task or None.
|
||||
2. WHEN a valid published task name is provided, THE TrinoClient SHALL return the matching Task with all fields populated.
|
||||
3. WHEN a task name that does not match any published task is provided, THE TrinoClient SHALL return None.
|
||||
4. WHEN the user provides a task name to the get_task_details tool, THE MCP_Server SHALL resolve the task using the name and return the full task details.
|
||||
5. IF the provided identifier matches neither a task ID nor a task name, THEN THE MCP_Server SHALL return a "not found" message.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Implementation Plan: Task Name Column
|
||||
|
||||
## Overview
|
||||
|
||||
Add the `name__c` database column to the Task model and all task queries, enabling users to see and look up tasks by their short human-readable name. Changes span model, database layer, protocol, and MCP tool output.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Add name field to Task model and update TrinoClient queries
|
||||
- [x] 1.1 Add `name: Optional[str]` field to the Task dataclass in `models.py`
|
||||
- Insert `name: Optional[str]` after `id` field
|
||||
- Field must be Optional since `name__c` can be NULL in the database
|
||||
- _Requirements: 1.1, 1.4_
|
||||
|
||||
- [x] 1.2 Update `get_open_tasks` SQL queries in `trino_client.py` to SELECT `t.name__c`
|
||||
- Add `t.name__c` to the unlimited query SELECT clause
|
||||
- Add `t.name__c` to the CTE inner SELECT and outer SELECT in the limited query
|
||||
- Update row unpacking to extract the name value
|
||||
- Populate `name` field in Task construction (map NULL to None)
|
||||
- _Requirements: 1.2, 1.4_
|
||||
|
||||
- [x] 1.3 Update `get_task_by_id` SQL query in `trino_client.py` to SELECT `t.name__c`
|
||||
- Add `t.name__c` to the SELECT clause
|
||||
- Update row unpacking to extract the name value
|
||||
- Populate `name` field in Task construction (map NULL to None)
|
||||
- _Requirements: 1.3, 1.4_
|
||||
|
||||
- [ ]* 1.4 Write property test: Task name field preserves database value
|
||||
- **Property 1: Task name field preserves database value**
|
||||
- Generate random `(name__c_value | None)` values using Hypothesis
|
||||
- Construct Task objects via the row-mapping pattern, assert `task.name == input_value`
|
||||
- **Validates: Requirements 1.1, 1.2, 1.3, 1.4**
|
||||
|
||||
- [x] 2. Add `get_task_by_name` to protocol and TrinoClient
|
||||
- [x] 2.1 Add `get_task_by_name` method to `DBClient` protocol in `database/types.py`
|
||||
- Method signature: `def get_task_by_name(self, name: str) -> Task | None`
|
||||
- Include docstring: "Fetch one published task by its short name (name__c)."
|
||||
- _Requirements: 3.1_
|
||||
|
||||
- [x] 2.2 Implement `get_task_by_name` in `TrinoClient`
|
||||
- SQL query selects same columns as `get_task_by_id` plus `t.name__c`
|
||||
- WHERE clause filters on `t.name__c = ?` and `t.status__c = ?`
|
||||
- ORDER BY `t.createddate DESC` to handle duplicates (return first match)
|
||||
- Reuse same row-assembly logic as `get_task_by_id`
|
||||
- Return None for empty string input or no matching rows
|
||||
- _Requirements: 3.1, 3.2, 3.3_
|
||||
|
||||
- [ ]* 2.3 Write property test: Name lookup returns correct task
|
||||
- **Property 3: Name lookup returns correct task**
|
||||
- Generate random tasks with unique names, mock DB cursor
|
||||
- Call `get_task_by_name(name)`, assert returned task's name matches
|
||||
- **Validates: Requirements 3.2**
|
||||
|
||||
- [x] 3. Checkpoint
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 4. Update MCP tool output formatting
|
||||
- [x] 4.1 Add "Name" column to `list_open_tasks` output table
|
||||
- Add "Name" to the header list (between task_id and Title)
|
||||
- Add `str(task.name or "")` to each row
|
||||
- Update the empty-row fallback to include the extra column
|
||||
- _Requirements: 2.1_
|
||||
|
||||
- [x] 4.2 Add "Name" to `get_task_details` summary table
|
||||
- Add "Name" to the header list
|
||||
- Add `str(task.name or "")` to the row data
|
||||
- _Requirements: 2.2_
|
||||
|
||||
- [ ]* 4.3 Write property test: Task output contains name
|
||||
- **Property 2: Task output contains name**
|
||||
- Generate random Task objects with non-None names using `st.text(min_size=1)`
|
||||
- Format through output functions, assert name appears as substring
|
||||
- **Validates: Requirements 2.1, 2.2**
|
||||
|
||||
- [x] 5. Update `get_task_details` resolution logic
|
||||
- [x] 5.1 Add name-based fallback resolution in `get_task_details` tool
|
||||
- After `get_task_by_id` returns None, call `db_client.get_task_by_name(task_id)`
|
||||
- If both return None, return "Task not found or not published: {task_id}"
|
||||
- _Requirements: 3.4, 3.5_
|
||||
|
||||
- [ ]* 5.2 Write property test: Name resolution equivalence
|
||||
- **Property 4: Name resolution equivalence**
|
||||
- Generate a task with non-None name, mock both lookup methods
|
||||
- Call `get_task_details` with name and with ID, assert outputs identical
|
||||
- **Validates: Requirements 3.4**
|
||||
|
||||
- [ ]* 5.3 Write property test: Unknown identifier returns not-found
|
||||
- **Property 5: Unknown identifier returns not-found**
|
||||
- Generate random strings not matching any known task ID or name
|
||||
- Call `get_task_details`, assert output contains "not found"
|
||||
- **Validates: Requirements 3.3, 3.5**
|
||||
|
||||
- [x] 6. Update FakeDBClient in tests and add `get_task_by_name` stub
|
||||
- Add `name` field to `FakeTask` in existing test files
|
||||
- Add `get_task_by_name` method to `FakeDBClient`
|
||||
- Update existing test assertions that check table headers (add "Name" column)
|
||||
- _Requirements: 3.1_
|
||||
|
||||
- [x] 7. Final checkpoint
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- Each task references specific requirements for traceability
|
||||
- Property tests use Hypothesis with `@settings(max_examples=100)`
|
||||
- The implementation language is Python (matching the existing codebase)
|
||||
@@ -0,0 +1 @@
|
||||
{"specId": "2977bff5-33df-4acc-a529-5b047c431436", "workflowType": "requirements-first", "specType": "feature"}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,231 @@
|
||||
# Anforderungsdokument
|
||||
|
||||
## Einleitung
|
||||
|
||||
Dieses Dokument beschreibt die Anforderungen für ein neues Feature der Teamlandkarte: das **Matching gegen Team-Profile** als zusätzliche Profilart neben den bestehenden Kapazitätsprofilen. Bisher vergleicht das System eine offene Aufgabe ausschließlich mit Kapazitätsprofilen einzelner Mitarbeitender (`teamlandkarte_v_capacities_latest` und zugehörige Tabellen). Künftig soll der Nutzer pro Suchanfrage wählen können, ob eine Aufgabe gegen **Kapazitätsprofile** (bisheriges Verhalten) oder gegen **Team-Profile** (neue Profilart) gematcht wird.
|
||||
|
||||
Ein Team-Profil aggregiert Daten aus mehreren Datenbank-Views: Stammdaten und Beschreibung aus `teamlandkarte_v_teams_latest` (Spalten `about_us`, `offerings`, `interests`, `focus_name`), den Teamnamen über INNER JOIN mit `teamlandkarte_v_teammeter_organizational_units_latest` (Join `team_id = id`), Team-Kompetenzen aus `teamlandkarte_v_teammeter_team_competences_latest` (Join über `ouid`, mit Top-Kompetenz-Markierung über `top_competency`) sowie Team-Referenzen aus `teamlandkarte_v_team_references_latest` (Join über `ouid`, Partner-Auflösung über `teamlandkarte_v_partners_latest.id` via `partner_id`, plus Projektbeschreibung in `projects`).
|
||||
|
||||
Das neue Feature soll die bestehenden Matching-Verfahren (`score` und `llm_fulltext`) wiederverwenden, sodass der Nutzer beide Verfahren auch auf Team-Profile anwenden kann. MCP-Tools, Agenten-Konfigurationen, Architekturdokumentation und README werden entsprechend erweitert.
|
||||
|
||||
|
||||
## 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**: Bestehendes Modul für den LLM-basierten Volltext-Vergleich (`matching/llm_fulltext_matcher.py`).
|
||||
- **AzureOpenAIClient**: Wrapper für Azure-OpenAI-Aufrufe in `azure/openai_client.py`.
|
||||
- **LLM**: Large Language Model (Azure OpenAI Chat Completion).
|
||||
- **Capacity**: Frozen Dataclass `Capacity` in `models.py` (Kapazitätseintrag eines Mitarbeitenden).
|
||||
- **Team**: Neue Frozen Dataclass, die ein Team mit aggregierten Profilfeldern (Name, `about_us`, `offerings`, `interests`, `focus_name`, Kompetenzen, Referenzen) repräsentiert.
|
||||
- **Team_Id**: Wert der Spalte `team_id` in `teamlandkarte_v_teams_latest` bzw. der Spalte `id` in `teamlandkarte_v_teammeter_organizational_units_latest`. Eindeutiger fachlicher Identifikator eines Teams.
|
||||
- **Ouid**: Wert der Spalte `ouid`, die in `teamlandkarte_v_teams_latest`, `teamlandkarte_v_teammeter_team_competences_latest` und `teamlandkarte_v_team_references_latest` vorkommt und als Join-Schlüssel zwischen Stammdaten, Kompetenzen und Referenzen eines Teams dient.
|
||||
- **Team_Name**: Wert der Spalte mit dem Teamnamen aus `teamlandkarte_v_teammeter_organizational_units_latest`, ermittelt über INNER JOIN mit `teamlandkarte_v_teams_latest` (`team_id = id`).
|
||||
- **Team_About_Us**: Wert der Spalte `about_us` aus `teamlandkarte_v_teams_latest` (Beschreibung des Teams).
|
||||
- **Team_Offerings**: Wert der Spalte `offerings` aus `teamlandkarte_v_teams_latest` (Leistungen des Teams).
|
||||
- **Team_Interests**: Wert der Spalte `interests` aus `teamlandkarte_v_teams_latest` (Interessen des Teams).
|
||||
- **Team_Focus_Name**: Wert der Spalte `focus_name` aus `teamlandkarte_v_teams_latest` (Schwerpunkt des Teams).
|
||||
- **Team_Competence**: Eintrag aus `teamlandkarte_v_teammeter_team_competences_latest` mit den Feldern `competence_id` (Fremdschlüssel auf den Kompetenz-Namen analog zu Kapazitätskompetenzen) und `top_competency` (Boolean). Ein Team kann mehrere Team_Competence-Einträge haben (1:n über `ouid`).
|
||||
- **Top_Competency**: Boolean-Spalte `top_competency` aus `teamlandkarte_v_teammeter_team_competences_latest`, die kennzeichnet, ob eine Team_Competence eine Top-Kompetenz des Teams ist.
|
||||
- **Team_Reference**: Eintrag aus `teamlandkarte_v_team_references_latest` mit den Feldern `partner_id` (Fremdschlüssel auf `teamlandkarte_v_partners_latest.id`) und `projects` (Projektbeschreibung). Ein Team kann mehrere Team_Reference-Einträge haben (1:n über `ouid`).
|
||||
- **Partner**: Eintrag aus `teamlandkarte_v_partners_latest`. Eine Team_Reference verweist über die Spalte `partner_id` auf einen Partner; die Verknüpfung erfolgt über `teamlandkarte_v_team_references_latest.partner_id = teamlandkarte_v_partners_latest.id`.
|
||||
- **Partner_Name**: Wert der Spalte `name` aus `teamlandkarte_v_partners_latest`, der einer Team_Reference über `partner_id` zugeordnet ist. Ist `partner_id` `NULL` oder existiert kein passender Partner, gilt der Partner_Name als leere Zeichenkette.
|
||||
- **Team_Profile**: Aggregiertes Volltext-Profil eines Teams, bestehend aus `Team_Id`, `Team_Name`, `Team_Focus_Name`, `Team_About_Us`, `Team_Offerings`, `Team_Interests`, einer Liste von Kompetenznamen mit Top-Markierung sowie einer Liste von Referenzeinträgen (Partner_Name + Projektbeschreibung).
|
||||
- **Capacity_Profile**: Bestehendes aggregiertes Volltext-Profil einer Kapazität (Rolle, Kompetenzen, Beschreibung, Referenzen, Zertifikate).
|
||||
- **Task_Profile**: Bestehendes aggregiertes Volltext-Profil einer Aufgabe (Titel, Beschreibung, gesuchte Kompetenzen).
|
||||
- **Profile_Type**: Auswahlwert für die zu matchende Profilart. Erlaubte Werte: `capacity` (Kapazitätsprofile, bisheriges Verhalten) und `team` (Team-Profile, neu).
|
||||
- **Matching_Method**: Bestehender Auswahlwert für das Verfahren. Erlaubte Werte: `score` und `llm_fulltext`.
|
||||
- **Kategorie**: Eine der Ergebniskategorien `Top`, `Good`, `Partial`, `Low`, `Irrelevant`.
|
||||
- **Rationale**: Vom LLM erzeugte Kurzbegründung (1–2 Sätze) für die zugewiesene Kategorie (nur im `llm_fulltext`-Modus).
|
||||
- **find_matching_capacities**: MCP-Tool für die Suchrichtung Aufgabe→Kapazität (bestehend).
|
||||
- **find_matching_teams**: Neues MCP-Tool für die Suchrichtung Aufgabe→Team.
|
||||
- **SearchCache**: Bestehende Cache-Komponente für persistierte Suchergebnisse.
|
||||
- **Teamlandkarte_Agent**: GitHub-Copilot-Agent in `.github/agents/teamlandkarte_agent.md` und Kiro-Pendant in `.kiro/agents/teamlandkarte.md`.
|
||||
- **Architecture_Doc**: `docs/architecture.md`.
|
||||
- **Readme**: `README.md` im Repository-Root.
|
||||
|
||||
|
||||
## Anforderungen
|
||||
|
||||
### Anforderung 1: Auswahl des Profil-Typs für das Matching
|
||||
|
||||
**User Story:** Als Nutzer möchte ich beim Matching für eine offene Aufgabe entscheiden können, ob ich gegen Kapazitätsprofile oder gegen Team-Profile matche, damit ich je nach Fragestellung Personen oder Teams als Vorschläge erhalte.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL einen Profile_Type mit den erlaubten Werten `capacity` und `team` definieren.
|
||||
2. WHEN der Nutzer eine Aufgabe gegen Kapazitätsprofile matchen möchte, THE MCP_Server SHALL das bestehende Tool `find_matching_capacities` mit unverändertem Verhalten bereitstellen (Profile_Type implizit `capacity`).
|
||||
3. WHEN der Nutzer eine Aufgabe gegen Team-Profile matchen möchte, THE MCP_Server SHALL ein neues Tool `find_matching_teams` bereitstellen, das Profile_Type `team` realisiert.
|
||||
4. THE MCP_Server SHALL den verwendeten Profile_Type (`capacity` oder `team`) im Antwort-`META`-JSON sowie in der angezeigten Suchkonfiguration ausweisen.
|
||||
5. THE MCP_Server SHALL den Parameter `matching_method` (`score` oder `llm_fulltext`) auch im Tool `find_matching_teams` akzeptieren und mit den gleichen Default- und Validierungsregeln behandeln wie in `find_matching_capacities`.
|
||||
6. IF der Nutzer in `find_matching_teams` einen ungültigen Wert für `matching_method` übergibt, THEN THE MCP_Server SHALL eine Fehlermeldung zurückgeben, die die erlaubten Werte (`score`, `llm_fulltext`) auflistet, und die Suche nicht ausführen.
|
||||
|
||||
### Anforderung 2: Datenabfrage für Team-Stammdaten und Teamname
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System Team-Stammdaten inklusive Teamnamen aus der Datenbank lädt, damit Team-Profile vollständig aufgebaut werden können.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE DBClient SHALL eine Methode bereitstellen, die alle aktiven Teams aus `teamlandkarte_v_teams_latest` zurückgibt, inklusive der Spalten `team_id`, `ouid`, `about_us`, `offerings`, `interests` und `focus_name`.
|
||||
2. THE DBClient SHALL den Team_Name pro Team über INNER JOIN von `teamlandkarte_v_teams_latest` mit `teamlandkarte_v_teammeter_organizational_units_latest` über die Bedingung `teamlandkarte_v_teams_latest.team_id = teamlandkarte_v_teammeter_organizational_units_latest.id` ermitteln.
|
||||
3. WHEN für ein Team kein passender Eintrag in `teamlandkarte_v_teammeter_organizational_units_latest` existiert, THE DBClient SHALL dieses Team durch den INNER JOIN aus dem Ergebnis ausschließen.
|
||||
4. WHEN eine der Spalten `about_us`, `offerings`, `interests` oder `focus_name` `NULL` oder leer ist, THE DBClient SHALL für das jeweilige Feld eine leere Zeichenkette zurückgeben.
|
||||
5. THE DBClient SHALL eine Methode bereitstellen, die ein einzelnes Team anhand seiner Team_Id (oder Ouid) zurückgibt, einschließlich Team_Name über denselben INNER JOIN.
|
||||
6. THE TrinoClient SHALL alle neuen SQL-Abfragen ausschließlich als `SELECT`-Statements ausführen und die bestehende Read-Only-Guard `_ensure_select_only` verwenden.
|
||||
7. THE TrinoClient SHALL die neuen Abfragen über die bestehende Connection-Pool-Infrastruktur und die Retry-Logik (`_retry`) ausführen.
|
||||
|
||||
|
||||
### Anforderung 3: Datenabfrage für Team-Kompetenzen
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System die Kompetenzen eines Teams inklusive Top-Kompetenz-Markierung und aufgelöstem Kompetenz-Namen lädt, damit Team-Profile die fachlichen Fähigkeiten korrekt abbilden.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE DBClient SHALL eine Methode bereitstellen, die für eine gegebene Ouid alle zugeordneten Team_Competence-Einträge aus `teamlandkarte_v_teammeter_team_competences_latest` zurückgibt, inklusive der Felder `competence_id` und `top_competency`.
|
||||
2. THE DBClient SHALL den Kompetenz-Namen pro Team_Competence durch denselben Join-Mechanismus ermitteln, der bereits für Kapazitäts-Kompetenzen verwendet wird, sodass aus der `competence_id` der lesbare Kompetenz-Name abgeleitet wird.
|
||||
3. THE DBClient SHALL eine Batch-Variante bereitstellen, die für eine Liste von Ouid-Werten alle zugehörigen Team_Competence-Einträge inklusive aufgelöster Kompetenz-Namen in höchstens einer SQL-Abfrage lädt.
|
||||
4. WHEN ein Team keine Team_Competence-Einträge besitzt, THE DBClient SHALL eine leere Liste für dieses Team zurückgeben.
|
||||
5. WHEN `top_competency` für einen Team_Competence-Eintrag `NULL` ist, THE DBClient SHALL den Wert als `false` interpretieren.
|
||||
6. WHEN die `competence_id` eines Team_Competence-Eintrags zu keinem Kompetenz-Namen aufgelöst werden kann, THE DBClient SHALL diesen Eintrag aus dem Ergebnis ausschließen.
|
||||
7. THE DBClient SHALL die Reihenfolge der Team_Competence-Einträge pro Team deterministisch zurückgeben (primär: Top-Kompetenzen vor Nicht-Top-Kompetenzen, sekundär: Kompetenz-Name aufsteigend).
|
||||
|
||||
### Anforderung 4: Datenabfrage für Team-Referenzen
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System die Referenzen eines Teams inklusive Partner-Namen und Projektbeschreibung lädt, damit Team-Profile bisherige Projekte und Auftraggeber abbilden.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE DBClient SHALL eine Methode bereitstellen, die für eine gegebene Ouid alle zugeordneten Team_Reference-Einträge aus `teamlandkarte_v_team_references_latest` zurückgibt, einschließlich der Spalte `projects` und des über `partner_id` aufgelösten Partner_Name.
|
||||
2. THE DBClient SHALL den Partner_Name pro Team_Reference über LEFT JOIN auf `teamlandkarte_v_partners_latest` mit der Bedingung `teamlandkarte_v_team_references_latest.partner_id = teamlandkarte_v_partners_latest.id` ermitteln und die Spalte `name` als Partner_Name übernehmen.
|
||||
3. THE DBClient SHALL eine Batch-Variante bereitstellen, die für eine Liste von Ouid-Werten alle zugehörigen Team_Reference-Einträge inklusive aufgelöster Partner_Name in höchstens einer SQL-Abfrage lädt; der Partner-Join SHALL Bestandteil derselben Referenz-Abfrage sein und keine zusätzliche SQL-Abfrage erzeugen.
|
||||
4. WHEN ein Team keine Team_Reference-Einträge besitzt, THE DBClient SHALL eine leere Liste für dieses Team zurückgeben.
|
||||
5. IF die `partner_id` einer Team_Reference `NULL` ist oder der Join auf `teamlandkarte_v_partners_latest` keinen Treffer liefert, THEN THE DBClient SHALL den Partner_Name dieser Team_Reference als leere Zeichenkette zurückgeben und die Referenz dennoch mit dem Feld `projects` in der Ergebnisliste belassen.
|
||||
6. WHEN das Feld `projects` einer Team_Reference `NULL` oder ausschließlich Whitespace ist, THE DBClient SHALL diesen Eintrag aus dem Ergebnis ausschließen.
|
||||
7. THE DBClient SHALL die Reihenfolge der Team_Reference-Einträge pro Team deterministisch zurückgeben (z. B. nach Partner_Name aufsteigend, dann nach Projektbeschreibung aufsteigend).
|
||||
|
||||
|
||||
### Anforderung 5: Aufbau des Team_Profile
|
||||
|
||||
**User Story:** Als Entwickler möchte ich, dass das System aus den Datenbankfeldern ein konsistentes Volltext-Profil pro Team erzeugt, damit beide Matching-Verfahren eine einheitliche Eingabe erhalten.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL pro Team ein Team_Profile bilden, das die folgenden Felder enthält: `team_id`, `ouid`, `team_name`, `focus_name`, `about_us`, `offerings`, `interests`, `competences` (Liste von Einträgen mit Kompetenz-Namen und `top_competency`-Flag) und `references` (Liste von Einträgen mit `partner_name` und `projects`).
|
||||
2. WHEN ein Feld in der Datenbank leer oder `NULL` ist, THE MCP_Server SHALL das entsprechende Feld im Team_Profile mit einer leeren Zeichenkette bzw. einer leeren Liste belegen, ohne das gesamte Profil zu verwerfen.
|
||||
3. WHEN der Partner_Name eines Team_Reference-Eintrags leer ist, THE MCP_Server 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.
|
||||
4. THE MCP_Server SHALL das Team_Profile in einer für das LLM lesbaren, deterministischen Textstruktur serialisieren, in der jedes Feld klar mit einer Überschrift gekennzeichnet ist (z. B. `Teamname:`, `Schwerpunkt:`, `Über uns:`, `Leistungen:`, `Interessen:`, `Kompetenzen:`, `Referenzen:`).
|
||||
5. THE MCP_Server SHALL Top-Kompetenzen in der serialisierten Darstellung erkennbar markieren (z. B. durch ein vorangestelltes Symbol oder das Suffix `(Top)`), sodass das LLM und der Nutzer Top-Kompetenzen von Nicht-Top-Kompetenzen unterscheiden können.
|
||||
6. THE MCP_Server 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).
|
||||
7. THE MCP_Server SHALL die Reihenfolge der Felder in der serialisierten Darstellung über alle Teams konstant halten, sodass die Eingabe für das LLM bzw. den Score-Matcher deterministisch ist.
|
||||
|
||||
### Anforderung 6: Score-basiertes Matching für Team-Profile
|
||||
|
||||
**User Story:** Als Nutzer möchte ich Team-Profile auch im Score-basierten Modus matchen können, damit ich Teams mit denselben numerischen Bewertungen wie Kapazitäten vergleichen kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `find_matching_teams` mit `matching_method = "score"` aufgerufen wird, THE MCP_Server SHALL das Score-basierte Matching auf Team-Profile anwenden und für jedes Team eine Competence Score, eine Role Score und eine Overall Score berechnen.
|
||||
2. THE MCP_Server SHALL die Competence Score eines Teams aus den Team_Competence-Einträgen berechnen, wobei die Liste der Kompetenz-Namen analog zur Liste der Kapazitäts-Kompetenzen verwendet wird.
|
||||
3. THE MCP_Server SHALL die Role Score eines Teams aus dem Team_Focus_Name als Stellvertreter für die Rolle berechnen, da Teams keine Rolle im Sinne einer Kapazität besitzen.
|
||||
4. WHERE Top-Kompetenzen vorhanden sind, THE MCP_Server SHALL Top-Kompetenzen bei der Berechnung der Competence Score höher gewichten als Nicht-Top-Kompetenzen, wobei der Gewichtungsfaktor in der Konfiguration unter dem Schlüssel `matching.team.top_competency_weight` mit Standardwert `1.5` einstellbar ist.
|
||||
5. THE MCP_Server SHALL die Ergebnisse in dieselben Kategorien (`Top`, `Good`, `Partial`, `Low`, `Irrelevant`) einordnen, die auch für Kapazitätsprofile gelten.
|
||||
6. THE MCP_Server SHALL die Ergebnistabelle für `find_matching_teams` im `score`-Modus mit den Spalten `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Role Score`, `Competence Score`, `Overall Score`, `Category` ausgeben.
|
||||
7. THE MCP_Server SHALL die Verfügbarkeit eines Teams nicht prüfen, da Team-Profile keinen Verfügbarkeitszeitraum besitzen; ein etwaig übergebener Zeitraum SHALL ignoriert und in der `Applied Filters`-Tabelle als nicht wirksam markiert werden.
|
||||
|
||||
|
||||
### Anforderung 7: LLM-Volltext-Matching für Team-Profile
|
||||
|
||||
**User Story:** Als Nutzer möchte ich Team-Profile auch im LLM-Volltext-Modus matchen können, damit der Vergleich auf Basis der Beschreibungstexte (about_us, offerings, interests) und Referenzen erfolgt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `find_matching_teams` mit `matching_method = "llm_fulltext"` aufgerufen wird, THE LLM_Fulltext_Matcher SHALL für jedes Team einen LLM-Vergleich zwischen Task_Profile und Team_Profile durchführen.
|
||||
2. THE LLM_Fulltext_Matcher SHALL pro Team genau eine Kategorie aus der Menge `Top`, `Good`, `Partial`, `Low`, `Irrelevant` zurückgeben.
|
||||
3. THE LLM_Fulltext_Matcher SHALL pro Team eine Rationale mit 1 bis 2 Sätzen zurückgeben, die die Zuweisung in die jeweilige Kategorie erläutert.
|
||||
4. THE LLM_Fulltext_Matcher SHALL die LLM-Antwort als strukturiertes JSON pro Team anfordern und parsen (Felder: `category`, `rationale`).
|
||||
5. IF das LLM für ein Team eine Kategorie zurückgibt, die nicht in der erlaubten Menge liegt, THEN THE LLM_Fulltext_Matcher SHALL dieses Team der Kategorie `Irrelevant` zuordnen und die Rationale durch einen Hinweis auf die ungültige LLM-Antwort ergänzen.
|
||||
6. IF der LLM-Aufruf für ein Team fehlschlägt, THEN THE LLM_Fulltext_Matcher SHALL dieses Team in einer separaten Fehlerliste ausweisen und es nicht als reguläres Ergebnis kategorisieren.
|
||||
7. 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 `team_id` aufsteigend).
|
||||
8. WHEN `matching_method = "llm_fulltext"` verwendet wird, THE MCP_Server SHALL die Ergebnistabelle für `find_matching_teams` mit den Spalten `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Category`, `Begründung` ausgeben.
|
||||
|
||||
### Anforderung 8: Persistenz und Pagination der Team-Suche
|
||||
|
||||
**User Story:** Als Nutzer möchte ich auch bei einer Team-Suche durch Kategorien blättern und Filter anwenden können, damit der bestehende Such-Workflow konsistent bleibt.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. WHEN `find_matching_teams` ein Suchergebnis erzeugt, 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. THE MCP_Server SHALL im persistierten Suchergebnis (`SearchCache`) ein Feld `search_type` mit dem Wert `team_search` hinterlegen, um Team-Suchen von Kapazitäts-Suchen (`capacity_search`) zu unterscheiden.
|
||||
3. THE MCP_Server SHALL im `META`-JSON des Suchergebnisses sowohl `search_type = "team_search"` als auch das verwendete `matching_method` ausweisen, damit Folgewerkzeuge das Schema korrekt interpretieren können.
|
||||
4. WHEN `get_results_by_category` ein Team-Suchergebnis paginiert, THE MCP_Server SHALL die Ergebnistabelle mit den für Teams definierten Spalten (`Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, ...) ausgeben.
|
||||
5. WHEN `filter_search_results` ein Team-Suchergebnis filtert, THE MCP_Server SHALL die Filterung auf für Teams sinnvolle Filter beschränken (Schwerpunkt-Filter, Kompetenz-Filter, Top-Kompetenz-Filter).
|
||||
6. IF ein für Teams nicht anwendbarer Filter (z. B. `availability_date_start`, `availability_date_end`, `is_fully_available`) auf ein Team-Suchergebnis angewendet wird, THEN THE MCP_Server SHALL den Filter ignorieren und in der `Applied Filters`-Tabelle einen Hinweis aufnehmen, dass der Filter im Team-Suchmodus nicht wirksam ist.
|
||||
|
||||
### Anforderung 9: Detail- und Listen-Tools für Teams
|
||||
|
||||
**User Story:** Als Nutzer möchte ich einzelne Teams einsehen und eine Liste verfügbarer Teams abrufen können, damit ich Teams unabhängig von einem Matching-Lauf erkunden kann.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL ein Tool `list_teams` bereitstellen, das die ersten `limit` Teams (Default 20) als Markdown-Tabelle mit den Spalten `Team Id`, `Team Name`, `Schwerpunkt`, `Anzahl Kompetenzen`, `Anzahl Referenzen` ausgibt.
|
||||
2. THE MCP_Server SHALL ein Tool `get_team_details` bereitstellen, das anhand einer Team_Id ein einzelnes Team_Profile als Markdown-Tabelle plus Beschreibungstexte (`Über uns`, `Leistungen`, `Interessen`) und Listen (`Kompetenzen` mit Top-Markierung, `Referenzen` mit Partner_Name und Projektbeschreibung) ausgibt.
|
||||
3. IF kein Team mit der angegebenen Team_Id existiert, THEN THE MCP_Server SHALL eine Fehlermeldung zurückgeben, die die ungültige Team_Id nennt.
|
||||
4. THE MCP_Server SHALL die Listen `Kompetenzen` und `Referenzen` in der gleichen deterministischen Reihenfolge ausgeben, die der DBClient liefert (siehe Anforderungen 3.7 und 4.7).
|
||||
|
||||
|
||||
### Anforderung 10: Anpassung der Agenten-Konfigurationen
|
||||
|
||||
**User Story:** Als Nutzer möchte ich, dass sowohl der GitHub-Copilot-Agent `teamlandkarte_agent` als auch der Kiro-Pendant-Agent das neue Matching gegen Team-Profile kennen und mich aktiv nach der gewünschten Profilart 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 Profile_Type-Werte `capacity` und `team` dokumentieren.
|
||||
2. WHEN der Nutzer eine Suche nach passenden Profilen für eine Aufgabe startet, THE Teamlandkarte_Agent SHALL den Nutzer explizit nach dem gewünschten Profile_Type (`capacity` oder `team`) fragen, sofern dieser nicht bereits aus dem Verlauf hervorgeht.
|
||||
3. THE Teamlandkarte_Agent SHALL die Skills/Workflows so erweitern, dass `find_matching_teams` als alternatives Such-Tool zu `find_matching_capacities` verfügbar ist und mit dem Parameter `matching_method` aufgerufen wird.
|
||||
4. THE Teamlandkarte_Agent SHALL den Nutzer darauf hinweisen, dass bei einer Team-Suche keine Verfügbarkeitsfilter wirksam sind und die Ergebnisspalten von einer Kapazitäts-Suche abweichen.
|
||||
5. THE Teamlandkarte_Agent SHALL den bestehenden Bestätigungs-Workflow (`show_pending_requirements`, `confirm_requirements`) beibehalten und für beide Profile_Type-Werte gleich anwenden.
|
||||
6. THE Teamlandkarte_Agent SHALL die neuen Detail- und Listen-Tools (`list_teams`, `get_team_details`) in den Skills/Workflows erwähnen.
|
||||
|
||||
### Anforderung 11: Aktualisierung von Architektur- und README-Dokumentation
|
||||
|
||||
**User Story:** Als Entwickler oder Onboardee möchte ich, dass `architecture.md` und `README.md` das Matching gegen Team-Profile beschreiben, damit ich Architektur und Nutzung des Systems korrekt verstehe.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE Architecture_Doc SHALL einen Abschnitt enthalten, der das Team_Profile als zusätzliche Profilart beschreibt, einschließlich seiner Felder, Datenquellen und der Verknüpfungen zwischen den Views.
|
||||
2. THE Architecture_Doc SHALL die zusätzlichen Datenquellen (`teamlandkarte_v_teams_latest`, `teamlandkarte_v_teammeter_organizational_units_latest`, `teamlandkarte_v_teammeter_team_competences_latest`, `teamlandkarte_v_team_references_latest`) im Datenmodell- und Schema-Verifikationsabschnitt aufführen, einschließlich der relevanten Spalten.
|
||||
3. THE Architecture_Doc SHALL die Verknüpfungen zwischen `teamlandkarte_v_teams_latest.team_id` und `teamlandkarte_v_teammeter_organizational_units_latest.id` (INNER JOIN für Team_Name) sowie über `ouid` zu Kompetenzen und Referenzen dokumentieren.
|
||||
4. THE Architecture_Doc SHALL die Verknüpfung zwischen `teamlandkarte_v_team_references_latest.partner_id` und `teamlandkarte_v_partners_latest.id` sowie die Übernahme der Spalte `name` als Partner_Name in das Team_Profile dokumentieren.
|
||||
5. THE Architecture_Doc SHALL den Profile_Type-Parameter und seine Wertebereiche im Tool-Surface-Abschnitt für die neuen und geänderten Tools dokumentieren.
|
||||
6. THE Architecture_Doc SHALL den Runtime-View für die Suchrichtung Aufgabe→Team in beiden Matching-Methoden (`score` und `llm_fulltext`) ergänzen.
|
||||
7. THE Readme SHALL im Quick-Start- und Usage-Abschnitt erklären, wie der Nutzer zwischen `capacity`- und `team`-Suche wählt.
|
||||
8. THE Readme SHALL die zusätzlichen Datenbank-Views aufführen, die der Server für Team-Profile liest, einschließlich der Join-Bedingungen für Team_Name (über `team_id`/`id`), Kompetenzen und Referenzen (über `ouid`) sowie Partner (über `partner_id`).
|
||||
9. THE Readme SHALL beschreiben, dass für Team-Suchen keine Verfügbarkeitsfilter angewendet werden und welche Ergebnisspalten in den jeweiligen Modi (`score`, `llm_fulltext`) ausgegeben werden.
|
||||
|
||||
|
||||
### Anforderung 12: Konfiguration und Schema-Verifikation
|
||||
|
||||
**User Story:** Als Betreiber möchte ich, dass die neuen Datenbank-Views beim Start des Servers verifiziert werden und dass relevante Defaults konfigurierbar sind, damit Fehlkonfigurationen früh erkannt werden.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. THE MCP_Server SHALL beim Start die Existenz der Spalten `team_id`, `ouid`, `about_us`, `offerings`, `interests`, `focus_name` in `teamlandkarte_v_teams_latest` über die Schema-Verifikation prüfen.
|
||||
2. THE MCP_Server SHALL beim Start die Existenz der Spalte `id` (sowie der Spalte für den Teamnamen) in `teamlandkarte_v_teammeter_organizational_units_latest` über die Schema-Verifikation prüfen.
|
||||
3. THE MCP_Server SHALL beim Start die Existenz der Spalten `ouid`, `competence_id`, `top_competency` in `teamlandkarte_v_teammeter_team_competences_latest` über die Schema-Verifikation prüfen.
|
||||
4. THE MCP_Server SHALL beim Start die Existenz der Spalten `ouid`, `partner_id`, `projects` in `teamlandkarte_v_team_references_latest` über die Schema-Verifikation prüfen.
|
||||
5. THE MCP_Server SHALL die Konfigurationsdatei `config.toml` um einen optionalen Schlüssel `matching.team.top_competency_weight` (Default `1.5`) erweitern, der die Gewichtung von Top-Kompetenzen im Score-Matching steuert.
|
||||
6. IF `matching.team.top_competency_weight` einen nicht-numerischen Wert oder einen Wert kleiner als `1.0` enthält, THEN THE MCP_Server SHALL beim Start einen `ConfigError` mit beschreibender Meldung werfen.
|
||||
7. THE MCP_Server SHALL alle bestehenden Tests so erweitern oder ergänzen, dass sowohl die Profile_Type-Werte `capacity` als auch `team` (mit beiden Matching-Methoden, gemocktem LLM und gemockter DB) abgedeckt sind.
|
||||
|
||||
### Anforderung 13: Round-Trip-Eigenschaft der Team-Profil-Serialisierung
|
||||
|
||||
**User Story:** Als Entwickler möchte ich sicherstellen, dass die deterministische Serialisierung eines Team_Profile stabil ist und sich semantisch identische Eingaben auf identische Ausgaben abbilden, damit die LLM-Eingabe reproduzierbar und cachebar ist.
|
||||
|
||||
#### Akzeptanzkriterien
|
||||
|
||||
1. FOR ALL Team-Profile mit identischen Feldwerten in identischer Reihenfolge, THE MCP_Server SHALL identische serialisierte Strings produzieren (Determinismus).
|
||||
2. WHEN ein Team_Profile zweimal hintereinander aus identischen Datenbankzeilen aufgebaut und serialisiert wird, THE MCP_Server SHALL beide Male denselben Serialisierungsstring produzieren (Idempotenz der Profil-Bildung).
|
||||
3. THE MCP_Server SHALL in der serialisierten Darstellung jedes Profilfeld mit einer eindeutigen, festen Überschrift versehen, sodass aus dem Serialisierungsstring die Zuordnung der Werte zu den Feldern eindeutig ablesbar ist.
|
||||
4. THE MCP_Server SHALL die Reihenfolge der Listen-Elemente (`competences`, `references`) in der serialisierten Darstellung mit der vom DBClient gelieferten Reihenfolge übereinstimmen lassen, sodass keine Sortier-Diskrepanzen zwischen DB-Schicht und Serialisierungs-Schicht entstehen.
|
||||
@@ -0,0 +1,333 @@
|
||||
# Implementierungsplan: Team-Profil-Matching
|
||||
|
||||
## Übersicht
|
||||
|
||||
Convert the feature design into a series of prompts for a code-generation LLM that will implement each step with incremental progress. Make sure that each prompt builds on the previous prompts, and ends with wiring things together. There should be no hanging or orphaned code that isn't integrated into a previous step. Focus ONLY on tasks that involve writing, modifying, or testing code.
|
||||
|
||||
Die Implementierung erfolgt in **Python** (entsprechend des bestehenden Codestils
|
||||
des Repositorys). Property-Based Tests verwenden **Hypothesis**, parallel zu den
|
||||
existierenden `tests/test_*_pbt.py`-Modulen. Sub-Tasks mit `*` sind optional und
|
||||
werden nicht automatisch implementiert (gemäß Projekt-Konventionen). Jede der
|
||||
14 Korrektheits-Eigenschaften aus dem Designdokument wird in genau einem
|
||||
PBT-Sub-Task realisiert und ist mit `**Property N**` und der validierten
|
||||
Anforderung annotiert.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Datenmodelle und Konfiguration vorbereiten
|
||||
- [x] 1.1 `Team`, `TeamCompetence`, `TeamReference`, `ScoredTeam` in `src/teamlandkarte_mcp/models.py` ergänzen
|
||||
- Frozen Dataclasses mit den im Design festgelegten Feldern und Defaults (`field(default_factory=list)` für `competences`/`references`)
|
||||
- Typen-Hints und Docstrings analog zu `Capacity`/`ScoredCapacity`
|
||||
- _Requirements: 5.1, 6.1_
|
||||
|
||||
- [x] 1.2 `TeamMatchingConfig` und `MatchingConfig.team` in `src/teamlandkarte_mcp/config.py` ergänzen
|
||||
- Frozen Dataclass `TeamMatchingConfig` mit `top_competency_weight: float = 1.5`
|
||||
- Feld `team: TeamMatchingConfig` in `MatchingConfig` hinzufügen
|
||||
- Parser in `load_config` so erweitern, dass `[matching.team]` aus TOML gelesen wird
|
||||
- Validierung: nicht-numerischer Wert oder Wert < 1.0 → `ConfigError` mit Schlüsselname und fehlerhaftem Wert in der Meldung
|
||||
- Default-Wert `1.5` greift bei fehlendem Schlüssel
|
||||
- _Requirements: 12.5, 12.6_
|
||||
|
||||
- [x] 1.3 `config.toml.example` um `[matching.team]`-Block ergänzen
|
||||
- Kommentierter Abschnitt mit `top_competency_weight = 1.5`
|
||||
- Hinweis, dass der Wert numerisch und ≥ 1.0 sein muss
|
||||
- _Requirements: 12.5_
|
||||
|
||||
- [x] 1.4 PBT für Konfig-Validierung von `top_competency_weight`
|
||||
- **Property 13: Konfig-Validierung von `top_competency_weight`**
|
||||
- **Validates: Requirements 12.5, 12.6**
|
||||
- Hypothesis-Strategie: numerische Werte ≥ 1.0 (positiv), nicht-numerische und < 1.0 (negativ); fehlender Schlüssel → Default 1.5
|
||||
- Datei: `tests/test_team_config_top_weight_pbt.py`
|
||||
|
||||
- [x] 2. DBClient-Protokoll und SQL-Implementierung für Team-Daten
|
||||
- [x] 2.1 `DBClient`-Protokoll in `src/teamlandkarte_mcp/database/types.py` erweitern
|
||||
- Neue `TypedDict`s `TeamCompetenceRow` (`name`, `top_competency`) und `TeamReferenceRow` (`partner_name`, `projects`)
|
||||
- Methoden ergänzen: `get_all_teams`, `get_team_by_id`, `get_team_competences`, `batch_get_team_competences`, `get_team_references`, `batch_get_team_references`
|
||||
- Docstrings beschreiben Sortierung, NULL-Behandlung und INNER- vs. LEFT-JOIN-Semantik (siehe Design)
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7_
|
||||
|
||||
- [x] 2.2 `TrinoClient.get_all_teams` und `get_team_by_id` in `src/teamlandkarte_mcp/database/trino_client.py` implementieren
|
||||
- SQL: INNER JOIN `teamlandkarte_v_teams_latest` mit `teamlandkarte_v_teammeter_organizational_units_latest` über `team_id = id`
|
||||
- `COALESCE(..., '')` für `about_us`, `offerings`, `interests`, `focus_name`
|
||||
- `ORDER BY ou.name ASC, t.team_id ASC`
|
||||
- `_ensure_select_only` und `_retry` verwenden, Connection-Pool über bestehenden `_cursor`-Context
|
||||
- `get_all_teams` ruft intern `batch_get_team_competences` und `batch_get_team_references` auf und aggregiert zu `Team`-Instanzen
|
||||
- `get_team_by_id` liefert `None`, wenn kein Treffer (auch durch INNER JOIN ausgefiltert)
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7_
|
||||
|
||||
- [x] 2.3 `TrinoClient.get_team_competences` und `batch_get_team_competences` implementieren
|
||||
- SQL: Join auf `teamlandkarte_v_competences_latest` analog zum Capacity-Pfad zur Auflösung des Kompetenz-Namens
|
||||
- `COALESCE(top_competency, FALSE)` für NULL-Normalisierung
|
||||
- `ORDER BY tc.ouid ASC, CASE WHEN COALESCE(top_competency,FALSE) THEN 0 ELSE 1 END ASC, c.name ASC`
|
||||
- Batch-Variante in genau einer Query, `dict[str, list[TeamCompetenceRow]]` mit leeren Listen für OUIDs ohne Treffer
|
||||
- `_ensure_select_only` und `_retry` anwenden
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 2.6, 2.7_
|
||||
|
||||
- [x] 2.4 `TrinoClient.get_team_references` und `batch_get_team_references` implementieren
|
||||
- SQL: LEFT JOIN auf `teamlandkarte_v_partners_latest` über `partner_id = id` als Teil derselben Query (kein zusätzlicher Roundtrip)
|
||||
- `COALESCE(p.name, '')` für `partner_name`
|
||||
- Whitespace-only `projects` werden nach dem Fetch in Python ausgefiltert
|
||||
- `ORDER BY r.ouid ASC, partner_name ASC, r.projects ASC`
|
||||
- Batch-Variante in genau einer Query, `dict[str, list[TeamReferenceRow]]` mit leeren Listen für OUIDs ohne Treffer
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 2.6, 2.7_
|
||||
|
||||
- [x] 2.5 PBT für SELECT-Only-Eigenschaft aller neuen Trino-Queries
|
||||
- **Property 2: SELECT-Only-Eigenschaft aller neuen Trino-Queries**
|
||||
- **Validates: Requirements 2.6**
|
||||
- Stub-Cursor, der jede ausgeführte SQL durch `_ensure_select_only` validiert; Hypothesis-generierte Eingabe-Listen für die sechs neuen Methoden
|
||||
- Datei: `tests/test_team_trino_select_only_pbt.py`
|
||||
|
||||
- [x] 2.6 PBT für Stammdaten-Konsistenz und INNER-JOIN-Filter
|
||||
- **Property 3: Stammdaten-Konsistenz und INNER-JOIN-Filter**
|
||||
- **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
|
||||
- Hypothesis-Strategie: Listen aus Teams- und OU-Stub-Zeilen mit beliebigen NULL/Empty-Verteilungen; prüfen, dass NULL → "", INNER JOIN ohne Match → ausgeschlossen, `get_team_by_id` konsistent zu `get_all_teams`
|
||||
- Datei: `tests/test_team_master_data_pbt.py`
|
||||
|
||||
- [x] 2.7 PBT für Konsistenz von Einzel- und Batch-Variante der Kompetenzen
|
||||
- **Property 4: Kompetenz-Batch ist konsistent zur Einzel-Variante**
|
||||
- **Validates: Requirements 3.1, 3.3, 3.4, 3.5, 3.6, 3.7**
|
||||
- Hypothesis-Strategie: Mengen von OUIDs + zufällige Top/Name-Verteilungen + NULL für `top_competency`
|
||||
- Datei: `tests/test_team_competences_batch_pbt.py`
|
||||
|
||||
- [x] 2.8 PBT für Konsistenz von Einzel- und Batch-Variante der Referenzen
|
||||
- **Property 5: Referenz-Batch ist konsistent zur Einzel-Variante**
|
||||
- **Validates: Requirements 4.1, 4.3, 4.4, 4.5, 4.6, 4.7**
|
||||
- Hypothesis-Strategie: Referenzzeilen mit/ohne Partner und Whitespace-`projects`; prüfen, dass die Batch-Variante genau einen `cur.execute(...)`-Call gegen die References-View ausführt
|
||||
- Datei: `tests/test_team_references_batch_pbt.py`
|
||||
|
||||
- [x] 2.9 Unit-Tests für `TrinoClient`-Team-Queries (Snapshot)
|
||||
- SQL-String-Snapshot, dass INNER JOIN, `ORDER BY`, `COALESCE` und LEFT JOIN auf `partners` korrekt formuliert sind
|
||||
- Datei: `tests/test_trino_client_team_queries.py`
|
||||
- _Requirements: 2.1, 2.2, 3.1, 4.1, 4.2_
|
||||
|
||||
- [x] 3. Schema-Verifikation für die vier neuen Views
|
||||
- [x] 3.1 `schema_expected`-Map in `src/teamlandkarte_mcp/mcp_server.py` (`build_server`) erweitern
|
||||
- Einträge für `teamlandkarte_v_teams_latest`, `teamlandkarte_v_teammeter_organizational_units_latest`, `teamlandkarte_v_teammeter_team_competences_latest`, `teamlandkarte_v_team_references_latest`
|
||||
- Spalten gemäß Design (`team_id`, `ouid`, `about_us`, `offerings`, `interests`, `focus_name`; `id`, `name`; `ouid`, `competence_id`, `top_competency`; `ouid`, `partner_id`, `projects`)
|
||||
- Bestehender Pfad `verify_required_columns` wird unverändert verwendet
|
||||
- _Requirements: 12.1, 12.2, 12.3, 12.4_
|
||||
|
||||
- [x] 3.2 PBT für Schema-Verifikation der vier neuen Views
|
||||
- **Property 14: Schema-Verifikation deckt fehlende Spalten in den vier neuen Views auf**
|
||||
- **Validates: Requirements 12.1, 12.2, 12.3, 12.4**
|
||||
- Hypothesis-Strategie: Powerset der erwarteten Spalten pro View, jeweils minus eine zufällig gewählte Spalte → `SchemaIssue` mit Tabellenname und fehlender Spalte; vollständige Spaltenliste → kein Issue
|
||||
- Datei: `tests/test_team_schema_verification_pbt.py`
|
||||
|
||||
- [x] 4. Checkpoint - Datenzugriffsschicht
|
||||
- Sicherstellen, dass alle bisherigen Tests grün sind und der Server mit der erweiterten Schema-Verifikation startet. Bei Unklarheiten den Nutzer fragen.
|
||||
|
||||
- [x] 5. Profile-Erstellung und Serialisierung
|
||||
- [x] 5.1 `TeamCompetenceEntry`, `TeamReferenceEntry`, `TeamProfile` in `src/teamlandkarte_mcp/matching/profiles.py` ergänzen
|
||||
- Frozen Dataclasses mit den im Design definierten Feldern; Listenreihenfolgen entsprechen den Datenbank-Reihenfolgen
|
||||
- _Requirements: 5.1_
|
||||
|
||||
- [x] 5.2 `build_team_profile(team: Team) -> TeamProfile` implementieren
|
||||
- 1:1-Mapping von `Team`-Feldern auf `TeamProfile`; keine Re-Sortierung der Listen
|
||||
- NULL/Empty-Strings unverändert übernehmen (DB-Schicht hat bereits normalisiert)
|
||||
- _Requirements: 5.1, 5.2, 13.4_
|
||||
|
||||
- [x] 5.3 `serialize_team_profile(profile: TeamProfile) -> str` implementieren
|
||||
- Feste deutsche Überschriften: `Teamname:`, `Schwerpunkt:`, `Über uns:`, `Leistungen:`, `Interessen:`, `Kompetenzen:`, `Referenzen:` in genau dieser Reihenfolge
|
||||
- Top-Kompetenzen mit Suffix `(Top)` markieren
|
||||
- Referenz-Zeilen: `Partner: <partner_name> – Projekte: <projects>` bei vorhandenem Partner; bei leerem Partner nur `Projekte: <projects>` (kein Platzhalter)
|
||||
- Leere Listen als `Kompetenzen: (keine)` bzw. `Referenzen: (keine)` rendern
|
||||
- Reihenfolge der Listen-Elemente entspricht 1:1 der Eingabe
|
||||
- _Requirements: 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 13.1, 13.2, 13.3, 13.4_
|
||||
|
||||
- [x] 5.4 PBT für Determinismus und Vollständigkeit der Team-Profil-Serialisierung
|
||||
- **Property 6: Determinismus und Vollständigkeit der Team-Profil-Serialisierung**
|
||||
- **Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 13.1, 13.2, 13.3, 13.4**
|
||||
- Hypothesis-Strategie für `TeamProfile` analog zu `_capacity_profile` in `tests/test_profile_serialization_pbt.py`; prüft Determinismus, Idempotenz, Reihenfolge der Überschriften, `(Top)`-Marker, Partner-Optional-Verhalten und Listen-Reihenfolge
|
||||
- Datei: `tests/test_team_profile_serialization_pbt.py`
|
||||
|
||||
- [x] 5.5 Unit-Tests für `build_team_profile` und `serialize_team_profile`
|
||||
- Beispielbasiert: leere Strings, leere Listen, gemischte Top/Nicht-Top-Kompetenzen, Referenzen mit/ohne Partner_Name
|
||||
- Datei: `tests/test_team_profile_serialization_unit.py`
|
||||
- _Requirements: 5.1, 5.3, 5.4, 5.5, 5.6_
|
||||
|
||||
- [x] 6. Score-basiertes Matching für Team-Profile
|
||||
- [x] 6.1 `Matcher.match_teams` in `src/teamlandkarte_mcp/matching/matcher.py` implementieren
|
||||
- Signatur: `async def match_teams(self, teams, requirements, *, top_competency_weight) -> TeamMatchResult`
|
||||
- Kompetenz-Liste aus `team.competences` ableiten + paralleles Top-Set
|
||||
- `SimilarityEngine.compute_competence_similarity` analog zum Capacity-Pfad nutzen; pro Required-Kompetenz `weighted = min(1.0, raw_score * (top_weight if best_match in top_set else 1.0))`
|
||||
- Role Score über `SimilarityEngine.compute_role_similarity(req.role_name, team.focus_name)`
|
||||
- Overall Score über `compute_overall(...)` mit denselben Gewichten und Thresholds wie für Kapazitäten
|
||||
- Kategorisierung über `categorize(...)`
|
||||
- **Verfügbarkeitsprüfung wird nicht aufgerufen**; ein etwaiger Datumsbereich aus `req` wird ignoriert
|
||||
- Rückgabe: neues `TeamMatchResult` (`scored: list[ScoredTeam]`, `by_category: dict[str, list[ScoredTeam]]`)
|
||||
- `Matcher.summary_counts` so generalisieren, dass sowohl Capacity- als auch Team-Buckets unterstützt werden (strukturell `len`-basiert)
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.7_
|
||||
|
||||
- [x] 6.2 PBT für Top-Kompetenz-Monotonie im Score-Matching
|
||||
- **Property 7: Top-Kompetenz-Monotonie im Score-Matching**
|
||||
- **Validates: Requirements 6.4**
|
||||
- Hypothesis-Strategie: Kompetenz-Listen + `top_weight ∈ [1.0, 5.0]`; vergleicht `T_top` (alle Top) mit `T_plain`; bei `top_weight == 1.0` exakt 0.0 Differenz
|
||||
- Datei: `tests/test_team_score_top_weight_pbt.py`
|
||||
|
||||
- [x] 6.3 PBT für Score-Pfad-Validität und Verfügbarkeits-Ignoranz
|
||||
- **Property 8: Score-Pfad liefert gültige Score- und Kategorie-Werte**
|
||||
- **Validates: Requirements 6.1, 6.2, 6.3, 6.5, 6.7**
|
||||
- Hypothesis-Strategie für `Team`-Listen + Stub-`SimilarityEngine`; prüft Score-Bereich `[0,1]`, Kategorie-Set, Konsistenz mit `categorize(...)` und Invarianz unter `req.date_start`/`req.date_end`
|
||||
- Datei: `tests/test_team_score_pipeline_pbt.py`
|
||||
|
||||
- [x] 7. LLM-Volltext-Matching für Team-Profile
|
||||
- [x] 7.1 `LlmFulltextMatcher.match_teams` in `src/teamlandkarte_mcp/matching/llm_fulltext_matcher.py` implementieren
|
||||
- Signatur parallel zu `match_capacities`/`match_tasks`, akzeptiert `task_profile: TaskProfile` und `teams: list[Team]`
|
||||
- Pro Team `build_team_profile + serialize_team_profile`; User-Prompt im Format `=== Aufgabe ===` + `=== Team ===` mit `ID:`-Header
|
||||
- System-Prompt: bestehender Capacity-Prompt mit minimaler Wortwahl-Anpassung (`Kapazitätsprofil` → `Profil`); JSON-Schema `{"category", "rationale"}` unverändert
|
||||
- Concurrency über bestehende `asyncio.Semaphore` (`max_concurrency`)
|
||||
- Fehlerbehandlung: `LlmFulltextError` für JSON-Parse-Fehler und `AzureAPIError`; ungültige Kategorien werden via `normalize_category` auf `Irrelevant` gemappt mit Hinweis-Suffix in der Rationale
|
||||
- Sortierung pro Kategorie: `(category_rank, item_id_asc)` mit `item_id = team_id`
|
||||
- Rückgabe: bestehender `LlmFulltextResult`-Typ (`by_category` + `errors`)
|
||||
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7_
|
||||
|
||||
- [x] 7.2 PBT für LLM-Pfad als vollständige Partition mit gültigen Kategorien
|
||||
- **Property 9: LLM-Pfad ist eine vollständige Partition mit gültigen Kategorien**
|
||||
- **Validates: Requirements 7.1, 7.2, 7.3, 7.6, 7.7**
|
||||
- Hypothesis-Strategie: `mask: list[bool]` analog zu `tests/test_batch_completeness_pbt.py` (`_MaskLlm`-Pattern); prüft Vollständigkeit, Eindeutigkeit, Sortierung, gültige Kategorien und nicht-leere Rationales bei Erfolg
|
||||
- Datei: `tests/test_team_llm_fulltext_partition_pbt.py`
|
||||
|
||||
- [x] 7.3 PBT für ungültige LLM-Kategorien (Irrelevant-Fallback)
|
||||
- **Property 10: Ungültige LLM-Kategorien fallen auf Irrelevant zurück**
|
||||
- **Validates: Requirements 7.5**
|
||||
- Hypothesis-Strategie: zufällige Strings (außerhalb der erlaubten Kategorien) + JSON-Wrapper; prüft `category == "Irrelevant"` und Hinweis-Substring in `rationale`
|
||||
- Datei: `tests/test_team_llm_invalid_category_pbt.py`
|
||||
|
||||
- [x] 8. Checkpoint - Matching-Pipelines
|
||||
- Sicherstellen, dass die Score- und LLM-Pfade isoliert lauffähig sind und alle Tests grün sind. Bei Unklarheiten den Nutzer fragen.
|
||||
|
||||
- [x] 9. MCP-Tools für Team-Suche und Team-Detailansicht
|
||||
- [x] 9.1 `_ALLOWED_PROFILE_TYPES` und `_get_teams_cached()` in `src/teamlandkarte_mcp/mcp_server.py` ergänzen
|
||||
- String-Konstante `_ALLOWED_PROFILE_TYPES = ("capacity", "team")`
|
||||
- Neuer `QueryCache[list[Team]]`-Eintrag (`all_teams`) parallel zum Capacity-Cache; ruft `db_client.get_all_teams()`
|
||||
- _Requirements: 1.1_
|
||||
|
||||
- [x] 9.2 Tool `find_matching_teams` registrieren
|
||||
- Signatur: `async def find_matching_teams(role_name: str, competences: list[str], matching_method: str = "") -> str`
|
||||
- `_resolve_matching_method` und `_validate_requirements_minimum` wiederverwenden; ungültiges `matching_method` → Fehlermeldung mit erlaubten Werten, **kein** DB-/LLM-Aufruf
|
||||
- Confirm-Gate über `_require_confirmed_or_auto(req)`
|
||||
- Score-Pfad: `matcher.match_teams(...)` mit `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 `SearchCache` mit `results_payload["search_type"] = "team_search"` und `matching_method`
|
||||
- Antwort: `SEARCH_ID=<uuid>` + `META=<json>` mit `search_type` und `matching_method` + Markdown-Tabellen (Summary + Top-Kategorie)
|
||||
- _Requirements: 1.3, 1.4, 1.5, 1.6, 6.1, 7.1, 8.1, 8.2, 8.3_
|
||||
|
||||
- [x] 9.3 Tool `list_teams(limit: int = 20) -> str` registrieren
|
||||
- Markdown-Tabelle mit Spalten `Team Id`, `Team Name`, `Schwerpunkt`, `Anzahl Kompetenzen`, `Anzahl Referenzen`
|
||||
- Empty-Fallback-Text wie bei `list_free_capacities`
|
||||
- _Requirements: 9.1_
|
||||
|
||||
- [x] 9.4 Tool `get_team_details(team_id: str) -> str` registrieren
|
||||
- Markdown-Tabelle für ein Team plus Sektionen `## Über uns`, `## Leistungen`, `## Interessen`, `## Kompetenzen` (mit `(Top)`-Marker), `## Referenzen` (Partner_Name fett gedruckt, sonst nur Projekte) und `## Next steps`
|
||||
- Wenn `db_client.get_team_by_id(team_id)` `None` liefert → `f"Team not found: {team_id}"`
|
||||
- Reihenfolge der Listen entspricht der DB-Reihenfolge
|
||||
- _Requirements: 9.2, 9.3, 9.4_
|
||||
|
||||
- [x] 9.5 `_coerce_team(item)` Helper implementieren
|
||||
- Rehydratisiert persistierte SearchCache-Einträge zurück in `Team`-Instanzen (analog zu `_coerce_capacity`)
|
||||
- Handhabt sowohl `Team`-Instanzen als auch verschachtelte `dict`-Repräsentationen (mit/ohne `team`-Wrapper)
|
||||
- _Requirements: 8.4_
|
||||
|
||||
- [x] 9.6 `_format_results_table` für `search_type="team_search"` erweitern
|
||||
- Spalten im `score`-Modus: `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Role Score`, `Competence Score`, `Overall Score`, `Category`
|
||||
- Spalten im `llm_fulltext`-Modus: `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Category`, `Begründung`
|
||||
- `Top-Kompetenzen` ist die kommaseparierte Liste aller `competences` mit `top_competency=True`
|
||||
- `Begründung` nutzt bestehenden `_format_rationale_for_table`-Helper
|
||||
- Sortierkey im LLM-Modus: `(category_rank, item.get("team_id") or "")`
|
||||
- _Requirements: 6.6, 7.8, 8.4_
|
||||
|
||||
- [x] 9.7 `filter_search_results` und `get_results_by_category` für `team_search` erweitern
|
||||
- `role_filter` matcht gegen `team.focus_name`
|
||||
- `competence_filter` matcht gegen `[c["name"] for c in item["competences"]]`; Filterwerte mit Suffix `(Top)` werden auf Top-Kompetenzen beschränkt (Suffix wird vor Vergleich entfernt)
|
||||
- `availability_date_start`, `availability_date_end`, `is_fully_available` werden ignoriert; `Applied Filters`-Tabelle erhält für jeden Wert einen Hinweis mit Substring `team_search` und Markierung "nicht wirksam"
|
||||
- `task_*`-Filter bleiben für `team_search` inaktiv (existierender Pfad)
|
||||
- _Requirements: 6.7, 8.4, 8.5, 8.6_
|
||||
|
||||
- [x] 9.8 PBT für `matching_method`-Validierung in `find_matching_teams`
|
||||
- **Property 1: `matching_method`-Validierung in `find_matching_teams`**
|
||||
- **Validates: Requirements 1.5, 1.6**
|
||||
- Hypothesis-Strategie: zufällige Strings außerhalb `{"score","llm_fulltext"}` (case-insensitiv getrimmt) und nicht leer/None; prüft Fehlermeldung enthält beide erlaubten Werte und keine DB-/LLM-Aufrufe erfolgen
|
||||
- Datei: `tests/test_team_matching_method_validation_pbt.py`
|
||||
|
||||
- [x] 9.9 PBT für META und SearchCache-Markierung von Team-Suchen
|
||||
- **Property 11: META und SearchCache markieren Team-Suchen korrekt**
|
||||
- **Validates: Requirements 1.4, 8.1, 8.2, 8.3, 8.4**
|
||||
- Hypothesis-Strategie: zufällige Inputs + Snapshot-Parser für `SEARCH_ID` und `META`-JSON; prüft `search_type=="team_search"`, `matching_method ∈ {...}`, Konsistenz mit `SearchCache`-Eintrag und Header in der per-Kategorie-Tabelle
|
||||
- Datei: `tests/test_team_meta_search_type_pbt.py`
|
||||
|
||||
- [x] 9.10 PBT für Verfügbarkeitsfilter-Ignoranz in Team-Suchen
|
||||
- **Property 12: Verfügbarkeitsfilter werden in Team-Suchen ignoriert**
|
||||
- **Validates: Requirements 6.7, 8.5, 8.6**
|
||||
- Hypothesis-Strategie: zufällige Datumswerte + `is_fully_available`; prüft, dass die gefilterte Item-Menge gleich der ohne Verfügbarkeitsfilter bleibt und die `Applied Filters`-Tabelle den `team_search`-Hinweis enthält
|
||||
- Datei: `tests/test_team_filter_ignores_availability_pbt.py`
|
||||
|
||||
- [x] 9.11 Unit-Tests für `find_matching_teams`, `list_teams`, `get_team_details`
|
||||
- Tools sind über `FastMCP` registriert (Anforderungen 1.3, 9.1, 9.2)
|
||||
- `find_matching_capacities`-Snapshot-Test: Output für festen Input bleibt nach Refactoring unverändert (Anforderung 1.2)
|
||||
- Markdown-Outputs enthalten geforderte Header und Sektionen (Anforderungen 9.1, 9.2, 9.4)
|
||||
- `get_team_details("unknown")` enthält `unknown` in der Fehlermeldung (Anforderung 9.3)
|
||||
- Datei: `tests/test_team_tools_unit.py`
|
||||
- _Requirements: 1.2, 1.3, 9.1, 9.2, 9.3, 9.4_
|
||||
|
||||
- [x] 10. Checkpoint - MCP-Tool-Surface
|
||||
- Sicherstellen, dass alle bisherigen und neuen Tests grün sind und der Server `find_matching_teams`, `list_teams`, `get_team_details` korrekt registriert. Bei Unklarheiten den Nutzer fragen.
|
||||
|
||||
- [x] 11. Integrationstests
|
||||
- [x] 11.1 Smoke-Test für `find_matching_teams` im `score`-Modus
|
||||
- Gemockter Trino-Cursor + Stub-`SimilarityEngine`; voller Pfad `find_matching_teams` → `SearchCache` → `get_results_by_category` → `filter_search_results`
|
||||
- Datei: `tests/test_team_integration_score.py`
|
||||
- _Requirements: 6.1, 8.1, 8.2, 8.4_
|
||||
|
||||
- [x] 11.2 Smoke-Test für `find_matching_teams` im `llm_fulltext`-Modus
|
||||
- Gemockter Trino-Cursor + Mock-`AzureOpenAIClient`; voller Pfad inkl. `errors`-Liste bei simuliertem LLM-Fehler
|
||||
- Datei: `tests/test_team_integration_llm_fulltext.py`
|
||||
- _Requirements: 7.1, 7.6, 8.1, 8.2, 8.4_
|
||||
|
||||
- [x] 12. Dokumentation und Agenten-Konfiguration
|
||||
- [x] 12.1 `docs/architecture.md` um Team-Profil-Abschnitte ergänzen
|
||||
- Neuer Unterabschnitt "Team Profile" im Datenmodell-Kapitel mit Feldern, Datenquellen und View-Verknüpfungen
|
||||
- Datenquellen-Tabelle um die vier neuen Views inkl. relevanter Spalten erweitern
|
||||
- Joins dokumentieren: `teams_latest.team_id = organizational_units_latest.id` (INNER JOIN für Team_Name), `ouid` zu Kompetenzen/Referenzen, `team_references_latest.partner_id = partners_latest.id` (LEFT JOIN, Spalte `name` als Partner_Name)
|
||||
- `Profile_Type`-Parameter und Wertebereiche im Tool-Surface-Abschnitt für `find_matching_capacities`/`find_matching_teams`/`list_teams`/`get_team_details` dokumentieren
|
||||
- Runtime-View-Abschnitt um Aufgabe→Team in beiden Matching-Methoden ergänzen
|
||||
- _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6_
|
||||
|
||||
- [x] 12.2 `README.md` um Team-Suche-Hinweise ergänzen
|
||||
- Quick-Start- und Usage-Abschnitt: Wahl zwischen `capacity`- und `team`-Suche
|
||||
- Auflistung der zusätzlichen Datenbank-Views inkl. Join-Bedingungen
|
||||
- Hinweis: Verfügbarkeitsfilter wirken in Team-Suchen nicht; abweichende Ergebnisspalten in `score`/`llm_fulltext`
|
||||
- _Requirements: 11.7, 11.8, 11.9_
|
||||
|
||||
- [x] 12.3 `.github/agents/teamlandkarte_agent.md` und `.kiro/agents/teamlandkarte.md` aktualisieren
|
||||
- `Profile_Type`-Werte `capacity` und `team` mit Bedeutung dokumentieren
|
||||
- Initiale Captures-Phase um explizite Frage nach `Profile_Type` ergänzen (sofern nicht aus Verlauf ersichtlich)
|
||||
- Skills/Workflows um `find_matching_teams`, `list_teams`, `get_team_details` erweitern; `matching_method`-Parameter erwähnen
|
||||
- Hinweis: Verfügbarkeitsfilter sind in Team-Suchen nicht wirksam; Ergebnisspalten weichen ab
|
||||
- Bestätigungs-Workflow (`show_pending_requirements`, `confirm_requirements`) bleibt für beide `Profile_Type`-Werte unverändert
|
||||
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6_
|
||||
|
||||
- [x] 12.4 Doku-Snapshot-Tests
|
||||
- Prüft, dass `docs/architecture.md`, `README.md`, `.kiro/agents/teamlandkarte.md`, `.github/agents/teamlandkarte_agent.md` die definierten Stichworte enthalten (`Profile_Type`, `find_matching_teams`, `team_search`, `top_competency_weight`)
|
||||
- Datei: `tests/test_team_documentation_snapshots.py`
|
||||
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8, 11.9_
|
||||
|
||||
- [x] 13. Erweiterung der bestehenden Test-Suite um Team-Profil-Abdeckung
|
||||
- [x] 13.1 Bestehende End-to-End- und Routing-Tests um Team-Pfade ergänzen
|
||||
- `tests/test_mcp_routing_unit.py`, `tests/test_search_cache.py`, `tests/test_pagination.py`, `tests/test_filters.py` so erweitern, dass beide `Profile_Type`-Werte (`capacity`, `team`) mit beiden `matching_method`-Werten (gemocktes LLM und gemockte DB) abgedeckt sind
|
||||
- _Requirements: 12.7_
|
||||
|
||||
- [x] 14. Final-Checkpoint - Vollständigkeit und Konsistenz
|
||||
- Sicherstellen, dass alle Pflicht-Tasks (ohne `*`) abgeschlossen sind, alle Tests grün sind und die Dokumentation konsistent ist. Bei Unklarheiten den Nutzer fragen.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Sub-Tasks mit `*` sind optional und werden nur auf ausdrückliche Anweisung implementiert.
|
||||
- Jede Korrektheits-Eigenschaft aus dem Designdokument ist genau einem PBT-Sub-Task zugeordnet (Properties 1–14 in den Tasks 1.4, 2.5–2.8, 3.2, 5.4, 6.2, 6.3, 7.2, 7.3, 9.8, 9.9, 9.10).
|
||||
- Checkpoints (Tasks 4, 8, 10, 14) erzwingen einen grünen Test-Lauf vor dem nächsten Abschnitt.
|
||||
- Die Implementierungssprache ist Python (gemäß bestehendem Codestil); PBT-Framework ist Hypothesis.
|
||||
|
||||
## Workflow-Abschluss
|
||||
|
||||
Dieser Workflow erstellt ausschließlich Design- und Planungsartefakte. Die
|
||||
eigentliche Implementierung beginnt, sobald der Nutzer in `tasks.md` einen Task
|
||||
über "Start task" anstößt.
|
||||
Reference in New Issue
Block a user