- Added a new target architecture document for the AI Prompt System, detailing context types, composition, and planning phases. - Refactored the backend to utilize a shared function for loading AI prompt rows, reducing SQL duplication in the `exercise_ai` module. - Incremented the application version to 0.8.159 and updated the changelog to reflect these changes, including enhancements to the AI prompt management and documentation links.
22 KiB
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
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_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)
-- 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)
# 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
[
{
"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:
{
"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,categorynicht ä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:
{
"context": "exercise",
"example_data": {
"exercise_id": 42
}
}
Response: 200 OK
{
"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
{
"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
# 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
# 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_slugoder eigene Tabelleai_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