All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 49s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m26s
- Added new fields for goal query, user notes, max steps, and search query in the AiPromptPreviewBody to support planning prompts. - Integrated planning prompt handling in the preview_ai_prompt function, allowing for distinct processing of planning and exercise prompts. - Introduced LLM usage tracking in openrouter_chat_completion and planning_exercise_suggest functions to monitor AI call metrics. - Updated frontend components to accommodate new input fields for planning prompts, enhancing user experience and functionality.
273 lines
9.0 KiB
Python
273 lines
9.0 KiB
Python
"""
|
|
Admin-Vorschau: Platzhalter für Planungs-Prompts (Progressionsgraph, Pfad-QS, Suggest).
|
|
|
|
Nutzt repräsentative Beispieldaten + echte Katalog-Auszüge aus der DB.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Dict, List, Mapping, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from planning_exercise_semantics import brief_to_summary_dict, build_semantic_brief
|
|
from planning_intent_context import build_planning_intent_context
|
|
|
|
PLANNING_PROMPT_SLUGS = frozenset(
|
|
{
|
|
"planning_progression_start_target",
|
|
"planning_progression_goal_analysis",
|
|
"planning_progression_roadmap",
|
|
"planning_progression_stage_spec",
|
|
"planning_exercise_query_semantics",
|
|
"planning_exercise_path_qa",
|
|
"planning_exercise_search_intent",
|
|
"planning_exercise_search_rank",
|
|
"planning_exercise_expectation_profile",
|
|
}
|
|
)
|
|
|
|
|
|
class PlanningPromptPreviewInput(BaseModel):
|
|
goal_query: str = Field(
|
|
default="Mae Geri vom Grundschritt bis zur kontrollierten Kumite-Nähe",
|
|
max_length=2000,
|
|
)
|
|
user_notes: str = Field(default="Fokus Breitensport, ohne Wettkampfdruck.", max_length=2000)
|
|
max_steps: int = Field(default=5, ge=2, le=10)
|
|
search_query: Optional[str] = Field(default=None, max_length=2000)
|
|
|
|
|
|
def is_planning_prompt_slug(slug: str) -> bool:
|
|
return (slug or "").strip().lower() in PLANNING_PROMPT_SLUGS
|
|
|
|
|
|
def _compact_json(obj: Any) -> str:
|
|
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
def _sample_goal_analysis() -> Dict[str, Any]:
|
|
return {
|
|
"primary_topic": "Mae Geri",
|
|
"start_assumption": "Grundstellung und einfache Frontkick-Bewegung bekannt",
|
|
"target_state": "Kontrollierter Mae Geri in Kumite-Nähe mit Hüftöffnung",
|
|
"success_criteria": [
|
|
"Hüfte öffnet vor dem Kick",
|
|
"Ballen trifft Zielzone",
|
|
"Rückzug ohne Balanceverlust",
|
|
],
|
|
"constraints": {
|
|
"partner_required": False,
|
|
"excluded_themes": ["reine Kraft ohne Technikbezug"],
|
|
"trainer_notes": "Breitensport, kein Wettkampf",
|
|
},
|
|
}
|
|
|
|
|
|
def _sample_major_steps(max_steps: int) -> List[Dict[str, Any]]:
|
|
phases = ["einstieg", "grundlage", "vertiefung", "anwendung", "perfektion"]
|
|
titles = [
|
|
"Grundstellung und Mae Geri Einstieg",
|
|
"Hüftöffnung und Ballen-Fokus",
|
|
"Koordination und Rückzug",
|
|
"Anwendung in Partnerübung",
|
|
"Qualität unter leichtem Druck",
|
|
]
|
|
out: List[Dict[str, Any]] = []
|
|
for i in range(max_steps):
|
|
out.append(
|
|
{
|
|
"index": i,
|
|
"phase": phases[min(i, len(phases) - 1)],
|
|
"title": titles[min(i, len(titles) - 1)],
|
|
"learning_goal": titles[min(i, len(titles) - 1)],
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _sample_path_steps() -> List[Dict[str, Any]]:
|
|
return [
|
|
{
|
|
"index": 1,
|
|
"exercise_id": 101,
|
|
"title": "Mae Geri — Stand und Hüftöffnung",
|
|
"goal": "Frontkick mit geöffneter Hüfte aus Grundstellung",
|
|
"is_bridge": False,
|
|
"is_ai_proposal": False,
|
|
"reasons": ["Stufen-Gate: Grundlagen"],
|
|
},
|
|
{
|
|
"index": 2,
|
|
"exercise_id": 102,
|
|
"title": "Mae Geri — Ballen und Rückzug",
|
|
"goal": "Präziser Ballentreffer mit kontrolliertem Rückzug",
|
|
"is_bridge": False,
|
|
"is_ai_proposal": False,
|
|
"reasons": ["Nachfolger im Graph"],
|
|
},
|
|
]
|
|
|
|
|
|
def _sample_planning_context() -> Dict[str, Any]:
|
|
return {
|
|
"scope": "progression_path",
|
|
"goal_query": "Mae Geri vom Grundschritt bis zur Kumite-Nähe",
|
|
"stage_index": 1,
|
|
"learning_goal": "Hüftöffnung und Ballen-Fokus",
|
|
}
|
|
|
|
|
|
def _sample_target_profile() -> Dict[str, Any]:
|
|
return {
|
|
"primary_focus": "Kihon",
|
|
"training_type": "Breitensport",
|
|
"skill_expectations": ["Geri Waza", "Koordination"],
|
|
}
|
|
|
|
|
|
def _sample_candidates() -> List[Dict[str, Any]]:
|
|
return [
|
|
{
|
|
"exercise_id": 101,
|
|
"title": "Mae Geri — Stand und Hüftöffnung",
|
|
"summary": "Frontkick mit Hüftöffnung",
|
|
"skill_names": ["Geri Waza"],
|
|
"score_hint": 0.82,
|
|
},
|
|
{
|
|
"exercise_id": 102,
|
|
"title": "Mae Geri — Ballen und Rückzug",
|
|
"summary": "Ballentreffer mit Rückzug",
|
|
"skill_names": ["Geri Waza", "Koordination"],
|
|
"score_hint": 0.76,
|
|
},
|
|
]
|
|
|
|
|
|
def _load_catalog_variables(cur) -> Dict[str, str]:
|
|
from planning_exercise_intent import (
|
|
_load_compact_catalog,
|
|
_load_skills_catalog_compact,
|
|
)
|
|
|
|
return {
|
|
"skills_catalog_json": _compact_json(_load_skills_catalog_compact(cur)),
|
|
"focus_areas_catalog_json": _compact_json(_load_compact_catalog(cur, "focus_areas", "id")),
|
|
"training_types_catalog_json": _compact_json(_load_compact_catalog(cur, "training_types", "id")),
|
|
"style_directions_catalog_json": _compact_json(_load_compact_catalog(cur, "style_directions", "id")),
|
|
"target_groups_catalog_json": _compact_json(_load_compact_catalog(cur, "target_groups", "id")),
|
|
}
|
|
|
|
|
|
def resolve_planning_prompt_preview_variables(
|
|
cur,
|
|
slug: str,
|
|
body: PlanningPromptPreviewInput,
|
|
) -> Dict[str, str]:
|
|
"""Mustache-Variablen für Planungs-Prompt-Vorschau im Admin."""
|
|
s = (slug or "").strip().lower()
|
|
if s not in PLANNING_PROMPT_SLUGS:
|
|
raise ValueError(f"Kein Planungs-Prompt-Slug: {slug!r}")
|
|
|
|
goal_query = (body.goal_query or "").strip() or "Mae Geri Progression"
|
|
search_query = (body.search_query or "").strip() or goal_query
|
|
max_steps = int(body.max_steps)
|
|
brief = build_semantic_brief(goal_query)
|
|
brief_json = _compact_json(brief_to_summary_dict(brief))
|
|
goal_analysis = _sample_goal_analysis()
|
|
major_steps = _sample_major_steps(max_steps)
|
|
intent_ctx = build_planning_intent_context(
|
|
goal_query=goal_query,
|
|
goal_analysis=goal_analysis,
|
|
semantic_brief=brief,
|
|
extra_context=(body.user_notes or "").strip() or None,
|
|
)
|
|
intent_ctx_json = _compact_json(intent_ctx.to_api_dict())
|
|
ctx = _sample_planning_context()
|
|
target = _sample_target_profile()
|
|
catalogs = _load_catalog_variables(cur)
|
|
|
|
if s == "planning_progression_start_target":
|
|
return {
|
|
"goal_query": goal_query,
|
|
"semantic_brief_json": brief_json,
|
|
"user_notes": (body.user_notes or "").strip(),
|
|
}
|
|
|
|
if s == "planning_progression_goal_analysis":
|
|
return {
|
|
"goal_query": goal_query,
|
|
"semantic_brief_json": brief_json,
|
|
}
|
|
|
|
if s == "planning_progression_roadmap":
|
|
return {
|
|
"goal_query": goal_query,
|
|
"goal_analysis_json": _compact_json(goal_analysis),
|
|
"semantic_brief_json": brief_json,
|
|
"max_steps": str(max_steps),
|
|
}
|
|
|
|
if s == "planning_progression_stage_spec":
|
|
return {
|
|
"goal_query": goal_query,
|
|
"goal_analysis_json": _compact_json(goal_analysis),
|
|
"major_steps_json": _compact_json(major_steps),
|
|
"intent_context_json": intent_ctx_json,
|
|
"semantic_brief_json": brief_json,
|
|
}
|
|
|
|
if s == "planning_exercise_query_semantics":
|
|
return {
|
|
"search_query": search_query,
|
|
"semantic_brief_json": brief_json,
|
|
}
|
|
|
|
if s == "planning_exercise_path_qa":
|
|
return {
|
|
"goal_query": goal_query,
|
|
"semantic_brief_json": brief_json,
|
|
"steps_json": _compact_json(_sample_path_steps()),
|
|
"gaps_json": _compact_json([]),
|
|
"bridge_inserts_json": _compact_json([]),
|
|
}
|
|
|
|
if s == "planning_exercise_search_intent":
|
|
return {
|
|
"search_query": search_query,
|
|
"heuristic_intent": "progression_next",
|
|
"scenario_hint": "preset_next",
|
|
"planning_context_json": _compact_json(ctx),
|
|
"target_profile_json": _compact_json(target),
|
|
**catalogs,
|
|
}
|
|
|
|
if s == "planning_exercise_search_rank":
|
|
return {
|
|
"search_query": search_query,
|
|
"intent": "progression_next",
|
|
"planning_context_json": _compact_json(ctx),
|
|
"target_profile_json": _compact_json(target),
|
|
"candidates_json": _compact_json(_sample_candidates()),
|
|
"result_limit": "5",
|
|
}
|
|
|
|
if s == "planning_exercise_expectation_profile":
|
|
return {
|
|
"heuristic_intent": "suggest_next",
|
|
"planning_context_json": _compact_json(ctx),
|
|
"target_profile_json": _compact_json(target),
|
|
**{k: v for k, v in catalogs.items() if k != "style_directions_catalog_json"},
|
|
}
|
|
|
|
raise ValueError(f"Planungs-Prompt-Slug nicht implementiert: {slug!r}")
|
|
|
|
|
|
__all__ = [
|
|
"PLANNING_PROMPT_SLUGS",
|
|
"PlanningPromptPreviewInput",
|
|
"is_planning_prompt_slug",
|
|
"resolve_planning_prompt_preview_variables",
|
|
]
|