Refactor Catalog Prompt Slot Management and Enhance Fallback Logic
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced a new function `_resolve_entry_slot_values` to streamline the merging of stored slot values with fallbacks, improving code clarity and maintainability. - Updated `get_catalog_entry_slots` and `resolve_catalog_prompt_variables` functions to utilize the new fallback logic, enhancing the handling of catalog entries. - Enhanced the `CatalogPromptSlotsEditor` component to display fallback information for slots, improving user experience in managing catalog prompts. - Incremented version numbers and updated changelog to reflect the new features and improvements.
This commit is contained in:
parent
53f2b027cc
commit
7e5ef4561a
|
|
@ -15,6 +15,7 @@ from planning_catalog_context import (
|
|||
PlanningCatalogContextItem,
|
||||
catalog_context_has_items,
|
||||
)
|
||||
from catalog_slot_fallbacks import merge_stored_slots_with_fallbacks
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dimensionen (Prioritätsreihenfolge)
|
||||
|
|
@ -196,6 +197,20 @@ def _fallback_slot_type_rows() -> List[Dict[str, Any]]:
|
|||
return rows
|
||||
|
||||
|
||||
def _resolve_entry_slot_values(
|
||||
stored: Mapping[str, str],
|
||||
row: Mapping[str, Any],
|
||||
catalog_kind: str,
|
||||
) -> Dict[str, str]:
|
||||
"""DB → Namens-Fallback → Stammdaten-Beschreibung (nur description)."""
|
||||
return merge_stored_slots_with_fallbacks(
|
||||
stored,
|
||||
catalog_kind=catalog_kind,
|
||||
name=str(row.get("name") or ""),
|
||||
stammdaten_description=str(row.get("description") or ""),
|
||||
)
|
||||
|
||||
|
||||
def get_catalog_entry_slots(cur, catalog_kind: str, catalog_id: int) -> Dict[str, Any]:
|
||||
cfg = _KIND_BY_NAME.get((catalog_kind or "").strip())
|
||||
if not cfg:
|
||||
|
|
@ -204,17 +219,13 @@ def get_catalog_entry_slots(cur, catalog_kind: str, catalog_id: int) -> Dict[str
|
|||
if not row:
|
||||
raise LookupError("Katalog-Eintrag nicht gefunden")
|
||||
stored = _load_slots_for_entry(cur, cfg.kind, catalog_id)
|
||||
merged: Dict[str, str] = {}
|
||||
for slot in SLOT_KEYS:
|
||||
if slot == "description":
|
||||
merged[slot] = stored.get("description") or row.get("description") or ""
|
||||
else:
|
||||
merged[slot] = stored.get(slot, "")
|
||||
merged = _resolve_entry_slot_values(stored, row, cfg.kind)
|
||||
return {
|
||||
"catalog_kind": cfg.kind,
|
||||
"catalog_id": int(catalog_id),
|
||||
"name": row["name"],
|
||||
"slots": merged,
|
||||
"stored_slots": {k: stored.get(k, "") for k in SLOT_KEYS},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -330,15 +341,12 @@ def resolve_catalog_prompt_variables(
|
|||
if not row:
|
||||
continue
|
||||
stored = _load_slots_for_entry(cur, cfg.kind, row["id"]) if _slot_types_table_ready(cur) else {}
|
||||
slot_values: Dict[str, str] = {}
|
||||
slot_values = _resolve_entry_slot_values(stored, row, cfg.kind)
|
||||
for sk in SLOT_KEYS:
|
||||
if sk == "description":
|
||||
slot_values[sk] = stored.get("description") or row.get("description") or ""
|
||||
else:
|
||||
slot_values[sk] = stored.get(sk, "")
|
||||
pk = placeholder_key(cfg.kind, sk)
|
||||
variables[pk] = slot_values[sk]
|
||||
if slot_values[sk].strip() and sk in LLM_SLOT_KEYS:
|
||||
text = slot_values.get(sk, "")
|
||||
variables[pk] = text
|
||||
if text.strip() and sk in LLM_SLOT_KEYS:
|
||||
has_any = True
|
||||
active_slots.append(pk)
|
||||
|
||||
|
|
@ -349,6 +357,7 @@ def resolve_catalog_prompt_variables(
|
|||
"is_primary": bool(active.is_primary),
|
||||
"weight": float(active.weight),
|
||||
"filled_slots": [k for k in LLM_SLOT_KEYS if slot_values.get(k, "").strip()],
|
||||
"stored_slots": [k for k in SLOT_KEYS if (stored.get(k) or "").strip()],
|
||||
}
|
||||
|
||||
section = _render_dimension_section(cfg.label_de, row["name"], slot_values, slot_keys=profile)
|
||||
|
|
@ -379,7 +388,11 @@ def get_rematch_guard_for_catalog(
|
|||
if not active:
|
||||
continue
|
||||
stored = _load_slots_for_entry(cur, cfg.kind, active.id)
|
||||
guard = (stored.get("rematch_guard") or "").strip()
|
||||
row = _load_catalog_row(cur, cfg.table, active.id)
|
||||
if not row:
|
||||
continue
|
||||
slot_values = _resolve_entry_slot_values(stored, row, cfg.kind)
|
||||
guard = (slot_values.get("rematch_guard") or "").strip()
|
||||
if guard:
|
||||
return guard
|
||||
return None
|
||||
|
|
|
|||
284
backend/catalog_slot_fallbacks.py
Normal file
284
backend/catalog_slot_fallbacks.py
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
"""
|
||||
Namensbasierte Fallback-Slots — bis Admin/DB befüllt sind (H1-Registry-Inhalt).
|
||||
|
||||
DB-Werte in catalog_prompt_slots haben immer Vorrang. Fallbacks füllen nur leere Slot-Keys.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Dict, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
_UMLAUT_MAP = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss", "Ä": "ae", "Ö": "oe", "Ü": "ue"})
|
||||
|
||||
SlotPack = Dict[str, str]
|
||||
|
||||
# (catalog_kind, name_pattern_lower) — erste passende Regel gewinnt; * = Default pro Kind
|
||||
_FALLBACK_RULES: Tuple[Tuple[str, str, SlotPack], ...] = (
|
||||
# --- focus_area ---
|
||||
(
|
||||
"focus_area",
|
||||
"gewaltschutz",
|
||||
{
|
||||
"description": (
|
||||
"Planung zielt auf Prävention, Deeskalation, Grenzen und sichere Übungsformen — "
|
||||
"nicht auf Wettkampf-Perfektion oder Technik-Show."
|
||||
),
|
||||
"hints_on_progression": (
|
||||
"Phasen: Wahrnehmung → Grenzen → Deeskalation → sichere Übungsformen; "
|
||||
"keine Kumite-Perfektionsstufen erzwingen."
|
||||
),
|
||||
"hints_on_exercise": (
|
||||
"Übungen mit Rollen, Kommunikation, Ausweichen; keine rein technischen Kick-Fokus-Inseln ohne Bezug."
|
||||
),
|
||||
"hints_on_path_qa": (
|
||||
"Gute Pfade bauen Sicherheit, Kommunikation und Alternativen auf; "
|
||||
"„Lücken“ sind fehlende Deeskalations- oder Rollenspiel-Stufen, nicht fehlende Kick-Varianten."
|
||||
),
|
||||
"anti_patterns": "Nicht nach Kumite-Tiefe, Explosivität oder Wettkampf-Belastung bewerten.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"focus_area",
|
||||
"selbstverteidigung",
|
||||
{
|
||||
"description": (
|
||||
"Praktische Selbstverteidigung: realistische Szenarien, Sicherheit und "
|
||||
"anwendungsnahe Progression — nicht Show-Technik oder Wettkampf-Kata."
|
||||
),
|
||||
"hints_on_progression": (
|
||||
"Von Wahrnehmung und Distanz zu einfachen Abwehrmustern und kontrollierter Anwendung."
|
||||
),
|
||||
"hints_on_exercise": "Partnerübungen mit klaren Sicherheitsregeln; Szenario-Bezug wichtiger als Stil-Show.",
|
||||
"hints_on_path_qa": (
|
||||
"Lücken bei Szenario- oder Sicherheitsstufen sind relevant; "
|
||||
"fehlende Kick-Varianten oder Wettkampftiefe sind kein Mangel."
|
||||
),
|
||||
"anti_patterns": "Keine Wettkampf- oder Kata-Perfektion als QS-Maßstab.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"focus_area",
|
||||
"fitness",
|
||||
{
|
||||
"description": (
|
||||
"Fitness- und Konditionsorientierung mit sicherer Belastungssteuerung; "
|
||||
"Technikbezug nur wo fachlich sinnvoll."
|
||||
),
|
||||
"hints_on_progression": "Progression von niedriger zu moderater Belastung; klare Pausen und Technikhygiene.",
|
||||
"hints_on_path_qa": (
|
||||
"Keine Wettkampf-Spezialisierung als Pflicht-Kriterium; "
|
||||
"Belastungssteigerung ohne Technikbezug abwerten."
|
||||
),
|
||||
"anti_patterns": "Keine Kumite-Perfektion oder Wettkampf-Kombinationen als QS-Maßstab verlangen.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"focus_area",
|
||||
"karate",
|
||||
{
|
||||
"description": (
|
||||
"Technik-Curriculum im Karate-Kontext: aufeinander aufbauende Kihon-Progression "
|
||||
"mit klaren Qualitätsankern (Stand, Hüfte, Kime)."
|
||||
),
|
||||
"hints_on_progression": (
|
||||
"Typische Phasen: Einstieg → Grundlagen → Koordination/Kraft → Anwendung → optional Vertiefung; "
|
||||
"Grundlagen vor Perfektion."
|
||||
),
|
||||
"hints_on_exercise": (
|
||||
"Kihon und Partnerübungen mit Technikbezug; reine Kraft-/Ausdauer-Inseln nur mit klarer Begründung."
|
||||
),
|
||||
"hints_on_path_qa": (
|
||||
"Kohärente Progression Grundlagen → Anwendung → Vertiefung; "
|
||||
"Übergänge ohne Sprünge; themenfremde Kraft-/Ausdauer-Inseln abwerten."
|
||||
),
|
||||
"anti_patterns": "Keine pauschale Perfektions-Stufe verlangen, wenn Kontext Breitensport ist.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"focus_area",
|
||||
"*",
|
||||
{
|
||||
"description": "Technik- oder Themen-Curriculum mit didaktisch aufeinander aufbauenden Stufen.",
|
||||
"hints_on_progression": "Grundlagen vor Anwendung; moderate Sprünge zwischen Stufen vermeiden.",
|
||||
"hints_on_path_qa": (
|
||||
"Kohärente Progression zum Anfrage-Thema; "
|
||||
"Lücken sind fehlende Zwischenstufen im Lernpfad, nicht fehlende Nebenthemen."
|
||||
),
|
||||
"hints_on_exercise": "Übungen mit klarem Bezug zum Pfad-Thema und zur Stufe.",
|
||||
},
|
||||
),
|
||||
# --- training_type ---
|
||||
(
|
||||
"training_type",
|
||||
"breitensport",
|
||||
{
|
||||
"description": (
|
||||
"Partizipation, Verständlichkeit, Freude am Bewegen; weniger maximale Spezialisierung."
|
||||
),
|
||||
"hints_on_progression": "Moderater Schwierigkeitsanstieg; Perfektionsphasen optional.",
|
||||
"hints_on_path_qa": (
|
||||
"Hohe OK-Rate bei moderatem Schwierigkeitsanstieg; "
|
||||
"„Perfektion“-Stufen nur optional, nicht als Pflicht-Lücke."
|
||||
),
|
||||
"rematch_guard": "Keine leeren Slots erzwingen, nur um eine Leistungs-Perfektionsstufe zu füllen.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"training_type",
|
||||
"leistungssport",
|
||||
{
|
||||
"description": "Leistungsorientiertes Training mit höherer Anspruchskurve und Spezialisierung.",
|
||||
"hints_on_progression": "Belastungs- und Kombinationsprogressionen sind erwünscht.",
|
||||
"hints_on_path_qa": (
|
||||
"Höhere Anspruchskurven sind passend; Lücken in Spezialisierung können echte Hinweise sein."
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"training_type",
|
||||
"wettkampf",
|
||||
{
|
||||
"description": (
|
||||
"Wettkampforientiertes Training mit höherer Anspruchskurve und belastungsnahen Phasen."
|
||||
),
|
||||
"hints_on_progression": "Anwendungs- und Druckphasen zeitig einplanen.",
|
||||
"hints_on_path_qa": (
|
||||
"Spezialisierung, Kombination und Belastung unter Druck sind relevant; "
|
||||
"Lücken in Anwendungs- oder Perfektionsphasen können echte Hinweise sein."
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"training_type",
|
||||
"*",
|
||||
{
|
||||
"hints_on_path_qa": "Didaktische Kohärenz wichtiger als maximale Spezialisierung — Kontext beachten.",
|
||||
},
|
||||
),
|
||||
# --- target_group ---
|
||||
(
|
||||
"target_group",
|
||||
"kinder",
|
||||
{
|
||||
"description": (
|
||||
"Kinder: kurze Einheiten, spielerische Einstiege, Sicherheit und altersgerechte Komplexität."
|
||||
),
|
||||
"hints_on_progression": "Spielerische Einstiege; kurze Abschnitte; Sicherheit vor Perfektion.",
|
||||
"hints_on_path_qa": (
|
||||
"Didaktik ohne Überforderung; klare Regeln und Sicherheit vor Perfektion; "
|
||||
"Lücken bei Spiel-/Rollenelementen wichtiger als Wettkampftiefe."
|
||||
),
|
||||
"anti_patterns": "Keine Erwachsenen-Wettkampf-Perfektion als QS-Maßstab.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"target_group",
|
||||
"leistungssportler",
|
||||
{
|
||||
"description": "Leistungsgruppe: höhere Anspruchskurven und Spezialisierung sind fachlich passend.",
|
||||
"hints_on_progression": "Anspruchskurve und Spezialisierung dürfen steiler sein.",
|
||||
"hints_on_path_qa": (
|
||||
"Höhere Anspruchskurven, Belastungs- und Kombinationsprogressionen sind relevant; "
|
||||
"Lücken in Spezialisierung können echte Hinweise sein."
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"target_group",
|
||||
"breitensportler",
|
||||
{
|
||||
"description": "Breitensport: Partizipation und Verständlichkeit vor maximaler Spezialisierung.",
|
||||
"hints_on_path_qa": (
|
||||
"Moderate Progression; Perfektions-Lücken sind selten echte Mängel."
|
||||
),
|
||||
"anti_patterns": "Keine Leistungssport-Perfektion als Pflicht-Kriterium.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"target_group",
|
||||
"*",
|
||||
{
|
||||
"hints_on_path_qa": "Zielgruppe im Tempo und in der Komplexität berücksichtigen.",
|
||||
},
|
||||
),
|
||||
# --- style_direction ---
|
||||
(
|
||||
"style_direction",
|
||||
"shotokan",
|
||||
{
|
||||
"description": (
|
||||
"Shotokan-Linie: klare Kihon-Struktur, Hüft- und Standarbeit als wiederkehrende Qualitätsanker."
|
||||
),
|
||||
"hints_on_progression": "Nuancen in Stellung und Hüfttechnik; kein neuer Planungstyp.",
|
||||
"hints_on_path_qa": "Konsistenz von Stand, Hüfte und Kime entlang des Pfads bewerten.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"style_direction",
|
||||
"*",
|
||||
{
|
||||
"hints_on_progression": (
|
||||
"Stil-spezifische Nuancen (Stand, Hüfte, Rhythmus) einbeziehen — ohne Stilwechsel zu erzwingen."
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def normalize_catalog_name_key(name: str) -> str:
|
||||
s = unicodedata.normalize("NFKD", (name or "").translate(_UMLAUT_MAP))
|
||||
s = s.encode("ascii", "ignore").decode("ascii").lower()
|
||||
s = re.sub(r"[^a-z0-9]+", "_", s).strip("_")
|
||||
return s or "unknown"
|
||||
|
||||
|
||||
def get_fallback_slots_for_entry(catalog_kind: str, name: str) -> SlotPack:
|
||||
kind = (catalog_kind or "").strip().lower()
|
||||
norm = normalize_catalog_name_key(name)
|
||||
default: SlotPack = {}
|
||||
for rule_kind, pattern, pack in _FALLBACK_RULES:
|
||||
if rule_kind != kind:
|
||||
continue
|
||||
if pattern == "*":
|
||||
default = dict(pack)
|
||||
continue
|
||||
if pattern in norm or norm.startswith(pattern) or pattern in (name or "").lower():
|
||||
return dict(pack)
|
||||
return default
|
||||
|
||||
|
||||
def merge_stored_slots_with_fallbacks(
|
||||
stored: Mapping[str, str],
|
||||
*,
|
||||
catalog_kind: str,
|
||||
name: str,
|
||||
stammdaten_description: str = "",
|
||||
) -> Dict[str, str]:
|
||||
"""DB + Stammdaten-Beschreibung + Namens-Fallback."""
|
||||
fallbacks = get_fallback_slots_for_entry(catalog_kind, name)
|
||||
out: Dict[str, str] = {}
|
||||
for key in (
|
||||
"description",
|
||||
"hints_on_progression",
|
||||
"hints_on_exercise",
|
||||
"hints_on_path_qa",
|
||||
"anti_patterns",
|
||||
"rematch_guard",
|
||||
):
|
||||
if key == "description":
|
||||
out[key] = (
|
||||
(stored.get(key) or "").strip()
|
||||
or (fallbacks.get(key) or "").strip()
|
||||
or (stammdaten_description or "").strip()
|
||||
)
|
||||
else:
|
||||
out[key] = (stored.get(key) or "").strip() or (fallbacks.get(key) or "").strip()
|
||||
return out
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_fallback_slots_for_entry",
|
||||
"merge_stored_slots_with_fallbacks",
|
||||
"normalize_catalog_name_key",
|
||||
]
|
||||
167
backend/migrations/094_catalog_prompt_slots_full_seed.sql
Normal file
167
backend/migrations/094_catalog_prompt_slots_full_seed.sql
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
-- Migration 094: Vollständige Befüllung catalog_prompt_slots (H1-Inhalte + Defaults für alle Stammdaten)
|
||||
|
||||
CREATE TEMP TABLE IF NOT EXISTS _catalog_slot_seed (
|
||||
catalog_kind VARCHAR(32) NOT NULL,
|
||||
name_pattern TEXT NOT NULL,
|
||||
slot_key VARCHAR(64) NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
TRUNCATE _catalog_slot_seed;
|
||||
|
||||
-- Primärfokus Karate (häufigster Technik-Pfad)
|
||||
INSERT INTO _catalog_slot_seed (catalog_kind, name_pattern, slot_key, content) VALUES
|
||||
('focus_area', 'Karate', 'description',
|
||||
'Technik-Curriculum im Karate-Kontext: aufeinander aufbauende Kihon-Progression mit klaren Qualitätsankern (Stand, Hüfte, Kime).'),
|
||||
('focus_area', 'Karate', 'hints_on_progression',
|
||||
'Typische Phasen: Einstieg → Grundlagen → Koordination/Kraft → Anwendung → optional Vertiefung; Grundlagen vor Perfektion.'),
|
||||
('focus_area', 'Karate', 'hints_on_exercise',
|
||||
'Kihon und Partnerübungen mit Technikbezug; reine Kraft-/Ausdauer-Inseln nur mit klarer Begründung.'),
|
||||
('focus_area', 'Karate', 'hints_on_path_qa',
|
||||
'Kohärente Progression Grundlagen → Anwendung → Vertiefung; Übergänge ohne Sprünge; themenfremde Kraft-/Ausdauer-Inseln abwerten.'),
|
||||
('focus_area', 'Karate', 'anti_patterns',
|
||||
'Keine pauschale Perfektions-Stufe verlangen, wenn der Trainingsstil Breitensport ist.');
|
||||
|
||||
-- Selbstverteidigung
|
||||
INSERT INTO _catalog_slot_seed VALUES
|
||||
('focus_area', 'Selbstverteidigung', 'description',
|
||||
'Praktische Selbstverteidigung: realistische Szenarien, Sicherheit und anwendungsnahe Progression — nicht Show-Technik oder Wettkampf-Kata.'),
|
||||
('focus_area', 'Selbstverteidigung', 'hints_on_progression',
|
||||
'Von Wahrnehmung und Distanz zu einfachen Abwehrmustern und kontrollierter Anwendung.'),
|
||||
('focus_area', 'Selbstverteidigung', 'hints_on_exercise',
|
||||
'Partnerübungen mit klaren Sicherheitsregeln; Szenario-Bezug wichtiger als Stil-Show.'),
|
||||
('focus_area', 'Selbstverteidigung', 'hints_on_path_qa',
|
||||
'Lücken bei Szenario- oder Sicherheitsstufen sind relevant; fehlende Kick-Varianten oder Wettkampftiefe sind kein Mangel.'),
|
||||
('focus_area', 'Selbstverteidigung', 'anti_patterns',
|
||||
'Keine Wettkampf- oder Kata-Perfektion als QS-Maßstab.');
|
||||
|
||||
-- Gewaltschutz (ergänzt 092)
|
||||
INSERT INTO _catalog_slot_seed VALUES
|
||||
('focus_area', 'Gewaltschutz', 'hints_on_progression',
|
||||
'Phasen: Wahrnehmung → Grenzen → Deeskalation → sichere Übungsformen; keine Kumite-Perfektionsstufen erzwingen.'),
|
||||
('focus_area', 'Gewaltschutz', 'hints_on_exercise',
|
||||
'Übungen mit Rollen, Kommunikation, Ausweichen; keine rein technischen Kick-Fokus-Inseln ohne Bezug.');
|
||||
|
||||
-- Fitness (falls vorhanden)
|
||||
INSERT INTO _catalog_slot_seed VALUES
|
||||
('focus_area', 'Fitness', 'description',
|
||||
'Fitness- und Konditionsorientierung mit sicherer Belastungssteuerung; Technikbezug nur wo fachlich sinnvoll.'),
|
||||
('focus_area', 'Fitness', 'hints_on_progression',
|
||||
'Progression von niedriger zu moderater Belastung; klare Pausen und Technikhygiene.'),
|
||||
('focus_area', 'Fitness', 'hints_on_path_qa',
|
||||
'Keine Wettkampf-Spezialisierung als Pflicht-Kriterium; Belastungssteigerung ohne Technikbezug abwerten.'),
|
||||
('focus_area', 'Fitness', 'anti_patterns',
|
||||
'Keine Kumite-Perfektion oder Wettkampf-Kombinationen als QS-Maßstab verlangen.');
|
||||
|
||||
-- Trainingsstile (global)
|
||||
INSERT INTO _catalog_slot_seed VALUES
|
||||
('training_type', 'Breitensport', 'hints_on_progression',
|
||||
'Moderater Schwierigkeitsanstieg; Perfektionsphasen optional.'),
|
||||
('training_type', 'Breitensport', 'anti_patterns',
|
||||
'Keine Leistungssport-Perfektion als Pflicht-Lücke.'),
|
||||
('training_type', 'Leistungssport', 'description',
|
||||
'Leistungsorientiertes Training mit höherer Anspruchskurve und Spezialisierung.'),
|
||||
('training_type', 'Leistungssport', 'hints_on_progression',
|
||||
'Belastungs- und Kombinationsprogressionen sind erwünscht.'),
|
||||
('training_type', 'Leistungssport', 'hints_on_path_qa',
|
||||
'Höhere Anspruchskurven sind passend; Lücken in Spezialisierung können echte Hinweise sein.'),
|
||||
('training_type', 'Wettkampf', 'hints_on_progression',
|
||||
'Anwendungs- und Druckphasen zeitig einplanen.');
|
||||
|
||||
-- Zielgruppen
|
||||
INSERT INTO _catalog_slot_seed VALUES
|
||||
('target_group', 'Breitensportler', 'description',
|
||||
'Breitensport: Partizipation und Verständlichkeit vor maximaler Spezialisierung.'),
|
||||
('target_group', 'Breitensportler', 'hints_on_path_qa',
|
||||
'Moderate Progression; Perfektions-Lücken sind selten echte Mängel.'),
|
||||
('target_group', 'Breitensportler', 'anti_patterns',
|
||||
'Keine Leistungssport-Perfektion als Pflicht-Kriterium.'),
|
||||
('target_group', 'Kinder', 'hints_on_progression',
|
||||
'Spielerische Einstiege; kurze Abschnitte; Sicherheit vor Perfektion.'),
|
||||
('target_group', 'Leistungssportler', 'hints_on_progression',
|
||||
'Anspruchskurve und Spezialisierung dürfen steiler sein.');
|
||||
|
||||
-- Stilrichtungen (generisch + Shotokan-Details via 092)
|
||||
INSERT INTO _catalog_slot_seed VALUES
|
||||
('style_direction', 'Goju-Ryu', 'hints_on_progression',
|
||||
'Stil-Nuancen (Stand, Atem, Kime) einbeziehen — kein Stilwechsel erzwingen.'),
|
||||
('style_direction', 'Wado-Ryu', 'hints_on_progression',
|
||||
'Stil-Nuancen (Stand, Atem, Kime) einbeziehen — kein Stilwechsel erzwingen.'),
|
||||
('style_direction', 'Shito-Ryu', 'hints_on_progression',
|
||||
'Stil-Nuancen (Stand, Atem, Kime) einbeziehen — kein Stilwechsel erzwingen.'),
|
||||
('style_direction', 'Kyokushin', 'hints_on_progression',
|
||||
'Stil-Nuancen (Stand, Belastung, Kime) einbeziehen — kein Stilwechsel erzwingen.');
|
||||
|
||||
-- Fokusbereiche: aus Seed-Tabelle
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT s.catalog_kind, fa.id, s.slot_key, s.content
|
||||
FROM _catalog_slot_seed s
|
||||
JOIN focus_areas fa ON fa.name ILIKE s.name_pattern
|
||||
WHERE s.catalog_kind = 'focus_area'
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key)
|
||||
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
|
||||
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT s.catalog_kind, tt.id, s.slot_key, s.content
|
||||
FROM _catalog_slot_seed s
|
||||
JOIN training_types tt ON tt.name ILIKE s.name_pattern
|
||||
WHERE s.catalog_kind = 'training_type'
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key)
|
||||
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
|
||||
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT s.catalog_kind, tg.id, s.slot_key, s.content
|
||||
FROM _catalog_slot_seed s
|
||||
JOIN target_groups tg ON tg.name ILIKE s.name_pattern
|
||||
WHERE s.catalog_kind = 'target_group'
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key)
|
||||
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
|
||||
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT s.catalog_kind, sd.id, s.slot_key, s.content
|
||||
FROM _catalog_slot_seed s
|
||||
JOIN style_directions sd ON sd.name ILIKE s.name_pattern
|
||||
WHERE s.catalog_kind = 'style_direction'
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key)
|
||||
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
|
||||
|
||||
-- Default-Technik-Pack für Fokusbereiche ohne hints_on_path_qa (außer Gewaltschutz/Fitness)
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT 'focus_area', fa.id, 'hints_on_path_qa',
|
||||
'Kohärente Progression zum Anfrage-Thema; Lücken sind fehlende Zwischenstufen im Lernpfad, nicht fehlende Nebenthemen.'
|
||||
FROM focus_areas fa
|
||||
WHERE fa.name NOT ILIKE 'Gewaltschutz'
|
||||
AND fa.name NOT ILIKE 'Fitness'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM catalog_prompt_slots cps
|
||||
WHERE cps.catalog_kind = 'focus_area' AND cps.catalog_id = fa.id AND cps.slot_key = 'hints_on_path_qa'
|
||||
AND TRIM(cps.content) <> ''
|
||||
)
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO NOTHING;
|
||||
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT 'focus_area', fa.id, 'hints_on_progression',
|
||||
'Grundlagen vor Anwendung; moderate Sprünge zwischen Stufen vermeiden.'
|
||||
FROM focus_areas fa
|
||||
WHERE fa.name NOT ILIKE 'Gewaltschutz'
|
||||
AND fa.name NOT ILIKE 'Fitness'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM catalog_prompt_slots cps
|
||||
WHERE cps.catalog_kind = 'focus_area' AND cps.catalog_id = fa.id AND cps.slot_key = 'hints_on_progression'
|
||||
AND TRIM(cps.content) <> ''
|
||||
)
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO NOTHING;
|
||||
|
||||
-- Stilrichtungen ohne Eintrag: generischer Progressions-Hinweis
|
||||
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
|
||||
SELECT 'style_direction', sd.id, 'hints_on_progression',
|
||||
'Stil-spezifische Nuancen (Stand, Hüfte, Rhythmus) einbeziehen — ohne Stilwechsel zu erzwingen.'
|
||||
FROM style_directions sd
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM catalog_prompt_slots cps
|
||||
WHERE cps.catalog_kind = 'style_direction' AND cps.catalog_id = sd.id AND cps.slot_key = 'hints_on_progression'
|
||||
AND TRIM(cps.content) <> ''
|
||||
)
|
||||
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO NOTHING;
|
||||
|
||||
DROP TABLE IF EXISTS _catalog_slot_seed;
|
||||
38
backend/tests/test_catalog_slot_fallbacks.py
Normal file
38
backend/tests/test_catalog_slot_fallbacks.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"""Tests Namens-Fallback für Katalog-Prompt-Slots."""
|
||||
from catalog_slot_fallbacks import get_fallback_slots_for_entry, merge_stored_slots_with_fallbacks
|
||||
from catalog_prompt_slots import _resolve_entry_slot_values
|
||||
|
||||
|
||||
def test_karate_fallback_has_path_qa():
|
||||
pack = get_fallback_slots_for_entry("focus_area", "Karate")
|
||||
assert "Kohärente Progression" in pack.get("hints_on_path_qa", "")
|
||||
|
||||
|
||||
def test_db_value_overrides_fallback():
|
||||
merged = merge_stored_slots_with_fallbacks(
|
||||
{"hints_on_path_qa": "Eigener QS-Text."},
|
||||
catalog_kind="focus_area",
|
||||
name="Karate",
|
||||
stammdaten_description="Traditionelles Karate",
|
||||
)
|
||||
assert merged["hints_on_path_qa"] == "Eigener QS-Text."
|
||||
|
||||
|
||||
def test_empty_db_uses_karate_fallback():
|
||||
merged = _resolve_entry_slot_values(
|
||||
{},
|
||||
{"name": "Karate", "description": "Traditionelles Karate"},
|
||||
"focus_area",
|
||||
)
|
||||
assert "Kihon-Progression" in merged["description"] or "Technik-Curriculum" in merged["description"]
|
||||
assert "Kohärente Progression" in merged["hints_on_path_qa"]
|
||||
|
||||
|
||||
def test_gewaltschutz_fallback_no_kumite():
|
||||
merged = _resolve_entry_slot_values(
|
||||
{},
|
||||
{"name": "Gewaltschutz", "description": "Gewaltprävention"},
|
||||
"focus_area",
|
||||
)
|
||||
assert "Deeskalation" in merged["hints_on_path_qa"]
|
||||
assert "Kumite-Tiefe" in merged["anti_patterns"]
|
||||
|
|
@ -90,13 +90,13 @@ def test_granular_placeholder_focus_area_hints_on_path_qa():
|
|||
assert resolved["has_catalog_guidance"] == "true"
|
||||
|
||||
|
||||
def test_description_fallback_from_stammdaten():
|
||||
def test_unknown_focus_uses_default_description_pack():
|
||||
cur = _mock_cur(
|
||||
rows_by_table={
|
||||
"focus_areas": {
|
||||
4: {
|
||||
"name": "Gewaltschutz",
|
||||
"description": "Gewaltprävention und Deeskalation",
|
||||
"name": "Sonderfokus Alpha",
|
||||
"description": "Kurze Stammdaten-Beschreibung",
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -106,7 +106,9 @@ def test_description_fallback_from_stammdaten():
|
|||
focus_areas=[PlanningCatalogContextItem(id=4, is_primary=True)],
|
||||
)
|
||||
resolved = resolve_catalog_prompt_variables(cur, catalog)
|
||||
assert resolved[placeholder_key("focus_area", "description")] == "Gewaltprävention und Deeskalation"
|
||||
desc = resolved[placeholder_key("focus_area", "description")]
|
||||
assert "Technik- oder Themen-Curriculum" in desc
|
||||
assert resolved[placeholder_key("focus_area", "hints_on_path_qa")]
|
||||
|
||||
|
||||
def test_empty_without_catalog():
|
||||
|
|
@ -116,15 +118,15 @@ def test_empty_without_catalog():
|
|||
assert out["catalog_guidance_block"] == ""
|
||||
|
||||
|
||||
def test_unknown_entry_no_guidance_block():
|
||||
def test_unknown_entry_gets_default_technique_fallback():
|
||||
cur = _mock_cur(rows_by_table={"focus_areas": {99: {"name": "Unbekannter Fokus XYZ"}}})
|
||||
catalog = ProgressionPlanningCatalogContext(
|
||||
focus_areas=[PlanningCatalogContextItem(id=99, is_primary=True)],
|
||||
)
|
||||
out = build_catalog_guidance_for_prompt(cur, catalog)
|
||||
assert out["has_catalog_guidance"] is False
|
||||
assert out["catalog_guidance_block"] == ""
|
||||
assert out["has_catalog_guidance"] is True
|
||||
assert "Unbekannter Fokus XYZ" in out["catalog_context_json"]
|
||||
assert "Zwischenstufen" in out["catalog_guidance_block"] or "Progression" in out["catalog_guidance_block"]
|
||||
|
||||
|
||||
def test_merge_planning_prompt_variables_granular_keys():
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.236"
|
||||
APP_VERSION = "0.8.237"
|
||||
BUILD_DATE = "2026-05-22"
|
||||
DB_SCHEMA_VERSION = "20260607093"
|
||||
DB_SCHEMA_VERSION = "20260607094"
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
||||
|
|
@ -53,6 +53,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.237",
|
||||
"date": "2026-05-22",
|
||||
"changes": [
|
||||
"Migration 094: catalog_prompt_slots vollständig befüllt (Karate, SV, alle Trainingsstile/Zielgruppen).",
|
||||
"catalog_slot_fallbacks: Namens-Fallback bis Admin-Override — gleiche Qualität wie H1-Registry.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.236",
|
||||
"date": "2026-05-22",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export default function CatalogPromptSlotsEditor({ catalogKind, catalogId, entry
|
|||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [storedSlots, setStoredSlots] = useState({})
|
||||
|
||||
const applicableTypes = useMemo(() => {
|
||||
const kind = (catalogKind || '').trim()
|
||||
|
|
@ -38,6 +39,9 @@ export default function CatalogPromptSlotsEditor({ catalogKind, catalogId, entry
|
|||
])
|
||||
setSlotTypes(Array.isArray(typesRes?.slot_types) ? typesRes.slot_types : [])
|
||||
setSlots(slotsRes?.slots && typeof slotsRes.slots === 'object' ? { ...slotsRes.slots } : {})
|
||||
setStoredSlots(
|
||||
slotsRes?.stored_slots && typeof slotsRes.stored_slots === 'object' ? { ...slotsRes.stored_slots } : {}
|
||||
)
|
||||
setLoaded(true)
|
||||
} catch (e) {
|
||||
setError(e.message || String(e))
|
||||
|
|
@ -109,10 +113,17 @@ export default function CatalogPromptSlotsEditor({ catalogKind, catalogId, entry
|
|||
const key = st.slot_key
|
||||
const ph = `{{${catalogKind}_${key}}}`
|
||||
const isCodeOnly = st.for_code && !st.for_llm
|
||||
const fromFallback =
|
||||
!(storedSlots[key] || '').trim() && (slots[key] || '').trim() && key !== 'description'
|
||||
return (
|
||||
<div key={key} className="form-row">
|
||||
<label className="form-label">
|
||||
{st.display_name || key}
|
||||
{fromFallback ? (
|
||||
<span style={{ marginLeft: '6px', fontSize: '12px', color: 'var(--accent)' }}>
|
||||
(Standard-Vorlage)
|
||||
</span>
|
||||
) : null}
|
||||
{isCodeOnly ? (
|
||||
<span style={{ marginLeft: '6px', fontSize: '12px', color: 'var(--text3)' }}>
|
||||
(primär Code)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user