Files
Orchestrator/bahn/teamlandkarte-mcp/docs/architecture.md
T
ankn a5f8fb49ab 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.
2026-06-30 20:39:52 +02:00

43 KiB
Raw Blame History

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

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

MCP Client (assistant)
  <-> MCP protocol (tool calls, stdio JSON-RPC)
Teamlandkarte MCP Server (Python)
  <-> Trino (read-only SQL, connection pool)
  <-> Azure OpenAI (chat completions)
  <-> Local caches (DB query cache + search session cache)

4. Solution Strategy

4.1 Architecture Approach

Layered Architecture with clear separation:

  1. MCP Interface Layer (mcp_server.py): Exposes tools to client, formats Markdown output
  2. Business Logic Layer (matching/): Matcher, scorer, SimilarityEngine, VocabularyCache, BM25, RRF, AutoTagger, LlmFulltextMatcher (LLM-based full-text matching with rationale)
  3. Data Access Layer (database/): TrinoClient, connection pool, query logger, schema verification, read-only guard
  4. External Integration Layer (azure/): AzureOpenAIClient, CostTracker
  5. Infrastructure (cache/, utils/, config.py): QueryCache, SearchCache, date/markdown utilities, configuration loading

4.2 Key Design Decisions

Decision Rationale
BM25+RRF competence matching Lexical matching eliminates embedding false-positives; zero-out rule ensures no token overlap = score 0.0
LLM-based role similarity Semantic role comparison requires understanding synonyms and hierarchy; cached per-run
LLM-based role inference Selects from DB vocabulary; constrained to known roles only
LLM-based full-text matching (alternative method) llm_fulltext mode compares complete capacity/task profiles (description, references, certificates, skills) via Azure OpenAI Chat Completion and returns a category + 12 sentence rationale per item; no numeric scores
Optional LLM auto-tagging Bridges lexical gap for BM25 by expanding candidate competences with covered synonyms
Global BM25 index One index over all candidates' competences ensures stable IDF weights across the pool
Hard confirmation gating (default) Prevents accidental matching runs; user must confirm requirements
Markdown table output Better rendering in LLM clients; table-first outputs for robust parsing
Connection pooling Bounded pool (default 4) for Trino connections; thread-safe acquire/release
Strict startup schema verification Fail-fast when upstream views change columns
Two-tier caching DB query cache (TTL) + search session cache (TTL) for interactive exploration
Fuzzy filtering fuzzywuzzy for post-hoc filtering of search results by role/competence text

4.3 Technology Stack

Layer Technology Justification
Runtime Python 3.13 Modern features, strong typing
MCP SDK mcp (FastMCP) MCP server runtime, stdio transport
Database Client trino Trino/Presto SQL connectivity
Caching cachetools (TTLCache) In-memory TTL caches for DB queries and search sessions
Configuration TOML (tomllib) Human-readable, validated at startup
Competence Matching rank-bm25 (BM25Okapi) Lexical ranking, no native extensions
Fuzzy Matching fuzzywuzzy + python-Levenshtein Fast fuzzy string matching for filters
LLM Integration openai (AsyncAzureOpenAI) Azure OpenAI chat completions
Environment python-dotenv Load credentials from .env

5. Building Block View

5.1 Level 0: System Context

+-------------------------------------------+
|       Teamlandkarte MCP Server            |
|       (Task <-> Capacity Matching)        |
+-------------------------------------------+

5.2 Level 1: Container View

