shinkan-jinkendo/.claude/docs/technical/AI_PROMPT_SYSTEM_SPEC.md
Lars 9f4678f418
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 36s
Test Suite / playwright-tests (push) Successful in 1m16s
Implement exercise_instruction_rewrite for AI Prompt System
- Added `exercise_instruction_rewrite` functionality to enhance AI-generated instructions, incorporating fields for goal, execution, preparation, and trainer notes.
- Updated `ExerciseFormAiPromptContext` to include new fields and methods for instruction handling.
- Enhanced the `run_exercise_form_ai_suggestion` function to support instruction rewriting and validation.
- Modified API endpoints and frontend components to integrate instruction features, including a new button for AI instruction revision.
- Incremented application version to 0.8.163 and updated changelog to reflect these changes, including migration details and new functionality.
2026-05-22 18:53:36 +02:00

625 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# KI-Prompt-System Universelle Admin-Konfiguration
**Version:** 1.1
**Datum:** 2026-05-30
**Status:** Kern umgesetzt (`ai_prompts`, `prompt_resolver`, Superadmin-HTTP-API); Kaskaden geplant (Abschnitt 8)
**Zielbild (Roadmap):** `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` — Kontext-Arten, Composition, Planung/Rahmen, Phasenplan.
**Ist-Stand API (Superadmin):**
- `GET /api/admin/ai-prompts`, `GET /api/admin/ai-prompts/{id}`, `PUT …`, `POST …/preview`, `POST …/reset-template`, `GET /api/admin/ai-prompts/catalog/placeholders`
- Spalte **`openrouter_model`** (Migration **070**): Optional pro Prompt-Zeile; OpenRouter **`model`**-Parameter; **`NULL`/leer ⇒ `OPENROUTER_MODEL`** aus der Umgebung.
**Autor:** Claude Code
**Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System
**Verwandt (Skill-Katalog in Übungs-KI):** `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md` — Tabelle **`ai_skill_retrieval_profiles`** (`config`-JSON ergänzt inhaltliche Prompt-/Katalog-Steuerung neben Platzhaltern).
---
## 1. Konzept
### 1.1 Ziel
Alle KI-Aufrufe in Shinkan sind durch **admin-konfigurierbare Prompt-Templates**
steuerbar. Kein KI-Aufruf ist fest im Code verdrahtet.
**Admins können:**
- Prompt-Texte anpassen (ohne Code-Änderung)
- Neue Prompt-Typen hinzufügen
- Prompts aktivieren/deaktivieren
- Prompts testen (Preview mit aufgelösten Platzhaltern)
- Platzhalter-Katalog einsehen
### 1.2 Anwendungsfälle in Shinkan
| Prompt-Slug | Verwendung |
|-------------|-----------|
| `exercise_summary` | Generiert `exercises.summary` aus goal + execution |
| `exercise_skill_suggestions` | Empfiehlt Skills + Stufen für eine Übung |
| `exercise_instruction_rewrite` | Überarbeitet Anleitung: goal, execution, preparation, trainer_notes (JSON, prägnantes HTML) |
| `exercise_category_suggestions` | Empfiehlt Fokusbereich, Stil, Zielgruppe |
| `model_skill_level_description` | Generiert Stufen-Beschreibung in der Fähigkeitsmatrix |
| `training_plan_notes` | Erzeugt Trainer-Notizen für Trainingseinheiten |
| `wiki_import_cleanup` | Bereinigt importierten Wikitext in lesbares Deutsch |
### 1.3 Architektur (aus Mitai übernommen, vereinfacht)
```
Admin konfiguriert Prompt-Template in DB (ai_prompts)
KI-Aufruf: Backend lädt Template, löst {{Platzhalter}} auf
OpenRouter API → Antwort
Antwort wird dem Trainer als Vorschlag angeboten (nicht blind gespeichert)
```
---
## 2. Datenbank
### 2.1 Migration (020_ai_prompts.sql)
```sql
-- Migration 020: AI Prompt System (admin-konfigurierbar)
-- Basis: Mitai Jinkendo prompts system (vereinfacht für Shinkan MVP)
-- Autor: Claude Code
DO $$
BEGIN
-- ============================================================================
-- AI PROMPTS (Admin-verwaltete Prompt-Templates)
-- ============================================================================
CREATE TABLE IF NOT EXISTS ai_prompts (
id SERIAL PRIMARY KEY,
slug VARCHAR(100) NOT NULL UNIQUE, -- z.B. 'exercise_summary'
display_name VARCHAR(200) NOT NULL,
description TEXT,
-- Template mit {{Platzhalter}}-Syntax
template TEXT NOT NULL,
-- Kategorie: welcher Bereich der App nutzt diesen Prompt?
category VARCHAR(50) DEFAULT 'exercise'
CHECK (category IN ('exercise', 'training', 'matrix', 'import', 'admin')),
-- Output-Format: was gibt die KI zurück?
output_format VARCHAR(10) DEFAULT 'text'
CHECK (output_format IN ('text', 'json')),
-- JSON Schema für Validierung des KI-Outputs (nur wenn output_format='json')
output_schema JSONB,
-- System-Default: kann auf Original zurückgesetzt werden
is_system_default BOOLEAN DEFAULT false,
default_template TEXT, -- Backup des originalen Templates
-- Admin-Controls
active BOOLEAN DEFAULT true,
sort_order INT DEFAULT 0,
-- Meta
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ai_prompts_slug ON ai_prompts(slug);
CREATE INDEX IF NOT EXISTS idx_ai_prompts_category ON ai_prompts(category);
CREATE INDEX IF NOT EXISTS idx_ai_prompts_active ON ai_prompts(active, sort_order);
-- ============================================================================
-- TRIGGER
-- ============================================================================
DROP TRIGGER IF EXISTS ai_prompts_update ON ai_prompts;
CREATE TRIGGER ai_prompts_update
BEFORE UPDATE ON ai_prompts
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
-- ============================================================================
-- SYSTEM-DEFAULT PROMPTS
-- ============================================================================
INSERT INTO ai_prompts (slug, display_name, description, template, category, output_format, is_system_default, default_template, sort_order) VALUES
-- 1. Exercise Summary
('exercise_summary',
'Übungs-Zusammenfassung',
'Generiert eine kurze Zusammenfassung (2-3 Sätze) einer Übung für Listen und Trainingspläne.',
$$Du bist Assistent für einen Kampfsport-Trainer.
Erstelle eine prägnante Zusammenfassung dieser Übung für die Anzeige in Listen und Trainingsplänen.
Die Zusammenfassung soll:
- 2-3 Sätze lang sein (maximal 200 Zeichen)
- Das Wesentliche: Was trainiert die Übung? Wie läuft sie ab?
- Sachlich und klar (keine Werbebotschaften)
- Auf Deutsch
Übung: {{exercise_title}}
Fokusbereich: {{exercise_focus_area}}
Ziel: {{exercise_goal}}
Durchführung: {{exercise_execution}}
Antworte NUR mit dem Zusammenfassungstext, ohne Anführungszeichen.$$,
'exercise', 'text', true,
$$Du bist Assistent für einen Kampfsport-Trainer.
Erstelle eine prägnante Zusammenfassung dieser Übung für die Anzeige in Listen und Trainingsplänen.
Die Zusammenfassung soll:
- 2-3 Sätze lang sein (maximal 200 Zeichen)
- Das Wesentliche: Was trainiert die Übung? Wie läuft sie ab?
- Sachlich und klar (keine Werbebotschaften)
- Auf Deutsch
Übung: {{exercise_title}}
Fokusbereich: {{exercise_focus_area}}
Ziel: {{exercise_goal}}
Durchführung: {{exercise_execution}}
Antworte NUR mit dem Zusammenfassungstext, ohne Anführungszeichen.$$,
1),
-- 2. Skill Suggestions
('exercise_skill_suggestions',
'Fähigkeiten-Empfehlungen',
'Empfiehlt passende Fähigkeiten + Stufen aus dem Katalog für eine Übung.',
$$Du bist Assistent für einen Kampfsport-Trainer.
Analysiere diese Übung und empfehle passende Fähigkeiten aus dem Katalog.
Übung: {{exercise_title}}
Fokusbereich: {{exercise_focus_area}}
Ziel: {{exercise_goal}}
Durchführung: {{exercise_execution}}
Verfügbare Fähigkeiten:
{{skills_catalog}}
Wähle maximal 5 passende Fähigkeiten. Für jede gib an:
- skill_id (aus der Liste)
- required_level: Voraussetzung (einsteiger|grundlagen|aufbau|fortgeschritten|experte)
- target_level: Ziel nach regelmäßigem Training (gleiche Werte)
- intensity: Trainingsintensität (niedrig|mittel|hoch)
Antworte NUR als JSON-Array:
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch"}]
Wenn keine Fähigkeit passt, antworte mit [].$$,
'exercise', 'json', true, NULL, 2),
-- 3. Category Suggestions
('exercise_category_suggestions',
'Kategorie-Empfehlungen',
'Empfiehlt Fokusbereich, Stilrichtung und Zielgruppe für eine Übung.',
$$Du bist Assistent für einen Kampfsport-Trainer.
Ordne diese Übung in die Katalog-Struktur ein.
Übung: {{exercise_title}}
Beschreibung: {{exercise_goal}}
Durchführung: {{exercise_execution}}
Verfügbare Fokusbereiche: {{focus_areas_catalog}}
Verfügbare Stilrichtungen: {{style_directions_catalog}}
Verfügbare Zielgruppen: {{target_groups_catalog}}
Wähle die passendsten Zuordnungen. Antworte NUR als JSON:
{
"focus_areas": [{"id": 1, "is_primary": true}],
"style_directions": [{"id": 2, "is_primary": true}],
"target_groups": [{"id": 5, "is_primary": true}]
}$$,
'exercise', 'json', true, NULL, 3),
-- 4. Matrix Level Description
('model_skill_level_description',
'Fähigkeitsmatrix-Stufenbeschreibung',
'Generiert Beschreibungen für Stufen in der Fähigkeitsmatrix.',
$$Du bist Fachexperte für {{focus_area}} und erstellst Lernziel-Beschreibungen.
Modell: {{model_name}}
Fokusbereich: {{focus_area}}
Zielgruppe: {{target_group}}
Fähigkeit: {{skill_name}}
Stufenanzahl: {{level_count}}
Stufen: {{level_names}}
Beschreibe für JEDE Stufe konkret und beobachtbar, was ein Schüler auf dieser Stufe
der Fähigkeit "{{skill_name}}" können soll.
Antworte NUR als JSON-Array ({{level_count}} Einträge):
[
{"level_number": 1, "description": "...", "observable_criteria": "..."},
{"level_number": 2, "description": "...", "observable_criteria": "..."}
]$$,
'matrix', 'json', true, NULL, 4),
-- 5. Wiki Import Cleanup
('wiki_import_cleanup',
'Wiki-Text-Bereinigung',
'Bereinigt importierten Wikitext in lesbares, strukturiertes Deutsch.',
$$Bereinige folgenden Text aus einem Trainings-Wiki für die Darstellung in einer modernen App.
Original-Text:
{{wiki_raw_text}}
Regeln:
- Entferne alle Wiki-Markup-Syntax ([[Links]], {{Templates}}, ==Überschriften==)
- Behalte den fachlichen Inhalt vollständig
- Schreibe in klarem, professionellem Deutsch
- Strukturiere mit Absätzen (nicht mit Wiki-Markup)
- Maximal 1000 Zeichen
Antworte NUR mit dem bereinigten Text.$$,
'import', 'text', true, NULL, 5)
ON CONFLICT (slug) DO NOTHING;
RAISE NOTICE 'Migration 020 completed successfully (AI Prompt System)';
END $$;
```
---
## 3. Platzhalter-Katalog
### 3.1 Verfügbare Platzhalter
Alle `{{Platzhalter}}` werden vom Backend-Service `prompt_resolver.py` aufgelöst.
**Kontext: exercise** (für exercise_summary, skill_suggestions, category_suggestions)
| Platzhalter | Beschreibung | Beispielwert |
|-------------|--------------|--------------|
| `{{exercise_title}}` | Titel der Übung | "Maai - Distanzübung" |
| `{{exercise_goal}}` | Ziel (erste 500 Zeichen) | "Distanzgefühl entwickeln..." |
| `{{exercise_execution}}` | Durchführung (erste 500 Zeichen) | "1. Partnerwahl..." |
| `{{exercise_preparation}}` | Vorbereitung | "Matten auslegen..." |
| `{{exercise_focus_area}}` | Primärer Fokusbereich | "Karate" |
| `{{exercise_duration}}` | Dauer | "15-20 min" |
| `{{exercise_group_size}}` | Gruppengröße | "8-12 Personen" |
| `{{skills_catalog}}` | Liste aller Skills: "ID Name (Kategorie)" | "- ID 1: Dachi Waza (Kihon)\n..." |
| `{{focus_areas_catalog}}` | Liste aller Fokusbereiche | "- ID 1: Karate\n..." |
| `{{style_directions_catalog}}` | Liste aller Stilrichtungen | "- ID 1: Shotokan\n..." |
| `{{target_groups_catalog}}` | Liste aller Zielgruppen | "- ID 1: Breitensportler\n..." |
**Kontext: matrix** (für model_skill_level_description)
| Platzhalter | Beschreibung | Beispielwert |
|-------------|--------------|--------------|
| `{{model_name}}` | Name des Reifegradmodells | "Karate Shotokan Breitensport" |
| `{{focus_area}}` | Fokusbereich des Modells | "Karate" |
| `{{style_direction}}` | Stilrichtung | "Shotokan" |
| `{{target_group}}` | Zielgruppe | "Breitensportler" |
| `{{skill_name}}` | Fähigkeitsname | "Distanzgefühl" |
| `{{skill_description}}` | Fähigkeitsbeschreibung | "Kontrolle der Kampfdistanz..." |
| `{{level_count}}` | Anzahl der Stufen | "5" |
| `{{level_names}}` | Stufen-Namen kommagetrennt | "Einsteiger, Grundlagen, Aufbau, Fortgeschritten, Experte" |
**Kontext: import**
| Platzhalter | Beschreibung |
|-------------|--------------|
| `{{wiki_raw_text}}` | Roher Wikitext aus MediaWiki |
### 3.2 Platzhalter-Auflösung (Backend)
```python
# backend/prompt_resolver.py
class ExercisePromptContext:
"""Kontext für exercise-bezogene Prompts."""
def resolve(self, template: str, exercise_data: dict, db) -> str:
variables = {
"exercise_title": exercise_data.get("title", ""),
"exercise_goal": exercise_data.get("goal", "")[:500],
"exercise_execution": exercise_data.get("execution", "")[:500],
"exercise_preparation": exercise_data.get("preparation", ""),
"exercise_focus_area": self._get_primary_focus_area(exercise_data, db),
"exercise_duration": self._format_duration(exercise_data),
"exercise_group_size": self._format_group_size(exercise_data),
"skills_catalog": self._format_skills_catalog(db),
"focus_areas_catalog": self._format_catalog(db, "focus_areas"),
"style_directions_catalog":self._format_catalog(db, "style_directions"),
"target_groups_catalog": self._format_catalog(db, "target_groups"),
}
return self._replace_placeholders(template, variables)
```
### 3.3 Unbekannte Platzhalter
Wenn ein Platzhalter nicht aufgelöst werden kann:
- `{{unknown_key}}` → bleibt als `[NICHT VERFÜGBAR]` im Template
- Warnung im API-Response
- Kein Abbruch des KI-Aufrufs
---
## 4. API-Endpoints
### 4.1 Übersicht
| Method | Endpoint | Beschreibung |
|--------|----------|--------------|
| GET | `/admin/ai-prompts` | Liste aller Prompts (Admin) |
| GET | `/admin/ai-prompts/{id}` | Prompt-Detail |
| POST | `/admin/ai-prompts` | Neuen Prompt anlegen |
| PUT | `/admin/ai-prompts/{id}` | Prompt bearbeiten |
| DELETE | `/admin/ai-prompts/{id}` | Prompt löschen (nur custom, nicht system) |
| POST | `/admin/ai-prompts/{id}/reset` | Auf System-Default zurücksetzen |
| POST | `/admin/ai-prompts/{id}/preview` | Platzhalter auflösen ohne KI-Call |
| GET | `/admin/ai-prompts/placeholders` | Platzhalter-Katalog mit Beschreibungen |
| POST | `/admin/ai-prompts/test` | Prompt mit Beispiel-Daten testen (echter KI-Call) |
### 4.2 `GET /admin/ai-prompts`
**Response:** `200 OK`
```json
[
{
"id": 1,
"slug": "exercise_summary",
"display_name": "Übungs-Zusammenfassung",
"description": "Generiert eine kurze Zusammenfassung...",
"category": "exercise",
"output_format": "text",
"active": true,
"is_system_default": true,
"is_modified": false,
"sort_order": 1
}
]
```
`is_modified`: true wenn `template != default_template`
---
### 4.3 `PUT /admin/ai-prompts/{id}`
**Request Body:**
```json
{
"template": "Du bist ein erfahrener Karate-Trainer...\n{{exercise_title}}...",
"active": true,
"display_name": "Übungs-Zusammenfassung (angepasst)"
}
```
**Response:** `200 OK` (Prompt-Objekt)
**Constraints:**
- Nur `template`, `display_name`, `description`, `active`, `sort_order` änderbar
- `slug`, `output_format`, `category` nicht änderbar (würde Code brechen)
- System-Default-Backup bleibt immer erhalten
---
### 4.4 `POST /admin/ai-prompts/{id}/preview`
Löst Platzhalter auf und zeigt das finale Prompt **OHNE KI-Call**.
**Request Body:**
```json
{
"context": "exercise",
"example_data": {
"exercise_id": 42
}
}
```
**Response:** `200 OK`
```json
{
"resolved_template": "Du bist Assistent für einen Kampfsport-Trainer.\nÜbung: Maai - Distanzübung\n...",
"placeholders_resolved": ["exercise_title", "exercise_goal"],
"placeholders_missing": [],
"estimated_tokens": 320
}
```
---
### 4.5 `GET /admin/ai-prompts/placeholders`
Liefert den vollständigen Platzhalter-Katalog.
**Response:** `200 OK`
```json
{
"categories": {
"exercise": [
{
"key": "exercise_title",
"placeholder": "{{exercise_title}}",
"description": "Titel der Übung",
"example": "Maai - Distanzübung",
"required_context": "exercise_id oder title direkt"
}
],
"matrix": [...],
"import": [...]
}
}
```
---
## 5. Verwendung in Backend-Code
### 5.1 Standard-Pattern für KI-Aufrufe
```python
# backend/services/ai_service.py
async def run_ai_prompt(
slug: str,
context_data: dict,
db,
openrouter_key: str
) -> dict:
"""
Lädt Prompt aus DB, löst Platzhalter auf, ruft KI auf.
Wirft AiNotConfiguredError wenn key fehlt oder Prompt inaktiv.
"""
# 1. Prompt laden
prompt = db.fetchrow("SELECT * FROM ai_prompts WHERE slug=$1 AND active=true", slug)
if not prompt:
raise AiNotConfiguredError(f"Prompt '{slug}' nicht gefunden oder inaktiv")
# 2. Platzhalter auflösen
resolved = resolve_placeholders(
template=prompt["template"],
context=context_data,
db=db
)
# 3. KI-Call
response = await call_openrouter(
prompt=resolved,
model=settings.openrouter_model,
api_key=openrouter_key
)
# 4. JSON validieren (wenn output_format='json')
if prompt["output_format"] == "json":
response = parse_and_validate_json(response, prompt["output_schema"])
return {
"output": response,
"prompt_slug": slug,
"ai_generated": True,
"model": settings.openrouter_model
}
```
### 5.2 Aufruf in exercises Router
```python
# backend/routers/exercises.py
@router.post("/exercises/ai/suggest")
async def suggest_for_exercise(data: ExerciseSuggestRequest, session=Depends(require_auth)):
result = {}
# Summary
try:
summary = await ai_service.run_ai_prompt(
slug="exercise_summary",
context_data={"exercise": data.dict()},
db=db,
openrouter_key=settings.openrouter_api_key
)
result["summary"] = summary
except AiNotConfiguredError:
result["summary"] = None
# Skills
try:
skills = await ai_service.run_ai_prompt(
slug="exercise_skill_suggestions",
context_data={"exercise": data.dict()},
db=db,
openrouter_key=settings.openrouter_api_key
)
result["skills"] = skills
except AiNotConfiguredError:
result["skills"] = None
return result
```
---
## 6. Frontend: Admin-UI
### 6.1 Route
```
/admin/ai-prompts → AdminAiPromptsPage (Liste)
/admin/ai-prompts/{id} → AdminAiPromptDetailPage (Edit)
```
### 6.2 Layout (Prompt-Editor)
```
┌───────────────────────────────────────────────┐
│ ✨ KI-Prompts [+ Neu] │
│ ───────────────────────────────────────────── │
│ exercise_summary [Aktiv] [Bearbeiten] │
│ exercise_skill_suggestions [Aktiv] [Bearbeiten]│
│ model_skill_level_description [Aktiv] │
└───────────────────────────────────────────────┘
── Detailansicht ──────────────────────────────────
┌───────────────────────────────────────────────┐
│ Übungs-Zusammenfassung │
│ slug: exercise_summary · Kategorie: exercise │
│ ───────────────────────────────────────────── │
│ Template: │
│ ┌────────────────────────────────────────────┐ │
│ │ Du bist Assistent für einen Kampfsport- │ │
│ │ Trainer. ... │ │
│ │ {{exercise_title}} │ │ ← Syntaxhighlight
│ │ {{exercise_goal}} │ │
│ └────────────────────────────────────────────┘ │
│ [Verfügbare Platzhalter ▼] [Vorschau] │
│ ───────────────────────────────────────────── │
│ [Mit Beispiel testen] [Original wiederherstellen]│
│ [Speichern] │
└───────────────────────────────────────────────┘
```
**Syntax-Highlighting:** `{{placeholder}}` in Farbe hervorheben.
**Platzhalter-Panel:** Ausklappbare Liste aller verfügbaren Platzhalter (aus `/admin/ai-prompts/placeholders`).
---
## 7. Zusammenspiel KI_FEATURES_SPEC ↔ AI_PROMPT_SYSTEM_SPEC
Die `KI_FEATURES_SPEC.md` beschreibt die **User-Flows** (wann wird KI angeboten,
wie sieht die Bestätigung aus).
Die `AI_PROMPT_SYSTEM_SPEC.md` (diese Datei) beschreibt die **technische Basis**
(DB-Schema, Platzhalter-System, Admin-UI).
```
KI_FEATURES_SPEC: POST /exercises/ai/suggest
AI_PROMPT_SYSTEM_SPEC: ai_service.run_ai_prompt("exercise_summary", ...)
DB: ai_prompts WHERE slug='exercise_summary'
Template + {{Platzhalter}} auflösen
OpenRouter API
```
---
## 8. Prompt-Kaskaden (geplant — nicht implementiert)
**Ziel:** Vorlagen, die andere Prompts einbinden oder in feste Stufen (System → Fach → Ausgabeformat) zerlegt werden — ohne die DB-Templates mit duplizierten Fliesstexten zu zersplittern.
**Konzeptskizze:**
- Optional neues Feld `base_slug` oder eigene Tabelle `ai_prompt_composition` (Reihenfolge, Rolle: `system|user|prepend`).
- Platzhaltersyntax z. B. `{{include_prompt:slug}}` mit **maximaler Verschachtelungstiefe** und Zykluserkennung.
- Auflösungsreihenfolge: (1) eingebundene Slugs expandieren, (2) Kontext-Variablen wie heute ersetzen.
Bis zur Umsetzung bleiben zusammengesetzte Anweisungen im **einen** Template pro Slug (wie `exercise_skill_suggestions` mit `{{skills_catalog}}`).
---
**Version:** 1.1
**Datum:** 2026-05-30
**Status:** Teile umgesetzt (DB 067/069, Resolver, Superadmin-API + UI); Kaskaden offen