shinkan-jinkendo/backend/ai_prompt_runtime.py
Lars 9f4678f418
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 36s
Test Suite / playwright-tests (push) Successful in 1m16s
Implement exercise_instruction_rewrite for AI Prompt System
- Added `exercise_instruction_rewrite` functionality to enhance AI-generated instructions, incorporating fields for goal, execution, preparation, and trainer notes.
- Updated `ExerciseFormAiPromptContext` to include new fields and methods for instruction handling.
- Enhanced the `run_exercise_form_ai_suggestion` function to support instruction rewriting and validation.
- Modified API endpoints and frontend components to integrate instruction features, including a new button for AI instruction revision.
- Incremented application version to 0.8.163 and updated changelog to reflect these changes, including migration details and new functionality.
2026-05-22 18:53:36 +02:00

115 lines
3.3 KiB
Python

"""
Gemeinsame KI-Prompt-Laufzeit (Shinkan): DB-Lesezugriff ai_prompts + Kontext-Arten.
Bleibt ohne Import von exercise_ai (kein Zirkel). Domänen wie exercise_ai nutzen
load_ai_prompt_row und die Enum; Platzhalter bauen sie selbst oder über geteilte Builder.
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, Mapping, Optional, Tuple
from prompt_resolver import MustacheRenderResult, render_mustache_template
_EXERCISE_AI_SLUGS = frozenset(
{
"exercise_summary",
"exercise_skill_suggestions",
"exercise_instruction_rewrite",
}
)
class AiPromptContextKind(str, Enum):
"""
Logischer Kontext fuer Platzhalter/Builder — erweiterbar fuer Planung/Rahmen
ohne bestehende Slugs zu invalidieren.
"""
EXERCISE_FORM_AI = "exercise_form_ai"
def context_kind_for_slug(slug: str) -> Optional[AiPromptContextKind]:
"""Ordnet einen DB-Slug einer Kontext-Art zu, sofern registriert."""
s = (slug or "").strip().lower()
if s in _EXERCISE_AI_SLUGS:
return AiPromptContextKind.EXERCISE_FORM_AI
return None
def load_ai_prompt_row(cur, slug: str, *, active_only: bool = True) -> Optional[Dict[str, Any]]:
"""
Laedt eine Zeile ai_prompts fuer Laufzeit-Orchestrierung.
active_only=True: inaktive Prompts werden wie fehlend behandelt (503 im Aufrufer).
"""
if active_only:
cur.execute(
"""
SELECT slug, display_name, template, output_format, active, openrouter_model
FROM ai_prompts
WHERE slug = %s AND active = true
""",
(slug,),
)
else:
cur.execute(
"""
SELECT slug, display_name, template, output_format, active, openrouter_model
FROM ai_prompts
WHERE slug = %s
""",
(slug,),
)
row = cur.fetchone()
if not row:
return None
d = dict(row)
if active_only and not d.get("active", True):
return None
return d
class AiPromptUnavailableError(LookupError):
"""Kein aktiver Prompt fuer slug (oder Zeile fehlt)."""
def __init__(self, slug: str) -> None:
self.slug = (slug or "").strip()
super().__init__(self.slug)
def render_ai_prompt_template_for_row(
row: Mapping[str, Any],
variables: Mapping[str, str],
) -> MustacheRenderResult:
"""Ersetzt Platzhalter anhand einer bereits geladenen ai_prompts-Zeile (z. B. Admin-Vorschauch, inkl. inaktiv)."""
return render_mustache_template(str(row.get("template") or ""), variables)
def load_and_render_ai_prompt(
cur,
slug: str,
variables: Mapping[str, str],
*,
active_only: bool = True,
) -> Tuple[Dict[str, Any], MustacheRenderResult]:
"""
Laedt einen aktiven Prompt und wendet Mustache-Variablen an.
Wirft AiPromptUnavailableError, wenn die Zeile fehlt oder (bei active_only) inaktiv ist.
"""
row = load_ai_prompt_row(cur, slug, active_only=active_only)
if not row:
raise AiPromptUnavailableError(slug)
rr = render_ai_prompt_template_for_row(row, variables)
return dict(row), rr
__all__ = [
"AiPromptContextKind",
"AiPromptUnavailableError",
"context_kind_for_slug",
"load_ai_prompt_row",
"load_and_render_ai_prompt",
"render_ai_prompt_template_for_row",
]