+------------------------------------------------------------+
|                  Teamlandkarte MCP Server                   |
|                                                            |
|  +-------------------------------------------------------+ |
|  | MCP Interface Layer (mcp_server.py)                    | |
|  | - task browsing + details                              | |
|  | - requirement capture + confirmation gate              | |
|  | - task->capacity matching + search refinement          | |
|  | - capacity browsing + capacity->task matching          | |
|  +----------------------------+---------------------------+ |
|                               |                             |
|  +----------------------------v---------------------------+ |
|  | Business Logic Layer (matching/)                        | |
|  | - Matcher (orchestrates matching runs)                  | |
|  | - SimilarityEngine (BM25+RRF competence, LLM role)     | |
|  | - VocabularyCache (LLM role inference from DB vocab)    | |
|  | - Scorer (weighted overall score + categorization)      | |
|  | - Bm25Index + bm25_rank_competences                    | |
|  | - reciprocal_rank_fusion (RRF normalization)            | |
|  | - AutoTagger (optional LLM competence expansion)        | |
|  | - LlmFulltextMatcher (LLM full-text profile matching)   | |
|  +----------------------------+---------------------------+ |
|                               |                             |
|  +----------------------------v---------------------------+ |
|  | Data Access Layer (database/)                           | |
|  | - TrinoClient (SQL queries, implements DBClient)        | |
|  | - ConnectionPool (bounded, thread-safe)                 | |
|  | - SchemaVerifier (startup fail-fast)                    | |
|  | - ReadOnly guard (SELECT/WITH only)                     | |
|  | - QueryLogger (redacted SQL logging)                    | |
|  +----------------------------+---------------------------+ |
|                               |                             |
|  +----------------------------v---------------------------+ |
|  | External Integration (azure/)                           | |
|  | - AzureOpenAIClient (chat completions, retry/backoff)   | |
|  | - CostTracker (session-level token accounting)          | |
|  +--------------------------------------------------------+ |
|                                                            |
|  +--------------------------------------------------------+ |
|  | Infrastructure (cache/, utils/, config.py)              | |
|  | - QueryCache (TTL, DB results)                          | |
|  | - SearchCache (TTL, search sessions + filters)          | |
|  | - dates.py (ISO parsing, overlap checks)                | |
|  | - markdown.py (table rendering)                         | |
|  | - config.py (TOML loading + validation)                 | |
|  +--------------------------------------------------------+ |
+------------------------------------------------------------+
         |                          |
         v                          v
+-----------------+       +-----------------+
|  Open Data Lake |       |  Azure OpenAI   |
|  (Trino)        |       |  (Chat API)     |
+-----------------+       +-----------------+

5.3 Level 2: Source Module Map

src/teamlandkarte_mcp/
├── __init__.py
├── __main__.py          # CLI entry point, arg parsing, logging setup
├── config.py            # TOML config loading + validation (AppConfig)
├── logging_config.py    # Logging to stderr (MCP stdio safety)
├── mcp_server.py        # FastMCP server, all tool definitions, SessionState
├── models.py            # Frozen dataclasses: Task, Capacity, Requirements, ScoredCapacity
├── azure/
│   ├── openai_client.py # AsyncAzureOpenAI wrapper (chat completions, retry)
│   └── cost_tracker.py  # Session-level token/cost accounting
├── cache/
│   ├── query_cache.py   # Generic TTLCache wrapper for DB queries
│   └── search_cache.py  # Search session store (search_id, filter_id, TTL)
├── database/
│   ├── types.py         # DBClient Protocol definition
│   ├── db_client.py     # Factory: create_db_client() -> TrinoClient
│   ├── trino_client.py  # TrinoClient (implements DBClient, uses pool)
│   ├── pool.py          # ConnectionPool (bounded, thread-safe, LIFO)
│   ├── schema_verifier.py # Startup column verification
│   ├── read_only.py     # SQL guard (SELECT/WITH only)
│   ├── query_logger.py  # Redacted query logging
│   └── hive_client.py   # (empty, reserved)
├── matching/
│   ├── matcher.py       # Matcher: orchestrates matching runs
│   ├── scorer.py        # compute_overall(), categorize()
│   ├── similarity.py    # SimilarityEngine (BM25+RRF competence, LLM role)
│   ├── vocabulary.py    # VocabularyCache (LLM role inference)
│   ├── bm25.py          # Bm25Index, bm25_rank_competences, _tokenize
│   ├── rrf.py           # reciprocal_rank_fusion (zero-out rule)
│   ├── auto_tagger.py   # AutoTagger (LLM competence expansion)
│   ├── llm_fulltext_matcher.py  # LlmFulltextMatcher (LLM full-text profile matching)
│   ├── profiles.py      # CapacityProfile, TaskProfile + serializers
│   ├── task_analyzer.py # (deprecated stub)
│   └── task_helpers.py  # (deprecated stub)
└── utils/
    ├── dates.py         # parse_iso_date, availability_overlaps
    └── markdown.py      # md_table (GFM table rendering)

5.4 Tool Surface (MCP Tools)

Discovery (DB tasks)

