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.
102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
# Feature: llm-batch-matching, Property 5: Korrekte Zuordnung (Response-Mapping)
|
|
"""Property-based test for correct response-to-candidate mapping in batch matching.
|
|
|
|
For any list of capacities with item_id-specific category assignments, each
|
|
item must land in exactly the category that the mock LLM returned for it.
|
|
This validates that concurrent execution does not mix up responses between
|
|
candidates.
|
|
|
|
**Validates: Requirements 3.4**
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
from hypothesis import given, settings
|
|
from hypothesis import strategies as st
|
|
|
|
from teamlandkarte_mcp.matching.llm_fulltext_matcher import LlmFulltextMatcher
|
|
from teamlandkarte_mcp.matching.profiles import TaskProfile
|
|
from teamlandkarte_mcp.models import Capacity
|
|
|
|
_ID_RE = re.compile(r"ID:\s*(\S+)")
|
|
|
|
_CATEGORIES: tuple[str, ...] = ("Top", "Good", "Partial", "Low", "Irrelevant")
|
|
|
|
|
|
class _MapLlm:
|
|
def __init__(self, id_to_category: dict[str, str]) -> None:
|
|
self._map = id_to_category
|
|
|
|
async def chat_completion(self, system: str, user: str) -> str:
|
|
match = _ID_RE.search(user)
|
|
cap_id = match.group(1) if match else "unknown"
|
|
category = self._map.get(cap_id, "Irrelevant")
|
|
await asyncio.sleep(0.005) # simulate I/O
|
|
return json.dumps({"category": category, "rationale": f"r{cap_id}"})
|
|
|
|
|
|
class _StubDb:
|
|
def batch_get_capacity_descriptions(self, capacity_ids: list[Any]) -> dict[str, str | None]:
|
|
return {str(i): None for i in capacity_ids}
|
|
|
|
def batch_get_capacity_certificates(self, capacity_ids: list[Any]) -> dict[str, list[str]]:
|
|
return {str(i): [] for i in capacity_ids}
|
|
|
|
def batch_get_capacity_references(self, capacity_ids: list[Any]) -> dict[str, list[dict]]:
|
|
return {str(i): [] for i in capacity_ids}
|
|
|
|
|
|
def _make_capacity(cap_id: int) -> Capacity:
|
|
return Capacity(
|
|
id=cap_id,
|
|
owner_name=f"o{cap_id}",
|
|
role_name="R",
|
|
role_level=None,
|
|
begin_date=None,
|
|
end_date=None,
|
|
competences=[],
|
|
)
|
|
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(
|
|
ids=st.lists(st.integers(min_value=1, max_value=99), min_size=2, max_size=15, unique=True),
|
|
max_concurrency=st.integers(min_value=1, max_value=5),
|
|
data=st.data(),
|
|
)
|
|
def test_batch_correct_mapping(
|
|
ids: list[int], max_concurrency: int, data: st.DataObject
|
|
) -> None:
|
|
"""Property 5: each item lands in exactly the category the mock assigned."""
|
|
id_to_category = {str(i): data.draw(st.sampled_from(_CATEGORIES)) for i in ids}
|
|
capacities = [_make_capacity(i) for i in ids]
|
|
task_profile = TaskProfile(id="t", title="T", description="D", skills=[])
|
|
|
|
matcher = LlmFulltextMatcher(
|
|
db=_StubDb(), client=_MapLlm(id_to_category), max_concurrency=max_concurrency,
|
|
)
|
|
result = asyncio.run(
|
|
matcher.match_capacities(task_profile=task_profile, capacities=capacities)
|
|
)
|
|
|
|
# Build a mapping from item_id to actual category in the result
|
|
actual_mapping: dict[str, str] = {}
|
|
for cat, items in result.by_category.items():
|
|
for it in items:
|
|
actual_mapping[it.item_id] = cat
|
|
|
|
# Assert: every capacity ended up in the correct category
|
|
for cap_id_str, expected_cat in id_to_category.items():
|
|
assert cap_id_str in actual_mapping, (
|
|
f"Capacity {cap_id_str} not found in any category"
|
|
)
|
|
assert actual_mapping[cap_id_str] == expected_cat, (
|
|
f"Capacity {cap_id_str}: expected {expected_cat}, "
|
|
f"got {actual_mapping[cap_id_str]}"
|
|
)
|