Enhance Planning AI Context with Catalog Exercise Hints
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m15s
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced support for catalog exercise hints in various functions, including `_build_stage_ai_context`, `try_suggest_ai_stage_step`, and `build_gap_fill_goal_text`, improving the contextual information available for AI suggestions. - Added a new function `catalog_exercise_hints_block` to consolidate hints retrieval from the catalog context. - Updated the `PlanningIntentContext` to include catalog exercise hints, ensuring they are part of the API output. - Implemented tests to verify the inclusion of catalog exercise hints in goal text generation and rematch logic. - Incremented version numbers and updated changelog to reflect these enhancements.
This commit is contained in:
parent
35fe31774b
commit
3181cc126d
|
|
@ -19,6 +19,8 @@ from planning_exercise_form_context import (
|
||||||
prior_path_steps_before_major,
|
prior_path_steps_before_major,
|
||||||
)
|
)
|
||||||
from planning_exercise_semantics import PlanningSemanticBrief, brief_to_summary_dict
|
from planning_exercise_semantics import PlanningSemanticBrief, brief_to_summary_dict
|
||||||
|
from planning_catalog_context import ProgressionPlanningCatalogContext
|
||||||
|
from planning_prompt_variables import catalog_exercise_hints_block
|
||||||
|
|
||||||
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
|
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
|
||||||
|
|
||||||
|
|
@ -54,6 +56,7 @@ def _build_stage_ai_context(
|
||||||
step_after: Optional[Mapping[str, Any]] = None,
|
step_after: Optional[Mapping[str, Any]] = None,
|
||||||
prior_steps: Optional[Sequence[Mapping[str, Any]]] = None,
|
prior_steps: Optional[Sequence[Mapping[str, Any]]] = None,
|
||||||
start_situation: Optional[str] = None,
|
start_situation: Optional[str] = None,
|
||||||
|
catalog_exercise_hints: Optional[str] = None,
|
||||||
) -> ExerciseFormAiPromptContext:
|
) -> ExerciseFormAiPromptContext:
|
||||||
"""KI-Kontext für unbesetzte Roadmap-Stufe (keine Brücke zwischen falschen Array-Indizes)."""
|
"""KI-Kontext für unbesetzte Roadmap-Stufe (keine Brücke zwischen falschen Array-Indizes)."""
|
||||||
gap = dict(spec.get("gap") or {})
|
gap = dict(spec.get("gap") or {})
|
||||||
|
|
@ -97,6 +100,9 @@ def _build_stage_ai_context(
|
||||||
sketch = (spec.get("sketch") or "").strip()
|
sketch = (spec.get("sketch") or "").strip()
|
||||||
if sketch and sketch != learning_goal:
|
if sketch and sketch != learning_goal:
|
||||||
goal_parts.extend(["", f"Kontext: {sketch}"])
|
goal_parts.extend(["", f"Kontext: {sketch}"])
|
||||||
|
hints = (catalog_exercise_hints or "").strip()
|
||||||
|
if hints:
|
||||||
|
goal_parts.extend(["", "Katalog-Hinweise Übungsanlage:", hints])
|
||||||
goal = "\n".join(goal_parts)
|
goal = "\n".join(goal_parts)
|
||||||
|
|
||||||
focus_hint = topic if brief.topic_type == "technique" else None
|
focus_hint = topic if brief.topic_type == "technique" else None
|
||||||
|
|
@ -118,6 +124,7 @@ def try_suggest_ai_stage_step(
|
||||||
brief: PlanningSemanticBrief,
|
brief: PlanningSemanticBrief,
|
||||||
spec: Mapping[str, Any],
|
spec: Mapping[str, Any],
|
||||||
steps: Sequence[Mapping[str, Any]],
|
steps: Sequence[Mapping[str, Any]],
|
||||||
|
catalog: Optional[ProgressionPlanningCatalogContext] = None,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""KI-Vorschlag für leere Roadmap-Stufe."""
|
"""KI-Vorschlag für leere Roadmap-Stufe."""
|
||||||
major_idx = spec.get("roadmap_major_step_index")
|
major_idx = spec.get("roadmap_major_step_index")
|
||||||
|
|
@ -136,6 +143,8 @@ def try_suggest_ai_stage_step(
|
||||||
if not gap.get("learning_goal"):
|
if not gap.get("learning_goal"):
|
||||||
gap["learning_goal"] = spec.get("title_hint") or spec.get("sketch")
|
gap["learning_goal"] = spec.get("title_hint") or spec.get("sketch")
|
||||||
|
|
||||||
|
exercise_hints = catalog_exercise_hints_block(cur, catalog) if catalog else ""
|
||||||
|
|
||||||
ctx = _build_stage_ai_context(
|
ctx = _build_stage_ai_context(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
|
|
@ -143,6 +152,7 @@ def try_suggest_ai_stage_step(
|
||||||
step_before=step_before,
|
step_before=step_before,
|
||||||
step_after=step_after,
|
step_after=step_after,
|
||||||
prior_steps=prior_steps,
|
prior_steps=prior_steps,
|
||||||
|
catalog_exercise_hints=exercise_hints or None,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
ai_payload = run_exercise_form_ai_suggestion(cur, ctx=ctx)
|
ai_payload = run_exercise_form_ai_suggestion(cur, ctx=ctx)
|
||||||
|
|
@ -189,6 +199,7 @@ def _build_gap_ai_context(
|
||||||
gap: Mapping[str, Any],
|
gap: Mapping[str, Any],
|
||||||
title_hint: Optional[str] = None,
|
title_hint: Optional[str] = None,
|
||||||
sketch_hint: Optional[str] = None,
|
sketch_hint: Optional[str] = None,
|
||||||
|
catalog_exercise_hints: Optional[str] = None,
|
||||||
) -> ExerciseFormAiPromptContext:
|
) -> ExerciseFormAiPromptContext:
|
||||||
topic = (brief.primary_topic or "Technik").strip()
|
topic = (brief.primary_topic or "Technik").strip()
|
||||||
phase = gap.get("expected_phase") or "vertiefung"
|
phase = gap.get("expected_phase") or "vertiefung"
|
||||||
|
|
@ -206,6 +217,9 @@ def _build_gap_ai_context(
|
||||||
]
|
]
|
||||||
if sketch:
|
if sketch:
|
||||||
goal_parts.extend(["", f"Hinweis: {sketch}"])
|
goal_parts.extend(["", f"Hinweis: {sketch}"])
|
||||||
|
hints = (catalog_exercise_hints or "").strip()
|
||||||
|
if hints:
|
||||||
|
goal_parts.extend(["", "Katalog-Hinweise Übungsanlage:", hints])
|
||||||
goal = "\n".join(goal_parts)
|
goal = "\n".join(goal_parts)
|
||||||
|
|
||||||
focus_hint = topic if brief.topic_type == "technique" else None
|
focus_hint = topic if brief.topic_type == "technique" else None
|
||||||
|
|
@ -271,8 +285,10 @@ def try_suggest_ai_bridge_step(
|
||||||
gap: Mapping[str, Any],
|
gap: Mapping[str, Any],
|
||||||
title_hint: Optional[str] = None,
|
title_hint: Optional[str] = None,
|
||||||
sketch_hint: Optional[str] = None,
|
sketch_hint: Optional[str] = None,
|
||||||
|
catalog: Optional[ProgressionPlanningCatalogContext] = None,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""Ruft exercise AI suggest auf — kein Speichern in DB."""
|
"""Ruft exercise AI suggest auf — kein Speichern in DB."""
|
||||||
|
exercise_hints = catalog_exercise_hints_block(cur, catalog) if catalog else ""
|
||||||
ctx = _build_gap_ai_context(
|
ctx = _build_gap_ai_context(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
|
|
@ -281,6 +297,7 @@ def try_suggest_ai_bridge_step(
|
||||||
gap=gap,
|
gap=gap,
|
||||||
title_hint=title_hint,
|
title_hint=title_hint,
|
||||||
sketch_hint=sketch_hint,
|
sketch_hint=sketch_hint,
|
||||||
|
catalog_exercise_hints=exercise_hints or None,
|
||||||
)
|
)
|
||||||
g_plain = strip_html_to_plain(ctx.goal)
|
g_plain = strip_html_to_plain(ctx.goal)
|
||||||
if not g_plain.strip() and not (ctx.title or "").strip():
|
if not g_plain.strip() and not (ctx.title or "").strip():
|
||||||
|
|
@ -478,6 +495,7 @@ def build_gap_fill_goal_text(
|
||||||
step_a: Optional[Mapping[str, Any]] = None,
|
step_a: Optional[Mapping[str, Any]] = None,
|
||||||
step_b: Optional[Mapping[str, Any]] = None,
|
step_b: Optional[Mapping[str, Any]] = None,
|
||||||
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
||||||
|
catalog_exercise_hints: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Ausführlicher Zieltext für KI-Neuanlage aus Pfad-, Roadmap- und Stufen-Kontext."""
|
"""Ausführlicher Zieltext für KI-Neuanlage aus Pfad-, Roadmap- und Stufen-Kontext."""
|
||||||
topic = (brief.primary_topic or "Technik").strip()
|
topic = (brief.primary_topic or "Technik").strip()
|
||||||
|
|
@ -555,6 +573,9 @@ def build_gap_fill_goal_text(
|
||||||
parts.append(f"Qualitätsprüfung: {spec['rationale']}")
|
parts.append(f"Qualitätsprüfung: {spec['rationale']}")
|
||||||
if spec.get("sketch"):
|
if spec.get("sketch"):
|
||||||
parts.append(f"Skizze: {spec['sketch']}")
|
parts.append(f"Skizze: {spec['sketch']}")
|
||||||
|
hints = (catalog_exercise_hints or "").strip()
|
||||||
|
if hints:
|
||||||
|
parts.append(f"Katalog-Hinweise Übungsanlage:\n{hints}")
|
||||||
parts.append(
|
parts.append(
|
||||||
"Die Übung muss die Stufe didaktisch erfüllen: klare Voraussetzungen, messbares Stufenziel, "
|
"Die Übung muss die Stufe didaktisch erfüllen: klare Voraussetzungen, messbares Stufenziel, "
|
||||||
"Bezug zum Gesamtpfad — keine generische Kraftübung ohne Technikbezug. "
|
"Bezug zum Gesamtpfad — keine generische Kraftübung ohne Technikbezug. "
|
||||||
|
|
@ -571,6 +592,7 @@ def build_gap_fill_offer(
|
||||||
brief: Optional[PlanningSemanticBrief] = None,
|
brief: Optional[PlanningSemanticBrief] = None,
|
||||||
proposal: Optional[Mapping[str, Any]] = None,
|
proposal: Optional[Mapping[str, Any]] = None,
|
||||||
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
||||||
|
catalog_exercise_hints: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
source = spec.get("source")
|
source = spec.get("source")
|
||||||
idx = int(spec.get("insert_after_index") or 0)
|
idx = int(spec.get("insert_after_index") or 0)
|
||||||
|
|
@ -603,6 +625,7 @@ def build_gap_fill_offer(
|
||||||
step_a=step_a,
|
step_a=step_a,
|
||||||
step_b=step_b,
|
step_b=step_b,
|
||||||
roadmap_snapshot=enriched_snapshot or None,
|
roadmap_snapshot=enriched_snapshot or None,
|
||||||
|
catalog_exercise_hints=catalog_exercise_hints,
|
||||||
)
|
)
|
||||||
ctx_preview = enriched_snapshot or None
|
ctx_preview = enriched_snapshot or None
|
||||||
offer: Dict[str, Any] = {
|
offer: Dict[str, Any] = {
|
||||||
|
|
@ -642,6 +665,7 @@ def apply_gap_fill_after_qa(
|
||||||
max_ai_proposals: int = 3,
|
max_ai_proposals: int = 3,
|
||||||
auto_insert_proposals: bool = False,
|
auto_insert_proposals: bool = False,
|
||||||
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
||||||
|
catalog: Optional[ProgressionPlanningCatalogContext] = None,
|
||||||
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]:
|
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
Erzeugt gap_fill_offers für die UI; optional KI-Vorschläge einfügen.
|
Erzeugt gap_fill_offers für die UI; optional KI-Vorschläge einfügen.
|
||||||
|
|
@ -653,6 +677,7 @@ def apply_gap_fill_after_qa(
|
||||||
out = list(steps)
|
out = list(steps)
|
||||||
proposals: List[Dict[str, Any]] = []
|
proposals: List[Dict[str, Any]] = []
|
||||||
offers: List[Dict[str, Any]] = []
|
offers: List[Dict[str, Any]] = []
|
||||||
|
exercise_hints = catalog_exercise_hints_block(cur, catalog) if catalog else ""
|
||||||
|
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
source = spec.get("source")
|
source = spec.get("source")
|
||||||
|
|
@ -666,6 +691,7 @@ def apply_gap_fill_after_qa(
|
||||||
brief=brief,
|
brief=brief,
|
||||||
spec=spec,
|
spec=spec,
|
||||||
steps=out,
|
steps=out,
|
||||||
|
catalog=catalog,
|
||||||
)
|
)
|
||||||
offer = build_gap_fill_offer(
|
offer = build_gap_fill_offer(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
|
|
@ -674,6 +700,7 @@ def apply_gap_fill_after_qa(
|
||||||
brief=brief,
|
brief=brief,
|
||||||
proposal=proposal,
|
proposal=proposal,
|
||||||
roadmap_snapshot=roadmap_snapshot,
|
roadmap_snapshot=roadmap_snapshot,
|
||||||
|
catalog_exercise_hints=exercise_hints or None,
|
||||||
)
|
)
|
||||||
offers.append(offer)
|
offers.append(offer)
|
||||||
if proposal and auto_insert_proposals:
|
if proposal and auto_insert_proposals:
|
||||||
|
|
@ -700,6 +727,7 @@ def apply_gap_fill_after_qa(
|
||||||
brief=brief,
|
brief=brief,
|
||||||
proposal=None,
|
proposal=None,
|
||||||
roadmap_snapshot=roadmap_snapshot,
|
roadmap_snapshot=roadmap_snapshot,
|
||||||
|
catalog_exercise_hints=exercise_hints or None,
|
||||||
)
|
)
|
||||||
offers.append(offer)
|
offers.append(offer)
|
||||||
continue
|
continue
|
||||||
|
|
@ -719,6 +747,7 @@ def apply_gap_fill_after_qa(
|
||||||
gap=gap,
|
gap=gap,
|
||||||
title_hint=str(spec.get("title_hint") or ""),
|
title_hint=str(spec.get("title_hint") or ""),
|
||||||
sketch_hint=str(spec.get("sketch") or ""),
|
sketch_hint=str(spec.get("sketch") or ""),
|
||||||
|
catalog=catalog,
|
||||||
)
|
)
|
||||||
|
|
||||||
offer = build_gap_fill_offer(
|
offer = build_gap_fill_offer(
|
||||||
|
|
@ -728,6 +757,7 @@ def apply_gap_fill_after_qa(
|
||||||
brief=brief,
|
brief=brief,
|
||||||
proposal=proposal,
|
proposal=proposal,
|
||||||
roadmap_snapshot=roadmap_snapshot,
|
roadmap_snapshot=roadmap_snapshot,
|
||||||
|
catalog_exercise_hints=exercise_hints or None,
|
||||||
)
|
)
|
||||||
offers.append(offer)
|
offers.append(offer)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,11 @@ from planning_catalog_context import (
|
||||||
load_catalog_context_from_graph_row,
|
load_catalog_context_from_graph_row,
|
||||||
merge_catalog_context_into_target,
|
merge_catalog_context_into_target,
|
||||||
)
|
)
|
||||||
|
from catalog_prompt_slots import get_rematch_guard_for_catalog
|
||||||
from planning_exercise_profiles import PlanningTargetProfile
|
from planning_exercise_profiles import PlanningTargetProfile
|
||||||
from planning_path_qa_pipeline import run_multistage_path_qa
|
from planning_path_qa_pipeline import run_multistage_path_qa
|
||||||
from planning_path_rematch import (
|
from planning_path_rematch import (
|
||||||
|
apply_rematch_catalog_guard,
|
||||||
collect_rematch_slot_indices,
|
collect_rematch_slot_indices,
|
||||||
filter_rematch_slot_indices,
|
filter_rematch_slot_indices,
|
||||||
prune_stripped_after_rematch,
|
prune_stripped_after_rematch,
|
||||||
|
|
@ -1737,6 +1739,13 @@ def _run_roadmap_rematch_loop(
|
||||||
stripped_off_topic=current_stripped if round_idx == 0 else [],
|
stripped_off_topic=current_stripped if round_idx == 0 else [],
|
||||||
off_topic_steps=off_topic_before_strip if round_idx == 0 and use_initial_off_topic else [],
|
off_topic_steps=off_topic_before_strip if round_idx == 0 and use_initial_off_topic else [],
|
||||||
)
|
)
|
||||||
|
catalog_context = _resolve_planning_catalog_context(cur, body)
|
||||||
|
rematch_guard = get_rematch_guard_for_catalog(cur, catalog_context)
|
||||||
|
slot_indices = apply_rematch_catalog_guard(
|
||||||
|
slot_indices,
|
||||||
|
rematch_reasons,
|
||||||
|
rematch_guard=rematch_guard,
|
||||||
|
)
|
||||||
if not slot_indices:
|
if not slot_indices:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -2191,6 +2200,7 @@ def _run_evaluate_only_path_qa(
|
||||||
max_ai_proposals=0,
|
max_ai_proposals=0,
|
||||||
auto_insert_proposals=False,
|
auto_insert_proposals=False,
|
||||||
roadmap_snapshot=path_roadmap_snapshot,
|
roadmap_snapshot=path_roadmap_snapshot,
|
||||||
|
catalog=catalog_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
multistage_qa = run_multistage_path_qa(
|
multistage_qa = run_multistage_path_qa(
|
||||||
|
|
@ -4294,6 +4304,7 @@ def suggest_progression_path(
|
||||||
max_ai_proposals=0,
|
max_ai_proposals=0,
|
||||||
auto_insert_proposals=False,
|
auto_insert_proposals=False,
|
||||||
roadmap_snapshot=path_roadmap_snapshot,
|
roadmap_snapshot=path_roadmap_snapshot,
|
||||||
|
catalog=catalog_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
if roadmap_gap_offers:
|
if roadmap_gap_offers:
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,10 @@ class PlanningIntentContext:
|
||||||
context_notes: str = ""
|
context_notes: str = ""
|
||||||
topic_type: str = "general"
|
topic_type: str = "general"
|
||||||
technique_sibling_excludes: List[str] = field(default_factory=list)
|
technique_sibling_excludes: List[str] = field(default_factory=list)
|
||||||
|
catalog_exercise_hints: Optional[str] = None
|
||||||
|
|
||||||
def to_api_dict(self) -> Dict[str, Any]:
|
def to_api_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
out = {
|
||||||
"source_query": self.source_query,
|
"source_query": self.source_query,
|
||||||
"primary_topic": self.primary_topic,
|
"primary_topic": self.primary_topic,
|
||||||
"topic_type": self.topic_type,
|
"topic_type": self.topic_type,
|
||||||
|
|
@ -61,6 +62,10 @@ class PlanningIntentContext:
|
||||||
"technique_sibling_excludes": self.technique_sibling_excludes[:16],
|
"technique_sibling_excludes": self.technique_sibling_excludes[:16],
|
||||||
"context_notes": self.context_notes[:1200] or None,
|
"context_notes": self.context_notes[:1200] or None,
|
||||||
}
|
}
|
||||||
|
hints = (self.catalog_exercise_hints or "").strip()
|
||||||
|
if hints:
|
||||||
|
out["catalog_exercise_hints"] = hints[:2000]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def build_planning_intent_context(
|
def build_planning_intent_context(
|
||||||
|
|
@ -70,6 +75,8 @@ def build_planning_intent_context(
|
||||||
goal_analysis: Optional[Mapping[str, Any]] = None,
|
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||||
extra_context: Optional[str] = None,
|
extra_context: Optional[str] = None,
|
||||||
primary_topic: Optional[str] = None,
|
primary_topic: Optional[str] = None,
|
||||||
|
cur=None,
|
||||||
|
catalog=None,
|
||||||
) -> PlanningIntentContext:
|
) -> PlanningIntentContext:
|
||||||
"""Intent aus Anfrage, Zielanalyse und optionalem Kontext — ohne Sonderregeln pro Thema."""
|
"""Intent aus Anfrage, Zielanalyse und optionalem Kontext — ohne Sonderregeln pro Thema."""
|
||||||
ga = dict(goal_analysis or {})
|
ga = dict(goal_analysis or {})
|
||||||
|
|
@ -124,6 +131,12 @@ def build_planning_intent_context(
|
||||||
if line not in path_success:
|
if line not in path_success:
|
||||||
path_success.insert(0, line)
|
path_success.insert(0, line)
|
||||||
|
|
||||||
|
catalog_hints = ""
|
||||||
|
if cur is not None and catalog is not None:
|
||||||
|
from planning_prompt_variables import catalog_exercise_hints_block
|
||||||
|
|
||||||
|
catalog_hints = catalog_exercise_hints_block(cur, catalog)
|
||||||
|
|
||||||
return PlanningIntentContext(
|
return PlanningIntentContext(
|
||||||
source_query=(goal_query or "").strip(),
|
source_query=(goal_query or "").strip(),
|
||||||
primary_topic=topic,
|
primary_topic=topic,
|
||||||
|
|
@ -133,6 +146,7 @@ def build_planning_intent_context(
|
||||||
explicit_exclusions=explicit,
|
explicit_exclusions=explicit,
|
||||||
technique_sibling_excludes=siblings[:16],
|
technique_sibling_excludes=siblings[:16],
|
||||||
context_notes=combined_notes[:1200],
|
context_notes=combined_notes[:1200],
|
||||||
|
catalog_exercise_hints=catalog_hints or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple
|
||||||
|
|
||||||
from planning_progression_roadmap import ProgressionRoadmapContext, StageSpecArtifact
|
from planning_progression_roadmap import ProgressionRoadmapContext, StageSpecArtifact
|
||||||
|
|
||||||
|
_UNFILLED_REMATCH_REASON = "Keine passende Übung für Roadmap-Stufe"
|
||||||
|
|
||||||
|
|
||||||
def _slot_priority_for_rematch(
|
def _slot_priority_for_rematch(
|
||||||
body,
|
body,
|
||||||
|
|
@ -103,7 +105,7 @@ def collect_rematch_slot_indices(
|
||||||
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
||||||
idx, spec = item[0], item[1]
|
idx, spec = item[0], item[1]
|
||||||
midx = getattr(spec, "major_step_index", idx)
|
midx = getattr(spec, "major_step_index", idx)
|
||||||
_register(int(midx), "Keine passende Übung für Roadmap-Stufe")
|
_register(int(midx), _UNFILLED_REMATCH_REASON)
|
||||||
elif isinstance(item, dict):
|
elif isinstance(item, dict):
|
||||||
midx = _resolve_major(item)
|
midx = _resolve_major(item)
|
||||||
if midx is not None:
|
if midx is not None:
|
||||||
|
|
@ -114,6 +116,25 @@ def collect_rematch_slot_indices(
|
||||||
return indices, reasons
|
return indices, reasons
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rematch_catalog_guard(
|
||||||
|
slot_indices: Set[int],
|
||||||
|
rematch_reasons: Mapping[int, str],
|
||||||
|
*,
|
||||||
|
rematch_guard: Optional[str],
|
||||||
|
) -> Set[int]:
|
||||||
|
"""
|
||||||
|
Bei gesetztem rematch_guard (Katalog-Slot): kein Auto-Rematch nur wegen leerer Roadmap-Stufe.
|
||||||
|
Off-Topic, explizite QS-Hinweise und Stufen-Verfeinerung bleiben aktiv.
|
||||||
|
"""
|
||||||
|
if not (rematch_guard or "").strip():
|
||||||
|
return set(slot_indices)
|
||||||
|
return {
|
||||||
|
idx
|
||||||
|
for idx in slot_indices
|
||||||
|
if str(rematch_reasons.get(idx) or "").strip() != _UNFILLED_REMATCH_REASON
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def filter_rematch_slot_indices(
|
def filter_rematch_slot_indices(
|
||||||
steps: Sequence[Mapping[str, Any]],
|
steps: Sequence[Mapping[str, Any]],
|
||||||
slot_indices: Set[int],
|
slot_indices: Set[int],
|
||||||
|
|
@ -372,6 +393,7 @@ def prune_stripped_after_rematch(
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"apply_rematch_catalog_guard",
|
||||||
"collect_rematch_slot_indices",
|
"collect_rematch_slot_indices",
|
||||||
"filter_rematch_slot_indices",
|
"filter_rematch_slot_indices",
|
||||||
"prune_stripped_after_rematch",
|
"prune_stripped_after_rematch",
|
||||||
|
|
|
||||||
|
|
@ -1244,6 +1244,8 @@ def run_progression_roadmap_pipeline(
|
||||||
resolved.target_state,
|
resolved.target_state,
|
||||||
),
|
),
|
||||||
primary_topic=goal_analysis.primary_topic,
|
primary_topic=goal_analysis.primary_topic,
|
||||||
|
cur=cur,
|
||||||
|
catalog=catalog,
|
||||||
)
|
)
|
||||||
|
|
||||||
heuristic_specs = build_stage_specs(
|
heuristic_specs = build_stage_specs(
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,27 @@ _PLANNING_PROMPT_VARIABLE_PROVIDERS: tuple[PlanningPromptVariableProvider, ...]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def catalog_exercise_hints_block(
|
||||||
|
cur,
|
||||||
|
catalog: Optional[ProgressionPlanningCatalogContext] = None,
|
||||||
|
*,
|
||||||
|
slug: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Kombinierte hints_on_exercise aus aktivem Katalog-Kontext (Gap-Fill / Intent)."""
|
||||||
|
if cur is None or catalog is None:
|
||||||
|
return ""
|
||||||
|
from catalog_prompt_slots import CATALOG_KINDS, placeholder_key
|
||||||
|
|
||||||
|
merged = merge_planning_prompt_variables(cur, {}, catalog=catalog, slug=slug)
|
||||||
|
lines: list[str] = []
|
||||||
|
for cfg in CATALOG_KINDS:
|
||||||
|
key = placeholder_key(cfg.kind, "hints_on_exercise")
|
||||||
|
text = (merged.get(key) or "").strip()
|
||||||
|
if text:
|
||||||
|
lines.append(f"{cfg.label_de}: {text}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def merge_planning_prompt_variables(
|
def merge_planning_prompt_variables(
|
||||||
cur,
|
cur,
|
||||||
base_variables: Mapping[str, str],
|
base_variables: Mapping[str, str],
|
||||||
|
|
@ -113,6 +134,7 @@ def planning_prompt_placeholder_catalog() -> dict:
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"catalog_exercise_hints_block",
|
||||||
"merge_planning_prompt_variables",
|
"merge_planning_prompt_variables",
|
||||||
"planning_prompt_placeholder_catalog",
|
"planning_prompt_placeholder_catalog",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,20 @@ def test_strip_off_topic_steps_from_path():
|
||||||
assert [s["exercise_id"] for s in out] == [1, 2, 4]
|
assert [s["exercise_id"] for s in out] == [1, 2, 4]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_gap_fill_goal_text_includes_catalog_exercise_hints():
|
||||||
|
from planning_exercise_semantics import build_semantic_brief
|
||||||
|
|
||||||
|
brief = build_semantic_brief("Mae Geri")
|
||||||
|
text = build_gap_fill_goal_text(
|
||||||
|
goal_query="Mae Geri lernen",
|
||||||
|
brief=brief,
|
||||||
|
spec={"source": "roadmap_unfilled", "phase": "grundlage", "title_hint": "Stand"},
|
||||||
|
catalog_exercise_hints="Primärfokus: Kihon und Partnerübungen mit Technikbezug.",
|
||||||
|
)
|
||||||
|
assert "Katalog-Hinweise Übungsanlage" in text
|
||||||
|
assert "Kihon" in text
|
||||||
|
|
||||||
|
|
||||||
def test_build_gap_fill_goal_text_includes_topic():
|
def test_build_gap_fill_goal_text_includes_topic():
|
||||||
brief = build_semantic_brief("Mae Geri Perfektion")
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
||||||
text = build_gap_fill_goal_text(
|
text = build_gap_fill_goal_text(
|
||||||
|
|
|
||||||
|
|
@ -336,3 +336,29 @@ def test_filter_rematch_skips_preserved_slots():
|
||||||
off_topic_steps=[],
|
off_topic_steps=[],
|
||||||
)
|
)
|
||||||
assert filtered == {1}
|
assert filtered == {1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_rematch_catalog_guard_skips_unfilled_only():
|
||||||
|
from planning_path_rematch import apply_rematch_catalog_guard
|
||||||
|
|
||||||
|
indices = {0, 1, 2}
|
||||||
|
reasons = {
|
||||||
|
0: "QS-Tier-1",
|
||||||
|
1: "Keine passende Übung für Roadmap-Stufe",
|
||||||
|
2: "Passt nicht zur Haupttechnik",
|
||||||
|
}
|
||||||
|
filtered = apply_rematch_catalog_guard(
|
||||||
|
indices,
|
||||||
|
reasons,
|
||||||
|
rematch_guard="Keine leeren Slots erzwingen.",
|
||||||
|
)
|
||||||
|
assert filtered == {0, 2}
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_rematch_catalog_guard_inactive_without_guard():
|
||||||
|
from planning_path_rematch import apply_rematch_catalog_guard
|
||||||
|
|
||||||
|
indices = {1}
|
||||||
|
reasons = {1: "Keine passende Übung für Roadmap-Stufe"}
|
||||||
|
assert apply_rematch_catalog_guard(indices, reasons, rematch_guard=None) == {1}
|
||||||
|
assert apply_rematch_catalog_guard(indices, reasons, rematch_guard="") == {1}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.237"
|
APP_VERSION = "0.8.238"
|
||||||
BUILD_DATE = "2026-05-22"
|
BUILD_DATE = "2026-05-22"
|
||||||
DB_SCHEMA_VERSION = "20260607094"
|
DB_SCHEMA_VERSION = "20260607095"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
||||||
|
|
@ -53,11 +53,20 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.238",
|
||||||
|
"date": "2026-05-22",
|
||||||
|
"changes": [
|
||||||
|
"Planungs-KI H1.5: rematch_guard im Auto-Rematch (kein Rematch nur wegen leerer Roadmap-Stufe).",
|
||||||
|
"hints_on_exercise in Gap-Fill, Übungs-KI-Kontext und intent_context_json (Roadmap-Pipeline).",
|
||||||
|
"Migration 095: keine Migrations-Seeds in catalog_prompt_slots (Prod-sichere Katalogstruktur).",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.237",
|
"version": "0.8.237",
|
||||||
"date": "2026-05-22",
|
"date": "2026-05-22",
|
||||||
"changes": [
|
"changes": [
|
||||||
"Migration 094: catalog_prompt_slots vollständig befüllt (Karate, SV, alle Trainingsstile/Zielgruppen).",
|
"Migration 094/095: Katalog-Prompt-Slots ohne Migrations-Seeds — Inhalte Admin oder Laufzeit-Fallback.",
|
||||||
"catalog_slot_fallbacks: Namens-Fallback bis Admin-Override — gleiche Qualität wie H1-Registry.",
|
"catalog_slot_fallbacks: Namens-Fallback bis Admin-Override — gleiche Qualität wie H1-Registry.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||||
|
|
||||||
**Stand:** 2026-05-22 (F15 Graph-Match & getrennte Pfad-QS, lokal nach **0.8.233**)
|
**Stand:** 2026-05-22 (Katalog-Prompt-Slots H2/H1.5, **0.8.238**)
|
||||||
**App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11–F14, Katalog-Kontext); **F15** siehe §2.8 — DB unverändert (`DB_SCHEMA_VERSION`, Migration **088**).
|
**App-Version / DB-Schema:** App **`0.8.238`**; DB Migration **095** (keine Slot-Seeds; Prod-sichere Katalogstruktur).
|
||||||
|
|
||||||
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
||||||
|
|
||||||
|
|
@ -115,7 +115,8 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
| **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** |
|
| **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** |
|
||||||
| **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** |
|
| **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** |
|
||||||
| **F15** | Unified Slot-Review (Match-Dialog), getrennte Pfad-QS, `findings_stale` | ✅ lokal (nach 0.8.233) |
|
| **F15** | Unified Slot-Review (Match-Dialog), getrennte Pfad-QS, `findings_stale` | ✅ lokal (nach 0.8.233) |
|
||||||
| **H1** | Katalog-Prompt-Snippets (modulare LLM-Anweisungen) | 🔲 Spec **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
|
| **H1–H2.1** | Katalog-Prompt-Slots (DB, Resolver, Admin-UI, granulare Prompts) | ✅ **0.8.234–238** — Spec **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
|
||||||
|
| **H1.5** | `rematch_guard`, `hints_on_exercise` in Rematch/Gap-Fill/Intent | ✅ **0.8.238** |
|
||||||
|
|
||||||
**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet; **H1:** zusätzlich Prompt-Snippets), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16.
|
**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet; **H1:** zusätzlich Prompt-Snippets), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16.
|
||||||
|
|
||||||
|
|
@ -148,7 +149,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
5. Phase D′ — Auto KI-Gap-Fill bei persistent leeren Slots
|
5. Phase D′ — Auto KI-Gap-Fill bei persistent leeren Slots
|
||||||
6. **Trainingsplanung Phase G** — Gruppenkontext-Pack, Scopes `training_section` / `framework_slot` (Ist-Doku §16)
|
6. **Trainingsplanung Phase G** — Gruppenkontext-Pack, Scopes `training_section` / `framework_slot` (Ist-Doku §16)
|
||||||
7. Technik-Katalog konfigurierbar (Backlog)
|
7. Technik-Katalog konfigurierbar (Backlog)
|
||||||
8. **H1** — Katalog-Prompt-Snippets (modulare LLM-Anweisungen)
|
8. **H1–H2.1** — Katalog-Prompt-Slots ✅ · **Prompt-Editor-Überarbeitung** → eigene Session
|
||||||
|
|
||||||
#### Übungs-KI Formular / Schnellanlage (Stand **0.8.171**)
|
#### Übungs-KI Formular / Schnellanlage (Stand **0.8.171**)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Planungs-KI — Katalog-Prompt-Slots (Snippets)
|
# Planungs-KI — Katalog-Prompt-Slots (Snippets)
|
||||||
|
|
||||||
**Stand:** 2026-05-22
|
**Stand:** 2026-05-22
|
||||||
**Status:** **H2** umgesetzt (0.8.235) · **H2.1** Admin-UI + granulare Prompts (0.8.236)
|
**Status:** **H2.1** umgesetzt (0.8.236) · **H1.5** Rematch-Guard + Gap-Fill-Hints (0.8.238) · **Prompt-Editor-Überarbeitung** bewusst ausgelagert
|
||||||
**Bezüge:** `PLANNING_PROGRESSION_GRAPH_KI.md` §4.4 · `AI_PROMPT_TARGET_ARCHITECTURE.md` §2.4 · `planning_catalog_context.py` · `catalog_prompt_slots.py`
|
**Bezüge:** `PLANNING_PROGRESSION_GRAPH_KI.md` §4.4 · `AI_PROMPT_TARGET_ARCHITECTURE.md` §2.4 · `planning_catalog_context.py` · `catalog_prompt_slots.py`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -173,12 +173,12 @@ Hardcodierte `SNIPPET_REGISTRY` — Proof of Concept für `catalog_guidance_bloc
|
||||||
|
|
||||||
- [x] Slot-Editor an Fokusbereich / Trainingsstil / Zielgruppe / Stilrichtung (`CatalogPromptSlotsEditor`, Stammdaten-Katalog)
|
- [x] Slot-Editor an Fokusbereich / Trainingsstil / Zielgruppe / Stilrichtung (`CatalogPromptSlotsEditor`, Stammdaten-Katalog)
|
||||||
- [x] Prompt-Templates mit granularen Platzhaltern (Migration 093)
|
- [x] Prompt-Templates mit granularen Platzhaltern (Migration 093)
|
||||||
- [ ] Platzhalter-Hilfe im KI-Prompt-Editor (erweitert)
|
- [ ] **Prompt-Editor komplett überarbeiten** (Platzhalter-Hilfe, UX) — **eigene Session**
|
||||||
|
|
||||||
### H1.5
|
### H1.5 ✓ (0.8.238)
|
||||||
|
|
||||||
- [ ] `rematch_guard` im Rematch-Loop
|
- [x] `rematch_guard` im Rematch-Loop (`apply_rematch_catalog_guard`)
|
||||||
- [ ] Intent-Prompts + Gap-Fill: `hints_on_exercise`
|
- [x] Gap-Fill + Intent: `hints_on_exercise` via `catalog_exercise_hints_block` / `intent_context_json`
|
||||||
|
|
||||||
### H3 — Trainingsplanung (Phase G)
|
### H3 — Trainingsplanung (Phase G)
|
||||||
|
|
||||||
|
|
@ -211,6 +211,7 @@ Hardcodierte `SNIPPET_REGISTRY` — Proof of Concept für `catalog_guidance_bloc
|
||||||
|
|
||||||
| Datum | Änderung |
|
| Datum | Änderung |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
|
| 2026-05-22 | **H1.5** (0.8.238): rematch_guard; hints_on_exercise in Gap-Fill/Intent; Migration 095 ohne Seeds |
|
||||||
| 2026-05-22 | **H2.1** (0.8.236): Admin-UI `CatalogPromptSlotsEditor`; Migration 093 granulare Prompt-Templates |
|
| 2026-05-22 | **H2.1** (0.8.236): Admin-UI `CatalogPromptSlotsEditor`; Migration 093 granulare Prompt-Templates |
|
||||||
| 2026-05-22 | **H2** (0.8.235): Slot-Typ-Register + `catalog_prompt_slots` DB, granulare Platzhalter, Admin-API |
|
| 2026-05-22 | **H2** (0.8.235): Slot-Typ-Register + `catalog_prompt_slots` DB, granulare Platzhalter, Admin-API |
|
||||||
| 2026-05-22 | Konzept §4–§8: zwei Ebenen Slot-Typ vs. Slot-Wert; Platzhalter `{kind}_{slot_key}` |
|
| 2026-05-22 | Konzept §4–§8: zwei Ebenen Slot-Typ vs. Slot-Wert; Platzhalter `{kind}_{slot_key}` |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user