All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 47s
Test Suite / playwright-tests (push) Successful in 1m14s
- Implemented optional LLM-Rerank functionality in the planning exercise suggestion process, allowing for improved exercise ranking based on user-defined criteria. - Updated the `suggestPlanningExercises` API to accept `planned_exercise_ids` for client-side overrides, enhancing flexibility in exercise selection. - Enhanced the `ExercisePickerModal` to reflect LLM ranking status and support new planning context features. - Incremented application version to 0.8.170 and updated changelog to document the new features and improvements in the planning AI capabilities.
124 lines
3.5 KiB
Python
124 lines
3.5 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
|
|
|
|
_PLANNING_AI_SLUGS = frozenset(
|
|
{
|
|
"planning_exercise_search_rank",
|
|
}
|
|
)
|
|
|
|
_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.
|
|
"""
|
|
|
|
PLANNING_EXERCISE_SEARCH = "planning_exercise_search"
|
|
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 _PLANNING_AI_SLUGS:
|
|
return AiPromptContextKind.PLANNING_EXERCISE_SEARCH
|
|
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",
|
|
]
|