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**:
|
||||
Reference in New Issue
Block a user