# KI-Prompt-System – Universelle Admin-Konfiguration **Version:** 1.0 **Datum:** 2026-04-24 **Status:** DRAFT **Autor:** Claude Code **Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System --- ## 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) ```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 ``` --- **Version:** 1.0 **Datum:** 2026-04-24 **Status:** DRAFT