- Implemented a new SQL migration for wiki import tracking tables. - Created an import router for handling MediaWiki imports of exercises, skills, and methods. - Developed a Semantic MediaWiki API client for direct API interactions. - Added a mapper to convert SMW properties to local database fields. - Introduced background tasks for asynchronous import processing. - Implemented logging and error handling for import operations. - Added endpoints for previewing imports, checking import status, and managing import references.
603 lines
20 KiB
Markdown
603 lines
20 KiB
Markdown
# 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)
|
||
- is_primary: true wenn Hauptfähigkeit
|
||
|
||
Antworte NUR als JSON-Array:
|
||
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
|
||
|
||
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
|