Tool Responsibility Dependencies
list_open_tasks List newest published tasks DB
get_task_details Table-first task fields + inferred role DB, LLM (role inference)
validate_task_requirements Compare DB fields vs LLM-inferred role + competences DB, LLM
infer_primary_role Infer closest role from task id or free text DB vocab, LLM

Requirement capture + confirmation gate

Tool Responsibility Dependencies
extract_requirements LLM-based inference for role + competences from free text DB vocab, LLM
collect_structured_requirement_data Accept user-supplied fields and stage requirements session state
update_requirements (deprecated stub, directs to collect_structured_requirement_data) --
start_guided_capture Start step-by-step guided capture session state
guided_set_description Set description (step 1/4) session state
guided_set_role Set role (step 2/4) session state
guided_set_time_range Set date range (step 3/4) session state
guided_set_competences Set competences (step 4/4) session state
show_pending_requirements Show review table for pending requirements session state
request_requirements_confirmation Mark that assistant asked user to confirm session state
confirm_requirements Confirm or reject staged requirements session state

Task -> capacity matching

Tool Responsibility Dependencies
find_matching_capacities Compute matches and store a search session (search_id). Accepts matching_method ("score" | "llm_fulltext"). DB, BM25, LLM (role) or LlmFulltextMatcher, caches
filter_search_results Refine stored results (fuzzy role/competence, availability, min_similarity). min_similarity is ignored in llm_fulltext mode and surfaced as a hint in the Applied Filters table. SearchCache
get_results_by_category Page through stored results by category. Renders Begründung column instead of score columns when the search was run in llm_fulltext mode. SearchCache

Capacity browsing + capacity -> task matching

Tool Responsibility Dependencies
list_free_capacities List recently created free capacities DB
get_capacity_details Table-first capacity fields + next steps DB
find_matching_tasks Find tasks that match a specific capacity. Accepts matching_method ("score" | "llm_fulltext"). DB, BM25, LLM (role) or LlmFulltextMatcher, caches

Team browsing + task -> team matching

Tool Responsibility Dependencies
list_teams List the first N teams (default 20) as a Markdown table (Team Id, Team Name, Schwerpunkt, Anzahl Kompetenzen, Anzahl Referenzen). DB
get_team_details Table-first team fields plus sections ## Über uns, ## Leistungen, ## Interessen, ## Kompetenzen (with (Top) marker), ## Referenzen (Partner_Name + Projekte) and ## Next steps. DB
find_matching_teams Match a task against team profiles and store a search session (search_id). Accepts matching_method ("score" | "llm_fulltext"). Persists search_type = "team_search" and the chosen matching_method in SearchCache and the META JSON. DB, BM25, LLM (role) or LlmFulltextMatcher, caches

Profile_Type parameter

Parameter Allowed values Default Selection mechanism Affected tools
Profile_Type "capacity", "team" implicit per tool name Tool selection (find_matching_capacitiescapacity, find_matching_teamsteam); persisted as search_type ("capacity_search" / "team_search") in SearchCache and META JSON find_matching_capacities, find_matching_teams, list_teams, get_team_details
  • Profile_Type = "capacity" (existing behavior): match a task against employee capacity profiles. find_matching_capacities is the entry point; availability filters apply.
  • Profile_Type = "team" (new): match a task against aggregated team profiles. find_matching_teams is the entry point; list_teams and get_team_details browse and inspect team profiles independently of a matching run. Availability filters are not applied for team searches and are surfaced as not-effective hints in the Applied Filters table.

Matching method parameter

Parameter Allowed values Default Affected tools
matching_method "score", "llm_fulltext" [matching].default_method (TOML, default "score") find_matching_capacities, find_matching_tasks, find_matching_teams
  • "score": existing BM25+RRF competence + LLM role similarity. Output includes Role Score, Competence Score, Overall Score, Category. For team searches, the team's focus_name is used as the role stand-in and top competences are weighted by matching.team.top_competency_weight (default 1.5).
  • "llm_fulltext": LLM-based full-text comparison via LlmFulltextMatcher. Output replaces all score columns with a single Begründung column (12 sentence rationale from the LLM). The persisted META JSON contains matching_method so downstream tools (get_results_by_category, filter_search_results) know which schema to render.

For team searches, the result table columns are:

