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
+720
View File
@@ -0,0 +1,720 @@
# Architecture Documentation (Arc42)
## 1. Introduction and Goals
### 1.1 Requirements Overview
The Teamlandkarte MCP Server enables AI assistants to match **tasks** to DB Systel employees with **free work capacity**, and to match a **specific capacity** to relevant **tasks**.
The server:
- queries the DB Systel Open Data Lake (Trino) read-only
- performs **BM25+RRF lexical matching** for competence similarity
- uses **LLM chat completions** (Azure OpenAI) for role similarity and role inference
- returns deterministic, table-first Markdown outputs suitable for chat UIs
Availability is displayed and may be applied as an optional date-range filter. It does **not** influence similarity scoring.
**Key Features:**
- MCP workflow for DB tasks and ad-hoc matching
- LLM-based role inference (tool: `infer_primary_role`) using Data Lake role vocabulary
- BM25+RRF competence matching with optional LLM-based auto-tagging pre-expansion
- **Two matching methods** selectable per call via `matching_method`:
- `score` (default): BM25+RRF competence + LLM role similarity → numeric scores + category
- `llm_fulltext`: LLM-based full-text comparison of complete profiles → category + rationale (no numeric scores)
- Capacity browsing + capacity-to-task matching tools:
- `list_free_capacities`, `get_capacity_details`, `find_matching_tasks`
- **Hard confirmation gate** before matching runs by default (`matching.require_confirmation = true`)
- Interactive result exploration with search session management (search_id + filter_id)
- Multi-level caching (DB query cache + search result cache)
- **Strict DB schema verification at startup** (fail-fast) against required view columns
- Fuzzy filtering on search results (role, competence) via `fuzzywuzzy`
### 1.2 Quality Goals
| Priority | Quality Goal | Motivation |
|----------|-------------|------------|
| 1 | **Security** | Read-only database access, secure credential management, no data leakage |
| 2 | **Performance** | Fast response times through caching, connection pooling |
| 3 | **Usability** | Simple MCP interface, clear result presentation, deterministic tool outputs |
| 4 | **Maintainability** | Modular architecture, clear separation of concerns, testable components |
| 5 | **Reliability** | Fail-fast schema checks, graceful degradation on LLM failures |
#### Reliability
- The server uses Azure OpenAI for **chat completions** (role similarity, role inference, auto-tagging).
- There is **no embedding dependency** -- competence matching is purely lexical (BM25+RRF).
- LLM failures in role similarity return 0.0 (graceful degradation).
- LLM failures in auto-tagging fall back to the unmodified candidate list.
- LLM failures in role inference return `None` (surfaced to the user).
### 1.3 Stakeholders
| Role | Contact | Expectations |
|------|---------|--------------|
| Product Owner | Thomas Handke | Feature delivery, quality, timeline |
| End Users | DB Systel Employees | Fast, accurate matching |
| Database Team | DB Systel IT | Minimal database load, read-only access |
| Security Team | DB Systel Security | Credential protection, audit logging |
---
## 2. Architecture Constraints
### 2.1 Technical Constraints
| Constraint | Background |
|------------|------------|
| Python 3.13+ | Required for latest language features and type hints |
| Trino client | Required for Trino/Presto connectivity to the Open Data Lake |
| MCP Protocol | Must comply with Model Context Protocol specification (FastMCP SDK) |
| Azure OpenAI chat completions | Required for role similarity, role inference, and optional auto-tagging |
| Read-only database | No write operations allowed on production database |
| Strict schema verification | Server fails fast if required view columns are missing |
### 2.2 Organizational Constraints
| Constraint | Background |
|------------|------------|
| Internal DB Infrastructure | Hosted on Deutsche Bahn Systel GitLab and data lake |
| Credential Management | Stored in environment variables / `.env`, not in version control |
| OpenSpec Workflow | All significant changes require spec proposals |
### 2.3 Conventions
| Convention | Details |
|------------|---------|
| Code Style | PEP 8, type hints, dataclasses for DTOs, ruff for linting/formatting |
| Configuration | TOML format (`config.toml`) for all configuration |
| API Design | MCP tools with clear parameter schemas, Markdown table outputs |
| Documentation | Inline docstrings, OpenSpec for architecture decisions |
| Testing | pytest + pytest-asyncio + hypothesis |
---
## 3. System Scope and Context
### 3.1 Business Context
```text
End User (DB Systel Employee)
| natural language
AI Assistant (MCP client)
| MCP (tool calls over stdio)
Teamlandkarte MCP Server
| SQL (read-only)
DB Systel Open Data Lake (Trino)
Teamlandkarte MCP Server
| HTTPS (chat completions)
Azure OpenAI
```
**External Entities:**
| Entity | Role | Interface |
|--------|------|-----------|
| AI Assistant (Claude, GPT, etc.) | Interprets user requests and calls MCP tools | MCP Protocol over stdio |
| DB Systel Open Data Lake | Stores employee capacity, competence, task, and role data | Trino/Presto SQL |
| Azure OpenAI | Provides chat completions for role similarity + inference + auto-tagging | HTTPS (Azure OpenAI REST API) |
| End User | Requests matching through natural conversation | Natural language via AI assistant |
### 3.2 Technical Context
```text
MCP Client (assistant)
<-> MCP protocol (tool calls, stdio JSON-RPC)
Teamlandkarte MCP Server (Python)
<-> Trino (read-only SQL, connection pool)
<-> Azure OpenAI (chat completions)
<-> Local caches (DB query cache + search session cache)
```
---
## 4. Solution Strategy
### 4.1 Architecture Approach
**Layered Architecture** with clear separation:
1. **MCP Interface Layer** (`mcp_server.py`): Exposes tools to client, formats Markdown output
2. **Business Logic Layer** (`matching/`): Matcher, scorer, SimilarityEngine, VocabularyCache, BM25, RRF, AutoTagger, **LlmFulltextMatcher** (LLM-based full-text matching with rationale)
3. **Data Access Layer** (`database/`): TrinoClient, connection pool, query logger, schema verification, read-only guard
4. **External Integration Layer** (`azure/`): AzureOpenAIClient, CostTracker
5. **Infrastructure** (`cache/`, `utils/`, `config.py`): QueryCache, SearchCache, date/markdown utilities, configuration loading
### 4.2 Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| **BM25+RRF competence matching** | Lexical matching eliminates embedding false-positives; zero-out rule ensures no token overlap = score 0.0 |
| **LLM-based role similarity** | Semantic role comparison requires understanding synonyms and hierarchy; cached per-run |
| **LLM-based role inference** | Selects from DB vocabulary; constrained to known roles only |
| **LLM-based full-text matching (alternative method)** | `llm_fulltext` mode compares complete capacity/task profiles (description, references, certificates, skills) via Azure OpenAI Chat Completion and returns a category + 12 sentence rationale per item; no numeric scores |
| **Optional LLM auto-tagging** | Bridges lexical gap for BM25 by expanding candidate competences with covered synonyms |
| **Global BM25 index** | One index over all candidates' competences ensures stable IDF weights across the pool |
| **Hard confirmation gating (default)** | Prevents accidental matching runs; user must confirm requirements |
| **Markdown table output** | Better rendering in LLM clients; table-first outputs for robust parsing |
| **Connection pooling** | Bounded pool (default 4) for Trino connections; thread-safe acquire/release |
| **Strict startup schema verification** | Fail-fast when upstream views change columns |
| **Two-tier caching** | DB query cache (TTL) + search session cache (TTL) for interactive exploration |
| **Fuzzy filtering** | `fuzzywuzzy` for post-hoc filtering of search results by role/competence text |
### 4.3 Technology Stack
| Layer | Technology | Justification |
|-------|------------|---------------|
| Runtime | Python 3.13 | Modern features, strong typing |
| MCP SDK | `mcp` (FastMCP) | MCP server runtime, stdio transport |
| Database Client | `trino` | Trino/Presto SQL connectivity |
| Caching | `cachetools` (TTLCache) | In-memory TTL caches for DB queries and search sessions |
| Configuration | TOML (`tomllib`) | Human-readable, validated at startup |
| Competence Matching | `rank-bm25` (BM25Okapi) | Lexical ranking, no native extensions |
| Fuzzy Matching | `fuzzywuzzy` + `python-Levenshtein` | Fast fuzzy string matching for filters |
| LLM Integration | `openai` (AsyncAzureOpenAI) | Azure OpenAI chat completions |
| Environment | `python-dotenv` | Load credentials from `.env` |
---
## 5. Building Block View
### 5.1 Level 0: System Context
```
+-------------------------------------------+
| Teamlandkarte MCP Server |
| (Task <-> Capacity Matching) |
+-------------------------------------------+
```
### 5.2 Level 1: Container View
```
+------------------------------------------------------------+
| Teamlandkarte MCP Server |
| |
| +-------------------------------------------------------+ |
| | MCP Interface Layer (mcp_server.py) | |
| | - task browsing + details | |
| | - requirement capture + confirmation gate | |
| | - task->capacity matching + search refinement | |
| | - capacity browsing + capacity->task matching | |
| +----------------------------+---------------------------+ |
| | |
| +----------------------------v---------------------------+ |
| | Business Logic Layer (matching/) | |
| | - Matcher (orchestrates matching runs) | |
| | - SimilarityEngine (BM25+RRF competence, LLM role) | |
| | - VocabularyCache (LLM role inference from DB vocab) | |
| | - Scorer (weighted overall score + categorization) | |
| | - Bm25Index + bm25_rank_competences | |
| | - reciprocal_rank_fusion (RRF normalization) | |
| | - AutoTagger (optional LLM competence expansion) | |
| | - LlmFulltextMatcher (LLM full-text profile matching) | |
| +----------------------------+---------------------------+ |
| | |
| +----------------------------v---------------------------+ |
| | Data Access Layer (database/) | |
| | - TrinoClient (SQL queries, implements DBClient) | |
| | - ConnectionPool (bounded, thread-safe) | |
| | - SchemaVerifier (startup fail-fast) | |
| | - ReadOnly guard (SELECT/WITH only) | |
| | - QueryLogger (redacted SQL logging) | |
| +----------------------------+---------------------------+ |
| | |
| +----------------------------v---------------------------+ |
| | External Integration (azure/) | |
| | - AzureOpenAIClient (chat completions, retry/backoff) | |
| | - CostTracker (session-level token accounting) | |
| +--------------------------------------------------------+ |
| |
| +--------------------------------------------------------+ |
| | Infrastructure (cache/, utils/, config.py) | |
| | - QueryCache (TTL, DB results) | |
| | - SearchCache (TTL, search sessions + filters) | |
| | - dates.py (ISO parsing, overlap checks) | |
| | - markdown.py (table rendering) | |
| | - config.py (TOML loading + validation) | |
| +--------------------------------------------------------+ |
+------------------------------------------------------------+
| |
v v
+-----------------+ +-----------------+
| Open Data Lake | | Azure OpenAI |
| (Trino) | | (Chat API) |
+-----------------+ +-----------------+
```
### 5.3 Level 2: Source Module Map
```
src/teamlandkarte_mcp/
├── __init__.py
├── __main__.py # CLI entry point, arg parsing, logging setup
├── config.py # TOML config loading + validation (AppConfig)
├── logging_config.py # Logging to stderr (MCP stdio safety)
├── mcp_server.py # FastMCP server, all tool definitions, SessionState
├── models.py # Frozen dataclasses: Task, Capacity, Requirements, ScoredCapacity
├── azure/
│ ├── openai_client.py # AsyncAzureOpenAI wrapper (chat completions, retry)
│ └── cost_tracker.py # Session-level token/cost accounting
├── cache/
│ ├── query_cache.py # Generic TTLCache wrapper for DB queries
│ └── search_cache.py # Search session store (search_id, filter_id, TTL)
├── database/
│ ├── types.py # DBClient Protocol definition
│ ├── db_client.py # Factory: create_db_client() -> TrinoClient
│ ├── trino_client.py # TrinoClient (implements DBClient, uses pool)
│ ├── pool.py # ConnectionPool (bounded, thread-safe, LIFO)
│ ├── schema_verifier.py # Startup column verification
│ ├── read_only.py # SQL guard (SELECT/WITH only)
│ ├── query_logger.py # Redacted query logging
│ └── hive_client.py # (empty, reserved)
├── matching/
│ ├── matcher.py # Matcher: orchestrates matching runs
│ ├── scorer.py # compute_overall(), categorize()
│ ├── similarity.py # SimilarityEngine (BM25+RRF competence, LLM role)
│ ├── vocabulary.py # VocabularyCache (LLM role inference)
│ ├── bm25.py # Bm25Index, bm25_rank_competences, _tokenize
│ ├── rrf.py # reciprocal_rank_fusion (zero-out rule)
│ ├── auto_tagger.py # AutoTagger (LLM competence expansion)
│ ├── llm_fulltext_matcher.py # LlmFulltextMatcher (LLM full-text profile matching)
│ ├── profiles.py # CapacityProfile, TaskProfile + serializers
│ ├── task_analyzer.py # (deprecated stub)
│ └── task_helpers.py # (deprecated stub)
└── utils/
├── dates.py # parse_iso_date, availability_overlaps
└── markdown.py # md_table (GFM table rendering)
```
### 5.4 Tool Surface (MCP Tools)
#### Discovery (DB tasks)
| Tool | Responsibility | Dependencies |
|------|----------------|--------------|
| `list_open_tasks` | List newest published tasks | DB |
| `get_task_details` | Table-first task fields + inferred role | DB, LLM (role inference) |
| `validate_task_requirements` | Compare DB fields vs LLM-inferred role + competences | DB, LLM |
| `infer_primary_role` | Infer closest role from task id or free text | DB vocab, LLM |
#### Requirement capture + confirmation gate
| Tool | Responsibility | Dependencies |
|------|----------------|--------------|
| `extract_requirements` | LLM-based inference for role + competences from free text | DB vocab, LLM |
| `collect_structured_requirement_data` | Accept user-supplied fields and stage requirements | session state |
| `update_requirements` | (deprecated stub, directs to `collect_structured_requirement_data`) | -- |
| `start_guided_capture` | Start step-by-step guided capture | session state |
| `guided_set_description` | Set description (step 1/4) | session state |
| `guided_set_role` | Set role (step 2/4) | session state |
| `guided_set_time_range` | Set date range (step 3/4) | session state |
| `guided_set_competences` | Set competences (step 4/4) | session state |
| `show_pending_requirements` | Show review table for pending requirements | session state |
| `request_requirements_confirmation` | Mark that assistant asked user to confirm | session state |
| `confirm_requirements` | Confirm or reject staged requirements | session state |
#### Task -> capacity matching
| Tool | Responsibility | Dependencies |
|------|----------------|--------------|
| `find_matching_capacities` | Compute matches and store a search session (`search_id`). Accepts `matching_method` (`"score"` \| `"llm_fulltext"`). | DB, BM25, LLM (role) or LlmFulltextMatcher, caches |
| `filter_search_results` | Refine stored results (fuzzy role/competence, availability, min_similarity). `min_similarity` is ignored in `llm_fulltext` mode and surfaced as a hint in the Applied Filters table. | SearchCache |
| `get_results_by_category` | Page through stored results by category. Renders `Begründung` column instead of score columns when the search was run in `llm_fulltext` mode. | SearchCache |
#### Capacity browsing + capacity -> task matching
| Tool | Responsibility | Dependencies |
|------|----------------|--------------|
| `list_free_capacities` | List recently created free capacities | DB |
| `get_capacity_details` | Table-first capacity fields + next steps | DB |
| `find_matching_tasks` | Find tasks that match a specific capacity. Accepts `matching_method` (`"score"` \| `"llm_fulltext"`). | DB, BM25, LLM (role) or LlmFulltextMatcher, caches |
#### Team browsing + task -> team matching
| Tool | Responsibility | Dependencies |
|------|----------------|--------------|
| `list_teams` | List the first *N* teams (default 20) as a Markdown table (`Team Id`, `Team Name`, `Schwerpunkt`, `Anzahl Kompetenzen`, `Anzahl Referenzen`). | DB |
| `get_team_details` | Table-first team fields plus sections `## Über uns`, `## Leistungen`, `## Interessen`, `## Kompetenzen` (with `(Top)` marker), `## Referenzen` (Partner_Name + Projekte) and `## Next steps`. | DB |
| `find_matching_teams` | Match a task against team profiles and store a search session (`search_id`). Accepts `matching_method` (`"score"` \| `"llm_fulltext"`). Persists `search_type = "team_search"` and the chosen `matching_method` in `SearchCache` and the META JSON. | DB, BM25, LLM (role) or LlmFulltextMatcher, caches |
#### Profile_Type parameter
| Parameter | Allowed values | Default | Selection mechanism | Affected tools |
|-----------|---------------|---------|---------------------|----------------|
| `Profile_Type` | `"capacity"`, `"team"` | implicit per tool name | Tool selection (`find_matching_capacities``capacity`, `find_matching_teams``team`); persisted as `search_type` (`"capacity_search"` / `"team_search"`) in `SearchCache` and META JSON | `find_matching_capacities`, `find_matching_teams`, `list_teams`, `get_team_details` |
- `Profile_Type = "capacity"` (existing behavior): match a task against employee capacity profiles. `find_matching_capacities` is the entry point; availability filters apply.
- `Profile_Type = "team"` (new): match a task against aggregated team profiles. `find_matching_teams` is the entry point; `list_teams` and `get_team_details` browse and inspect team profiles independently of a matching run. Availability filters are **not** applied for team searches and are surfaced as not-effective hints in the `Applied Filters` table.
#### Matching method parameter
| Parameter | Allowed values | Default | Affected tools |
|-----------|---------------|---------|----------------|
| `matching_method` | `"score"`, `"llm_fulltext"` | `[matching].default_method` (TOML, default `"score"`) | `find_matching_capacities`, `find_matching_tasks`, `find_matching_teams` |
- `"score"`: existing BM25+RRF competence + LLM role similarity. Output includes `Role Score`, `Competence Score`, `Overall Score`, `Category`. For team searches, the team's `focus_name` is used as the role stand-in and top competences are weighted by `matching.team.top_competency_weight` (default `1.5`).
- `"llm_fulltext"`: LLM-based full-text comparison via `LlmFulltextMatcher`. Output replaces all score columns with a single `Begründung` column (12 sentence rationale from the LLM). The persisted META JSON contains `matching_method` so downstream tools (`get_results_by_category`, `filter_search_results`) know which schema to render.
For team searches, the result table columns are:
| Mode | Columns |
|------|---------|
| `team_search` × `score` | `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Role Score`, `Competence Score`, `Overall Score`, `Category` |
| `team_search` × `llm_fulltext` | `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Category`, `Begründung` |
### 5.5 LlmFulltextMatcher (component)
The `LlmFulltextMatcher` (in `matching/llm_fulltext_matcher.py`) is the business-logic component behind `matching_method = "llm_fulltext"`.
| Aspect | Details |
|--------|---------|
| **Inputs** | A `TaskProfile` (id, title, description, skills) for `match_capacities`; a `CapacityProfile` (id, owner_name, role_name, competences, description, references with `partner_name`/`projects`, certificates) for `match_tasks`. The candidate list (`capacities` or `tasks`) is already prefiltered by the MCP tool (same prefilter as in `score` mode, e.g. availability). |
| **Profile sources** | Built from in-memory `Capacity`/`Task` objects plus extras loaded from the DB via `DBClient.batch_get_capacity_descriptions`, `batch_get_capacity_certificates`, and `batch_get_capacity_references` (with the partner LEFT JOIN inside the same SQL query). |
| **Outputs** | `LlmFulltextResult` with `by_category: dict[str, list[LlmFulltextItem]]` (categories `Top`/`Good`/`Partial`/`Low`/`Irrelevant`; each item carries `category`, ungekürzte `rationale`, raw fields) and `errors: list[LlmFulltextError]` for items where the LLM call/parse failed. |
| **External dependency** | Azure OpenAI Chat Completion via `AzureOpenAIClient.chat_completion(...)` with `response_format=json_object`, deterministic German system prompt, JSON schema `{"category": "...", "rationale": "..."}`. One LLM call per candidate. |
| **Determinism** | Profile serialization is field-stable; results within a category are sorted lexicographically by `item_id`; invalid LLM categories map to `Irrelevant` with a hint appended to the rationale. |
| **Persistence** | Items are stored in `SearchCache` with `category` + ungekürzte `rationale` (no numeric score fields); `errors` are persisted alongside; META JSON carries `matching_method`. |
---
## 6. Runtime View (Key Flows)
### 6.1 Server startup (fail-fast)
1. Parse CLI arguments (`--config`, `--log-level`)
2. Configure logging (stderr only, MCP stdio safety)
3. Load and validate `config.toml` + environment variables
4. Create DB client (TrinoClient with connection pool)
5. Verify required DB view columns (schema verification -> fail-fast on mismatch)
6. Initialize caches (QueryCache, SearchCache)
7. Initialize Azure OpenAI client + CostTracker
8. Optionally construct AutoTagger (if `use_auto_tagging = true`)
9. Initialize SimilarityEngine, VocabularyCache, Matcher
10. Register all MCP tools
11. Start MCP server (stdio transport via `mcp.run()`)
### 6.2 Task -> capacity matching
1. Capture requirements (guided capture or `extract_requirements` or `collect_structured_requirement_data`)
2. (If `require_confirmation = true`) confirmation gate:
- `show_pending_requirements` -> user reviews
- `request_requirements_confirmation` -> assistant marks confirmation requested
- `confirm_requirements(true)` -> user confirms
3. Run `find_matching_capacities(role_name, competences, date_start?, date_end?, matching_method?)`:
- Resolve `matching_method` (parameter > `[matching].default_method` > `"score"`)
- Query all capacities + competences from DB (cached)
- Filter by date overlap (same prefilter for both methods)
- Branch by `matching_method`:
- `"score"`: build global BM25 index, run BM25+RRF competence similarity + LLM role similarity, compute weighted overall score + categorize
- `"llm_fulltext"`: batch-load `description`, `certificates`, `references` (with partner LEFT JOIN) for the filtered capacities; build `TaskProfile` + `CapacityProfile`; one Azure OpenAI Chat Completion per capacity (`response_format=json_object`); LLM returns `{category, rationale}`; invalid categories → `Irrelevant` with hint; LLM/JSON failures recorded in a separate `errors` list
- Store results in SearchCache (payload includes `matching_method`; LLM mode stores `category` + ungekürzte `rationale` per item, no score fields) -> return `search_id`
4. Browse/filter results:
- `get_results_by_category(search_id, category, page, page_size)` (renders `Begründung` column in LLM mode)
- `filter_search_results(search_id, ...)` -> returns `filter_id`. In LLM mode, `min_similarity` is ignored and surfaced as a hint; sorting falls back to `(category_rank, item_id)`.
### 6.3 Capacity -> task matching
1. Browse capacities (`list_free_capacities`)
2. Inspect (`get_capacity_details`)
3. Run `find_matching_tasks(capacity_id, matching_method?)`:
- Resolve `matching_method` (parameter > `[matching].default_method` > `"score"`)
- Query capacity details + competences from DB
- Query open tasks from DB (same prefilter for both methods)
- Branch by `matching_method`:
- `"score"`: for each task infer role + competences, compute BM25+RRF + LLM role similarity
- `"llm_fulltext"`: load `description`, `certificates`, `references` (with partner LEFT JOIN) for the capacity; build `CapacityProfile` once; for each task build a `TaskProfile` and run one Azure OpenAI Chat Completion; same error/category-normalization handling as in 6.2
- Store results as a search session (`matching_method` persisted) -> return `search_id`
4. Browse/filter results (same tools as task->capacity)
### 6.4 Task -> team matching
1. Capture requirements as in 6.2 (guided capture, `extract_requirements`, or `collect_structured_requirement_data`)
2. Optional confirmation gate (same as in 6.2): `show_pending_requirements` -> `request_requirements_confirmation` -> `confirm_requirements(true)`
3. Run `find_matching_teams(role_name, competences, matching_method?)`:
- Resolve `matching_method` (parameter > `[matching].default_method` > `"score"`); reject unknown values with an error listing the allowed values, without any DB or LLM call
- Validate minimum requirements (`competences` non-empty)
- Load all teams from the DB-backed cache `_get_teams_cached()`. The DB layer joins `teamlandkarte_v_teams_latest` with `teamlandkarte_v_teammeter_organizational_units_latest` (INNER JOIN over `team_id = id` for the team name), and aggregates competences (`teamlandkarte_v_teammeter_team_competences_latest` joined to `teamlandkarte_v_competences_latest` over `competence_id = id`) and references (`teamlandkarte_v_team_references_latest` LEFT JOIN `teamlandkarte_v_partners_latest` over `partner_id = id`, exposing `name` as `Partner_Name`). Joins between team master data, competences, and references run over `ouid`.
- **No availability prefilter**: team profiles do not carry an availability range; any provided date range is ignored and surfaced as a not-effective hint in the `Applied Filters` table.
- Branch by `matching_method`:
- `"score"`: build the requirements competence list, derive each team's competence list and parallel top-competence set from `Team.competences`, and compute competence similarity via the existing `SimilarityEngine`. Top competences are upweighted by `matching.team.top_competency_weight` (default `1.5`); the per-required score is clamped to `[0, 1]`. Role similarity uses `Team.focus_name` as the role stand-in (`compute_role_similarity(req.role_name, team.focus_name)`). Overall score and category use the same `compute_overall(...)` / `categorize(...)` thresholds as for capacities.
- `"llm_fulltext"`: build a `TaskProfile` from the requirements and a `TeamProfile` per team via `build_team_profile` + `serialize_team_profile`. The user prompt is `=== Aufgabe ===` + `=== Team ===` (with an `ID:` header). The system prompt and JSON schema (`{"category": ..., "rationale": ...}`) are reused from the capacity LLM path; invalid categories are normalized to `Irrelevant` with a hint suffix in the rationale; LLM/JSON errors land in a separate `errors` list.
- Store results in `SearchCache` with `search_type = "team_search"` and `matching_method = ...` -> return `search_id`. The META JSON exposes both `search_type` and `matching_method` so `get_results_by_category` / `filter_search_results` render the team-specific columns.
4. Browse/filter results:
- `get_results_by_category(search_id, category, page, page_size)` (renders `Begründung` column in `llm_fulltext` mode)
- `filter_search_results(search_id, ...)`. `role_filter` matches against `Team_Focus_Name`; `competence_filter` matches the competence names of the team and supports a `(Top)` suffix that restricts the filter to top competences. `availability_date_start`, `availability_date_end`, and `is_fully_available` are ignored for `team_search` and surfaced as `team_search` not-effective hints in the `Applied Filters` table.
### 6.5 Role inference flow
1. `infer_primary_role(task_id=... | task_text=...)`
2. Fetch all role names from DB vocabulary view
3. Send task text + role list to LLM (JSON response format)
4. Validate returned role exists in DB vocabulary
5. Return `(role_name, confidence)` or `None` on failure
---
## 7. Deployment View
```
+-------------------------------------------------+
| Developer Machine / CI |
| |
| +--------------------------------------------+ |
| | MCP Client (IDE / AI Assistant) | |
| | <-> stdio (JSON-RPC) | |
| | teamlandkarte-mcp process | |
| | (Python 3.13, single process) | |
| +--------------------------------------------+ |
| |
| config.toml + .env (credentials) |
+-------------------------------------------------+
| |
v v
+-----------------+ +-----------------+
| Trino cluster | | Azure OpenAI |
| (Data Lake) | | (Chat API) |
+-----------------+ +-----------------+
```
**Requirements:**
- Python 3.13+ with project dependencies installed
- Trino connectivity (host/port via `config.toml`, credentials via env vars)
- Azure OpenAI endpoint + chat deployment + API key (env var `AZURE_OPENAI_LLM_API_KEY`)
- Environment variables: `DATA_LAKE_USERNAME`, `DATA_LAKE_PASSWORD`, `AZURE_OPENAI_LLM_API_KEY`
---
## 8. Cross-cutting Concepts
### 8.1 Deterministic outputs
- Tools produce **table-first Markdown output** (GFM tables via `md_table()`).
- Search tools emit deterministic headers with `SEARCH_ID=...` / `FILTER_ID=...` for robust client parsing.
- Scoring formula is deterministic: `overall = competence_weight * competence_score + role_weight * role_score`.
### 8.2 Caching strategy
| Cache | Implementation | TTL | Purpose |
|-------|---------------|-----|---------|
| DB query cache | `QueryCache` (cachetools TTLCache) | Configurable (`db_ttl_hours`, default 12h) | Avoid repeated expensive Trino queries |
| Search session cache | `SearchCache` (cachetools TTLCache) | Configurable (`search_ttl_minutes`, default 60min) | Enable interactive exploration of results |
| LLM role similarity cache | In-memory dict on `SimilarityEngine` | Per matching run (cleared between runs) | Avoid duplicate LLM calls for same role pair |
### 8.3 Security
- **Read-only DB access**: `ensure_select_only()` guard rejects non-SELECT/WITH queries
- **No write operations**: Server never modifies the Data Lake
- **Credentials via environment**: `DATA_LAKE_USERNAME`, `DATA_LAKE_PASSWORD`, `AZURE_OPENAI_LLM_API_KEY` -- never in config files
- **Query logging**: Parameters are redacted before logging (`query_logger.py`)
- **Logging to stderr**: MCP stdio transport requires stdout to remain clean JSON-RPC
### 8.4 Error handling
- **Schema verification failure**: Server refuses to start (fail-fast)
- **DB connectivity**: Lazy validation on first DB-backed tool call
- **LLM failures (role similarity)**: Return 0.0, no caching of failed result
- **LLM failures (auto-tagging)**: Return unmodified candidate list (graceful degradation)
- **LLM failures (role inference)**: Return `None`, logged as warning
- **Connection pool exhaustion**: `PoolExhaustedError` raised immediately (no blocking wait)
- **Config errors**: `ConfigError` raised at startup with descriptive message
### 8.5 Connection pooling
- `ConnectionPool` (generic, thread-safe, LIFO queue)
- Bounded at `pool_size` (default 4) connections
- Connections created lazily on demand
- `closeall()` for graceful shutdown
---
## 9. Scoring and Matching Algorithm
### 9.1 Overall scoring formula
```
overall_score = competence_weight * competence_score + role_weight * role_score
```
Default weights: `competence_weight = 0.8`, `role_weight = 0.2` (must sum to 1.0).
### 9.2 Competence scoring (BM25 + RRF)
For each required competence:
1. Query the global BM25 index (built over all filtered candidates' competences)
2. Filter ranked results to the current candidate's competence set
3. Apply Reciprocal Rank Fusion (k=60) to normalize scores to [0.0, 1.0]
4. **Zero-out rule**: No token overlap -> score 0.0 (eliminates false positives)
Aggregate: `competence_score = mean(per_required_competence_scores)`
Threshold for "matched": score >= 0.5.
### 9.3 Role scoring (LLM)
- LLM chat completion compares two role names semantically -> similarity in [0.0, 1.0]
- Identical roles (case-insensitive) -> 1.0 (short-circuit, no LLM call)
- Empty/unknown roles -> 0.0 (short-circuit)
- Results cached symmetrically per matching run
### 9.4 Categorization
| Category | Threshold |
|----------|-----------|
| Top | overall >= configured `top` (default 0.8) |
| Good | overall >= configured `good` (default 0.65) |
| Partial | overall >= configured `partial` (default 0.5) |
| Low | overall >= configured `low` (default 0.3) |
| Irrelevant | overall < configured `low` |
(Thresholds are configurable via `[matching.thresholds]`.)
### 9.5 Optional: Auto-Tagging (LLM competence expansion)
When `matching.similarity.use_auto_tagging = true`:
1. Before BM25 scoring, the AutoTagger asks the LLM which required competences are already covered by the candidate's existing competences (via synonym, abbreviation, cross-language equivalence)
2. Covered required-competence names are appended to the candidate's working list
3. Expansion is **ephemeral** -- never persisted to DB
4. On LLM failure, scoring proceeds with the unmodified list
---
## 10. Configuration Reference
### 10.1 `config.toml` structure
```toml
[database]
backend = "trino" # Only "trino" supported
host = "..."
port = 443
http_scheme = "https"
verify_ssl = true
catalog = "hive"
schema = "tier1_open_lake"
connect_timeout = 10
pool_size = 4
[matching]
competence_weight = 0.8 # Must sum to 1.0 with role_weight
role_weight = 0.2
require_confirmation = true # Hard gate before matching
default_method = "score" # Default for matching_method when callers omit it.
# Allowed: "score" | "llm_fulltext".
[matching.thresholds]
top = 0.8
good = 0.65
partial = 0.5
low = 0.3
[matching.fuzzy]
min_similarity = 0.7 # Fuzzy filter threshold
[matching.similarity]
use_auto_tagging = false # LLM pre-expansion for BM25
[cache]
db_ttl_hours = 12
search_ttl_minutes = 60
max_size = 100
[azure_openai]
endpoint = "https://..."
api_version = "2024-02-15-preview"
chat_deployment = "gpt-4.1"
show_costs_in_output = false
```
### 10.2 Required environment variables
| Variable | Purpose |
|----------|---------|
| `DATA_LAKE_USERNAME` | Trino login username |
| `DATA_LAKE_PASSWORD` | Trino login password |
| `AZURE_OPENAI_LLM_API_KEY` | Azure OpenAI API key for chat completions |
---
## 11. Data Model
### 11.1 Core domain objects (frozen dataclasses)
| Class | Fields | Purpose |
|-------|--------|---------|
| `Task` | id, name, title, description, start_date, end_date, created_date, skills | Published task from DB |
| `Capacity` | id, owner_name, role_name, role_level, begin_date, end_date, competences | Employee capacity entry |
| `Requirements` | role_name, competences, date_start, date_end, description | Structured matching input |
| `ScoredCapacity` | capacity, competence_score, role_score, overall_score, category, matched_competences, missing_competences | Matching result |
| `Team` | team_id, ouid, team_name, focus_name, about_us, offerings, interests, competences, references | Aggregated team master data |
| `TeamCompetence` | name, top_competency | Single team competence with top marker |
| `TeamReference` | partner_name, projects | Single team reference; `partner_name` may be empty |
| `ScoredTeam` | team, competence_score, role_score, overall_score, category, matched_competences, missing_competences | Team matching result (score mode) |
### 11.2 Team Profile
A **Team Profile** is the second profile type next to the existing capacity profile. It aggregates fields from four read-only views into a single immutable `Team` instance and is used both for browsing (`list_teams`, `get_team_details`) and for matching (`find_matching_teams` in both `score` and `llm_fulltext` modes).
| Field | Type | Source | Notes |
|-------|------|--------|-------|
| `team_id` | `str` | `teamlandkarte_v_teams_latest.team_id` | Stable identifier; persisted in `SearchCache`. |
| `ouid` | `str` | `teamlandkarte_v_teams_latest.ouid` | Join key for competences and references. |
| `team_name` | `str` | `teamlandkarte_v_teammeter_organizational_units_latest.name` | Resolved via INNER JOIN `teams_latest.team_id = organizational_units_latest.id`. Teams without an OU match are excluded. |
| `focus_name` | `str` | `teamlandkarte_v_teams_latest.focus_name` | NULL → `""`. Used as role stand-in in `score` mode. |
| `about_us` | `str` | `teamlandkarte_v_teams_latest.about_us` | NULL → `""`. |
| `offerings` | `str` | `teamlandkarte_v_teams_latest.offerings` | NULL → `""`. |
| `interests` | `str` | `teamlandkarte_v_teams_latest.interests` | NULL → `""`. |
| `competences` | `list[TeamCompetence]` | `teamlandkarte_v_teammeter_team_competences_latest` joined to `teamlandkarte_v_competences_latest` over `competence_id = id` | Joined on `ouid`. `top_competency = COALESCE(top_competency, FALSE)`. Order: top competences first, then name ascending. |
| `references` | `list[TeamReference]` | `teamlandkarte_v_team_references_latest` LEFT JOIN `teamlandkarte_v_partners_latest` over `partner_id = id` | Joined on `ouid`. Partner_Name from `p.name` (`COALESCE(..., '')`). Whitespace-only `projects` are filtered. Order: partner_name asc, projects asc. |
**Profile serialization** (`build_team_profile` + `serialize_team_profile` in `matching/profiles.py`) emits a deterministic, German-headed text block in fixed order: `Teamname:`, `Schwerpunkt:`, `Über uns:`, `Leistungen:`, `Interessen:`, `Kompetenzen:`, `Referenzen:`. Top competences are suffixed with `(Top)`. Reference rows render as `Partner: <partner_name> Projekte: <projects>` when the partner is known, and as `Projekte: <projects>` (no placeholder) when the partner is empty. Empty lists render as `Kompetenzen: (keine)` / `Referenzen: (keine)`. List ordering matches the DB-provided order.
### 11.3 Database views (Trino)
The server queries read-only views in the `tier1_open_lake` schema. Schema verification at startup checks:
- `teamlandkarte_v_capacity_roles_latest`: requires columns `name`, `active`, `staffing_board_relevant`
- `teamlandkarte_v_capacities_latest`: requires column `creation_date`
- `teamlandkarte_v_teams_latest`: requires columns `team_id`, `ouid`, `about_us`, `offerings`, `interests`, `focus_name`
- `teamlandkarte_v_teammeter_organizational_units_latest`: requires columns `id`, `name`
- `teamlandkarte_v_teammeter_team_competences_latest`: requires columns `ouid`, `competence_id`, `top_competency`
- `teamlandkarte_v_team_references_latest`: requires columns `ouid`, `partner_id`, `projects`
#### Additional views read in `llm_fulltext` mode
The following columns/views are not part of the startup schema verification but are read at runtime when `matching_method = "llm_fulltext"`:
| View | Columns used | Used for |
|------|--------------|----------|
| `teamlandkarte_v_capacities_latest` | `description` | `CapacityProfile.description` |
| `teamlandkarte_v_capacity_certificates_latest` | `capacity_id`, `description` | `CapacityProfile.certificates` (1:n via `capacity_id`) |
| `teamlandkarte_v_capacity_references_latest` | `capacity_id`, `partner_id`, `projects` | `CapacityProfile.references[].projects` (1:n via `capacity_id`) |
| `teamlandkarte_v_partners_latest` | `id`, `name` | Partner name per reference (`CapacityProfile.references[].partner_name`) |
**Reference ↔ Partner join:** `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id`. The partner `name` column is exposed as `partner_name` (via `COALESCE(p.name, '')`) inside the same SQL query that loads the references, so no extra round-trip is required. When `partner_id` is `NULL` or no partner matches, `partner_name` is the empty string and the reference is still returned with its `projects` text. The LEFT JOIN keeps the reference list complete even for unknown partners.
#### Views read for team profiles
The following views back the `Team` data type and are loaded by `find_matching_teams`, `list_teams`, and `get_team_details`:
| View | Columns used | Used for |
|------|--------------|----------|
| `teamlandkarte_v_teams_latest` | `team_id`, `ouid`, `about_us`, `offerings`, `interests`, `focus_name` | Team master data and free-text fields |
| `teamlandkarte_v_teammeter_organizational_units_latest` | `id`, `name` | `Team.team_name` via INNER JOIN over `teams_latest.team_id = organizational_units_latest.id` |
| `teamlandkarte_v_teammeter_team_competences_latest` | `ouid`, `competence_id`, `top_competency` | Team competences with top markers (joined to `teamlandkarte_v_competences_latest` over `competence_id = id` to resolve the competence name) |
| `teamlandkarte_v_team_references_latest` | `ouid`, `partner_id`, `projects` | Team references with partner names |
| `teamlandkarte_v_partners_latest` | `id`, `name` | `Partner_Name` per team reference |
**Team_Name join (INNER JOIN):** `teamlandkarte_v_teams_latest.team_id = teamlandkarte_v_teammeter_organizational_units_latest.id`. Teams without a matching OU row are excluded from `get_all_teams` and `get_team_by_id`.
**Competence and reference joins:** Both `teamlandkarte_v_teammeter_team_competences_latest` and `teamlandkarte_v_team_references_latest` are joined to the team master data over the shared `ouid` column.
**Reference ↔ Partner join (LEFT JOIN):** `teamlandkarte_v_team_references_latest.partner_id = teamlandkarte_v_partners_latest.id`. The `name` column from `teamlandkarte_v_partners_latest` is exposed as `Partner_Name` via `COALESCE(p.name, '')` inside the same SQL query that loads the references. When `partner_id` is `NULL` or no partner matches, `Partner_Name` is the empty string and the reference is still returned with its `projects` text. Whitespace-only `projects` are filtered out in the DB layer.
---
## 12. Testing
- **Unit tests**: pytest + pytest-asyncio
- **Property-based tests**: hypothesis (for scoring, BM25, RRF invariants)
- **Type checking**: mypy (strict)
- **Linting/formatting**: ruff
- **No integration test infrastructure**: DB and Azure OpenAI are mocked in tests
@@ -0,0 +1,338 @@
# System prompt: Teamlandkarte Capacity Matching Assistant
You are a task-to-capacity matching assistant for the Teamlandkarte MCP server.
## Goals
- Help the user find matching capacities for a task.
- Provide a predictable workflow.
- Prefer safe, deterministic behavior.
- Default language: respond in **German** (tools/tables may be English if returned). If the user writes in English, you may respond in English.
- Unless the user explicitly asks otherwise, always operate on the **latest** search.
## When the user request does not map to a tool (must-do)
If the user does not express a request that clearly maps to one of the existing tools,
output a numbered list of the available next actions (in a sensible order) and ask the
user to choose one.
- If this happens in the **very first user message** of a chat session:
- first greet the user briefly and introduce yourself,
- then show the numbered options.
The numbered list must contain **only short, content-focused descriptions** (no tool
names, no technical hints, no notes like "optional").
Suggested options order:
1. Bestehende Aufgaben ansehen (Liste öffentlicher Aufgaben)
2. Details zu einer konkreten Aufgabe ansehen
3. Anforderungen einer Aufgabe prüfen (Validierung)
4. Passende Kapazitäten zu einer bestehenden Aufgabe finden
5. Neue Aufgabe / Anforderungen definieren (geführte Abfrage oder manuell)
6. Freie Kapazitäten ansehen (neueste Einträge)
7. Details zu einer konkreten Kapazität ansehen
8. Passende Aufgaben für eine konkrete Kapazität finden
Do not call tools until the user picks a path.
If the user asks for something that is out of scope (e.g., editing the database, changing Azure resources), explain briefly what you *can* do and offer the closest matching next actions.
## After each successful request (must-do)
After you have successfully handled the users request (answer given and/or tool call(s) executed without error), you MUST end your message with a numbered list of **sensible next actions** the user can take.
Rules:
- The list must be short (typically 35 items) and adapted to the current context.
- Use the same style rules as above: only content-focused descriptions (no tool names, no technical hints).
- If there is an active search context, include at least one option to view more results (other category/page) and one option to refine/filter.
- If there is no active search context, prefer options that lead the user forward (inspect details, validate, start matching).
## Mandatory requirement capture (must-do)
Before calling `find_matching_capacities(...)`, you MUST have (and confirm with the user) these fields:
- `role_name` (or explicitly set `role_name="Beliebige Rolle"` only if the user refuses to specify a role)
- `competences` (a non-empty list; ask for missing/important competences)
- `date_start` / `date_end` (ask for a time range; open-ended allowed)
Additionally, you MUST capture a **concrete topic/goal description** of the task.
- A single skill request like “JavaScript” is **not** sufficient as description.
- If the user only mentions skills/role, ask: "Worum geht es inhaltlich (Ziel/Scope)?".
If any of these are missing, DO NOT run matching yet. You must ask targeted follow-up questions.
### Confirmation gating semantics (important)
- The server's confirmation state is stored in in-memory session state.
- This state is **not** safe as a multi-client / multi-tenant source of truth.
- Therefore, treat confirmation gating primarily as a **client-side UX rule**:
you (the agent) must only execute matching after an explicit user Yes/No.
### Confirmation workflow (must-do, strict)
Never auto-confirm.
- If `matching.require_confirmation = true`:
1. After requirements have been captured/updated (including guided capture), call `show_pending_requirements()` to display the review table.
2. You MUST ask the user explicitly: "Soll ich diese Anforderungen so übernehmen und die Suche starten?" (Ja/Nein).
3. Only if the user replies "Ja" (or equivalent), call `confirm_requirements(confirm=true)`.
- If the user says "Nein": call `confirm_requirements(confirm=false)` and continue requirement capture.
- If `matching.require_confirmation = false`:
- Do **not** ask for confirmation.
- Do **not** call `show_pending_requirements()` / `confirm_requirements()`.
- Proceed directly to matching once requirements are complete.
Do NOT call `confirm_requirements(confirm=true)` in the same turn as requirement capture.
If you are unsure whether confirmation is enabled, prefer to **assume it is enabled** and ask for confirmation (safer UX) *unless* the user explicitly asks to skip confirmation.
### How to decide between `extract_requirements(...)` and guided capture
Use `extract_requirements(task_description, ...)` only when the user provides a reasonably complete description.
Use **guided capture** when:
- the user is doing an ad-hoc search (no DB task), AND
- the description is vague/too short, or key fields are missing.
Rule of thumb:
- If you would still need to ask **more than one** follow-up question, start guided capture.
### Ad-hoc searches with incomplete descriptions (must-do)
If the user is looking for a capacity **not based on an existing DB task** and does **not** provide a complete task description (e.g. only a role, only a few skills, or a very short/vague sentence), you MUST start **guided capture**.
- Trigger condition (ad-hoc + incomplete description):
- no `task_id` / not a DB-task workflow, AND
- the user did not provide a sufficiently detailed task description.
**Definition (operational):** treat the description as *incomplete* unless it contains at least:
- a short **scope/goal** (what will be built/done), AND
- at least **2 concrete competences/technologies** (or one competence + clear domain), AND
- at least a rough **time range** (start/end or open-ended)
If these are not present, do not call `extract_requirements(...)`. Start guided capture instead.
Then do:
1. `start_guided_capture()`
2. `guided_set_description(...)` (use the user's current text as initial description; ask for missing context)
3. `guided_set_role(...)`
4. `guided_set_time_range(date_start?, date_end?)`
5. `guided_set_competences([...])`
6. `show_pending_requirements()`
7. Ask user for confirmation
8. `confirm_requirements(confirm=true)` (if required)
9. `find_matching_capacities(...)`
### Minimum follow-up questions when the user is underspecified
If the user provides only a vague request or a single skill (e.g. “Ich suche jemanden mit JavaScript.”), you MUST ask (at minimum) these three questions before matching:
1. **Role:** Welche Rolle soll die Person haben (z.B. Frontend Developer, Fullstack, QA, Tech Lead)?
2. **Time range:** Ab wann und bis wann wird die Person benötigt? (Start/Ende oder open-ended)
3. **More competences:** Welche weiteren wichtigen Skills sind relevant? (z.B. TypeScript, React/Angular/Vue, Node.js, Testing)
If the user refuses to specify a role, set `role_name="Beliebige Rolle"` explicitly and continue.
## Search context & Cherry Studio reliability (critical)
These rules exist because some clients occasionally reuse a wrong or stale UUID and/or fail to render full tool output.
### Always track the latest search id
Maintain an explicit local variable: `LATEST_SEARCH_ID`.
- Update `LATEST_SEARCH_ID` **only** from the **most recent tool output** you executed.
- Accept only IDs explicitly labeled as:
- `Using SEARCH_ID=<uuid>` (preferred), or
- `SEARCH_ID=<uuid>`, or
- `META={..."search_id": "..."...}`
- Never invent or "recover" a search id.
- Never accept a user-provided `search_id` unless it exactly equals `LATEST_SEARCH_ID`.
### Echo rule (Cherry Studio rendering workaround)
After every **successful** call to:
- `find_matching_capacities`: include in your **next** assistant message
1) `Using SEARCH_ID=<uuid>` (copied exactly), and
2) the `## Summary` table with counts for Top/Good/Partial/Low.
- `filter_search_results`: include in your **next** assistant message
1) `Using SEARCH_ID=<uuid>`, and
2) the `filter_id` plus the **Applied Filters** table.
- `get_results_by_category`: include in your **next** assistant message
1) `Using SEARCH_ID=<uuid>`, and
2) the `Category`, `Page`, and `Total items in category` lines.
Keep these echoes short and verbatim. Do not paraphrase IDs.
### Before calling refinement/pagination tools
Immediately before calling `get_results_by_category(...)` or `filter_search_results(...)`, write:
- `Using SEARCH_ID=<LATEST_SEARCH_ID>`
Then call the tool with that exact value.
### Hard failover on invalid/expired search
If `get_results_by_category` or `filter_search_results` returns:
- `status=unknown_or_expired`, or
- `META` contains `"status": "unknown_or_expired"`, or
- text indicating invalid/expired search id
Then do **not** retry with another guessed id. Rerun `find_matching_capacities(...)` to create a fresh search and replace `LATEST_SEARCH_ID`.
### Stable behavior when role is unspecified
If the user did not specify a role, always send `role_name="Beliebige Rolle"` (never an empty string) and keep using it for retries.
## Tools and when to use them
### Discovery (DB tasks)
- `list_open_tasks(limit=...)`
- Use when the user wants to browse tasks.
- Output is a Markdown table with `task_id`.
- `get_task_details(task_id)`
- Use to inspect a task and see relevant fields.
- `validate_task_requirements(task_id)`
- Use to compare DB skills vs inferred skills and time ranges.
- This tool is not part of the default matching workflow and must only be used when the user explicitly asks for validation.
- `infer_primary_role(task_id=... | task_text=...)`
- Use to suggest the single closest role for a task.
### Capacity browsing + capacity→task matching
- `list_free_capacities(limit=...)`
- Use when the user wants to browse recent free capacities.
- `get_capacity_details(capacity_id)`
- Use to inspect a capacity before matching.
- `find_matching_tasks(capacity_id)`
- Use to find tasks that match a specific capacity.
### Requirement capture (free text or structured)
- `extract_requirements(task_description, confirm_requirements=true)`
- Uses embeddings-only inference.
- Dates are NOT extracted from free text. Ask for a time range and capture via structured/guided tools.
- `collect_structured_requirement_data(role_name, competences, date_start?, date_end?, confirm_requirements=true)`
- Use when the user provides explicit fields.
- Guided capture tools (step-by-step):
- `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)`
- `update_requirements(change_description, confirm_requirements=true)`
- Use to modify already-captured requirements (e.g. add a competence, change the role).
- Accepts a free-text change description; the server applies the delta.
- `show_pending_requirements()`
- Use to display pending requirements for user review.
- `confirm_requirements(confirm=true)`
- Use to confirm pending requirements before searching if required.
### Search execution
- `find_matching_capacities(role_name, competences, date_start?, date_end?)`
- Runs matching and returns a JSON block with `search_id`.
- Treat the returned `search_id` as the new **latest** search.
### Search refinement and pagination (always reuse the latest search)
- `get_results_by_category(search_id, filter_id?, category, page, page_size)`
- Use to display `Top`, `Good`, `Partial`, `Low`, `Irrelevant` results.
- Numeric score columns are always visible (Role/Competence/Overall).
- Availability is shown as overlap percentage with the reference time range.
- `filter_search_results(search_id, ...)`
- Use for refining results for both directions (capacity-search and task-search).
- Supports full-coverage availability filtering (`is_fully_available=true`).
- Task-search filters include `task_text_filter` and `task_competence_filter`.
## Default workflows
### Workflow A: User has a DB task id
1. `get_task_details(task_id)`
2. (Only if the user explicitly requests validation) `validate_task_requirements(task_id)`
3. If role unclear, `infer_primary_role(task_id=...)`
4. Capture/update requirements (pick ONE approach):
- `collect_structured_requirement_data(...)`, or
- `extract_requirements(task_description=...)`
5. **Review (must be after the last requirements update):** `show_pending_requirements()` (only if `matching.require_confirmation = true`)
6. Ask the user explicitly to confirm (Yes/No) (only if `matching.require_confirmation = true`)
7. `confirm_requirements(confirm=true)` (only on Yes; on No: `confirm_requirements(confirm=false)`) (only if `matching.require_confirmation = true`)
8. Run matching:
- `find_matching_capacities(role_name, competences, date_start?, date_end?)`
9. After matching, show other categories via `get_results_by_category` using `LATEST_SEARCH_ID`.
### Workflow B: User provides free-text description
1. `extract_requirements(task_description, confirm_requirements=true)`
2. Ask for missing fields, especially date range (dates are not extracted).
3. If `matching.require_confirmation = true`: `show_pending_requirements()`
4. If `matching.require_confirmation = true`: ask user for confirmation
5. If `matching.require_confirmation = true`: `confirm_requirements(confirm=true)`
6. `find_matching_capacities(role_name, competences, date_start?, date_end?)`
7. For other categories/pages: `get_results_by_category` using `LATEST_SEARCH_ID`.
### Workflow C: Capacity-driven matching
1. `list_free_capacities(limit=...)`
2. `get_capacity_details(capacity_id)`
3. `find_matching_tasks(capacity_id)`
## Handling “show me Good results”
- If you have `LATEST_SEARCH_ID`: call `get_results_by_category(search_id=<LATEST_SEARCH_ID>, category="Good", page=1, page_size=20)`.
- If you do not have `LATEST_SEARCH_ID`: run a new `find_matching_capacities(...)` first.
## Embeddings: performance + determinism (important)
The server uses Azure OpenAI embeddings for competence and role similarity.
- Embeddings are **prefetched globally per matching run** (required + all
candidates; competences + roles). Similarity computation should not trigger
embeddings inside inner loops.
- Cache layering applies:
1) in-memory cache
2) SQLite embedding cache
3) Azure embeddings API
- Azure embeddings requests may be **batched** (`input=[...]`) and chunked
sequentially. Chunk size is configurable via
`azure_openai.embedding_batch_size` (default: 128).
- Failure semantics remain strict: if embeddings fail, the matching run fails
(no heuristic fallback).
## BM25 + RRF competence matching (optional, disabled by default)
When `matching.similarity.use_bm25_search = true` is set in `config.toml`, competence
similarity uses **BM25 + Reciprocal Rank Fusion (RRF)** instead of embeddings:
- **Score interpretation changes**: a `Competence Score` of `0.0` means
**no lexical token overlap** between the required competence and any candidate
competence — it is **not** a semantic distance. Cross-language synonyms or
abbreviations that do not share tokens will score `0.0`.
- **Exact canonical terms matter**: "Python" matches "Python" perfectly; "ML" does
**not** match "Machine Learning" unless Auto-Tagging is enabled.
- When displaying results to the user, note that BM25 mode eliminates false positives
(non-overlapping skills score exactly 0.0) but may miss valid synonym matches.
### Auto-Tagging (`use_auto_tagging = true`)
When `matching.similarity.use_auto_tagging = true` is also set:
- An LLM pre-expands each candidate's competence list with canonical equivalents of
required competences it already covers (synonyms, abbreviations, cross-language terms,
e.g. "ML" → "Machine Learning").
- The expansion is **ephemeral** (per-call, not stored in the database).
- A positive `Competence Score` with auto-tagging enabled may reflect an
**LLM-inferred equivalence**, not a literal token match; the rationale field will still
say `"BM25+RRF: best match '...' (score ...)"`.
- If the LLM call fails for any reason, BM25 scoring continues on the original
(unexpanded) candidate list — the run does **not** fail.
- Role similarity is always **embedding-based**, regardless of these flags.
@@ -0,0 +1,82 @@
# Azure OpenAI setup
This project requires Azure OpenAI for **embeddings only**:
- **Embeddings** (`text-embedding-3-large`, **3072 dims**):
- role inference (against Data Lake role vocabulary)
- competence inference (against Data Lake competence vocabulary)
- similarity scoring for matching
There are **no chat/LLM features** anymore.
## 1) Create an embedding deployment in Azure
In your Azure OpenAI resource, create an embedding deployment for:
- `text-embedding-3-large`
Notes:
- The deployment **name** must match what you set in `config.toml`.
- This project uses 3072 embedding dimensions.
## 2) Configure `config.toml`
Copy the template:
- `cp config.toml.example config.toml`
Set the Azure section:
```toml
[azure_openai]
endpoint = "https://<your-resource>.openai.azure.com"
api_version = "2024-02-15-preview"
embedding_deployment = "text-embedding-3-large"
# Embeddings batching (number of inputs per embeddings API request)
embedding_batch_size = 128
# Optional
show_costs_in_output = false
```
Notes:
- Embeddings requests may be batched (`input=[...]`) and chunked sequentially.
Increase `embedding_batch_size` to reduce round-trips, or decrease it if you
suspect request-size related failures.
## 3) Configure credentials via environment variables
The server reads the API key from:
- `AZURE_OPENAI_EMBEDDING_API_KEY`
Recommended: keep secrets in a local `.env` file (do not commit):
```bash
AZURE_OPENAI_EMBEDDING_API_KEY="..."
```
The server also needs database credentials:
- `DATA_LAKE_USERNAME`
- `DATA_LAKE_PASSWORD`
## 4) Verify your setup
### 4.1 Quick import/run smoke check
Start the server with debug logs and confirm it loads your config:
- `uv run teamlandkarte-mcp --config config.toml --log-level DEBUG`
### 4.2 Typical Azure errors
- HTTP 401/403: wrong key or wrong resource
- HTTP 404: deployment name mismatch
- HTTP 429: throttling; warm the embedding cache and/or reduce concurrency
- network/TLS: verify corporate proxy/TLS setup
See `docs/troubleshooting.md` for additional details.
+19
View File
@@ -0,0 +1,19 @@
# Security notes
## Credentials
- `config.toml` is intentionally ignored via `.gitignore`.
- If `config.toml` (or any file containing credentials) was ever committed to git history, treat those credentials as compromised.
## Credential rotation checklist
1. Rotate the database password / token.
2. Invalidate any exposed API tokens.
3. Re-issue credentials with least-privilege read-only access.
4. Audit logs for suspicious access.
## Git history cleanup (if needed)
If a secret accidentally entered git history, remove it from history using secure tools (for example `git filter-repo`) and force-push.
Do **not** rely on a simple revert; secrets remain accessible in history.
@@ -0,0 +1,314 @@
# Semantic Similarity & False Positives
## Problem
When searching for **Python** and **Machine Learning**, a candidate with only **JavaScript, TypeScript, Node.js, Vue.js, PWA, CI/CD, Web Architectures** receives a score of **0.392** (39%).
This seems too high for someone who has **none of the searched competences**.
## Root Cause: Semantic Embeddings
The `per_skill` strategy works as follows:
1. For each **required** competence, find the **best match** from candidate competences
2. Average those best-match scores
The Azure OpenAI embedding model (`text-embedding-3-large`) finds **semantic similarity** between concepts, not just exact matches:
### Example Similarities
| Required | Best Candidate Match | Why Similar? | Approx. Score |
|----------|---------------------|--------------|---------------|
| Python | JavaScript / TypeScript / Node.js | All programming languages | ~0.50-0.60 |
| Machine Learning | Progressive Web App / general software dev | Broader technical domain | ~0.20-0.30 |
**Average:** `(0.55 + 0.25) / 2 = 0.40` → explains the 0.392 score!
## Why This Happens
Embedding models are trained to understand:
- **Python** and **JavaScript** are both programming languages
- They share many concepts (variables, functions, loops, OOP)
- Semantically, they're closer to each other than to "accounting" or "project management"
This is **by design** - embeddings capture semantic relationships, not just exact string matches.
## Solution Options
### Option 1: Raise the `partial` Threshold ⭐ RECOMMENDED
**Change in `config.toml`:**
```toml
[matching.thresholds]
top = 0.8
good = 0.65
partial = 0.5 # ← was 0.4, now 0.5
low = 0.3 # ← was 0.2, now 0.3
```
**Effect:**
- Score 0.392 → category "Low" (instead of "Partial")
- Clearer distinction between "somewhat related" and "actually matching"
- More balanced category distribution
**Pros:**
- Simple one-line config change
- Makes categories more meaningful
- Doesn't affect matching, only categorization
**Cons:**
- Might push some legitimate partial matches into "Low"
---
### Option 2: Stricter Competence Inference
**Change in `config.toml`:**
```toml
[matching.inference]
max_competences = 16
min_similarity = 0.6 # ← was 0.4, raise to 0.6 or 0.7
```
**Effect:**
- When inferring competences from descriptions, only closer semantic matches count
- Doesn't affect manual competence lists
**Pros:**
- Reduces noise in inferred competences
- More precise matching
**Cons:**
- Only affects inference, not manual searches
- Might miss some valid related competences
---
### Option 3: More Specific Competences
**Instead of:**
```
Required: ["Python", "Machine Learning"]
```
**Use:**
```
Required: ["Python", "scikit-learn", "TensorFlow", "PyTorch", "pandas", "NumPy"]
```
**Effect:**
- More specific competences are harder to match accidentally
- "scikit-learn" is semantically very different from "Vue.js"
**Pros:**
- More precise matching
- Better reflects actual skill requirements
**Cons:**
- Requires more detailed competence lists
- Might miss candidates who have Python but use different ML libraries
---
### Option 4: Switch to `aggregate` Strategy
**Change in `config.toml`:**
```toml
[matching.similarity]
strategy = "aggregate" # ← was "per_skill"
```
**How it works:**
- Computes **mean embedding** of all required competences
- Computes **mean embedding** of all candidate competences
- Returns cosine similarity between the two means
**Effect with your example:**
- Required mean: average of [Python, Machine Learning]
- Candidate mean: average of [JavaScript, TypeScript, Node.js, Vue.js, PWA, CI/CD, Web Arch]
- The diluted candidate mean has **lower similarity** to Python/ML than per-skill
**Pros:**
- Better for "overall technical fit" vs "specific skill coverage"
- Reduces false positives from cross-domain matches
**Cons:**
- Extra candidate competences dilute the score (see SCORE_BUG_FIX.md)
- All required competences get the same score
- Less granular than per-skill
---
### Option 5: Filter by Matched Competences
**Use MCP tools:**
```python
# After search, filter to only show results where specific competences matched
filter_search_results(
search_id="...",
filter_id="python-ml-only",
min_similarity=0.65, # High threshold for "matched"
)
```
**Effect:**
- Only shows results where similarity >= 0.65 for Python or Machine Learning
- Filters out the JavaScript/TypeScript candidates
**Pros:**
- Most precise control
- Can experiment with different thresholds interactively
**Cons:**
- Requires post-search filtering
- Adds an extra step to workflow
---
## Recommended Approach
**Combination of Option 1 + Option 5:**
1. **Raise `partial` threshold to 0.5:**
```toml
[matching.thresholds]
partial = 0.5
```
2. **Use interactive filtering when needed:**
- For broad exploration: accept lower scores
- For precise matching: apply `filter_search_results()` with higher thresholds
This gives you:
- ✅ Clearer category boundaries (0.392 → Low, not Partial)
- ✅ Flexibility to tighten results when needed
- ✅ No loss of recall (candidates still appear, just in correct category)
---
### Option 6: Switch to BM25 + RRF (lexical matching) ⭐ BEST for exact skill names
**Change in `config.toml`:**
```toml
[matching.similarity]
use_bm25_search = true
```
**How it works:**
- For each required competence, ranks candidate competences using BM25 (term-frequency/
inverse-document-frequency lexical scoring) and normalizes via Reciprocal Rank Fusion.
- Candidates with **no shared token** with the required competence receive score **0.0**
— the false-positive problem is completely eliminated.
**Effect with your example:**
- Required: `["Python", "Machine Learning"]`
- Candidate: `["JavaScript", "TypeScript", "Node.js", "Vue.js"]`
- Score: **0.0** (no token overlap at all)
**Pros:**
- ✅ Eliminates false positives for lexically disjoint skills
- ✅ No Azure embedding calls for competence matching (faster, lower cost)
- ✅ Deterministic — same query always returns same ranked order
**Cons:**
- ❌ Misses synonyms and cross-language pairs:
- "ML" ≠ "Machine Learning" (no token overlap)
- "Softwarearchitektur" ≠ "Software Architecture"
- "Data Science" ≠ "Machine Learning"
- ❌ Role similarity still uses embeddings (unchanged)
**Mitigation:** combine with `use_auto_tagging = true` (see Option 7 below).
---
### Option 7: BM25 + RRF + Auto-Tagging ⭐ BEST for mixed-language / abbreviation-heavy datasets
**Change in `config.toml`:**
```toml
[matching.similarity]
use_bm25_search = true
use_auto_tagging = true
[azure_openai]
chat_deployment = "gpt-4.1" # your chat deployment name
```
**And set the env variable:**
```
AZURE_OPENAI_LLM_API_KEY=<your-chat-api-key>
```
**How it works:**
- Before BM25 scoring, the LLM is asked: "which of these *required* competences are
already covered by the candidate's *existing* competences (via synonym, abbreviation,
or cross-language equivalence)?"
- The canonical required-competence names identified by the LLM are appended to the
candidate's working list for this BM25 call only (result is **never persisted**).
- BM25 then finds token matches including the LLM-expanded terms.
**Effect:**
- "ML" → LLM identifies "Machine Learning" as covered → added → BM25 matches ✅
- "CI" → LLM identifies "CI/CD" as covered → BM25 matches ✅
- "Softwarearchitektur" → LLM identifies "Software Architecture" → BM25 matches ✅
**Pros:**
- ✅ Eliminates false positives (BM25 zero-out rule)
- ✅ Bridges synonyms, abbreviations, and cross-language pairs (LLM)
- ✅ Expansion is ephemeral — no database pollution
**Cons:**
- ❌ One additional LLM call per candidate per matching run (latency + cost)
- ❌ Requires `AZURE_OPENAI_LLM_API_KEY` environment variable
---
## Strategy Comparison
| Strategy | False positives | Synonyms/Cross-language | Cost | Speed |
|----------|----------------|------------------------|------|-------|
| `per_skill` (default) | ⚠️ Moderate | ✅ Good | Azure embeddings | Fast (cached) |
| `aggregate` | ⚠️ Moderate | ✅ Good | Azure embeddings | Fast (cached) |
| `use_bm25_search` | ✅ Eliminated | ❌ Misses | No embedding calls | Very fast |
| `use_bm25_search` + `use_auto_tagging` | ✅ Eliminated | ✅ Good | + 1 LLM call/candidate | Moderate |
---
## Technical Background
### Why Embeddings Find Semantic Similarity
The `text-embedding-3-large` model is trained on massive amounts of text including:
- Programming language documentation
- Technical tutorials and courses
- Stack Overflow discussions
- GitHub repositories
It learns that:
- **Python** and **JavaScript** both appear in programming contexts
- They share similar syntax concepts
- Developers often know both
- They're used for similar tasks (web development, data processing)
This makes the embeddings **semantically aware**, which is powerful for fuzzy matching but can cause false positives when you need exact skill matches.
### Per-Skill vs Aggregate
**Per-Skill:**
- Finds best match for each required competence
- Good for: "Must have Python AND Machine Learning"
- Risk: Cross-domain false positives (Python ↔ JavaScript)
**Aggregate:**
- Computes overall semantic fit
- Good for: "General technical background in this domain"
- Risk: Diluted by extra competences
Choose based on your use case!
---
## See Also
- `SCORE_BUG_FIX.md` - Detailed test coverage for scoring behavior
- `docs/architecture.md` - Similarity strategy documentation
- `config.toml.example` - Configuration options
@@ -0,0 +1,161 @@
---
name: test-agent
description: >
Specialized agent for developing and maintaining the Teamlandkarte MCP Server —
a capacity/task matching system for DB Systel employees. Use this agent when working
on the Teamlandkarte codebase: adding features, fixing bugs, writing tests, refactoring,
or understanding the matching/capacity system.
tools: ["read", "write", "shell", "web"]
---
You are a specialized development assistant for the **Teamlandkarte MCP Server** project.
This is a Python 3.13+ MCP (Model Context Protocol) server that enables AI assistants to match
DB Systel employees with free work capacity to task requirements, querying the DB Systel Open Data Lake.
## Project Overview
- **Language**: Python 3.13+
- **Package manager**: uv
- **Framework**: MCP (Model Context Protocol)
- **Database**: Trino/Presto (read-only access to DB Systel Open Data Lake)
- **AI**: Azure OpenAI embeddings for role/competence inference
- **Testing**: pytest + pytest-asyncio
- **Linting**: ruff (line-length 110)
- **Type checking**: mypy
- **Configuration**: TOML format (config.toml), credentials via .env
- **Source layout**: src/teamlandkarte_mcp/
- **Entry point**: `teamlandkarte-mcp` command (src/teamlandkarte_mcp/__main__.py)
## Project Structure
```
src/teamlandkarte_mcp/
├── __main__.py # CLI entry point
├── mcp_server.py # MCP server + tool definitions
├── config.py # Configuration management (TOML)
├── models.py # Data models
├── logging_config.py # Logging setup
├── azure/ # Azure OpenAI integration (embeddings)
├── database/ # Trino client, schema verification, read-only guard
├── matching/ # Matcher, scorer, similarity engine, BM25+RRF
├── cache/ # Query cache, search cache, embedding cache (SQLite)
└── utils/ # Shared utilities
```
## Commands
- **Run server**: `uv run teamlandkarte-mcp --config config.toml`
- **Run tests**: `uv run pytest`
- **Lint**: `uv run ruff check src/ tests/`
- **Format**: `uv run ruff format src/ tests/`
- **Type check**: `uv run mypy src/`
- **Install deps**: `uv sync`
## Architectural Patterns
### Embeddings-Only Inference
The system uses Azure OpenAI embeddings (text-embedding-3-large, 3072 dimensions) for semantic
similarity. There is no chat/LLM by default — matching is purely embedding-based unless
`use_auto_tagging = true` is configured.
### Table-First Markdown Output
All MCP tool outputs use Markdown tables as the primary format for structured data. This ensures
clean rendering in chat UIs. Always maintain this pattern when adding or modifying tool outputs.
### Confirmation Gate
Before any matching run executes, the system enforces a hard confirmation gate:
1. `show_pending_requirements` → displays what will be matched
2. `request_requirements_confirmation` → asks user to confirm
3. `confirm_requirements` → user explicitly confirms
Only after confirmation does matching proceed. Never bypass this pattern.
### Search Sessions
Search results are managed via `search_id` and `filter_id` identifiers. Results can be
filtered and paginated after the initial search. Maintain session state correctly.
### Multi-Level Caching
- **DB cache**: 12h TTL for Trino query results (cachetools)
- **Search cache**: 60min TTL for search results
- **Embedding cache**: Persistent SQLite with configurable TTL (default 30 days)
### Read-Only Database Access
The Trino connection is strictly read-only. Schema verification runs at startup (fail-fast).
Never write to the database. Never expose connection credentials in outputs.
## MCP Tools Categories
- **Discovery**: list_open_tasks, get_task_details, validate_task_requirements, infer_primary_role
- **Requirement Capture**: extract_requirements, collect_structured_requirement_data, guided capture flow
- **Confirmation Gate**: show_pending_requirements, request_requirements_confirmation, confirm_requirements
- **Matching**: find_matching_capacities, find_matching_tasks
- **Exploration**: filter_search_results, get_results_by_category
- **Capacity Browsing**: list_free_capacities, get_capacity_details
When designing new MCP tools:
- Produce deterministic outputs (same input → same output, modulo cache state)
- Use Markdown tables for structured data
- Define clear parameter schemas with descriptions
- Follow the existing naming convention (verb_noun pattern)
- Include proper error messages for invalid inputs
## Security Rules
1. **Credentials in env vars only** — never in config.toml, never in code, never in outputs
2. **Read-only DB** — never attempt writes to Trino
3. **Never expose secrets** — API keys, passwords, tokens must never appear in tool outputs or logs
4. **.env file** — contains DATA_LAKE_USERNAME, DATA_LAKE_PASSWORD, AZURE_OPENAI_EMBEDDING_API_KEY, AZURE_OPENAI_LLM_API_KEY
5. **config.toml** — not in VCS, contains endpoint URLs and tuning parameters only
## Configuration
Runtime configuration lives in `config.toml` (TOML format). See `config.toml.example` for the
full schema. Key sections:
- `[database]` — Trino connection (host, port, catalog, schema)
- `[matching]` — weights, thresholds, fuzzy settings, inference, similarity strategy
- `[cache]` — TTL settings
- `[embedding_cache]` — SQLite path and TTL
- `[azure_openai]` — endpoint, deployment, API version, batch size
## Testing Patterns
- Use **pytest** with **pytest-asyncio** for async tests
- Focus areas: confirmation gating, search robustness, filtering/pagination, cache behavior
- Mock external services (Trino, Azure OpenAI) in unit tests
- Test edge cases: empty results, invalid inputs, cache expiry, schema mismatches
- Run tests with: `uv run pytest` (or `uv run pytest -v` for verbose)
- Test files live in `tests/` directory
## OpenSpec Workflow
This project uses **OpenSpec** for spec-driven development. For significant changes
(new capabilities, breaking changes, architecture shifts):
1. Check existing specs: `openspec spec list --long`
2. Check active changes: `openspec list`
3. Create a proposal in `openspec/changes/<change-id>/`
4. Include: proposal.md, tasks.md, optional design.md, and spec deltas
5. Validate: `openspec validate <change-id> --strict`
6. Get approval before implementing
Read `openspec/AGENTS.md` for full workflow details.
## Code Style
- Line length: 110 characters (ruff configured)
- Use type hints everywhere (mypy strict)
- Follow existing patterns in the codebase
- Prefer `async/await` for I/O operations
- Use dataclasses or Pydantic models for structured data
- Keep functions focused and testable
- Document public APIs with docstrings
## When Working on This Project
1. Read relevant existing code before making changes
2. Match existing patterns and conventions
3. Run `uv run ruff check src/ tests/` after changes
4. Run `uv run mypy src/` for type safety
5. Run `uv run pytest` to verify nothing breaks
6. For significant changes, follow the OpenSpec workflow
7. Never commit credentials or secrets
8. Keep Markdown table outputs clean and consistent
@@ -0,0 +1,227 @@
# Troubleshooting
## The server fails at startup with `Missing config.toml`
- Copy the template and fill in non-secret settings:
- `cp config.toml.example config.toml`
- Ensure `--config` points to the correct file when starting the server.
## Database connectivity problems
Symptoms:
- `DatabaseError: Trino error: ...`
Checklist:
1. Verify host/port in `config.toml`.
2. Verify username/password.
3. Verify TLS settings: `http_scheme` and `verify_ssl`.
4. Ensure `database.backend = "trino"`.
5. Run the smoke check script: `scripts/trino_smoke_check.py`.
## Confirmation gate blocks matching
Symptoms:
- A matching tool refuses to run and tells you to confirm requirements.
Explanation:
- Requirement capture tools write **pending** requirements.
- When `matching.require_confirmation = true` (default), matching tools require an explicit user confirmation flow.
Additional note (multi-client environments):
- Pending/confirmed state is stored in an in-memory server session.
- If multiple clients share one server process, confirmation state can become
ambiguous. In such setups, enforce the confirmation workflow strictly on the
client/agent side.
Fix (recommended sequence):
1. Capture/update requirements (e.g. `extract_requirements`, `collect_structured_requirement_data`, `update_requirements`, guided tools).
2. Call `show_pending_requirements()` (review table) **after the last update**.
3. Ask the user to confirm (Yes/No).
4. Call `confirm_requirements(confirm=true)`.
5. Re-run the matching tool.
Optional (dev/testing):
- Set `matching.require_confirmation = false` in `config.toml` to auto-skip confirmation.
## confirm_requirements was called but matching is still blocked
Symptoms:
- The assistant called `confirm_requirements(confirm=true)` but matching tools still refuse to run.
Explanation:
- The workflow is two-step: the assistant must first request user confirmation.
- The recommended sequence is:
1. `show_pending_requirements()` (review table)
2. Ask the user to confirm (Yes/No)
3. `confirm_requirements(confirm=true)`
Fix:
- If requirements were updated after the review step, you must re-run `show_pending_requirements()` and ask the user again.
- Then call `confirm_requirements(confirm=true)`.
## Guided capture feels stuck
Guided capture is step-based. Call the tools in order:
1. `start_guided_capture()`
2. `guided_set_description(...)`
3. `guided_set_role(...)`
4. `guided_set_time_range(date_start?, date_end?)` (open-ended ranges are allowed)
5. `guided_set_competences([...])`
6. `show_pending_requirements()`
7. Ask the user to confirm (Yes/No)
8. `confirm_requirements(confirm=true)`
## "Unknown or expired search_id" (or filters/pagination stop working)
Symptoms:
- `get_results_by_category(...)` or `filter_search_results(...)` returns an error.
- Newer server versions also return a machine-readable status:
- `META.status=unknown_or_expired`
Common causes:
1. **TTL expiry**: the search results cache is time-limited.
- Controlled by `cache.search_ttl_minutes`.
2. **Eviction due to cache size**: the cache is LRU and bound by `cache.max_size`.
- If many searches are started, older `search_id`s can be evicted even within TTL.
3. **Process mismatch / restart**: in-memory caches are per server process.
- If the server restarts, or the client routes to a different instance, old `search_id`s are not available.
4. **Copy hygiene**: some chat UIs add backticks or whitespace.
- Prefer copying the UUID from the tool output header line `SEARCH_ID=<uuid>`.
- Do not include backticks, quotes, or extra whitespace.
Fix:
- Re-run the matching tool to create a new `search_id`.
- Increase `cache.search_ttl_minutes` and/or `cache.max_size` for longer interactive sessions.
- Ensure your MCP client uses a single long-lived server process.
## "Invalid search_id format (expected UUID)"
Cause:
- The server validates `search_id` inputs and rejects anything that is not a UUID.
Fix:
- Copy the value from the most recent tool output header:
- `Using SEARCH_ID=<uuid>` (first line)
- or `SEARCH_ID=<uuid>`
## Search tool output parsing (Cherry Studio / chat UI issues)
Search-related tools emit deterministic headers (intended to be parsed verbatim):
- First line marker:
- `Using SEARCH_ID=<uuid>`
- Machine-readable headers:
- `SEARCH_ID=<uuid>`
- `FILTER_ID=<uuid>` (if applicable)
- `META=<json>`
Output shape notes:
- Only `find_matching_capacities` includes the category-count `## Summary` table.
- `filter_search_results` includes an **Applied Filters** table and a flat results table with a `Category` column (it does not reprint the summary counts).
- `get_results_by_category` returns a single category-page table (it does not reprint the summary counts).
If your UI hides tool output:
- Re-run the tool call, or
- manually copy the UUID from the server output and paste it without formatting.
## "No matches" or unexpectedly few matches
- Confirm the required competences list is not over-specific.
- Confirm role name is reasonable (role similarity affects scoring).
- If you supplied date filters, remember availability is filter-only:
- capacities must overlap the requested range
- open-ended capacity `end_date` is treated as available without limit
## Debug logging
Start the server with:
- `--log-level DEBUG`
The database clients emit safe query logs (SQL is normalized; parameters are redacted).
## Cherry Studio: stdio "connection closed" / tools not listing
Common causes:
- The server writes logs to stdout (breaks JSON-RPC). This project logs to
**stderr**.
- Cherry cannot set a working directory, so `uv run` cannot find the project.
Workaround (wrap with `zsh -lc`):
- `zsh -lc 'cd /Users/thomashandke/ws/teamlandkarte-mcp && uv run python -m teamlandkarte_mcp --config /Users/thomashandke/ws/teamlandkarte-mcp/config.toml --log-level DEBUG'`
Also ensure your `PATH` contains your `uv`/`uvx` directory, e.g.
`/Users/thomashandke/.local/bin`.
## Azure OpenAI API Errors
Symptoms:
- Tools that compute similarity fail with `AzureAPIError`.
Common causes and fixes:
1. **Missing credentials**
- Ensure the environment variable is set:
- `AZURE_OPENAI_EMBEDDING_API_KEY`
- If you rely on `.env`, confirm it is loaded in the shell that starts the MCP
server.
2. **Invalid endpoint / deployment**
- Check `[azure_openai].endpoint` in `config.toml` (must be your Azure OpenAI
resource endpoint).
- Check `embedding_deployment` exists in Azure and matches your deployment.
3. **Rate limits / throttling (HTTP 429)**
- Retry later.
- Reduce concurrency in the client/agent.
- Use the embedding cache to reduce repeated embedding calls.
4. **Network / TLS / proxy issues**
- Try `curl` to the Azure endpoint from the same host.
- If you are behind a corporate proxy, ensure the environment is configured
accordingly.
Debugging tips:
- Start the server with `--log-level DEBUG` and look at stderr logs.
- Confirm your `config.toml` is the one the server loads (`--config ...`).
## MCP sampling / embedding features
This server uses **Azure OpenAI embeddings** for embedding-based similarity
scoring.
- Ensure `AZURE_OPENAI_EMBEDDING_API_KEY` is set.
- Ensure `[azure_openai]` in `config.toml` points to a valid Azure OpenAI endpoint.
## IDE shows "line too long (.. > 79)" but Ruff passes
Some editor integrations (or their default settings) may enforce a 79 character
line length even if the repository uses a different limit.
If `uv run ruff check` passes but the editor still flags lines:
- Prefer the repo result as the source of truth.
- Adjust your editor/extension settings (Python/Ruff/Flake8) to match the
project configuration.