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,607 @@
|
||||
# Design: Capacity Matching MCP Server
|
||||
|
||||
> **Note**: For comprehensive architecture documentation including ADRs, quality requirements, and deployment view, see [architecture.md](./architecture.md).
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
> **Update (2026-02)**: The server uses **Azure OpenAI** for role/requirements
|
||||
> extraction and similarity scoring via embeddings.
|
||||
> There are **no heuristic fallbacks** for these features.
|
||||
>
|
||||
> The embedding model is `text-embedding-3-large` with **3072 dimensions** and
|
||||
> results are cached in a local SQLite embedding cache.
|
||||
|
||||
## Embedding similarity acceleration (global prefetch + batch embeddings)
|
||||
|
||||
> **Update (2026-02)**: Embedding similarity is optimized via **global bulk prefetch**
|
||||
> plus **true Azure embeddings batch requests**. This is primarily implemented in
|
||||
> `SimilarityEngine` and `AzureOpenAIClient`.
|
||||
|
||||
### Motivation
|
||||
|
||||
The matching pipeline can be slow if embeddings are requested on-demand from inner
|
||||
loops. While the system already uses a persistent SQLite embedding cache
|
||||
(`EmbeddingCache`), additional optimization is necessary to avoid repeated:
|
||||
|
||||
- normalization + cache-key computation
|
||||
- SQLite reads
|
||||
- sequential Azure embedding calls for cache misses
|
||||
|
||||
### Goals
|
||||
|
||||
- Bulk prefetch embeddings for a matching run (roles + competences), across **all free
|
||||
capacities at once** (not person-by-person / incremental embedding).
|
||||
- Maintain deterministic behavior and strict error semantics.
|
||||
- Preserve existing similarity contracts (`per_skill` / `aggregate`) and tool outputs.
|
||||
|
||||
### Data flow
|
||||
|
||||
1. **Collect** all texts that will be embedded in the current run:
|
||||
- required competences
|
||||
- candidate competences (from **all free capacities**)
|
||||
- required roles
|
||||
- candidate roles (from **all free capacities**)
|
||||
2. **Normalize + deduplicate** using existing `_normalize_text()` logic:
|
||||
- skip empty/whitespace-only texts
|
||||
- emit a **Python logger warning** if all candidate competences normalize to empty
|
||||
3. **Resolve embeddings** using a layered cache approach:
|
||||
1) in-memory cache (instance-level, attached to `SimilarityEngine`; not created/cleared per invocation)
|
||||
2) persistent SQLite embedding cache (`EmbeddingCache`)
|
||||
3) Azure embeddings API (batch only the cache-missing texts; chunked)
|
||||
4. **Compute** cosine similarities locally using the prefetched mapping.
|
||||
|
||||
### Normalization and cache keys
|
||||
|
||||
Cache key behavior remains unchanged to avoid invalidating the on-disk cache:
|
||||
|
||||
- normalization: trim + collapse whitespace
|
||||
- key: SHA256 of `model|dims|normalized(text).lower()`
|
||||
|
||||
### Azure embeddings batch API + chunking
|
||||
|
||||
The Azure embeddings API supports embedding multiple inputs per request via
|
||||
`input=[...]`. The implementation requirements are:
|
||||
|
||||
- preserve stable mapping from input texts → returned vectors
|
||||
- rely on the `index` field in the API response if available
|
||||
- otherwise assume stable ordering
|
||||
- chunk large requests using `azure_openai.embedding_batch_size` (default: 128)
|
||||
- strict failure semantics:
|
||||
- the matching operation fails immediately on error
|
||||
- log the failing **chunk input list** via Python logging
|
||||
- cost tracking: log once per chunk with a count of embeddings requested
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- Batch calls reduce network requests dramatically but increase blast radius: one
|
||||
failing request affects multiple inputs.
|
||||
- mitigation: keep retry logic at the chunk level
|
||||
- Prefetch holds a larger in-memory mapping for the current run.
|
||||
- mitigation: only store vectors required for that run (after dedup)
|
||||
|
||||
### Tests
|
||||
|
||||
- prefetch/dedup: client is called once per unique normalized text (cache misses only)
|
||||
- cache layering: in-memory first, then SQLite, then Azure; no duplicate calls
|
||||
- chunking: multiple Azure requests when `len(missing_texts) > batch_size`
|
||||
- empty input handling: warning when all candidate competences normalize to empty
|
||||
- strict error propagation: batch failures identify/log which chunk failed
|
||||
|
||||
```text
|
||||
(diagram below is historical; “sampling” arrows represent the client’s LLM runtime.
|
||||
The MCP server itself does not rely on MCP “sampling” for Azure OpenAI calls.)
|
||||
```
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ MCP Client │
|
||||
│ (with LLM) │
|
||||
└──────┬───────┘
|
||||
│ MCP protocol (tool calls)
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ MCP Server │
|
||||
│ │
|
||||
│ Task Management Tools: │
|
||||
│ - list_open_tasks │
|
||||
│ - get_task_details (table-first) │
|
||||
│ - validate_task_requirements (table-first) │
|
||||
│ - find_capacities_for_task (requires confirm) │
|
||||
│ - infer_roles │
|
||||
│ │
|
||||
│ Requirement Gathering Tools: │
|
||||
│ - extract_requirements (writes pending) │
|
||||
│ - update_requirements (writes pending) │
|
||||
│ - collect_structured_requirement_data │
|
||||
│ (writes pending) │
|
||||
│ - start_guided_capture │
|
||||
│ - guided_set_description │
|
||||
│ - guided_set_role │
|
||||
│ - guided_set_time_range (open-ended) │
|
||||
│ - guided_set_competences (writes pending) │
|
||||
│ - confirm_requirements │
|
||||
│ │
|
||||
│ Search Execution & Refinement: │
|
||||
│ - find_matching_capacities (hard-gated) │
|
||||
│ - filter_search_results │
|
||||
│ - get_results_by_category │
|
||||
│ │
|
||||
│ Business Logic: Matcher / Scorer / Caches │
|
||||
│ │
|
||||
│ Integrations: │
|
||||
│ - Trino / Open Data Lake (read-only SQL) │
|
||||
│ - Azure OpenAI (embeddings only) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────┐
|
||||
│ Open Data Lake (Trino) │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Design
|
||||
|
||||
### 1. Configuration Layer (`src/teamlandkarte_mcp/config.py`)
|
||||
|
||||
**Purpose**: Load and validate configuration from `config.toml` and environment.
|
||||
|
||||
**Important**:
|
||||
- `config.toml` contains **non-secret** settings only (endpoints, model/deployment names, cache settings).
|
||||
- credentials come from environment (recommended: `.env`) and must not be committed:
|
||||
- `DATA_LAKE_USERNAME`
|
||||
- `DATA_LAKE_PASSWORD`
|
||||
- `AZURE_OPENAI_EMBEDDING_API_KEY`
|
||||
|
||||
**config.toml structure (excerpt)**:
|
||||
```toml
|
||||
[database]
|
||||
host = "..."
|
||||
port = 8446
|
||||
backend = "trino"
|
||||
http_scheme = "https"
|
||||
verify_ssl = true
|
||||
catalog = "hive"
|
||||
schema = "tier1_open_lake"
|
||||
|
||||
[azure_openai]
|
||||
endpoint = "https://<resource>.openai.azure.com"
|
||||
api_version = "2024-02-15-preview"
|
||||
embedding_deployment = "text-embedding-3-large"
|
||||
|
||||
[embedding_cache]
|
||||
path = ".cache/embeddings.sqlite3"
|
||||
ttl_days = 30
|
||||
|
||||
[matching.similarity]
|
||||
# `per_skill` = best-match per required competence
|
||||
# `aggregate` = mean(required) vs mean(candidate)
|
||||
strategy = "per_skill"
|
||||
```
|
||||
|
||||
### Roundtrip examples (tool-level sequences)
|
||||
|
||||
These examples describe the intended end-to-end client/server interaction.
|
||||
They explicitly include the strict **Review → Ask → Confirm** step before any
|
||||
matching execution when `matching.require_confirmation = true`.
|
||||
|
||||
#### Roundtrip A: Ad-hoc search (underspecified) using guided capture
|
||||
|
||||
1. `start_guided_capture()`
|
||||
2. `guided_set_description("...")` (must capture concrete scope/goal; skill-only is insufficient)
|
||||
3. `guided_set_role("Backend Developer")`
|
||||
4. `guided_set_time_range(date_start="2026-04-01", date_end="2026-06-30")`
|
||||
5. `guided_set_competences(["Python", "FastAPI", "Docker"])`
|
||||
6. **Review**: `show_pending_requirements()` (server returns a single review table)
|
||||
7. **Ask** (assistant → user): "Soll ich diese Anforderungen so übernehmen und die Suche starten?" (Ja/Nein)
|
||||
8. **Confirm** (only on "Ja"): `confirm_requirements(confirm=true)`
|
||||
9. Execute: `find_matching_capacities(role_name="Backend Developer", competences=[...], date_start="2026-04-01", date_end="2026-06-30")`
|
||||
- Server returns deterministic headers (`Using SEARCH_ID=...`, `SEARCH_ID=...`, `META=...`) plus `## Summary` and Top results.
|
||||
|
||||
If the user answers "Nein" in step 7, call `confirm_requirements(confirm=false)`
|
||||
and continue capturing/updating requirements.
|
||||
|
||||
#### Roundtrip B: DB task workflow (task_id-based)
|
||||
|
||||
1. `list_open_tasks(limit=...)`
|
||||
2. `get_task_details(task_id)`
|
||||
3. Optional helpers:
|
||||
- `infer_primary_role(task_id=...)` (if role unclear)
|
||||
- `validate_task_requirements(task_id)` (**only if the user explicitly requests validation**; validation is independent from matching)
|
||||
4. Capture/update requirements as needed (e.g. from task details)
|
||||
5. **Review**: `show_pending_requirements()`
|
||||
6. **Ask** (assistant → user): confirm Yes/No
|
||||
7. **Confirm** (only on "Yes"): `confirm_requirements(confirm=true)`
|
||||
8. Execute matching (depending on the client UX):
|
||||
- `find_capacities_for_task(task_id)` OR
|
||||
- `find_matching_capacities(...)`
|
||||
|
||||
> Alternative marker: If the assistant already displayed requirements to the user
|
||||
> outside of the server review table, it MAY call `request_requirements_confirmation()`
|
||||
> instead of `show_pending_requirements()`. The actual confirmation is still
|
||||
> performed exclusively via `confirm_requirements(confirm=true)` after user approval.
|
||||
|
||||
### 2. Database Layer (`src/teamlandkarte_mcp/database/trino_client.py`)
|
||||
|
||||
**Purpose**: Manage Trino/Presto connectivity to the Open Data Lake and execute read-only queries.
|
||||
|
||||
**Database Schema**:
|
||||
|
||||
**Capacity Tables** (existing):
|
||||
- `teamlandkarte_v_capacities_latest`
|
||||
- Fields: `id`, `owner_name`, `role_name`, `role_level`, `begin_date`, `end_date`, `deletion_reason`
|
||||
- Filter: `deletion_reason IS NULL` (active capacities only)
|
||||
- `teamlandkarte_v_capacity_competences_latest`
|
||||
- Fields: `capacity_id` (FK), `competence_id` (FK)
|
||||
- JOIN: Links capacities to competences
|
||||
- `teamlandkarte_v_competences_latest`
|
||||
- Fields: `id`, `name`
|
||||
- Stores competence/skill names
|
||||
|
||||
**Task Tables** (new for Round 5):
|
||||
- `beschaffungstool_kmp_task_latest`
|
||||
- Fields: `id`, `title__c`, `description__c`, `startdate__c`, `enddate__c`, `createddate`, `status__c`
|
||||
- Filter: `status__c = "Veröffentlicht"` (published tasks only)
|
||||
- Note: Role is NOT stored in DB, extracted from description
|
||||
- `beschaffungstool_kmp_skill_latest`
|
||||
- Fields: `task__c` (FK to task.id), `skillname__c`
|
||||
- JOIN: Links tasks to their required skills
|
||||
- Multiple rows per task (one per skill)
|
||||
|
||||
> Note: Trino catalog/schema are configured via `config.toml` (`catalog` / `schema`).
|
||||
> The queries in code use unqualified view names.
|
||||
|
||||
**Key Classes**:
|
||||
```python
|
||||
class TrinoClient:
|
||||
def __init__(self, config: DatabaseConfig):
|
||||
# Initialize Trino DB-API connection
|
||||
|
||||
def get_all_capacities_with_competences(self) -> List[Capacity]:
|
||||
"""Fetch all active capacities with their competences in a single query"""
|
||||
query = """
|
||||
SELECT
|
||||
cap.id,
|
||||
cap.owner_name,
|
||||
cap.role_name,
|
||||
cap.role_level,
|
||||
cap.begin_date,
|
||||
cap.end_date,
|
||||
comp.name as competence_name
|
||||
FROM teamlandkarte_v_capacities_latest cap
|
||||
LEFT JOIN teamlandkarte_v_capacity_competences_latest cap_comp
|
||||
ON cap.id = cap_comp.capacity_id
|
||||
LEFT JOIN teamlandkarte_v_competences_latest comp
|
||||
ON cap_comp.competence_id = comp.id
|
||||
WHERE cap.deletion_reason IS NULL
|
||||
ORDER BY cap.id, comp.name
|
||||
"""
|
||||
# Execute query with proper parameterization
|
||||
# Group results by capacity_id in Python
|
||||
# Return List[Capacity] with competences populated
|
||||
|
||||
def get_open_tasks(self, limit: int = 20) -> List[Task]:
|
||||
"""Fetch latest open tasks with their skills."""
|
||||
|
||||
# Apply LIMIT to tasks before joining skills, otherwise one task with
|
||||
# many skill rows can consume the full LIMIT.
|
||||
query = """
|
||||
WITH latest_tasks AS (
|
||||
SELECT
|
||||
t.id,
|
||||
t.title__c,
|
||||
t.description__c,
|
||||
t.startdate__c,
|
||||
t.enddate__c,
|
||||
t.createddate
|
||||
FROM beschaffungstool_kmp_task_latest t
|
||||
WHERE t.status__c = ?
|
||||
ORDER BY t.createddate DESC
|
||||
LIMIT ?
|
||||
)
|
||||
SELECT
|
||||
t.id,
|
||||
t.title__c,
|
||||
t.description__c,
|
||||
t.startdate__c,
|
||||
t.enddate__c,
|
||||
t.createddate,
|
||||
s.skillname__c
|
||||
FROM latest_tasks t
|
||||
LEFT JOIN beschaffungstool_kmp_skill_latest s
|
||||
ON t.id = s.task__c
|
||||
ORDER BY t.createddate DESC
|
||||
"""
|
||||
# Execute with limit parameter
|
||||
# Group results by task_id (multiple rows per task for skills)
|
||||
# Return List[Task] with skills populated
|
||||
|
||||
def get_task_by_id(self, task_id: str) -> Optional[Task]:
|
||||
"""Fetch a single task with its skills by ID"""
|
||||
query = """
|
||||
SELECT
|
||||
t.id,
|
||||
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.id = ? AND t.status__c = 'Veröffentlicht'
|
||||
"""
|
||||
# Execute with task_id parameter
|
||||
# Group skill rows into single Task object
|
||||
# Return Task or None if not found
|
||||
|
||||
@dataclass
|
||||
class Capacity:
|
||||
id: int
|
||||
owner_name: str
|
||||
role_name: str
|
||||
role_level: str
|
||||
begin_date: date
|
||||
end_date: date
|
||||
competences: List[str] = field(default_factory=list)
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
id: str
|
||||
title: str
|
||||
description: str
|
||||
start_date: Optional[date]
|
||||
end_date: Optional[date]
|
||||
created_date: datetime
|
||||
skills: List[str] = field(default_factory.list)
|
||||
```
|
||||
|
||||
**Safety**:
|
||||
- Connection uses read-only user (enforced by DB permissions)
|
||||
- Query validation to prevent any INSERT/UPDATE/DELETE
|
||||
- Parameterized queries to prevent SQL injection
|
||||
- Single join query reduces round-trips and complexity
|
||||
|
||||
**Implementation Note**:
|
||||
The query returns multiple rows per capacity (one per competence). Python code groups these rows by capacity_id to create the final List[Capacity] structure.
|
||||
|
||||
### 3. Cache Layer (`src/cache/`)
|
||||
|
||||
**Purpose**: Provide two-tier caching for database queries and search results.
|
||||
|
||||
**Search session behavior**:
|
||||
- Matching creates a new `search_id` (UUID) and stores full results in-memory.
|
||||
- Filtering creates a new `filter_id` under the same `search_id`.
|
||||
- Search-related tools emit a JSON block so clients can reliably capture IDs.
|
||||
- Tools reject non-UUID `search_id` early to avoid confusing cache-miss flows.
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
from cachetools import TTLCache
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import uuid
|
||||
|
||||
class QueryCache:
|
||||
"""Database query cache with 12-hour TTL"""
|
||||
def __init__(self, ttl_hours: int = 12, max_size: int = 100):
|
||||
self.cache = TTLCache(
|
||||
maxsize=max_size,
|
||||
ttl=ttl_hours * 3600
|
||||
)
|
||||
self.stats = {"hits": 0, "misses": 0}
|
||||
|
||||
def get_or_fetch(self, key: str, fetch_fn: callable) -> Any:
|
||||
if key in self.cache:
|
||||
self.stats["hits"] += 1
|
||||
return self.cache[key]
|
||||
|
||||
self.stats["misses"] += 1
|
||||
result = fetch_fn()
|
||||
self.cache[key] = result
|
||||
return result
|
||||
|
||||
class SearchCache:
|
||||
"""Search results cache with 60-minute TTL, keyed by search_id"""
|
||||
def __init__(self, ttl_minutes: int = 60, max_size: int = 100):
|
||||
self.cache = TTLCache(
|
||||
maxsize=max_size,
|
||||
ttl=ttl_minutes * 60
|
||||
)
|
||||
self.stats = {"hits": 0, "misses": 0}
|
||||
|
||||
def store_search(self, search_data: dict) -> str:
|
||||
"""Store search results and return search_id"""
|
||||
search_id = str(uuid.uuid4())
|
||||
self.cache[search_id] = json.dumps(search_data)
|
||||
return search_id
|
||||
|
||||
def get_search(self, search_id: str) -> Optional[dict]:
|
||||
"""Retrieve search results by search_id"""
|
||||
if search_id in self.cache:
|
||||
self.stats["hits"] += 1
|
||||
return json.loads(self.cache[search_id])
|
||||
|
||||
self.stats["misses"] += 1
|
||||
return None
|
||||
|
||||
def update_search(self, search_id: str, search_data: dict) -> bool:
|
||||
"""Update existing search results (e.g., add filters)"""
|
||||
if search_id in self.cache:
|
||||
self.cache[search_id] = json.dumps(search_data)
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
**Cache Keys**:
|
||||
|
||||
**QueryCache**:
|
||||
- `"all_capacities_with_competences"` - Complete dataset from database
|
||||
|
||||
**SearchCache**:
|
||||
- `<search_id>` (UUID) - Stores complete search results and associated filters as JSON:
|
||||
```json
|
||||
{
|
||||
"task_id": "T-12345", // NEW: Reference to database task (null for ad-hoc searches)
|
||||
"requirements": {
|
||||
"required_competences": ["Python", "FastAPI"],
|
||||
"preferred_role": "Backend Developer",
|
||||
"candidate_roles": ["Backend Developer", "Software Engineer"],
|
||||
"date_start": "2026-04-01",
|
||||
"date_end": "2026-06-30"
|
||||
},
|
||||
"results_by_category": {
|
||||
"Top": [...],
|
||||
"Good": [...],
|
||||
"Partial": [...],
|
||||
"Low": [...]
|
||||
},
|
||||
"filters": {
|
||||
"filter-1": {
|
||||
"criteria": {
|
||||
"role": "Developer",
|
||||
"competences": ["Python", "Docker"],
|
||||
"min_similarity": 0.7
|
||||
},
|
||||
"results_by_category": {
|
||||
"Top": [...],
|
||||
"Good": [...],
|
||||
"Partial": [...],
|
||||
"Low": [...]
|
||||
},
|
||||
"timestamp": "2026-02-11T14:35:00Z"
|
||||
},
|
||||
"filter-2": {
|
||||
"criteria": {
|
||||
"competences": ["Cloud"],
|
||||
"min_similarity": 0.7
|
||||
},
|
||||
"results_by_category": {...},
|
||||
"timestamp": "2026-02-11T14:40:00Z"
|
||||
}
|
||||
},
|
||||
"timestamp": "2026-02-11T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
- `task_id` field is `null` for ad-hoc searches (via `find_matching_capacities`)
|
||||
- `task_id` field contains database task ID for task-based searches (via `find_capacities_for_task`)
|
||||
- This allows tracking the source of each search for audit and debugging purposes
|
||||
|
||||
**Session State** (in-memory, per client session):
|
||||
- `"last_requirements"` - Stores TaskRequirements object for use with `update_requirements`
|
||||
- Cleared when new search is executed via `find_matching_capacities`
|
||||
|
||||
### 4. Matching Engine (`src/matching/matcher.py`)
|
||||
|
||||
**Purpose**: Analyze tasks and match capacities based on requirements.
|
||||
|
||||
**Key Components**:
|
||||
|
||||
#### Matching Logic
|
||||
```python
|
||||
# NOTE (2026-02): This section is historical pseudocode. The current
|
||||
# implementation uses Azure OpenAI via `AzureOpenAIClient` (chat + embeddings)
|
||||
# and does not rely on MCP sampling.
|
||||
|
||||
class CapacityMatcher:
|
||||
def __init__(
|
||||
self,
|
||||
config: MatchingConfig,
|
||||
similarity_engine,
|
||||
):
|
||||
self.config = config
|
||||
self.similarity_engine = similarity_engine
|
||||
|
||||
async def _match_competences(
|
||||
self,
|
||||
capacity_comps: List[str],
|
||||
required_comps: List[str],
|
||||
) -> float:
|
||||
"""Compute similarity via embeddings + cosine similarity."""
|
||||
|
||||
details = await self.similarity_engine.compute_competence_similarity(
|
||||
required=required_comps,
|
||||
candidate=capacity_comps,
|
||||
)
|
||||
if not details:
|
||||
return 0.0
|
||||
return sum(d["score"] for d in details.values()) / len(details)
|
||||
```
|
||||
|
||||
### 5. Scoring System (`src/matching/scorer.py`)
|
||||
|
||||
**Purpose**: Categorize match results into Top/Good/Partial/Low.
|
||||
|
||||
```python
|
||||
class ScoreCategory(Enum):
|
||||
TOP = "Top"
|
||||
GOOD = "Good"
|
||||
PARTIAL = "Partial"
|
||||
LOW = "Low"
|
||||
|
||||
class Scorer:
|
||||
def __init__(self, config: MatchingConfig):
|
||||
self.thresholds = {
|
||||
ScoreCategory.TOP: config.top_threshold,
|
||||
ScoreCategory.GOOD: config.good_threshold,
|
||||
ScoreCategory.PARTIAL: config.partial_threshold
|
||||
}
|
||||
|
||||
def categorize(self, score: float) -> ScoreCategory:
|
||||
if score >= self.thresholds[ScoreCategory.TOP]:
|
||||
return ScoreCategory.TOP
|
||||
elif score >= self.thresholds[ScoreCategory.GOOD]:
|
||||
return ScoreCategory.GOOD
|
||||
elif score >= self.thresholds[ScoreCategory.PARTIAL]:
|
||||
return ScoreCategory.PARTIAL
|
||||
else:
|
||||
return ScoreCategory.LOW
|
||||
|
||||
def group_by_category(self, results: List[MatchResult]) -> Dict[ScoreCategory, List[MatchResult]]:
|
||||
"""Group results by score category"""
|
||||
grouped = {cat: [] for cat in ScoreCategory}
|
||||
for result in results:
|
||||
category = self.categorize(result.overall_score)
|
||||
result.category = category
|
||||
grouped[category].append(result)
|
||||
return grouped
|
||||
```
|
||||
|
||||
### 6. MCP Server (`src/mcp_server.py`)
|
||||
|
||||
**Purpose**: Expose capacity matching as MCP tools.
|
||||
|
||||
**Workflow Design**:
|
||||
|
||||
The server provides a **multi-step workflow** with separate tools for each phase:
|
||||
|
||||
0. **Task Management Phase** (NEW in Round 5):
|
||||
- `list_open_tasks()` - Browse published tasks from database
|
||||
- `get_task_details()` - View task details with extracted role
|
||||
- `validate_task_requirements()` - Compare DB skills vs description-extracted skills
|
||||
- `find_capacities_for_task()` - Automated capacity search for database task
|
||||
|
||||
1. **Requirement Gathering Phase** (Ad-hoc):
|
||||
- `extract_requirements()` - Extract from natural language task description
|
||||
- `collect_structured_requirement_data()` - Interactive step-by-step collection
|
||||
- `update_requirements()` - Modify previously extracted requirements
|
||||
|
||||
2. **Search Execution Phase**:
|
||||
- `find_matching_capacities()` - Execute search with confirmed requirements
|
||||
|
||||
3. **Result Refinement Phase**:
|
||||
- `filter_search_results()` - Apply fuzzy filters to results
|
||||
- `get_results_by_category()` - Browse results by score category
|
||||
|
||||
**Key Design Principles**:
|
||||
- **Separation of Concerns**: Each tool has a single, clear responsibility
|
||||
- **LLM State Management**: The client-side LLM tracks requirement state across tool calls
|
||||
- **Explicit Confirmation**: Requirements are returned for user confirmation before search execution
|
||||
- **Filter Chain Tracking**: Filters use `filter_id` to maintain navigation hierarchy
|
||||
- **Database Task Integration**: Seamless workflow for published tasks with automatic validation
|
||||
- **Common Extraction Logic**: DRY principle with shared `_extract_requirements_from_description()` function
|
||||
- **Availability as filter**: Availability is displayed and can be used as a date-range filter (not part of scoring)
|
||||
|
||||
**MCP Tools**:
|
||||
@@ -0,0 +1,141 @@
|
||||
# Change: Add Capacity Matching MCP Server (Stage 1)
|
||||
|
||||
## Why
|
||||
DB Systel employees have free work capacities stored in the Open Data Lake, but there's no efficient way for AI assistants to match these capacities with tasks. Tasks are also stored in the database, but finding suitable capacities for them requires manual work. This MCP server will enable AI clients to automatically:
|
||||
1. Browse and search published tasks from the database
|
||||
2. Validate task requirements (comparing DB skills vs description-extracted skills)
|
||||
3. Find the best-suited employees for both database tasks and ad-hoc requirements
|
||||
4. Match capacities based on competences and role, with optional availability (date-range) filtering
|
||||
|
||||
## What Changes
|
||||
- Add MCP server implementation with **10-tool workflow** (task management, requirement gathering, search, and refinement)
|
||||
- Add **database task management** (4 new tools): list tasks, view details, validate requirements, automated search
|
||||
- Add **task database integration**: Query published tasks with skills from Beschaffungstool KMP tables
|
||||
- Add **requirement validation**: Compare database skills vs LLM-extracted skills from description
|
||||
- Add **common extraction function**: DRY principle with `_extract_requirements_from_description()`
|
||||
- Add **internal helper functions**: `extract_requirements_from_task()`, `extract_roles_from_task()`
|
||||
- Add database connection layer for Trino/Presto with single join query
|
||||
- Requirement extraction and semantic matching are implemented via **Azure OpenAI**:
|
||||
- Chat completions for role/requirement extraction
|
||||
- Embeddings + cosine similarity for semantic similarity scoring
|
||||
- Errors are deterministic (no heuristic fallbacks)
|
||||
|
||||
> **Historical note (2026-02)**: The initial Stage-1 proposal assumed only
|
||||
> client-side LLM access via MCP sampling and used heuristic fallbacks because
|
||||
> sampling was not available in the runtime. The current implementation has been
|
||||
> migrated to an Azure OpenAI-only integration (see change
|
||||
> `replace-heuristics-with-azure-openai`).
|
||||
|
||||
- Add **session state management** for requirement tracking across tool calls
|
||||
- Add configurable scoring system with categorical results (Top/Good/Partial/Low)
|
||||
- Add **two-tier caching**: database cache (12h TTL) and search results cache (60min TTL) with task_id tracking
|
||||
- Emit deterministic, machine-readable headers in search-related tool outputs so MCP
|
||||
clients can reliably capture `search_id` / `filter_id`:
|
||||
- first line: `Using SEARCH_ID=<uuid>`
|
||||
- plus `SEARCH_ID=<uuid>`, `FILTER_ID=<uuid>` (if applicable), `META=<json>`
|
||||
- Reject non-UUID `search_id` values early with a clear message
|
||||
- Track the latest search id internally for debugging/consistency, but do **not** expose a "last search id" tool (multi-user/session unsafe)
|
||||
- Add **requirement workflow**: extract from free text, collect structured, or update requirements
|
||||
- Add **explicit confirmation** before search execution
|
||||
- Remove availability from weighted scoring (availability is displayed and can be used as a filter)
|
||||
- Update default weights: competence 0.8, role 0.2
|
||||
- Keep date range as an optional availability filter (when provided)
|
||||
- Add interactive result exploration with **search session management** (search_id)
|
||||
- Add **fuzzy filtering** for post-search refinement (role and/or competences as list)
|
||||
- Add **filter tracking** with filter_id (no search_id proliferation)
|
||||
- Add **pagination** for result browsing (20 results per page)
|
||||
- Add database configuration in `database.toml` (Trino)
|
||||
- Add **Arc42 architecture documentation** with ADRs
|
||||
|
||||
### Strict two-step confirmation (Review → Ask → Confirm)
|
||||
|
||||
When `matching.require_confirmation = true` (default), the server enforces a
|
||||
strict, user-driven confirmation sequence before any matching run:
|
||||
|
||||
1. Requirements are captured/updated into **pending** state (e.g. via
|
||||
`extract_requirements`, `collect_structured_requirement_data`, `update_requirements`,
|
||||
or guided capture tools).
|
||||
2. The assistant must **review** pending requirements using
|
||||
`show_pending_requirements()` (preferred) or, if requirements were already
|
||||
shown to the user in chat, call `request_requirements_confirmation()` as a
|
||||
marker.
|
||||
3. The assistant must **ask** the user explicitly for Yes/No confirmation.
|
||||
4. Only after user approval, the assistant calls `confirm_requirements(confirm=true)`.
|
||||
If the user says No, it calls `confirm_requirements(confirm=false)` and
|
||||
continues requirement capture.
|
||||
|
||||
The server refuses to confirm requirements unless step 2 has happened first
|
||||
(prevents auto-confirm in the same turn as requirement capture).
|
||||
|
||||
**Stage 1 Scope:**
|
||||
This is the first development stage focusing on basic matching functionality and database task integration. Future stages will add more sophisticated features (to be specified later).
|
||||
|
||||
**Key Architectural Decisions:**
|
||||
- **Azure OpenAI integration**: Server calls Azure OpenAI directly for extraction
|
||||
and embeddings (no sampling dependency)
|
||||
- **10-tool workflow**: Separate tools for task management (4), requirement gathering (3), search execution (1), and result refinement (2)
|
||||
- **Database task integration**: Direct access to published tasks with skills from Beschaffungstool KMP
|
||||
- **Validation-only approach**: Validation compares DB vs description but always uses DB skills for search
|
||||
- **Common extraction function**: `_extract_requirements_from_description()` as single source of truth (DRY principle)
|
||||
- **Automatic task workflow**: `find_capacities_for_task()` fully automated with validation display
|
||||
- **Task reference tracking**: Search cache includes `task_id` field (null for ad-hoc, task ID for DB tasks)
|
||||
- **Explicit confirmation**: Requirements returned for user confirmation before search
|
||||
- **Session state**: In-memory storage of requirements for update workflow
|
||||
- **Filter tracking**: Filters stored within search cache using filter_id (not new search_id)
|
||||
- **Search sessions**: Complete results stored server-side, accessed via search_id (60min expiry)
|
||||
- **Single query pattern**: One JOIN query fetches all data, processed in Python
|
||||
- **Markdown output**: All tables formatted as markdown for better client rendering
|
||||
- **No truncation**: Full competence lists always shown
|
||||
- **No availability scoring**: Availability is displayed and can be used as a date-range filter, but does not influence the score
|
||||
- **Weights**: competence 0.8, role 0.2
|
||||
- **Fuzzy matching**: Filter tool uses fuzzywuzzy for flexible result refinement
|
||||
- **LLM integration**: Azure OpenAI (`gpt-4.1` chat + `text-embedding-3-large`)
|
||||
with embedding cache; no heuristic fallback
|
||||
|
||||
## Impact
|
||||
- **New capability**: `capacity-matching` - Core matching functionality with database task integration
|
||||
- **New files**:
|
||||
- `database.toml` - Database connection configuration
|
||||
- `architecture.md` - Arc42 architecture documentation with ADRs
|
||||
- `src/teamlandkarte_mcp/mcp_server.py` - MCP server implementation (10 tools)
|
||||
- `src/teamlandkarte_mcp/database/trino_client.py` - Trino client (capacity + task queries)
|
||||
- `src/teamlandkarte_mcp/matching/task_analyzer.py` - Azure-based task analysis
|
||||
- `src/matching/matcher.py` - Matching algorithm with common extraction function
|
||||
- `src/matching/scorer.py` - Scoring logic
|
||||
- `src/cache/query_cache.py` - Database query caching (12h TTL)
|
||||
- `src/cache/search_cache.py` - Search results caching (60min TTL) with task_id tracking
|
||||
- `src/config.py` - Configuration management
|
||||
- `requirements.txt` or updated `pyproject.toml` - Dependencies
|
||||
- **Modified files**:
|
||||
- `main.py` - Launch MCP server
|
||||
- `pyproject.toml` - Add dependencies
|
||||
- `README.md` - Update with task management features, validation workflow
|
||||
- **Dependencies added**:
|
||||
- `mcp` - Model Context Protocol SDK
|
||||
- `trino` - Trino/Presto database client
|
||||
- `cachetools` - Caching implementation
|
||||
- `fuzzywuzzy` - Fuzzy string matching for filters
|
||||
|
||||
## Security & Constraints
|
||||
- **Read-only operations**: Server must NEVER modify database data
|
||||
- **Credential storage**: Credentials are provided via environment variables
|
||||
(recommended: local `.env`): `DATA_LAKE_USERNAME`, `DATA_LAKE_PASSWORD`.
|
||||
`database.toml` contains non-secret connection settings and must not be
|
||||
committed.
|
||||
- **Network security**: Trino/Presto connection over port 8446
|
||||
- **Data filtering**:
|
||||
- Capacities: Only consider where `deletion_reason IS NULL`
|
||||
- Tasks: Only consider where `status__c = "Veröffentlicht"`
|
||||
- **LLM access**: Not available in the current server runtime. If reintroduced,
|
||||
it must use a supported MCP API.
|
||||
- **SQL injection prevention**: All queries use proper parameterization
|
||||
- **Validation scope**: Comparison only (skills and dates), role not validated (not in DB yet)
|
||||
|
||||
## Non-Goals (Future Stages)
|
||||
- Advanced filtering options beyond fuzzy matching
|
||||
- Capacity booking/reservation
|
||||
- Task modification or status updates
|
||||
- Historical analysis
|
||||
- Integration with other systems
|
||||
- Multi-language support
|
||||
- Role storage in database (currently extracted from description)
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Capacity Matching Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Workflow overview (Round 6)
|
||||
|
||||
The system SHALL support two primary workflows:
|
||||
|
||||
1. **Database task workflow (Phase 0)**: Users browse published tasks from the database, inspect details, optionally validate, and run a capacity search for an existing task.
|
||||
2. **Ad-hoc workflow (Phases 1-3)**: Users gather requirements (free-text extraction, structured input, or guided capture), optionally update requirements, explicitly confirm, execute search, and refine results.
|
||||
|
||||
The system SHALL expose the MCP tools required to support the workflows below. The implementation MAY add additional tools over time.
|
||||
|
||||
#### Scenario: Tool surface includes both DB-task and ad-hoc workflows
|
||||
|
||||
**Phase 0 – Task Management (Database tasks)**
|
||||
- `list_open_tasks(limit=20)`
|
||||
- `get_task_details(task_id)`
|
||||
- `validate_task_requirements(task_id)`
|
||||
- `find_capacities_for_task(task_id)`
|
||||
- `infer_roles(task_id? | task_description?, limit=5)`
|
||||
|
||||
**Phase 1 – Requirement Gathering (Ad-hoc)**
|
||||
- `extract_requirements(task_description)` → writes pending requirements
|
||||
- `collect_structured_requirement_data(role_name?, competences?, date_start?, date_end?, description?)` → writes pending requirements
|
||||
- `update_requirements(change_description)` → writes pending requirements
|
||||
- Guided capture tools:
|
||||
- `start_guided_capture()`
|
||||
- `guided_set_description(description)`
|
||||
- `guided_set_role(role_name)`
|
||||
- `guided_set_time_range(date_start?, date_end?)` (open-ended allowed)
|
||||
- `guided_set_competences(competences)` → writes pending requirements
|
||||
- Review/ask tools:
|
||||
- `show_pending_requirements()` → returns a review table and marks that user confirmation was requested
|
||||
- `request_requirements_confirmation()` → marks that the assistant asked the user to confirm (no table)
|
||||
- `confirm_requirements(confirm: bool = True)`
|
||||
|
||||
**Phase 2 – Search Execution (Ad-hoc)**
|
||||
- `find_matching_capacities(role_name, competences, date_start?, date_end?)`
|
||||
|
||||
**Phase 3 – Result Refinement**
|
||||
- `filter_search_results(search_id, role_filter?, competence_filter?, availability_date_start?, availability_date_end?, min_similarity=0.7)` → returns `filter_id`
|
||||
- `get_results_by_category(search_id, filter_id?, category="Top", page=1, page_size=20)`
|
||||
|
||||
### Requirement: Confirmation gating
|
||||
|
||||
The system SHALL ensure that matching execution is confirmation-gated by default and behaves deterministically based on configuration.
|
||||
|
||||
#### Scenario: Matching is refused until requirements are confirmed (default)
|
||||
|
||||
- Confirmation gating:
|
||||
- The server's confirmation state is stored in in-memory session state and is **not** safe as a multi-client / multi-tenant source of truth.
|
||||
Therefore, treat confirmation gating primarily as a **client-side UX rule**: clients should only trigger matching after an explicit user Yes/No.
|
||||
- If `matching.require_confirmation = true` (default), matching tools MUST refuse to run unless requirements have been explicitly confirmed.
|
||||
- If `matching.require_confirmation = false`, clients MUST NOT ask for confirmation and MUST NOT call confirmation tools; matching MUST proceed once requirements are complete.
|
||||
- The confirmation flow MUST be two-step:
|
||||
1) `show_pending_requirements()` (preferred) OR `request_requirements_confirmation()`
|
||||
2) user explicitly confirms in chat
|
||||
3) `confirm_requirements(confirm=true)`
|
||||
- Servers MAY refuse `confirm_requirements(confirm=true)` if step (1) did not occur.
|
||||
|
||||
### Requirement: Tool outputs and result headers
|
||||
|
||||
The system SHALL emit table-first tool output formats and deterministic, machine-readable headers for search-session tools.
|
||||
|
||||
#### Scenario: Tool results are table-first and include deterministic header metadata
|
||||
|
||||
- Tool outputs:
|
||||
- `get_task_details` MUST be table-first (summary table + description below)
|
||||
- `validate_task_requirements` MUST be table-first (single comparison table + short written summary)
|
||||
- `infer_roles` MUST return a ranked table: `Rank | Role | Rationale`
|
||||
|
||||
- Search session output headers (robust client parsing):
|
||||
- The tools `find_matching_capacities`, `filter_search_results`, and `get_results_by_category` MUST emit deterministic, machine-readable header lines.
|
||||
- On success, the first line MUST be exactly:
|
||||
- `Using SEARCH_ID=<uuid>`
|
||||
- The output MUST also contain:
|
||||
- `SEARCH_ID=<uuid>`
|
||||
- `FILTER_ID=<uuid>` (only if a filter was created/used)
|
||||
- `META=<json>` (MUST include at least `{ "search_id": "...", "status": "ok"|... }`)
|
||||
|
||||
### Requirement: Search session validation and table guarantees
|
||||
|
||||
The system SHALL validate search session identifiers deterministically and MUST always return well-formed Markdown tables for search result tools.
|
||||
|
||||
#### Scenario: Invalid or expired search_id inputs produce deterministic errors and tables are always present
|
||||
|
||||
- Search session validation:
|
||||
- `search_id` inputs MUST be validated as UUIDs.
|
||||
- If the `search_id` is not a UUID, the tool MUST return `META.status=invalid_search_id_format` and guidance to copy the id from tool output.
|
||||
- If the `search_id` is unknown/expired, the tool MUST return `META.status=unknown_or_expired` and guidance to re-run the matching tool.
|
||||
|
||||
- Table-first guarantees for search results:
|
||||
- `find_matching_capacities` MUST include the `## Summary` table.
|
||||
- `find_matching_capacities` MUST include a results table for the **first non-empty category** in this order: Top → Good → Partial → Low.
|
||||
- If all categories are empty, it MUST still emit a valid Markdown table (a single dummy row is acceptable).
|
||||
- `filter_search_results` and `get_results_by_category` MUST NOT repeat the summary counts table.
|
||||
- `filter_search_results` MUST output a flat results table (not split by category) and MUST include a `Category` column **separate** from the numeric `Score`.
|
||||
- `filter_search_results` MUST always return the results as a valid Markdown table (even for exactly one match, and also when there are zero matches).
|
||||
@@ -0,0 +1,230 @@
|
||||
## 1. Project Setup
|
||||
- [x] 1.1 Update `pyproject.toml` with required dependencies (mcp, trino, cachetools)
|
||||
- [x] 1.2 Create `database.toml` with connection configuration structure (template only, no real credentials)
|
||||
- [x] 1.3 Create project structure: `src/`, `src/database/`, `src/matching/`, `src/cache/`
|
||||
- [x] 1.4 Add `.gitignore` entry for `database.toml` to prevent credential exposure
|
||||
|
||||
## 2. Configuration Management
|
||||
- [x] 2.1 Create `src/config.py` to load database and matching configuration
|
||||
- [x] 2.2 Implement configuration validation (ensure required fields present)
|
||||
- [x] 2.3 Add configurable scoring thresholds (Top: ≥80%, Good: 60-79%, Partial: 40-59%, Low: <40%)
|
||||
- [x] 2.4 Add configurable matching weights (competence: 0.8, role: 0.2)
|
||||
- [x] 2.5 Validate that weights sum to 1.0
|
||||
- [x] 2.6 Support two cache TTL settings: db_ttl_hours (12h), search_ttl_minutes (60min)
|
||||
- [x] 2.7 Add fuzzy matching configuration: min_similarity (default: 0.7)
|
||||
|
||||
## 3. Database Layer
|
||||
- [x] 3.1 Create Trino client with connection handling
|
||||
- [x] 3.2 Implement connection pooling/management
|
||||
- [x] 3.3 **VERIFY SCHEMA**: Confirm database schema and field names with actual Open Data Lake tables (via Trino)
|
||||
- [x] 3.4 Implement single join query method:
|
||||
- [x] 3.4.1 `get_all_capacities_with_competences()` - Single query joining all 3 tables
|
||||
- [x] 3.4.2 Filter: deletion_reason IS NULL
|
||||
- [x] 3.4.3 Use proper parameterized queries (no string interpolation)
|
||||
- [x] 3.4.4 Group results by capacity_id in Python to build List[Capacity]
|
||||
- [x] 3.5 Ensure read-only operations (no INSERT/UPDATE/DELETE queries)
|
||||
- [x] 3.6 Add error handling for connection failures with retry logic (3x, exponential backoff)
|
||||
- [x] 3.7 Add query logging for debugging (without sensitive data)
|
||||
- [x] 3.8 **NEW**: Implement task database queries:
|
||||
- [x] 3.8.1 Create `Task` dataclass with fields: id, title__c, description__c, startdate__c, enddate__c, createddate, skills
|
||||
- [x] 3.8.2 `get_open_tasks(limit)` - Fetch published tasks sorted by createddate DESC
|
||||
- [x] 3.8.3 JOIN task table with task skills table (task.id = skill.task__c)
|
||||
- [x] 3.8.4 Filter: status__c = "Veröffentlicht"
|
||||
- [x] 3.8.5 Group results by task_id in Python to build List[Task]
|
||||
- [x] 3.8.6 `get_task_by_id(task_id)` - Fetch single task with skills by ID
|
||||
- [x] 3.8.7 Use parameterized queries for task_id parameter
|
||||
|
||||
## 4. Caching Layer
|
||||
- [x] 4.1 Create `src/cache/query_cache.py` for database cache (12h TTL)
|
||||
- [x] 4.2 Create `src/cache/search_cache.py` for search results cache (60min TTL)
|
||||
- [x] 4.3 Implement QueryCache with cachetools.TTLCache:
|
||||
- [x] 4.3.1 Key: "all_capacities_with_competences"
|
||||
- [x] 4.3.2 TTL: 12 hours
|
||||
- [x] 4.3.3 Cache statistics (hits/misses)
|
||||
- [x] 4.4 Implement SearchCache with cachetools.TTLCache:
|
||||
- [x] 4.4.1 Key: search_id (UUID)
|
||||
- [x] 4.4.2 Value: JSON-serialized search results with task_id (null or task ID), filters dict
|
||||
- [x] 4.4.3 TTL: 60 minutes
|
||||
- [x] 4.4.4 Methods: store_search() returns search_id, get_search(search_id) returns dict, update_search() for adding filters
|
||||
- [x] 4.5 Ensure thread-safe cache operations
|
||||
|
||||
## 5. Matching Engine
|
||||
- [x] 5.1 Create `src/matching/matcher.py` for matching logic
|
||||
- [x] 5.2 Implement task analysis using Azure OpenAI (chat completion):
|
||||
- [x] 5.2.1 **NEW**: Create common internal function `_extract_requirements_from_description(description)` as single source of truth
|
||||
- [x] 5.2.2 Free-text mode: `analyze_free_text()` delegates to common extraction function
|
||||
- [x] 5.2.3 Structured mode: Parse provided parameters directly
|
||||
- [x] 5.2.4 Generate and return task analysis summary for free-text mode
|
||||
- [x] 5.2.5 Handle Azure API errors (timeouts, invalid JSON) with retry logic
|
||||
- [x] 5.2.6 **NEW**: Implement internal helper `extract_requirements_from_task(task_id)` - fetches task from DB and uses common extraction
|
||||
- [x] 5.2.7 **UPDATED**: Implement internal helper `extract_roles_from_task(description)` - returns ranked role list; primary role is roles[0] (if any)
|
||||
- [x] 5.3 Implement matching algorithm:
|
||||
- [x] 5.3.1 Semantic competence matching via Azure OpenAI embeddings
|
||||
- [x] 5.3.2 Role name matching (exact and similarity)
|
||||
- [x] 5.3.3 Availability date range filtering (open-ended capacities: missing end_date means available without limit)
|
||||
- [x] 5.3.4 Availability is NOT scored. If date_start/date_end are provided, availability is used as a filter only.
|
||||
- [x] 5.3.5 Apply configured weights (competence + role) to calculate overall score
|
||||
- [x] 5.4 Create `src/matching/scorer.py` for scoring logic
|
||||
- [x] 5.5 Implement categorical scoring (Top/Good/Partial/Low) with configurable thresholds
|
||||
- [x] 5.6 Generate match score and category for each capacity
|
||||
- [x] 5.7 Group results by category for storage
|
||||
|
||||
## 6. MCP Server Implementation
|
||||
- [x] 6.1 Create `src/mcp_server.py` with MCP SDK integration
|
||||
- [x] 6.2 Integrate Azure OpenAI client for LLM access (chat + embeddings)
|
||||
- [x] 6.3 Implement session state management (in-memory, per client session)
|
||||
- [x] 6.3.1 Store `last_requirements` for use with update_requirements
|
||||
- [x] 6.3.2 Clear session state when new search is executed
|
||||
- [x] 6.4 **NEW**: Implement MCP tool: `list_open_tasks` (Phase 0)
|
||||
- [x] 6.4.1 Tool parameters: limit (default: 20)
|
||||
- [x] 6.4.2 Query database for published tasks (status = "Veröffentlicht")
|
||||
- [x] 6.4.3 Sort by createddate DESC (newest first)
|
||||
- [x] 6.4.4 Format as markdown table: Task ID, Title, Created Date, Start/End Dates, Skills Count
|
||||
- [x] 6.4.5 Return table with next steps hints
|
||||
- [x] 6.5 **NEW**: Implement MCP tool: `get_task_details` (Phase 0)
|
||||
- [x] 6.5.1 Tool parameters: task_id (required)
|
||||
- [x] 6.5.2 Fetch task from database by ID
|
||||
- [x] 6.5.3 Extract ranked roles from description using `extract_roles_from_task()` helper; assign primary role as roles[0]
|
||||
- [x] 6.5.4 Format task details with metadata, description, DB skills, extracted role
|
||||
- [x] 6.5.5 Return formatted details with next steps hints
|
||||
- [x] 6.6 **NEW**: Implement MCP tool: `validate_task_requirements` (Phase 0)
|
||||
- [x] 6.6.1 Tool parameters: task_id (required)
|
||||
- [x] 6.6.2 Fetch task from database
|
||||
- [x] 6.6.3 Extract requirements from description using `extract_requirements_from_task()` helper
|
||||
- [x] 6.6.4 Compare DB skills vs extracted skills (set operations)
|
||||
- [x] 6.6.5 Compare DB dates vs extracted dates
|
||||
- [x] 6.6.6 Build comparison table: Type, In Description, In Database, Status
|
||||
- [x] 6.6.7 Generate summary with matched/missing counts
|
||||
- [x] 6.6.8 Return validation table (informational only, no modifications)
|
||||
- [x] 6.7 **NEW**: Implement MCP tool: `find_capacities_for_task` (Phase 0)
|
||||
- [x] 6.7.1 Tool parameters: task_id (required)
|
||||
- [x] 6.7.2 Fetch task from database
|
||||
- [x] 6.7.3 Extract requirements for validation display
|
||||
- [x] 6.7.4 Run validation internally (build comparison table)
|
||||
- [x] 6.7.5 Build search requirements using DB skills, extracted role, DB dates
|
||||
- [x] 6.7.6 Execute capacity search (same as find_matching_capacities)
|
||||
- [x] 6.7.7 Store in search cache with task_id field set to task ID
|
||||
- [x] 6.7.8 Return validation table + search results (combined response)
|
||||
- [x] 6.8 **EXISTING**: Implement MCP tool: `extract_requirements` (Phase 1)
|
||||
- [x] 6.8.1 Tool parameters: task_description (required), confirm_requirements (default: True)
|
||||
- [x] 6.8.2 Use Azure OpenAI (chat completion) to extract structured requirements from free text
|
||||
- [x] 6.8.3 Store extracted requirements in session state
|
||||
- [x] 6.8.4 Return JSON-formatted requirements
|
||||
- [x] 6.8.5 Include confirmation prompt if confirm_requirements=True
|
||||
- [x] 6.9 **EXISTING**: Implement MCP tool: `collect_structured_requirement_data` (Phase 1)
|
||||
- [x] 6.9.1 Tool parameters: role_name, competences, date_start, date_end (all optional), confirm_requirements (default: True)
|
||||
- [x] 6.9.2 Check completeness: require at least role_name AND competences
|
||||
- [x] 6.9.3 Prompt for missing required parameters with examples
|
||||
- [x] 6.9.4 Store collected requirements in session state
|
||||
- [x] 6.9.5 Return JSON-formatted requirements with confirmation prompt
|
||||
- [x] 6.10 **EXISTING**: Implement MCP tool: `update_requirements` (Phase 1)
|
||||
- [x] 6.10.1 Tool parameters: change_description (required), confirm_requirements (default: True)
|
||||
- [x] 6.10.2 Check if requirements exist in session state
|
||||
- [x] 6.10.3 Use Azure OpenAI (chat completion) to interpret change and update requirements
|
||||
- [x] 6.10.4 Update session state with modified requirements
|
||||
- [x] 6.10.5 Return updated JSON-formatted requirements with confirmation prompt
|
||||
- [x] 6.11 **EXISTING**: Implement MCP tool: `find_matching_capacities` (Phase 2)
|
||||
- [x] 6.11.1 Tool parameters: role_name (required), competences (required), date_start (optional), date_end (optional)
|
||||
- [x] 6.11.2 **NO extraction or interpretation** - accept only explicit structured parameters
|
||||
- [x] 6.11.3 Execute matching algorithm (uses Azure OpenAI embeddings for competence matching; score uses competences+role only; availability is filter-only)
|
||||
- [x] 6.11.4 Generate summary table with count by score category (markdown format)
|
||||
- [x] 6.11.5 Store complete results in SearchCache with task_id=null, empty filters dict
|
||||
- [x] 6.11.6 Return search_id + summary + Top category results (markdown table)
|
||||
- [x] 6.11.7 Clear session state after successful search
|
||||
- [x] 6.12 **EXISTING**: Implement MCP tool: `filter_search_results` (Phase 3)
|
||||
- [x] 6.12.1 Ensure fuzzywuzzy dependency is installed
|
||||
- [x] 6.12.2 Tool parameters: search_id (required), role_filter (optional), competence_filter (optional List[str]), availability_date_start (optional), availability_date_end (optional), min_similarity (default: 0.7)
|
||||
- [x] 6.12.3 **ALWAYS use original search_id as input** (not filtered search_id)
|
||||
- [x] 6.12.4 Validate at least one filter is provided
|
||||
- [x] 6.12.5 Retrieve original search results from SearchCache
|
||||
- [x] 6.12.6 Collect all results across all categories
|
||||
- [x] 6.12.7 Apply fuzzy matching: role_filter (fuzzy match on role_name), competence_filter (ALL competences must match at least one capacity competence)
|
||||
- [x] 6.12.8 Combine filters with AND logic
|
||||
- [x] 6.12.9 Re-categorize filtered results using Scorer
|
||||
- [x] 6.12.10 Generate unique filter_id (e.g., "filter-1", "filter-2")
|
||||
- [x] 6.12.11 Store filtered results under search_data["filters"][filter_id]
|
||||
- [x] 6.12.12 Update search cache with filter data
|
||||
- [x] 6.12.13 Return filter_id + search_id + filter summary + Top category results
|
||||
- [x] 6.13 **EXISTING**: Implement MCP tool: `get_results_by_category` (Phase 3)
|
||||
- [x] 6.13.1 Tool parameters: search_id (required), filter_id (optional), category (default: "Top"), page (default: 1), page_size (default: 20)
|
||||
- [x] 6.13.2 Retrieve search results from SearchCache by search_id
|
||||
- [x] 6.13.3 If filter_id provided, retrieve filtered results from search_data["filters"][filter_id]
|
||||
- [x] 6.13.4 If filter_id not provided, use original search results
|
||||
- [x] 6.13.5 Extract category results, apply pagination
|
||||
- [x] 6.13.6 Format as markdown table with full competence lists
|
||||
- [x] 6.13.7 Return paginated results with page info and next page hint
|
||||
- [x] 6.14 Format results as markdown tables with fields: id, owner_name, role_name, role_level, begin_date, end_date, competences
|
||||
- [x] 6.15 Show full competence lists (no truncation)
|
||||
- [x] 6.16 Add tool descriptions and parameter schemas for MCP client
|
||||
|
||||
> **Update (2026-02)**: Items previously described as "via MCP sampling" are now
|
||||
> implemented with **Azure OpenAI** (chat + embeddings) as part of the change
|
||||
> `replace-heuristics-with-azure-openai`.
|
||||
>
|
||||
> There is **no runtime MCP sampling dependency** and **no heuristic fallback**
|
||||
> path in the current implementation.
|
||||
|
||||
## 7. Integration & Entry Point
|
||||
- [x] 7.1 Update `main.py` to launch MCP server
|
||||
- [x] 7.2 Add command-line arguments for server configuration (port, debug mode, etc.)
|
||||
- [x] 7.3 Implement graceful startup and shutdown
|
||||
- [x] 7.4 Add logging configuration
|
||||
|
||||
## 8. Testing & Validation
|
||||
- [x] 8.1 Manual testing with sample task descriptions
|
||||
- [x] 8.2 Verify database connection and single join query execution
|
||||
- [x] 8.3 Test both input modes (free-text via Azure OpenAI and structured)
|
||||
- [x] 8.4 **NEW**: Test incremental structured mode (partial parameters with prompts)
|
||||
- [x] 8.5 Validate scoring and categorization with updated weights (0.8/0.2)
|
||||
- [x] 8.6 Test that availability does not affect score and is only applied as an optional filter
|
||||
- [x] 8.7 Test two-tier caching behavior (DB cache 12h, search cache 60min)
|
||||
- [x] 8.8 Verify read-only constraint (no writes to database)
|
||||
- [x] 8.9 Test error handling (database unavailable, Azure OpenAI timeout, invalid search_id)
|
||||
- [x] 8.10 Test search session management (search_id creation and retrieval)
|
||||
- [x] 8.11 Test pagination in get_results_by_category
|
||||
- [x] 8.12 Verify markdown table formatting and full competence display
|
||||
- [x] 8.13 **NEW**: Test filter_search_results with role filter only
|
||||
- [x] 8.14 **NEW**: Test filter_search_results with competence filter only
|
||||
- [x] 8.15 **NEW**: Test filter_search_results with combined filters
|
||||
- [x] 8.16 **NEW**: Test multi-step filtering (filter a filtered search)
|
||||
- [x] 8.17 **NEW**: Test fuzzy matching with various similarity thresholds
|
||||
|
||||
## 9. Documentation
|
||||
- [x] 9.1 Update `README.md` with project description and setup instructions
|
||||
- [x] 9.2 Document `database.toml` structure and required fields
|
||||
- [x] 9.3 Document three MCP tools and their parameters (find_matching_capacities, get_results_by_category, filter_search_results)
|
||||
- [x] 9.4 Add usage examples for both input modes (free-text and incremental structured)
|
||||
- [x] 9.5 Document configuration options (weights, thresholds, cache TTLs, fuzzy matching)
|
||||
- [x] 9.6 Add troubleshooting guide
|
||||
- [x] 9.7 Document Azure OpenAI architecture and client-side LLM requirement
|
||||
- [x] 9.8 Document pagination and search session workflow
|
||||
- [x] 9.9 Document availability behavior (display + date-range filtering; open-ended capacities)
|
||||
- [x] 9.10 **NEW**: Document fuzzy filtering workflow and multi-step filtering
|
||||
- [x] 9.11 **NEW**: Add examples of filter_search_results usage
|
||||
|
||||
## 10. Security & Configuration
|
||||
- [x] 10.1 Ensure `database.toml` is in `.gitignore`
|
||||
- [x] 10.2 Create `database.toml.example` as template (already done)
|
||||
- [x] 10.3 Validate secure credential handling
|
||||
- [x] 10.4 Document security best practices
|
||||
- [x] 10.5 Add warning about credential rotation if database.toml was committed
|
||||
|
||||
## 11. Architecture Documentation
|
||||
- [x] 11.1 Review architecture.md (Arc42 format) - already created with 9 ADRs
|
||||
- [x] 11.2 Validate ADRs (Architecture Decision Records) align with implementation
|
||||
- [x] 11.3 Update component diagrams if needed during implementation
|
||||
- [x] 11.4 Verify ADR-007 (Availability is a Filter, Not a Scoring Criterion) implementation
|
||||
- [x] 11.5 **NEW**: Verify ADR-008 (Incremental Structured Mode) implementation
|
||||
- [x] 11.6 **NEW**: Verify ADR-009 (Fuzzy Filtering) implementation
|
||||
- [x] 11.7 Document any additional architectural decisions made during development
|
||||
|
||||
## Notes
|
||||
- All database operations must be read-only
|
||||
- LLM access only via Azure OpenAI (chat + embeddings)
|
||||
- Two separate caches with different TTLs (DB: 12h, Search: 60min)
|
||||
- Single join query for all data retrieval
|
||||
- Markdown tables for all output
|
||||
- Full competence lists (no truncation)
|
||||
- Search sessions via search_id (60min expiry)
|
||||
- Initial implementation focuses on core functionality; refinements in future stages
|
||||
- Total: tracked via `openspec list` (this markdown count is not authoritative)
|
||||
Reference in New Issue
Block a user