Mode Columns
team_search × score Team Name, Schwerpunkt, Top-Kompetenzen, Role Score, Competence Score, Overall Score, Category
team_search × llm_fulltext Team Name, Schwerpunkt, Top-Kompetenzen, Category, Begründung

5.5 LlmFulltextMatcher (component)

The LlmFulltextMatcher (in matching/llm_fulltext_matcher.py) is the business-logic component behind matching_method = "llm_fulltext".

Aspect Details
Inputs A TaskProfile (id, title, description, skills) for match_capacities; a CapacityProfile (id, owner_name, role_name, competences, description, references with partner_name/projects, certificates) for match_tasks. The candidate list (capacities or tasks) is already prefiltered by the MCP tool (same prefilter as in score mode, e.g. availability).
Profile sources Built from in-memory Capacity/Task objects plus extras loaded from the DB via DBClient.batch_get_capacity_descriptions, batch_get_capacity_certificates, and batch_get_capacity_references (with the partner LEFT JOIN inside the same SQL query).
Outputs LlmFulltextResult with by_category: dict[str, list[LlmFulltextItem]] (categories Top/Good/Partial/Low/Irrelevant; each item carries category, ungekürzte rationale, raw fields) and errors: list[LlmFulltextError] for items where the LLM call/parse failed.
External dependency Azure OpenAI Chat Completion via AzureOpenAIClient.chat_completion(...) with response_format=json_object, deterministic German system prompt, JSON schema {"category": "...", "rationale": "..."}. One LLM call per candidate.
Determinism Profile serialization is field-stable; results within a category are sorted lexicographically by item_id; invalid LLM categories map to Irrelevant with a hint appended to the rationale.
Persistence Items are stored in SearchCache with category + ungekürzte rationale (no numeric score fields); errors are persisted alongside; META JSON carries matching_method.

6. Runtime View (Key Flows)

6.1 Server startup (fail-fast)

  1. Parse CLI arguments (--config, --log-level)
  2. Configure logging (stderr only, MCP stdio safety)
  3. Load and validate config.toml + environment variables
  4. Create DB client (TrinoClient with connection pool)
  5. Verify required DB view columns (schema verification -> fail-fast on mismatch)
  6. Initialize caches (QueryCache, SearchCache)
  7. Initialize Azure OpenAI client + CostTracker
  8. Optionally construct AutoTagger (if use_auto_tagging = true)
  9. Initialize SimilarityEngine, VocabularyCache, Matcher
  10. Register all MCP tools
  11. Start MCP server (stdio transport via mcp.run())

6.2 Task -> capacity matching

  1. Capture requirements (guided capture or extract_requirements or collect_structured_requirement_data)
  2. (If require_confirmation = true) confirmation gate:
    • show_pending_requirements -> user reviews
    • request_requirements_confirmation -> assistant marks confirmation requested
    • confirm_requirements(true) -> user confirms
  3. Run find_matching_capacities(role_name, competences, date_start?, date_end?, matching_method?):
    • Resolve matching_method (parameter > [matching].default_method > "score")
    • Query all capacities + competences from DB (cached)
    • Filter by date overlap (same prefilter for both methods)
    • Branch by matching_method:
      • "score": build global BM25 index, run BM25+RRF competence similarity + LLM role similarity, compute weighted overall score + categorize
      • "llm_fulltext": batch-load description, certificates, references (with partner LEFT JOIN) for the filtered capacities; build TaskProfile + CapacityProfile; one Azure OpenAI Chat Completion per capacity (response_format=json_object); LLM returns {category, rationale}; invalid categories → Irrelevant with hint; LLM/JSON failures recorded in a separate errors list
    • Store results in SearchCache (payload includes matching_method; LLM mode stores category + ungekürzte rationale per item, no score fields) -> return search_id
  4. Browse/filter results:
    • get_results_by_category(search_id, category, page, page_size) (renders Begründung column in LLM mode)
    • filter_search_results(search_id, ...) -> returns filter_id. In LLM mode, min_similarity is ignored and surfaced as a hint; sorting falls back to (category_rank, item_id).

