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:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -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. 2050 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)