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,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 + 1–2 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 (1–2 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 user’s 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 3–5 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user