6.3 Capacity -> task matching

  1. Browse capacities (list_free_capacities)
  2. Inspect (get_capacity_details)
  3. Run find_matching_tasks(capacity_id, matching_method?):
    • Resolve matching_method (parameter > [matching].default_method > "score")
    • Query capacity details + competences from DB
    • Query open tasks from DB (same prefilter for both methods)
    • Branch by matching_method:
      • "score": for each task infer role + competences, compute BM25+RRF + LLM role similarity
      • "llm_fulltext": load description, certificates, references (with partner LEFT JOIN) for the capacity; build CapacityProfile once; for each task build a TaskProfile and run one Azure OpenAI Chat Completion; same error/category-normalization handling as in 6.2
    • Store results as a search session (matching_method persisted) -> return search_id
  4. Browse/filter results (same tools as task->capacity)

6.4 Task -> team matching

  1. Capture requirements as in 6.2 (guided capture, extract_requirements, or collect_structured_requirement_data)
  2. Optional confirmation gate (same as in 6.2): show_pending_requirements -> request_requirements_confirmation -> confirm_requirements(true)
  3. Run find_matching_teams(role_name, competences, matching_method?):
    • Resolve matching_method (parameter > [matching].default_method > "score"); reject unknown values with an error listing the allowed values, without any DB or LLM call
    • Validate minimum requirements (competences non-empty)
    • Load all teams from the DB-backed cache _get_teams_cached(). The DB layer joins teamlandkarte_v_teams_latest with teamlandkarte_v_teammeter_organizational_units_latest (INNER JOIN over team_id = id for the team name), and aggregates competences (teamlandkarte_v_teammeter_team_competences_latest joined to teamlandkarte_v_competences_latest over competence_id = id) and references (teamlandkarte_v_team_references_latest LEFT JOIN teamlandkarte_v_partners_latest over partner_id = id, exposing name as Partner_Name). Joins between team master data, competences, and references run over ouid.
    • No availability prefilter: team profiles do not carry an availability range; any provided date range is ignored and surfaced as a not-effective hint in the Applied Filters table.
    • Branch by matching_method:
      • "score": build the requirements competence list, derive each team's competence list and parallel top-competence set from Team.competences, and compute competence similarity via the existing SimilarityEngine. Top competences are upweighted by matching.team.top_competency_weight (default 1.5); the per-required score is clamped to [0, 1]. Role similarity uses Team.focus_name as the role stand-in (compute_role_similarity(req.role_name, team.focus_name)). Overall score and category use the same compute_overall(...) / categorize(...) thresholds as for capacities.
      • "llm_fulltext": build a TaskProfile from the requirements and a TeamProfile per team via build_team_profile + serialize_team_profile. The user prompt is === Aufgabe === + === Team === (with an ID: header). The system prompt and JSON schema ({"category": ..., "rationale": ...}) are reused from the capacity LLM path; invalid categories are normalized to Irrelevant with a hint suffix in the rationale; LLM/JSON errors land in a separate errors list.
    • Store results in SearchCache with search_type = "team_search" and matching_method = ... -> return search_id. The META JSON exposes both search_type and matching_method so get_results_by_category / filter_search_results render the team-specific columns.
  4. Browse/filter results:
    • get_results_by_category(search_id, category, page, page_size) (renders Begründung column in llm_fulltext mode)
    • filter_search_results(search_id, ...). role_filter matches against Team_Focus_Name; competence_filter matches the competence names of the team and supports a (Top) suffix that restricts the filter to top competences. availability_date_start, availability_date_end, and is_fully_available are ignored for team_search and surfaced as team_search not-effective hints in the Applied Filters table.

6.5 Role inference flow

  1. infer_primary_role(task_id=... | task_text=...)
  2. Fetch all role names from DB vocabulary view
  3. Send task text + role list to LLM (JSON response format)
  4. Validate returned role exists in DB vocabulary
  5. Return (role_name, confidence) or None on failure

7. Deployment View

+-------------------------------------------------+
|  Developer Machine / CI                          |
|                                                  |
|  +--------------------------------------------+ |
|  |  MCP Client (IDE / AI Assistant)           | |
|  |  <-> stdio (JSON-RPC)                      | |
|  |  teamlandkarte-mcp process                 | |
|  |  (Python 3.13, single process)             | |
|  +--------------------------------------------+ |
|                                                  |
|  config.toml + .env (credentials)                |
+-------------------------------------------------+
         |                          |
         v                          v
+-----------------+       +-----------------+
|  Trino cluster  |       |  Azure OpenAI   |
|  (Data Lake)    |       |  (Chat API)     |
+-----------------+       +-----------------+

