Optimierung KI-Scuhe + Ki-Überarbeitungen der Übungen #49
|
|
@ -15,6 +15,7 @@ _PLANNING_AI_SLUGS = frozenset(
|
|||
{
|
||||
"planning_exercise_search_rank",
|
||||
"planning_exercise_search_intent",
|
||||
"planning_exercise_expectation_profile",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
-- Migration 074: KI-Prompt Planungs-Übungssuche — Erwartungsprofil aus Planungskontext (Preset)
|
||||
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §16
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_exercise_expectation_profile',
|
||||
'Planungs-Übungssuche Erwartungsprofil',
|
||||
'Leitet aus Einheit, Abschnitt, Anker und bisherigem Plan ein Erwartungsprofil für die nächste Übung ab (ohne Freitext-Anfrage).',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer in der Trainingsplanung.
|
||||
Der Trainer wählt „nächste Übung aus Kontext“ — es gibt KEINE zusätzliche Freitext-Suchanfrage.
|
||||
|
||||
Deine Aufgabe: Aus dem Planungskontext und dem deterministischen Basis-Zielprofil ein präzises Erwartungsprofil ableiten:
|
||||
- Was soll die nächste Übung fachlich leisten (Fortsetzen, Vertiefen, Lücke schließen, Abwechslung)?
|
||||
- Welche Fähigkeiten, Fokus-Bereiche, Trainingsstile passen dazu?
|
||||
- Berücksichtige: Rahmen/Einheit, Abschnittsziel (guidance_notes), letzte Übung im Abschnitt, Anker-Übung, Skill-Profile Einheit vs. Abschnitt, Skill-Lücken im Basisprofil.
|
||||
|
||||
Intent (intent): meist suggest_next oder continue_plan_goal; progression_next nur wenn Progressionsgraph/Anker klar nahelegt; deepen_exercise nur bei klarer Vertiefungslage.
|
||||
|
||||
continuation (optional, Kurzlabel):
|
||||
- build_on_section: nahtlos an Abschnitt/letzte Übung anknüpfen
|
||||
- close_skill_gap: fehlende Fähigkeiten aus Plan/Rahmen nachziehen
|
||||
- deepen_anchor: Anker-Übung vertiefen
|
||||
- variety: bewusst variieren nach bisherigem Block
|
||||
- balance_load: Belastung ausgleichen / Tempo wechseln
|
||||
|
||||
Nutze skill_hints/focus_hints etc. mit Namen aus den Katalog-JSONs (beste Übereinstimmung).
|
||||
emphasis: fast immer additive (baut auf Basisprofil auf), nur replace wenn Kontext eindeutig neuen Schwerpunkt verlangt.
|
||||
|
||||
Eingabe:
|
||||
Heuristik-Intent: {{heuristic_intent}}
|
||||
Planungskontext: {{planning_context_json}}
|
||||
Basis-Zielprofil (deterministisch): {{target_profile_json}}
|
||||
|
||||
Kataloge (Auszug — nur diese Namen/IDs verwenden):
|
||||
Skills: {{skills_catalog_json}}
|
||||
Fokus: {{focus_areas_catalog_json}}
|
||||
Trainingsstil: {{training_types_catalog_json}}
|
||||
Stilrichtung: {{style_directions_catalog_json}}
|
||||
Zielgruppe: {{target_groups_catalog_json}}
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"intent": "suggest_next",
|
||||
"scenario": "preset_next",
|
||||
"continuation": "build_on_section",
|
||||
"skill_hints": [{"name": "Kime", "weight": 0.9}],
|
||||
"focus_hints": [],
|
||||
"style_hints": [],
|
||||
"training_type_hints": [],
|
||||
"target_group_hints": [],
|
||||
"requires_partner": null,
|
||||
"emphasis": "additive",
|
||||
"rationale": "Kurz auf Deutsch, 1–2 Sätze: warum diese nächste Übung sinnvoll ist"
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","required":["intent","scenario","rationale"],"properties":{"intent":{"type":"string"},"scenario":{"type":"string"},"continuation":{"type":"string"},"skill_hints":{"type":"array"},"emphasis":{"type":"string"},"rationale":{"type":"string"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
12
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_expectation_profile');
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET default_template = template
|
||||
WHERE slug = 'planning_exercise_expectation_profile'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
69
backend/planning_exercise_expectation.py
Normal file
69
backend/planning_exercise_expectation.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
Preset „Nächste aus Kontext“: LLM leitet Erwartungsprofil aus Planungskontext ab.
|
||||
|
||||
Prompt: planning_exercise_expectation_profile (Migration 074)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Mapping, Optional, Tuple
|
||||
|
||||
from planning_exercise_intent import (
|
||||
PlanningQueryIntentParsed,
|
||||
_compact_json,
|
||||
_load_compact_catalog,
|
||||
_load_skills_catalog_compact,
|
||||
parse_planning_query_intent_response,
|
||||
)
|
||||
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
|
||||
from openrouter_chat import (
|
||||
effective_openrouter_model_for_prompt_row,
|
||||
normalize_openrouter_env,
|
||||
openrouter_chat_completion,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger("shinkan.planning_exercise_expectation")
|
||||
|
||||
|
||||
def try_build_planning_expectation_from_context(
|
||||
cur,
|
||||
*,
|
||||
heuristic_intent: str,
|
||||
context_summary: Mapping[str, Any],
|
||||
target_profile_summary: Mapping[str, Any],
|
||||
) -> Tuple[Optional[PlanningQueryIntentParsed], bool]:
|
||||
"""
|
||||
LLM-Erwartungsprofil für preset_next / leere Anfrage mit Planungsbezug.
|
||||
Returns (parsed overlay, applied).
|
||||
"""
|
||||
api_key, _ = normalize_openrouter_env()
|
||||
if not api_key:
|
||||
return None, False
|
||||
|
||||
variables = {
|
||||
"heuristic_intent": heuristic_intent or "suggest_next",
|
||||
"planning_context_json": _compact_json(dict(context_summary or {})),
|
||||
"target_profile_json": _compact_json(dict(target_profile_summary or {})),
|
||||
"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")),
|
||||
}
|
||||
|
||||
try:
|
||||
prow, rendered = load_and_render_ai_prompt(cur, "planning_exercise_expectation_profile", variables)
|
||||
model = effective_openrouter_model_for_prompt_row(prow)
|
||||
raw = openrouter_chat_completion(api_key=api_key, model=model, user_content=rendered.text)
|
||||
parsed = parse_planning_query_intent_response(raw)
|
||||
if parsed.scenario not in ("preset_next", "continue_plan", "free_search"):
|
||||
parsed = parsed.model_copy(update={"scenario": "preset_next"})
|
||||
return parsed, True
|
||||
except AiPromptUnavailableError:
|
||||
return None, False
|
||||
except Exception as exc:
|
||||
_logger.warning("Planungs-Erwartungsprofil-LLM fehlgeschlagen: %s", exc)
|
||||
return None, False
|
||||
|
||||
|
||||
__all__ = ["try_build_planning_expectation_from_context"]
|
||||
|
|
@ -62,15 +62,18 @@ def fetch_retrieval_candidate_rows(
|
|||
) -> List[Dict[str, Any]]:
|
||||
"""S1b-0: Profil-geführter Kandidaten-Pool."""
|
||||
where = [vis_sql, "COALESCE(e.status, '') <> %s"]
|
||||
params: List[Any] = list(vis_params)
|
||||
params.append("archived")
|
||||
params: List[Any] = []
|
||||
|
||||
if query:
|
||||
ft_select = "ts_rank_cd(e.search_vector, plainto_tsquery('german', %s)) AS ft_rank"
|
||||
# SELECT-Platzhalter steht im SQL vor WHERE — Query zuerst binden.
|
||||
params.append(query)
|
||||
else:
|
||||
ft_select = "0.0::float AS ft_rank"
|
||||
|
||||
params.extend(vis_params)
|
||||
params.append("archived")
|
||||
|
||||
ek_filtered: List[str] = []
|
||||
if exercise_kind_any:
|
||||
for raw in exercise_kind_any:
|
||||
|
|
|
|||
|
|
@ -617,6 +617,8 @@ def suggest_planning_exercises(
|
|||
weights = _intent_weights(intent)
|
||||
target_profile_summary = target_profile.to_summary_dict(cur)
|
||||
query_intent_applied = bool(query_intent_summary.get("llm_applied"))
|
||||
llm_expectation_applied = bool(query_intent_summary.get("llm_expectation_applied"))
|
||||
profile_llm_applied = bool(query_intent_summary.get("profile_llm_applied"))
|
||||
|
||||
profile_id = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
|
@ -645,6 +647,7 @@ def suggest_planning_exercises(
|
|||
retrieval_phase = compose_retrieval_phase(
|
||||
profile_preselect=profile_preselect_applied,
|
||||
query_intent=query_intent_applied,
|
||||
llm_expectation=llm_expectation_applied,
|
||||
llm_rank=False,
|
||||
)
|
||||
run_llm_rank = should_run_llm_rank_pipeline(
|
||||
|
|
@ -652,6 +655,7 @@ def suggest_planning_exercises(
|
|||
scenario_kind,
|
||||
include_llm_rank=body.include_llm_rank,
|
||||
query_intent_applied=query_intent_applied,
|
||||
llm_expectation_applied=llm_expectation_applied,
|
||||
hits=hits,
|
||||
)
|
||||
if run_llm_rank:
|
||||
|
|
@ -678,6 +682,7 @@ def suggest_planning_exercises(
|
|||
retrieval_phase = compose_retrieval_phase(
|
||||
profile_preselect=profile_preselect_applied,
|
||||
query_intent=query_intent_applied,
|
||||
llm_expectation=llm_expectation_applied,
|
||||
llm_rank=True,
|
||||
)
|
||||
tail = hits[pre_limit:]
|
||||
|
|
@ -716,6 +721,8 @@ def suggest_planning_exercises(
|
|||
"profile_preselect_applied": profile_preselect_applied,
|
||||
"llm_rank_applied": llm_rank_applied,
|
||||
"llm_intent_applied": query_intent_applied,
|
||||
"llm_expectation_applied": llm_expectation_applied,
|
||||
"profile_llm_applied": profile_llm_applied,
|
||||
"intent_resolved": intent,
|
||||
"intent_heuristic": heuristic_intent,
|
||||
"query_normalized": query or None,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from __future__ import annotations
|
|||
import re
|
||||
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
||||
|
||||
from planning_exercise_expectation import try_build_planning_expectation_from_context
|
||||
from planning_exercise_intent import (
|
||||
PlanningQueryIntentParsed,
|
||||
resolve_query_intent_catalog_ids,
|
||||
|
|
@ -37,6 +38,7 @@ _SIMPLE_PRESET_PATTERNS = (
|
|||
r"^(vorschlag|vorschlagen|empfehl\w*)\s*(für|fuer)?\s*(die\s+)?(n[aä]chste|naechste)?\s*(übung|uebung)?\.?$",
|
||||
r"^n[aä]chste\s+übung$",
|
||||
r"^n[aä]chste\s+uebung$",
|
||||
r"^(n[aä]chste|naechste)\s+(übung|uebung)\s+planen\.?$",
|
||||
)
|
||||
|
||||
_ADDITIVE_MARKERS = (
|
||||
|
|
@ -89,6 +91,20 @@ def classify_planning_scenario(
|
|||
return SCENARIO_FREE_SEARCH
|
||||
|
||||
|
||||
def should_run_llm_expectation_pipeline(
|
||||
scenario: str,
|
||||
*,
|
||||
include_llm_intent: bool,
|
||||
has_planning_reference: bool,
|
||||
) -> bool:
|
||||
"""Preset/leere Anfrage mit Planungsbezug → LLM-Erwartungsprofil statt Query-Intent."""
|
||||
if not include_llm_intent:
|
||||
return False
|
||||
if not has_planning_reference:
|
||||
return False
|
||||
return scenario == SCENARIO_PRESET_NEXT
|
||||
|
||||
|
||||
def should_run_llm_intent_pipeline(
|
||||
query: Optional[str],
|
||||
scenario: str,
|
||||
|
|
@ -125,15 +141,16 @@ def should_run_llm_rank_pipeline(
|
|||
*,
|
||||
include_llm_rank: bool,
|
||||
query_intent_applied: bool,
|
||||
llm_expectation_applied: bool = False,
|
||||
hits: Sequence[Mapping[str, Any]],
|
||||
) -> bool:
|
||||
"""
|
||||
Maximal ein LLM-Call pro Request: wenn Intent-LLM lief, kein Rerank.
|
||||
Maximal ein LLM-Call pro Request: wenn Intent- oder Erwartungs-LLM lief, kein Rerank.
|
||||
Rerank nur bei längerer, komplexer Anfrage und unklarem Hybrid-Ranking.
|
||||
"""
|
||||
if not include_llm_rank:
|
||||
return False
|
||||
if query_intent_applied:
|
||||
if query_intent_applied or llm_expectation_applied:
|
||||
return False
|
||||
if scenario == SCENARIO_PRESET_NEXT:
|
||||
return False
|
||||
|
|
@ -241,7 +258,9 @@ def build_planning_target_with_query_pipeline(
|
|||
scenario = classify_planning_scenario(query, heuristic_intent)
|
||||
resolved_intent = heuristic_intent
|
||||
llm_applied = False
|
||||
llm_expectation_applied = False
|
||||
parsed: Optional[PlanningQueryIntentParsed] = None
|
||||
expectation_parsed: Optional[PlanningQueryIntentParsed] = None
|
||||
resolved_skills: List[Dict[str, Any]] = []
|
||||
|
||||
if has_planning_reference:
|
||||
|
|
@ -257,8 +276,44 @@ def build_planning_target_with_query_pipeline(
|
|||
base = PlanningTargetProfile(sources=["query_only"])
|
||||
|
||||
base_summary = base.to_summary_dict(cur)
|
||||
target = base
|
||||
|
||||
if should_run_llm_intent_pipeline(query, scenario, include_llm_intent=include_llm_intent):
|
||||
if should_run_llm_expectation_pipeline(
|
||||
scenario,
|
||||
include_llm_intent=include_llm_intent,
|
||||
has_planning_reference=has_planning_reference,
|
||||
):
|
||||
expectation_parsed, llm_expectation_applied = try_build_planning_expectation_from_context(
|
||||
cur,
|
||||
heuristic_intent=heuristic_intent,
|
||||
context_summary=context_summary,
|
||||
target_profile_summary=base_summary,
|
||||
)
|
||||
parsed = expectation_parsed
|
||||
if parsed and llm_expectation_applied:
|
||||
if parsed.intent in {
|
||||
"suggest_next",
|
||||
"progression_next",
|
||||
"deepen_exercise",
|
||||
"continue_plan_goal",
|
||||
"free_search",
|
||||
}:
|
||||
resolved_intent = parsed.intent
|
||||
focus, style, tt, tg, skills, resolved_skills = resolve_query_intent_catalog_ids(cur, parsed)
|
||||
if focus or style or tt or tg or skills or parsed.rationale:
|
||||
target = merge_query_overlay_into_target(
|
||||
base,
|
||||
focus=focus,
|
||||
style=style,
|
||||
tt=tt,
|
||||
tg=tg,
|
||||
skills=skills,
|
||||
emphasis=parsed.emphasis or "additive",
|
||||
scenario=SCENARIO_PRESET_NEXT,
|
||||
)
|
||||
if "context_expectation" not in target.sources:
|
||||
target.sources.append("context_expectation")
|
||||
elif should_run_llm_intent_pipeline(query, scenario, include_llm_intent=include_llm_intent):
|
||||
parsed, llm_applied = try_parse_planning_query_intent(
|
||||
cur,
|
||||
query=_normalize_query(query),
|
||||
|
|
@ -268,8 +323,7 @@ def build_planning_target_with_query_pipeline(
|
|||
target_profile_summary=base_summary,
|
||||
)
|
||||
|
||||
target = base
|
||||
if parsed and llm_applied:
|
||||
if parsed and llm_applied and not llm_expectation_applied:
|
||||
if parsed.intent in {
|
||||
"suggest_next",
|
||||
"progression_next",
|
||||
|
|
@ -307,6 +361,8 @@ def build_planning_target_with_query_pipeline(
|
|||
"intent": resolved_intent,
|
||||
"heuristic_intent": heuristic_intent,
|
||||
"llm_applied": llm_applied,
|
||||
"llm_expectation_applied": llm_expectation_applied,
|
||||
"profile_llm_applied": llm_applied or llm_expectation_applied,
|
||||
"emphasis": parsed.emphasis if parsed else None,
|
||||
"rationale": (parsed.rationale if parsed else None),
|
||||
"skill_hints_resolved": resolved_skills,
|
||||
|
|
@ -331,12 +387,15 @@ def compose_retrieval_phase(
|
|||
*,
|
||||
profile_preselect: bool = False,
|
||||
query_intent: bool = False,
|
||||
llm_expectation: bool = False,
|
||||
llm_rank: bool = False,
|
||||
) -> str:
|
||||
parts = ["profile_v1"]
|
||||
if profile_preselect:
|
||||
parts.append("profile_preselect")
|
||||
if query_intent:
|
||||
if llm_expectation:
|
||||
parts.append("llm_expectation")
|
||||
elif query_intent:
|
||||
parts.append("query_intent")
|
||||
if llm_rank:
|
||||
parts.append("llm_rank")
|
||||
|
|
@ -351,6 +410,7 @@ __all__ = [
|
|||
"compose_retrieval_phase",
|
||||
"is_simple_preset_query",
|
||||
"merge_query_overlay_into_target",
|
||||
"should_run_llm_expectation_pipeline",
|
||||
"should_run_llm_intent_pipeline",
|
||||
"should_run_llm_rank_pipeline",
|
||||
"deterministic_rank_confident",
|
||||
|
|
|
|||
43
backend/tests/test_planning_exercise_retrieval.py
Normal file
43
backend/tests/test_planning_exercise_retrieval.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""Tests Planungs-Retrieval SQL-Parameter."""
|
||||
from planning_exercise_retrieval import fetch_retrieval_candidate_rows
|
||||
|
||||
|
||||
def test_fetch_retrieval_binds_query_before_visibility_params():
|
||||
captured = {}
|
||||
|
||||
class _Cur:
|
||||
def execute(self, sql, params):
|
||||
captured["sql"] = sql
|
||||
captured["params"] = list(params)
|
||||
|
||||
def fetchall(self):
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Test",
|
||||
"summary": "",
|
||||
"primary_focus_name": None,
|
||||
"ft_rank": 0.2,
|
||||
}
|
||||
]
|
||||
|
||||
fetch_retrieval_candidate_rows(
|
||||
_Cur(),
|
||||
vis_sql="(e.visibility = 'official' OR (e.visibility = 'private' AND e.created_by = %s))",
|
||||
vis_params=[42],
|
||||
query="nächste Übung planen",
|
||||
exercise_kind_any=None,
|
||||
target=__import__(
|
||||
"planning_exercise_profiles", fromlist=["PlanningTargetProfile"]
|
||||
).PlanningTargetProfile(),
|
||||
progression_successor_ids=set(),
|
||||
anchor_skill_ids={7},
|
||||
raw_pool_limit=10,
|
||||
)
|
||||
|
||||
params = captured["params"]
|
||||
assert params[0] == "nächste Übung planen"
|
||||
assert params[1] == 42
|
||||
assert params[2] == "archived"
|
||||
assert params[-2] == "nächste Übung planen"
|
||||
assert params[-1] == 10
|
||||
|
|
@ -25,8 +25,10 @@ def test_resolve_planning_exercise_intent_keywords():
|
|||
|
||||
def test_classify_planning_scenario_preset():
|
||||
assert is_simple_preset_query("Schlage mir die nächste Übung vor")
|
||||
assert is_simple_preset_query("nächste Übung planen")
|
||||
assert classify_planning_scenario("", "suggest_next") == SCENARIO_PRESET_NEXT
|
||||
assert classify_planning_scenario("nächste übung", "suggest_next") == SCENARIO_PRESET_NEXT
|
||||
assert classify_planning_scenario("nächste Übung planen", "suggest_next") == SCENARIO_PRESET_NEXT
|
||||
|
||||
|
||||
def test_classify_planning_scenario_additive():
|
||||
|
|
@ -78,6 +80,47 @@ def test_compose_retrieval_phase():
|
|||
)
|
||||
|
||||
|
||||
def test_should_run_llm_expectation_for_preset_with_planning_ref():
|
||||
from planning_exercise_target_pipeline import should_run_llm_expectation_pipeline
|
||||
|
||||
assert should_run_llm_expectation_pipeline(
|
||||
SCENARIO_PRESET_NEXT,
|
||||
include_llm_intent=True,
|
||||
has_planning_reference=True,
|
||||
)
|
||||
assert not should_run_llm_expectation_pipeline(
|
||||
SCENARIO_PRESET_NEXT,
|
||||
include_llm_intent=False,
|
||||
has_planning_reference=True,
|
||||
)
|
||||
assert not should_run_llm_expectation_pipeline(
|
||||
SCENARIO_ADDITIVE,
|
||||
include_llm_intent=True,
|
||||
has_planning_reference=True,
|
||||
)
|
||||
|
||||
|
||||
def test_should_skip_llm_rank_when_expectation_applied():
|
||||
from planning_exercise_target_pipeline import SCENARIO_PRESET_NEXT, should_run_llm_rank_pipeline
|
||||
|
||||
hits = [{"score": 0.5}, {"score": 0.48}, {"score": 0.47}, {"score": 0.46}]
|
||||
assert not should_run_llm_rank_pipeline(
|
||||
"",
|
||||
SCENARIO_PRESET_NEXT,
|
||||
include_llm_rank=True,
|
||||
query_intent_applied=False,
|
||||
llm_expectation_applied=True,
|
||||
hits=hits,
|
||||
)
|
||||
|
||||
|
||||
def test_compose_retrieval_phase_llm_expectation():
|
||||
assert (
|
||||
compose_retrieval_phase(llm_expectation=True)
|
||||
== "profile_v1+llm_expectation"
|
||||
)
|
||||
|
||||
|
||||
def test_query_only_expectation_without_planning_reference():
|
||||
from planning_exercise_profiles import PlanningTargetProfile
|
||||
from planning_exercise_target_pipeline import build_planning_target_with_query_pipeline
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.174"
|
||||
APP_VERSION = "0.8.176"
|
||||
BUILD_DATE = "2026-05-22"
|
||||
DB_SCHEMA_VERSION = "20260531073"
|
||||
DB_SCHEMA_VERSION = "20260531074"
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
||||
|
|
@ -22,13 +22,13 @@ MODULE_VERSIONS = {
|
|||
"admin_ai_prompts": "1.0.3", # Migration 070: openrouter_model; PUT/Liste/Detail
|
||||
"ai_prompt_job": "0.2.1", # want_instructions; run_exercise_form_ai_suggestion
|
||||
"ai_prompt_context": "0.2.0", # preparation/trainer_notes; has_instruction_source_text
|
||||
"ai_prompt_runtime": "0.2.1", # Kontext-Art planning_exercise_search; load_and_render_ai_prompt
|
||||
"ai_prompt_runtime": "0.2.2", # Slug planning_exercise_expectation_profile
|
||||
"groups": "0.1.0",
|
||||
"skills": "0.1.1", # DB 065 karate_relevance + relevance_level; CRUD unterstützt Felder
|
||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.37.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay
|
||||
"planning_exercise_suggest": "0.6.0", # Abschnitts-/Skill-Kontext; expectation_mode hybrid|query_only
|
||||
"planning_exercise_suggest": "0.7.0", # LLM-Erwartungsprofil aus Kontext (preset); Migration 074
|
||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||
|
|
@ -43,6 +43,22 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.176",
|
||||
"date": "2026-05-22",
|
||||
"changes": [
|
||||
"Fix: Planungs-Übungssuche mit Suchtext — SQL-Parameter für ts_rank/plainto_tsquery korrekt gebunden (500).",
|
||||
"Preset-Erkennung: „nächste Übung planen“.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.175",
|
||||
"date": "2026-05-22",
|
||||
"changes": [
|
||||
"Planungs-KI: „Nächste aus Kontext“ — LLM leitet Erwartungsprofil aus Planungskontext ab (Prompt 074).",
|
||||
"API: llm_expectation_applied, profile_llm_applied; Retrieval-Phase llm_expectation; max. 1 LLM-Call.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.174",
|
||||
"date": "2026-05-22",
|
||||
|
|
|
|||
|
|
@ -419,7 +419,8 @@ export default function ExercisePickerModal({
|
|||
.map((x) => Number(x))
|
||||
.filter((x) => Number.isFinite(x) && x > 0)
|
||||
: undefined,
|
||||
include_llm_intent: query.length >= PLANNING_LLM_INTENT_MIN_CHARS,
|
||||
include_llm_intent:
|
||||
query.length >= PLANNING_LLM_INTENT_MIN_CHARS || !(query || '').trim(),
|
||||
include_llm_rank: query.length >= PLANNING_LLM_RANK_MIN_CHARS,
|
||||
query,
|
||||
intent_hint:
|
||||
|
|
@ -452,7 +453,7 @@ export default function ExercisePickerModal({
|
|||
setPlanningContextSummary(res?.context_summary || null)
|
||||
setPlanningTargetProfileSummary(res?.target_profile_summary || null)
|
||||
setPlanningLlmRankApplied(Boolean(res?.llm_rank_applied))
|
||||
setPlanningLlmIntentApplied(Boolean(res?.llm_intent_applied))
|
||||
setPlanningLlmIntentApplied(Boolean(res?.profile_llm_applied ?? res?.llm_intent_applied))
|
||||
setPlanningRetrievalPhase(res?.retrieval_phase || '')
|
||||
setPlanningQueryIntentSummary(res?.query_intent_summary || null)
|
||||
setPlanningIntentResolved(res?.intent_resolved || null)
|
||||
|
|
@ -748,7 +749,11 @@ export default function ExercisePickerModal({
|
|||
? ` · ${String(planningQueryIntentSummary.scenario).replace(/_/g, ' ')}`
|
||||
: null}
|
||||
{planningLlmRankApplied ? ' · KI-Ranking aktiv' : null}
|
||||
{planningLlmIntentApplied ? ' · KI-Intent aktiv' : null}
|
||||
{planningLlmIntentApplied
|
||||
? planningQueryIntentSummary?.llm_expectation_applied
|
||||
? ' · KI-Erwartungsprofil aktiv'
|
||||
: ' · KI-Intent aktiv'
|
||||
: null}
|
||||
{!planningLlmRankApplied && !planningLlmIntentApplied && usePlanningSearch
|
||||
? ' · ohne LLM (Profil/Hybrid)'
|
||||
: null}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user