shinkan-jinkendo/.claude/docs/technical/AI_PROMPT_SYSTEM_SPEC.md
Lars 294b09a5d9
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
Implement AI Skill Retrieval Profiles and Enhance Exercise AI Functionality
- Introduced migration 068 for `ai_skill_retrieval_profiles`, enabling configurable weights and quotes for skill catalog prioritization in exercise AI suggestions.
- Updated the `POST /api/exercises/ai/suggest` endpoint to include an optional `focus_areas_context` field, allowing for enhanced context in AI-generated suggestions.
- Enhanced the `exercise_ai` module to utilize context-based skill selection, incorporating scoring, category caps, and keyword patches for improved AI responses.
- Updated the ExerciseFormPageRoot component to pass focus area context to the AI suggestion API, streamlining user interaction with AI-generated content.
- Incremented version numbers in `backend/version.py` to reflect the latest changes and ensure accurate tracking in the changelog.
2026-05-22 09:49:08 +02:00

20 KiB
Raw Blame History

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

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, 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:

{
  "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

Version: 1.0 Datum: 2026-04-24 Status: DRAFT