Requirements:

  • Python 3.13+ with project dependencies installed
  • Trino connectivity (host/port via config.toml, credentials via env vars)
  • Azure OpenAI endpoint + chat deployment + API key (env var AZURE_OPENAI_LLM_API_KEY)
  • Environment variables: DATA_LAKE_USERNAME, DATA_LAKE_PASSWORD, AZURE_OPENAI_LLM_API_KEY

8. Cross-cutting Concepts

8.1 Deterministic outputs

  • Tools produce table-first Markdown output (GFM tables via md_table()).
  • Search tools emit deterministic headers with SEARCH_ID=... / FILTER_ID=... for robust client parsing.
  • Scoring formula is deterministic: overall = competence_weight * competence_score + role_weight * role_score.

8.2 Caching strategy

Cache Implementation TTL Purpose
DB query cache QueryCache (cachetools TTLCache) Configurable (db_ttl_hours, default 12h) Avoid repeated expensive Trino queries
Search session cache SearchCache (cachetools TTLCache) Configurable (search_ttl_minutes, default 60min) Enable interactive exploration of results
LLM role similarity cache In-memory dict on SimilarityEngine Per matching run (cleared between runs) Avoid duplicate LLM calls for same role pair

8.3 Security

  • Read-only DB access: ensure_select_only() guard rejects non-SELECT/WITH queries
  • No write operations: Server never modifies the Data Lake
  • Credentials via environment: DATA_LAKE_USERNAME, DATA_LAKE_PASSWORD, AZURE_OPENAI_LLM_API_KEY -- never in config files
  • Query logging: Parameters are redacted before logging (query_logger.py)
  • Logging to stderr: MCP stdio transport requires stdout to remain clean JSON-RPC

8.4 Error handling

  • Schema verification failure: Server refuses to start (fail-fast)
  • DB connectivity: Lazy validation on first DB-backed tool call
  • LLM failures (role similarity): Return 0.0, no caching of failed result
  • LLM failures (auto-tagging): Return unmodified candidate list (graceful degradation)
  • LLM failures (role inference): Return None, logged as warning
  • Connection pool exhaustion: PoolExhaustedError raised immediately (no blocking wait)
  • Config errors: ConfigError raised at startup with descriptive message

8.5 Connection pooling

  • ConnectionPool (generic, thread-safe, LIFO queue)
  • Bounded at pool_size (default 4) connections
  • Connections created lazily on demand
  • closeall() for graceful shutdown

9. Scoring and Matching Algorithm

9.1 Overall scoring formula

overall_score = competence_weight * competence_score + role_weight * role_score

Default weights: competence_weight = 0.8, role_weight = 0.2 (must sum to 1.0).

9.2 Competence scoring (BM25 + RRF)

For each required competence:

  1. Query the global BM25 index (built over all filtered candidates' competences)
  2. Filter ranked results to the current candidate's competence set
  3. Apply Reciprocal Rank Fusion (k=60) to normalize scores to [0.0, 1.0]
  4. Zero-out rule: No token overlap -> score 0.0 (eliminates false positives)

Aggregate: competence_score = mean(per_required_competence_scores)

Threshold for "matched": score >= 0.5.

9.3 Role scoring (LLM)

  • LLM chat completion compares two role names semantically -> similarity in [0.0, 1.0]
  • Identical roles (case-insensitive) -> 1.0 (short-circuit, no LLM call)
  • Empty/unknown roles -> 0.0 (short-circuit)
  • Results cached symmetrically per matching run

9.4 Categorization

Category Threshold
Top overall >= configured top (default 0.8)
Good overall >= configured good (default 0.65)
Partial overall >= configured partial (default 0.5)
Low overall >= configured low (default 0.3)
Irrelevant overall < configured low

(Thresholds are configurable via [matching.thresholds].)

9.5 Optional: Auto-Tagging (LLM competence expansion)

When matching.similarity.use_auto_tagging = true:

  1. Before BM25 scoring, the AutoTagger asks the LLM which required competences are already covered by the candidate's existing competences (via synonym, abbreviation, cross-language equivalence)
  2. Covered required-competence names are appended to the candidate's working list
  3. Expansion is ephemeral -- never persisted to DB
  4. On LLM failure, scoring proceeds with the unmodified list

10. Configuration Reference

10.1 config.toml structure

[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