Enhance Planning Context with Progression Gap Snapshot and Start/Target Analysis
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `build_progression_gap_snapshot` function to create a compact roadmap context for gap exercises, integrating start situation, target state, and stage specifications. - Updated `build_gap_fill_goal_text` to include roadmap snapshot details, enhancing the context for AI-generated exercises. - Enhanced `ProgressionPathSuggestRequest` and related components to support new structured inputs for start/target analysis, improving user experience and AI suggestions. - Incremented application version to 0.8.212 to reflect these changes.
This commit is contained in:
parent
fad1058d54
commit
f2650dac57
|
|
@ -6,7 +6,7 @@ Wird als ``planning_context_json`` in Übungs-Prompts (summary, skills, instruct
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
from typing import Any, Dict, List, Mapping, Optional
|
||||
|
||||
_MAX_JSON_CHARS = 6000
|
||||
_MAX_STRING = 800
|
||||
|
|
@ -85,6 +85,73 @@ def planning_context_prompt_variables(
|
|||
}
|
||||
|
||||
|
||||
def build_progression_gap_snapshot(
|
||||
*,
|
||||
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||
resolved_structured: Optional[Mapping[str, Any]] = None,
|
||||
stage_spec: Optional[Mapping[str, Any]] = None,
|
||||
semantic_brief: Optional[Mapping[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Kompakter Roadmap-Kontext für Lücken-Übungen (Start, Ziel, Stufe, Fähigkeiten-Hinweise)."""
|
||||
ga = dict(goal_analysis or {})
|
||||
rs = dict(resolved_structured or {})
|
||||
spec = dict(stage_spec or {})
|
||||
brief = dict(semantic_brief or {})
|
||||
|
||||
start = _trim_str(rs.get("start_situation") or ga.get("start_assumption"))
|
||||
target = _trim_str(rs.get("target_state") or ga.get("target_state"))
|
||||
notes = _trim_str(rs.get("roadmap_notes"))
|
||||
topic = _trim_str(ga.get("primary_topic") or brief.get("primary_topic"))
|
||||
|
||||
skill_hints: List[str] = []
|
||||
for item in (brief.get("must_phrases") or [])[:4]:
|
||||
t = _trim_str(item, limit=120)
|
||||
if t:
|
||||
skill_hints.append(t)
|
||||
arc = brief.get("development_arc")
|
||||
if isinstance(arc, list) and arc:
|
||||
skill_hints.append(f"Entwicklungsbogen: {' → '.join(str(x) for x in arc[:5])}")
|
||||
|
||||
success_path = [
|
||||
_trim_str(x, limit=200)
|
||||
for x in (ga.get("success_criteria") or [])
|
||||
if _trim_str(x, limit=200)
|
||||
][:4]
|
||||
stage_success = [
|
||||
_trim_str(x, limit=200)
|
||||
for x in (spec.get("success_criteria") or [])
|
||||
if _trim_str(x, limit=200)
|
||||
][:4]
|
||||
load_profile = [
|
||||
_trim_str(x, limit=80)
|
||||
for x in (spec.get("load_profile") or [])
|
||||
if _trim_str(x, limit=80)
|
||||
][:6]
|
||||
anti_patterns = [
|
||||
_trim_str(x, limit=200)
|
||||
for x in (spec.get("anti_patterns") or [])
|
||||
if _trim_str(x, limit=200)
|
||||
][:3]
|
||||
|
||||
snap: Dict[str, Any] = {
|
||||
"primary_topic": topic,
|
||||
"start_situation": start,
|
||||
"target_state": target,
|
||||
"roadmap_notes": notes,
|
||||
"stage_learning_goal": _trim_str(
|
||||
spec.get("learning_goal"), limit=1200
|
||||
),
|
||||
"stage_phase": _trim_str(spec.get("phase")),
|
||||
"stage_exercise_type": _trim_str(spec.get("exercise_type")),
|
||||
"stage_load_profile": load_profile or None,
|
||||
"stage_success_criteria": stage_success or None,
|
||||
"stage_anti_patterns": anti_patterns or None,
|
||||
"path_success_criteria": success_path or None,
|
||||
"skill_hints": skill_hints or None,
|
||||
}
|
||||
return {k: v for k, v in snap.items() if v is not None and v != "" and v != []}
|
||||
|
||||
|
||||
def build_progression_path_gap_planning_context(
|
||||
*,
|
||||
goal_query: str,
|
||||
|
|
@ -97,6 +164,10 @@ def build_progression_path_gap_planning_context(
|
|||
major_step_count: Optional[int] = None,
|
||||
roadmap_phase: Optional[str] = None,
|
||||
roadmap_learning_goal: Optional[str] = None,
|
||||
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||
resolved_structured: Optional[Mapping[str, Any]] = None,
|
||||
stage_spec: Optional[Mapping[str, Any]] = None,
|
||||
semantic_brief: Optional[Mapping[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
|
||||
offer = offer or {}
|
||||
|
|
@ -127,10 +198,18 @@ def build_progression_path_gap_planning_context(
|
|||
"path_step_count": path_step_count,
|
||||
"major_step_count": major_step_count,
|
||||
}
|
||||
snap = build_progression_gap_snapshot(
|
||||
goal_analysis=goal_analysis,
|
||||
resolved_structured=resolved_structured,
|
||||
stage_spec=stage_spec,
|
||||
semantic_brief=semantic_brief,
|
||||
)
|
||||
ctx.update(snap)
|
||||
return sanitize_planning_context_for_ai(ctx)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"build_progression_gap_snapshot",
|
||||
"build_progression_path_gap_planning_context",
|
||||
"compact_planning_context_json",
|
||||
"planning_context_prompt_variables",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from ai_prompt_job import run_exercise_form_ai_suggestion
|
|||
from exercise_ai import strip_html_to_plain
|
||||
|
||||
from planning_exercise_path_qa import find_step_pair_index
|
||||
from planning_exercise_semantics import PlanningSemanticBrief
|
||||
from planning_exercise_form_context import build_progression_gap_snapshot
|
||||
from planning_exercise_semantics import PlanningSemanticBrief, brief_to_summary_dict
|
||||
|
||||
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
|
||||
|
||||
|
|
@ -265,27 +266,62 @@ def build_gap_fill_goal_text(
|
|||
spec: Mapping[str, Any],
|
||||
step_a: Optional[Mapping[str, Any]] = None,
|
||||
step_b: Optional[Mapping[str, Any]] = None,
|
||||
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
||||
) -> str:
|
||||
"""Ausführlicher Zieltext für KI-Neuanlage aus dem Pfad-Kontext."""
|
||||
"""Ausführlicher Zieltext für KI-Neuanlage aus Pfad-, Roadmap- und Stufen-Kontext."""
|
||||
topic = (brief.primary_topic or "Technik").strip()
|
||||
phase = spec.get("phase") or "vertiefung"
|
||||
from_title = (step_a or {}).get("title") or spec.get("from_title") or "vorherigem Schritt"
|
||||
to_title = (step_b or {}).get("title") or spec.get("to_title") or "nächstem Schritt"
|
||||
arc = ", ".join(brief.development_arc or []) or "einstieg → grundlage → vertiefung → anwendung → perfektion"
|
||||
snap = dict(roadmap_snapshot or {})
|
||||
if not snap:
|
||||
snap = build_progression_gap_snapshot(semantic_brief=brief_to_summary_dict(brief))
|
||||
|
||||
parts = [
|
||||
f"Planungsziel (gesamter Pfad): {goal_query}",
|
||||
f"Hauptthema: {topic}",
|
||||
f"Entwicklungsphase dieser Übung: {phase}",
|
||||
f"Erwarteter Entwicklungsbogen: {arc}",
|
||||
f"Einordnung: didaktische Zwischenstufe zwischen „{from_title}“ und „{to_title}“.",
|
||||
f"Hauptthema: {snap.get('primary_topic') or topic}",
|
||||
]
|
||||
if snap.get("start_situation"):
|
||||
parts.append(f"Voraussetzung / Ausgangslage (Progression): {snap['start_situation']}")
|
||||
if snap.get("target_state"):
|
||||
parts.append(f"Gesamtziel der Progression: {snap['target_state']}")
|
||||
if snap.get("roadmap_notes"):
|
||||
parts.append(f"Ergänzender Kontext: {snap['roadmap_notes']}")
|
||||
stage_goal = snap.get("stage_learning_goal") or spec.get("title_hint")
|
||||
if stage_goal:
|
||||
parts.append(f"Lernziel dieser Roadmap-Stufe: {stage_goal}")
|
||||
parts.extend(
|
||||
[
|
||||
f"Entwicklungsphase dieser Übung: {snap.get('stage_phase') or phase}",
|
||||
f"Erwarteter Entwicklungsbogen: {arc}",
|
||||
f"Einordnung: didaktische Zwischenstufe zwischen „{from_title}“ und „{to_title}“.",
|
||||
]
|
||||
)
|
||||
if snap.get("stage_load_profile"):
|
||||
parts.append(f"Belastungsschwerpunkte: {', '.join(snap['stage_load_profile'])}")
|
||||
if snap.get("stage_success_criteria"):
|
||||
parts.append(
|
||||
"Erfolgskriterien dieser Stufe: "
|
||||
+ "; ".join(str(x) for x in snap["stage_success_criteria"][:4])
|
||||
)
|
||||
if snap.get("stage_anti_patterns"):
|
||||
parts.append(
|
||||
"Vermeiden: " + "; ".join(str(x) for x in snap["stage_anti_patterns"][:3])
|
||||
)
|
||||
if snap.get("skill_hints"):
|
||||
parts.append(
|
||||
"Fähigkeiten-/Fokus-Hinweise: "
|
||||
+ "; ".join(str(x) for x in snap["skill_hints"][:4])
|
||||
)
|
||||
if spec.get("rationale"):
|
||||
parts.append(f"Qualitätsprüfung: {spec['rationale']}")
|
||||
if spec.get("sketch"):
|
||||
parts.append(f"Skizze: {spec['sketch']}")
|
||||
parts.append(
|
||||
"Die Übung muss einen klaren, trainierbaren Bezug zum Hauptthema haben — "
|
||||
"keine generische Kraftübung ohne Technikbezug. Konkrete Durchführung, Ziel und Trainerhinweise ausformulieren."
|
||||
"Die Übung muss die Stufe didaktisch erfüllen: klare Voraussetzungen, messbares Stufenziel, "
|
||||
"Bezug zum Gesamtpfad — keine generische Kraftübung ohne Technikbezug. "
|
||||
"Konkrete Durchführung, Ziel und Trainerhinweise ausformulieren."
|
||||
)
|
||||
return "\n\n".join(parts)[:8000]
|
||||
|
||||
|
|
@ -297,6 +333,7 @@ def build_gap_fill_offer(
|
|||
goal_query: str = "",
|
||||
brief: Optional[PlanningSemanticBrief] = None,
|
||||
proposal: Optional[Mapping[str, Any]] = None,
|
||||
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
idx = int(spec.get("insert_after_index") or 0)
|
||||
offer_id = f"{spec.get('source')}-{idx}-{uuid.uuid4().hex[:8]}"
|
||||
|
|
@ -310,6 +347,7 @@ def build_gap_fill_offer(
|
|||
spec=spec,
|
||||
step_a=step_a,
|
||||
step_b=step_b,
|
||||
roadmap_snapshot=roadmap_snapshot,
|
||||
)
|
||||
offer: Dict[str, Any] = {
|
||||
"offer_id": offer_id,
|
||||
|
|
@ -345,6 +383,7 @@ def apply_gap_fill_after_qa(
|
|||
include_ai_calls: bool = True,
|
||||
max_ai_proposals: int = 3,
|
||||
auto_insert_proposals: bool = False,
|
||||
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
|
||||
) -> 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.
|
||||
|
|
@ -370,6 +409,7 @@ def apply_gap_fill_after_qa(
|
|||
goal_query=goal_query,
|
||||
brief=brief,
|
||||
proposal=None,
|
||||
roadmap_snapshot=roadmap_snapshot,
|
||||
)
|
||||
offers.append(offer)
|
||||
continue
|
||||
|
|
@ -397,6 +437,7 @@ def apply_gap_fill_after_qa(
|
|||
goal_query=goal_query,
|
||||
brief=brief,
|
||||
proposal=proposal,
|
||||
roadmap_snapshot=roadmap_snapshot,
|
||||
)
|
||||
offers.append(offer)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ planning_progression_roadmap.py und PLANNING_PROGRESSION_ROADMAP_SPEC.md.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Tuple
|
||||
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
|
@ -50,6 +50,7 @@ from planning_exercise_suggest import (
|
|||
_normalize_query,
|
||||
resolve_planning_exercise_intent,
|
||||
)
|
||||
from planning_exercise_form_context import build_progression_gap_snapshot
|
||||
from planning_progression_roadmap import (
|
||||
MajorStep,
|
||||
ProgressionRoadmapContext,
|
||||
|
|
@ -61,6 +62,7 @@ from planning_progression_roadmap import (
|
|||
resolve_step_exercise_kind_filter,
|
||||
roadmap_context_from_override,
|
||||
run_progression_roadmap_pipeline,
|
||||
run_start_target_resolve_only,
|
||||
stage_spec_retrieval_query,
|
||||
)
|
||||
from routers.training_planning import _has_planning_role
|
||||
|
|
@ -79,6 +81,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
include_llm_start_target: bool = True
|
||||
roadmap_first: bool = False
|
||||
roadmap_only: bool = False
|
||||
start_target_only: bool = False
|
||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
||||
target_state: Optional[str] = Field(default=None, max_length=2000)
|
||||
|
|
@ -87,6 +90,44 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
exercise_kind_any: Optional[List[str]] = None
|
||||
|
||||
|
||||
def _roadmap_gap_snapshot_for_spec(
|
||||
roadmap_ctx: Optional[ProgressionRoadmapContext],
|
||||
spec: Mapping[str, Any],
|
||||
*,
|
||||
semantic_brief: PlanningSemanticBrief,
|
||||
) -> Dict[str, Any]:
|
||||
"""Roadmap-Kontext für KI-Lücken-Übung (Start, Ziel, Stufenspec)."""
|
||||
major_idx = spec.get("roadmap_major_step_index")
|
||||
stage_spec_dict: Optional[Dict[str, Any]] = None
|
||||
if roadmap_ctx and major_idx is not None:
|
||||
for s in roadmap_ctx.stage_specs or []:
|
||||
if int(s.major_step_index) == int(major_idx):
|
||||
stage_spec_dict = s.model_dump()
|
||||
if roadmap_ctx.roadmap:
|
||||
for m in roadmap_ctx.roadmap.major_steps:
|
||||
if m.index == int(major_idx):
|
||||
stage_spec_dict["phase"] = m.phase
|
||||
break
|
||||
break
|
||||
ga = roadmap_ctx.goal_analysis.model_dump() if roadmap_ctx and roadmap_ctx.goal_analysis else None
|
||||
rs = (
|
||||
roadmap_ctx.resolved_structured.model_dump()
|
||||
if roadmap_ctx and roadmap_ctx.resolved_structured
|
||||
else None
|
||||
)
|
||||
brief_summary = (
|
||||
roadmap_ctx.semantic_brief
|
||||
if roadmap_ctx and roadmap_ctx.semantic_brief
|
||||
else brief_to_summary_dict(semantic_brief)
|
||||
)
|
||||
return build_progression_gap_snapshot(
|
||||
goal_analysis=ga,
|
||||
resolved_structured=rs,
|
||||
stage_spec=stage_spec_dict,
|
||||
semantic_brief=brief_summary,
|
||||
)
|
||||
|
||||
|
||||
def _roadmap_structured_from_body(body: ProgressionPathSuggestRequest) -> Optional[RoadmapStructuredInput]:
|
||||
start = (body.start_situation or "").strip() or None
|
||||
target = (body.target_state or "").strip() or None
|
||||
|
|
@ -516,7 +557,10 @@ def suggest_progression_path(
|
|||
|
||||
roadmap_first = bool(body.roadmap_first)
|
||||
roadmap_only = bool(body.roadmap_only)
|
||||
include_roadmap = roadmap_first or body.include_roadmap_preview or roadmap_only
|
||||
start_target_only = bool(body.start_target_only)
|
||||
include_roadmap = (
|
||||
roadmap_first or body.include_roadmap_preview or roadmap_only or start_target_only
|
||||
)
|
||||
progression_roadmap: Optional[Dict[str, Any]] = None
|
||||
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
||||
roadmap_edited = False
|
||||
|
|
@ -538,6 +582,15 @@ def suggest_progression_path(
|
|||
roadmap_edited = True
|
||||
max_steps = int(roadmap_ctx.max_steps)
|
||||
roadmap_first = True
|
||||
elif start_target_only:
|
||||
roadmap_ctx = run_start_target_resolve_only(
|
||||
goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
cur=cur,
|
||||
include_llm_start_target=body.include_llm_start_target,
|
||||
structured=roadmap_structured,
|
||||
)
|
||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
||||
elif include_roadmap:
|
||||
roadmap_ctx = run_progression_roadmap_pipeline(
|
||||
goal_query,
|
||||
|
|
@ -550,6 +603,28 @@ def suggest_progression_path(
|
|||
)
|
||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
||||
|
||||
if start_target_only:
|
||||
return {
|
||||
"goal_query": goal_query,
|
||||
"max_steps_requested": max_steps,
|
||||
"steps": [],
|
||||
"step_count": 0,
|
||||
"target_profile_summary": None,
|
||||
"semantic_brief_summary": brief_to_summary_dict(semantic_brief),
|
||||
"semantic_llm_applied": semantic_llm_applied,
|
||||
"query_intent_summary": {},
|
||||
"progression_graph_id": body.progression_graph_id,
|
||||
"path_qa": None,
|
||||
"gap_fill_offers": [],
|
||||
"progression_roadmap": progression_roadmap,
|
||||
"roadmap_first": False,
|
||||
"roadmap_only": False,
|
||||
"start_target_only": True,
|
||||
"roadmap_edited": False,
|
||||
"roadmap_unfilled_count": 0,
|
||||
"retrieval_phase": "start_target_only",
|
||||
}
|
||||
|
||||
if roadmap_only:
|
||||
return {
|
||||
"goal_query": goal_query,
|
||||
|
|
@ -615,6 +690,8 @@ def suggest_progression_path(
|
|||
steps=steps,
|
||||
brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
goal_analysis=roadmap_ctx.goal_analysis if roadmap_ctx else None,
|
||||
resolved_structured=roadmap_ctx.resolved_structured if roadmap_ctx else None,
|
||||
)
|
||||
for spec in roadmap_gap_specs:
|
||||
roadmap_gap_offers.append(
|
||||
|
|
@ -624,6 +701,9 @@ def suggest_progression_path(
|
|||
goal_query=goal_query,
|
||||
brief=semantic_brief,
|
||||
proposal=None,
|
||||
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
||||
roadmap_ctx, spec, semantic_brief=semantic_brief
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
|
@ -760,6 +840,21 @@ def suggest_progression_path(
|
|||
brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
)
|
||||
path_roadmap_snapshot = None
|
||||
if roadmap_ctx:
|
||||
path_roadmap_snapshot = build_progression_gap_snapshot(
|
||||
goal_analysis=(
|
||||
roadmap_ctx.goal_analysis.model_dump()
|
||||
if roadmap_ctx.goal_analysis
|
||||
else None
|
||||
),
|
||||
resolved_structured=(
|
||||
roadmap_ctx.resolved_structured.model_dump()
|
||||
if roadmap_ctx.resolved_structured
|
||||
else None
|
||||
),
|
||||
semantic_brief=roadmap_ctx.semantic_brief or brief_to_summary_dict(semantic_brief),
|
||||
)
|
||||
steps, ai_proposals, gap_fill_offers = apply_gap_fill_after_qa(
|
||||
cur,
|
||||
steps,
|
||||
|
|
@ -769,6 +864,7 @@ def suggest_progression_path(
|
|||
include_ai_calls=False,
|
||||
max_ai_proposals=0,
|
||||
auto_insert_proposals=False,
|
||||
roadmap_snapshot=path_roadmap_snapshot,
|
||||
)
|
||||
|
||||
if roadmap_gap_offers:
|
||||
|
|
|
|||
|
|
@ -794,6 +794,8 @@ def build_roadmap_unfilled_gap_specs(
|
|||
steps: Sequence[Mapping[str, Any]],
|
||||
brief: PlanningSemanticBrief,
|
||||
goal_query: str,
|
||||
goal_analysis: Optional[GoalAnalysisArtifact] = None,
|
||||
resolved_structured: Optional[RoadmapStructuredInput] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Gap-Fill-Angebote für Roadmap-Stufen ohne Bibliothekstreffer."""
|
||||
topic = (brief.primary_topic or "Technik").strip()
|
||||
|
|
@ -807,8 +809,18 @@ def build_roadmap_unfilled_gap_specs(
|
|||
f"Planungsziel: {goal_query}",
|
||||
f"Roadmap-Stufe {stage_spec.major_step_index + 1} ({phase}): {stage_spec.learning_goal}",
|
||||
]
|
||||
if resolved_structured and (resolved_structured.start_situation or "").strip():
|
||||
sketch_parts.append(f"Ausgangslage (Pfad): {resolved_structured.start_situation.strip()}")
|
||||
elif goal_analysis and (goal_analysis.start_assumption or "").strip():
|
||||
sketch_parts.append(f"Ausgangslage (Pfad): {goal_analysis.start_assumption.strip()}")
|
||||
if resolved_structured and (resolved_structured.target_state or "").strip():
|
||||
sketch_parts.append(f"Gesamtziel (Pfad): {resolved_structured.target_state.strip()}")
|
||||
elif goal_analysis and (goal_analysis.target_state or "").strip():
|
||||
sketch_parts.append(f"Gesamtziel (Pfad): {goal_analysis.target_state.strip()}")
|
||||
if stage_spec.success_criteria:
|
||||
sketch_parts.append(f"Erfolgskriterien: {', '.join(stage_spec.success_criteria[:3])}")
|
||||
if stage_spec.load_profile:
|
||||
sketch_parts.append(f"Belastung: {', '.join(stage_spec.load_profile[:4])}")
|
||||
specs.append(
|
||||
{
|
||||
"source": "roadmap_unfilled",
|
||||
|
|
@ -960,6 +972,48 @@ def _merge_structured_into_goal_analysis(
|
|||
)
|
||||
|
||||
|
||||
def run_start_target_resolve_only(
|
||||
goal_query: str,
|
||||
*,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
cur=None,
|
||||
include_llm_start_target: bool = True,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
) -> ProgressionRoadmapContext:
|
||||
"""Nur Start/Ziel/Ergänzungen auflösen — ohne Roadmap-Stufen (Review vor Major Steps)."""
|
||||
brief = semantic_brief or build_semantic_brief(goal_query)
|
||||
resolved, resolve_meta, llm_extract = resolve_roadmap_structured_input(
|
||||
goal_query,
|
||||
structured,
|
||||
brief=brief,
|
||||
cur=cur,
|
||||
include_llm=include_llm_start_target,
|
||||
)
|
||||
topic_override = None
|
||||
if llm_extract and (llm_extract.primary_topic or "").strip():
|
||||
topic_override = llm_extract.primary_topic.strip()
|
||||
goal_analysis = build_goal_analysis(
|
||||
goal_query,
|
||||
brief,
|
||||
structured=resolved,
|
||||
topic_override=topic_override,
|
||||
)
|
||||
ctx = ProgressionRoadmapContext(
|
||||
goal_query=goal_query.strip(),
|
||||
max_steps=2,
|
||||
semantic_brief=brief_to_summary_dict(brief),
|
||||
resolved_structured=resolved,
|
||||
start_target_extract=llm_extract,
|
||||
start_target_resolve=resolve_meta,
|
||||
goal_analysis=goal_analysis,
|
||||
llm_start_target_applied=resolve_meta.llm_start_target_applied,
|
||||
pipeline_phase="start_target_only",
|
||||
)
|
||||
if resolve_meta.llm_start_target_applied:
|
||||
ctx.prompt_slugs.append(PROMPT_SLUG_START_TARGET)
|
||||
return ctx
|
||||
|
||||
|
||||
def run_progression_roadmap_pipeline(
|
||||
goal_query: str,
|
||||
*,
|
||||
|
|
@ -1135,6 +1189,7 @@ __all__ = [
|
|||
"consolidate_micro_to_major",
|
||||
"develop_micro_objectives",
|
||||
"progression_roadmap_to_api_dict",
|
||||
"run_start_target_resolve_only",
|
||||
"run_progression_roadmap_pipeline",
|
||||
"try_llm_start_target_extract",
|
||||
"try_llm_goal_analysis",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ def post_progression_path_suggest(
|
|||
or body.include_ai_gap_fill
|
||||
or body.include_llm_roadmap
|
||||
or body.include_llm_start_target
|
||||
or (body.start_target_only and body.include_llm_start_target)
|
||||
)
|
||||
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
||||
if uses_ai:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests Planungs-KI Phase D — planning_context für suggestExerciseAi."""
|
||||
from planning_exercise_form_context import (
|
||||
build_progression_gap_snapshot,
|
||||
build_progression_path_gap_planning_context,
|
||||
planning_context_prompt_variables,
|
||||
sanitize_planning_context_for_ai,
|
||||
|
|
@ -44,3 +45,36 @@ def test_build_progression_path_gap_context():
|
|||
def test_sanitize_truncates_long_strings():
|
||||
ctx = sanitize_planning_context_for_ai({"goal_query": "x" * 900})
|
||||
assert len(ctx["goal_query"]) <= 800
|
||||
|
||||
|
||||
def test_build_progression_gap_snapshot_includes_start_target_and_stage():
|
||||
snap = build_progression_gap_snapshot(
|
||||
goal_analysis={
|
||||
"primary_topic": "Kumite Beinarbeit",
|
||||
"start_assumption": "gleichförmige Steppbewegung",
|
||||
"target_state": "explosiver Angriff mit Ausweichen",
|
||||
"success_criteria": ["nachvollziehbarer Übergang"],
|
||||
},
|
||||
resolved_structured={"roadmap_notes": "Kindergruppe"},
|
||||
stage_spec={
|
||||
"learning_goal": "variable Rhythmen",
|
||||
"load_profile": ["timing", "distanz"],
|
||||
"success_criteria": ["Reaktion unter Druck"],
|
||||
"anti_patterns": ["statisches Stehen"],
|
||||
},
|
||||
semantic_brief={"must_phrases": ["Beinarbeit"], "development_arc": ["grundlage", "anwendung"]},
|
||||
)
|
||||
assert snap["start_situation"] == "gleichförmige Steppbewegung"
|
||||
assert snap["stage_learning_goal"] == "variable Rhythmen"
|
||||
assert "timing" in snap["stage_load_profile"]
|
||||
assert snap["roadmap_notes"] == "Kindergruppe"
|
||||
|
||||
|
||||
def test_gap_planning_context_carries_snapshot_fields():
|
||||
ctx = build_progression_path_gap_planning_context(
|
||||
goal_query="Kumite Beinarbeit",
|
||||
goal_analysis={"start_assumption": "Start", "target_state": "Ziel"},
|
||||
stage_spec={"learning_goal": "Stufenziel", "load_profile": ["koordination"]},
|
||||
)
|
||||
assert ctx["start_situation"] == "Start"
|
||||
assert ctx["stage_learning_goal"] == "Stufenziel"
|
||||
|
|
|
|||
|
|
@ -91,3 +91,25 @@ def test_build_gap_fill_goal_text_includes_topic():
|
|||
assert "Mae Geri" in text or "mae geri" in text.lower()
|
||||
assert "anwendung" in text
|
||||
assert "Kihon" in text
|
||||
|
||||
|
||||
def test_build_gap_fill_goal_text_includes_roadmap_snapshot():
|
||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||
text = build_gap_fill_goal_text(
|
||||
goal_query="Kumite Beinarbeit",
|
||||
brief=brief,
|
||||
spec={"phase": "vertiefung", "title_hint": "variable Rhythmen"},
|
||||
step_a={"title": "Schritt A"},
|
||||
step_b={"title": "Schritt B"},
|
||||
roadmap_snapshot={
|
||||
"start_situation": "gleichförmige Steppbewegung",
|
||||
"target_state": "explosiver Angriff",
|
||||
"stage_learning_goal": "variable Rhythmen und multidirektionale Kontrolle",
|
||||
"stage_load_profile": ["timing", "distanz"],
|
||||
"skill_hints": ["Beinarbeit"],
|
||||
},
|
||||
)
|
||||
assert "gleichförmige Steppbewegung" in text
|
||||
assert "explosiver Angriff" in text
|
||||
assert "variable Rhythmen" in text
|
||||
assert "timing" in text
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from planning_progression_roadmap import (
|
|||
resolve_roadmap_structured_input,
|
||||
resolve_step_exercise_kind_filter,
|
||||
run_progression_roadmap_pipeline,
|
||||
run_start_target_resolve_only,
|
||||
stage_spec_exercise_kind_filter,
|
||||
stage_spec_retrieval_query,
|
||||
normalize_major_steps_for_override,
|
||||
|
|
@ -173,6 +174,15 @@ def test_resolve_structured_regex_fallback_without_llm():
|
|||
assert "dynamischen" in (resolved.target_state or "")
|
||||
|
||||
|
||||
def test_run_start_target_resolve_only_no_major_steps():
|
||||
ctx = run_start_target_resolve_only(KUMITE_GOAL, include_llm_start_target=False)
|
||||
assert ctx.pipeline_phase == "start_target_only"
|
||||
assert ctx.roadmap is None
|
||||
assert ctx.goal_analysis is not None
|
||||
assert "Steppbewegung" in ctx.goal_analysis.start_assumption
|
||||
assert ctx.resolved_structured is not None
|
||||
|
||||
|
||||
def test_resolve_structured_merges_user_and_llm_notes():
|
||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||
structured = RoadmapStructuredInput(roadmap_notes="Kindergruppe 10–12")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.211"
|
||||
APP_VERSION = "0.8.212"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260607087"
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
|||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||
"planning_exercise_suggest": "0.21.0", # LLM Start/Ziel-Extraktion (planning_progression_start_target)
|
||||
"planning_exercise_suggest": "0.21.1", # start_target_only + reicher gap-fill planning_context
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -201,8 +201,10 @@ export default function ExerciseProgressionPathBuilder({
|
|||
const [editableMajorSteps, setEditableMajorSteps] = useState([])
|
||||
const [roadmapDirty, setRoadmapDirty] = useState(false)
|
||||
const [loadingRoadmap, setLoadingRoadmap] = useState(false)
|
||||
const [loadingStartTarget, setLoadingStartTarget] = useState(false)
|
||||
const [loadingMatch, setLoadingMatch] = useState(false)
|
||||
const loading = loadingRoadmap || loadingMatch
|
||||
const [startTargetAnalyzed, setStartTargetAnalyzed] = useState(false)
|
||||
const loading = loadingRoadmap || loadingStartTarget || loadingMatch
|
||||
const [focusAreas, setFocusAreas] = useState([])
|
||||
const [skillsCatalog, setSkillsCatalog] = useState([])
|
||||
const [generatingOfferId, setGeneratingOfferId] = useState(null)
|
||||
|
|
@ -397,6 +399,9 @@ export default function ExerciseProgressionPathBuilder({
|
|||
pathSteps,
|
||||
editableMajorSteps,
|
||||
progressionRoadmap,
|
||||
startSituation,
|
||||
targetState,
|
||||
roadmapNotes,
|
||||
})
|
||||
try {
|
||||
const aiRes = await api.suggestExerciseAi({
|
||||
|
|
@ -531,6 +536,60 @@ export default function ExerciseProgressionPathBuilder({
|
|||
if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400))
|
||||
}
|
||||
|
||||
const applyStartTargetResponse = (res) => {
|
||||
const roadmap = res?.progression_roadmap || null
|
||||
setProgressionRoadmap((prev) => ({
|
||||
...(prev || {}),
|
||||
...roadmap,
|
||||
roadmap: prev?.roadmap || roadmap?.roadmap || null,
|
||||
stage_specs: prev?.stage_specs || roadmap?.stage_specs || [],
|
||||
}))
|
||||
applyResolvedStructuredFromRoadmap(roadmap, {
|
||||
setStartSituation,
|
||||
setTargetState,
|
||||
setRoadmapNotes,
|
||||
})
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setStartTargetAnalyzed(true)
|
||||
}
|
||||
|
||||
const analyzeStartTarget = async () => {
|
||||
const q = (goalQuery || '').trim()
|
||||
if (q.length < 3) {
|
||||
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
||||
return
|
||||
}
|
||||
if (!graphId) {
|
||||
alert('Zuerst einen Graphen wählen.')
|
||||
return
|
||||
}
|
||||
setLoadingStartTarget(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await api.suggestProgressionPath({
|
||||
query: q,
|
||||
max_steps: Number(maxSteps),
|
||||
include_llm_intent: false,
|
||||
include_path_qa: false,
|
||||
include_llm_path_qa: false,
|
||||
include_path_reorder: false,
|
||||
include_ai_gap_fill: false,
|
||||
include_roadmap_preview: false,
|
||||
include_llm_roadmap: false,
|
||||
include_llm_start_target: true,
|
||||
start_target_only: true,
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
||||
})
|
||||
applyStartTargetResponse(res)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError(e.message || 'Start/Ziel-Analyse fehlgeschlagen')
|
||||
} finally {
|
||||
setLoadingStartTarget(false)
|
||||
}
|
||||
}
|
||||
|
||||
const suggestRoadmap = async () => {
|
||||
const q = (goalQuery || '').trim()
|
||||
if (q.length < 3) {
|
||||
|
|
@ -541,6 +600,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
alert('Zuerst einen Graphen wählen.')
|
||||
return
|
||||
}
|
||||
const fieldsEmpty = !startSituation.trim() && !targetState.trim()
|
||||
setLoadingRoadmap(true)
|
||||
setError('')
|
||||
try {
|
||||
|
|
@ -554,7 +614,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
include_ai_gap_fill: false,
|
||||
include_roadmap_preview: true,
|
||||
include_llm_roadmap: true,
|
||||
include_llm_start_target: true,
|
||||
include_llm_start_target: fieldsEmpty,
|
||||
roadmap_only: true,
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
||||
|
|
@ -567,11 +627,14 @@ export default function ExerciseProgressionPathBuilder({
|
|||
setMaxSteps(majors.length)
|
||||
const roadmap = res?.progression_roadmap || null
|
||||
setProgressionRoadmap(roadmap)
|
||||
applyResolvedStructuredFromRoadmap(roadmap, {
|
||||
setStartSituation,
|
||||
setTargetState,
|
||||
setRoadmapNotes,
|
||||
})
|
||||
if (fieldsEmpty) {
|
||||
applyResolvedStructuredFromRoadmap(roadmap, {
|
||||
setStartSituation,
|
||||
setTargetState,
|
||||
setRoadmapNotes,
|
||||
})
|
||||
setStartTargetAnalyzed(true)
|
||||
}
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setPathSteps([])
|
||||
setTargetSummary(null)
|
||||
|
|
@ -767,18 +830,37 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
||||
Leer gelassen: Start/Ziel werden per KI aus dem Zieltext verstanden und formuliert (Fallback: Muster
|
||||
„von … bis …“). Manuelle Eingaben haben Vorrang.
|
||||
Optional zuerst „Start/Ziel analysieren“, anpassen, dann Roadmap-Stufen. Sind Start und Ziel leer,
|
||||
geschieht die Analyse beim Roadmap-Vorschlag automatisch mit. Manuelle Eingaben haben immer Vorrang.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={analyzeStartTarget}
|
||||
title="Nur Ausgangslage, Zielzustand und Ergänzungen per KI — ohne Roadmap-Stufen"
|
||||
>
|
||||
{loadingStartTarget ? 'Analyse …' : 'Start/Ziel analysieren'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={suggestRoadmap}
|
||||
title={
|
||||
startSituation.trim() && targetState.trim()
|
||||
? 'Roadmap-Stufen aus den gesetzten Start/Ziel-Feldern'
|
||||
: 'Start/Ziel-Analyse und Roadmap-Stufen in einem Schritt'
|
||||
}
|
||||
>
|
||||
{loadingRoadmap ? 'Roadmap …' : 'Roadmap vorschlagen'}
|
||||
</button>
|
||||
{startTargetAnalyzed && !editableMajorSteps.length ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
Start/Ziel bereit — Roadmap als Nächstes
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
|
|
@ -808,7 +890,8 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</p>
|
||||
) : null}
|
||||
|
||||
{progressionRoadmap?.goal_analysis ? (
|
||||
{(progressionRoadmap?.goal_analysis ||
|
||||
progressionRoadmap?.pipeline_phase === 'start_target_only') ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,18 @@ export function buildPickerPlanningContextForAi({
|
|||
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== ''))
|
||||
}
|
||||
|
||||
function stageSpecForMajorIndex(progressionRoadmap, majorIdx) {
|
||||
if (majorIdx == null || !progressionRoadmap) return null
|
||||
const specs = progressionRoadmap?.stage_specs
|
||||
if (!Array.isArray(specs)) return null
|
||||
const hit = specs.find((s) => Number(s.major_step_index) === Number(majorIdx))
|
||||
if (!hit) return null
|
||||
const majors = progressionRoadmap?.roadmap?.major_steps
|
||||
const major =
|
||||
Array.isArray(majors) && majors.find((m) => Number(m.index) === Number(majorIdx))
|
||||
return major ? { ...hit, phase: hit.phase || major.phase } : hit
|
||||
}
|
||||
|
||||
export function buildPathGapPlanningContextForAi({
|
||||
goalQuery = '',
|
||||
semanticBrief = null,
|
||||
|
|
@ -38,6 +50,9 @@ export function buildPathGapPlanningContextForAi({
|
|||
pathSteps = [],
|
||||
editableMajorSteps = [],
|
||||
progressionRoadmap = null,
|
||||
startSituation = '',
|
||||
targetState = '',
|
||||
roadmapNotes = '',
|
||||
} = {}) {
|
||||
const afterIdx = Number(offer?.insert_after_index)
|
||||
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
|
||||
|
|
@ -49,19 +64,63 @@ export function buildPathGapPlanningContextForAi({
|
|||
majorIdxRaw != null && Number.isFinite(Number(majorIdxRaw)) ? Number(majorIdxRaw) : null
|
||||
const majorStep =
|
||||
majorIdx != null && editableMajorSteps[majorIdx] ? editableMajorSteps[majorIdx] : null
|
||||
const stageSpec = stageSpecForMajorIndex(progressionRoadmap, majorIdx)
|
||||
const ga = progressionRoadmap?.goal_analysis || null
|
||||
const rs = progressionRoadmap?.resolved_structured || null
|
||||
|
||||
const start =
|
||||
(startSituation || '').trim() ||
|
||||
rs?.start_situation ||
|
||||
ga?.start_assumption ||
|
||||
null
|
||||
const target =
|
||||
(targetState || '').trim() || rs?.target_state || ga?.target_state || null
|
||||
const notes = (roadmapNotes || '').trim() || rs?.roadmap_notes || null
|
||||
|
||||
const skillHints = []
|
||||
if (Array.isArray(semanticBrief?.must_phrases)) {
|
||||
semanticBrief.must_phrases.slice(0, 4).forEach((p) => {
|
||||
const s = String(p || '').trim()
|
||||
if (s) skillHints.push(s)
|
||||
})
|
||||
}
|
||||
if (Array.isArray(semanticBrief?.development_arc) && semanticBrief.development_arc.length) {
|
||||
skillHints.push(
|
||||
`Entwicklungsbogen: ${semanticBrief.development_arc.slice(0, 5).join(' → ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
source: 'progression_path_gap_fill',
|
||||
goal_query: (goalQuery || '').trim() || null,
|
||||
primary_topic: semanticBrief?.primary_topic || null,
|
||||
primary_topic: ga?.primary_topic || semanticBrief?.primary_topic || null,
|
||||
progression_graph_id: graphId != null ? Number(graphId) : null,
|
||||
gap_source: offer?.source || null,
|
||||
gap_phase: offer?.phase || offer?.gap?.expected_phase || null,
|
||||
roadmap_major_step_index: majorIdx,
|
||||
roadmap_phase: majorStep?.phase || offer?.phase || null,
|
||||
roadmap_phase: majorStep?.phase || stageSpec?.phase || offer?.phase || null,
|
||||
roadmap_learning_goal:
|
||||
(majorStep?.learning_goal || offer?.title_hint || offer?.gap?.learning_goal || '').trim() ||
|
||||
null,
|
||||
start_situation: start,
|
||||
target_state: target,
|
||||
roadmap_notes: notes,
|
||||
stage_learning_goal: stageSpec?.learning_goal || null,
|
||||
stage_phase: stageSpec?.phase || majorStep?.phase || null,
|
||||
stage_exercise_type: stageSpec?.exercise_type || null,
|
||||
stage_load_profile: Array.isArray(stageSpec?.load_profile)
|
||||
? stageSpec.load_profile.slice(0, 6)
|
||||
: null,
|
||||
stage_success_criteria: Array.isArray(stageSpec?.success_criteria)
|
||||
? stageSpec.success_criteria.slice(0, 4)
|
||||
: null,
|
||||
stage_anti_patterns: Array.isArray(stageSpec?.anti_patterns)
|
||||
? stageSpec.anti_patterns.slice(0, 3)
|
||||
: null,
|
||||
path_success_criteria: Array.isArray(ga?.success_criteria)
|
||||
? ga.success_criteria.slice(0, 4)
|
||||
: null,
|
||||
skill_hints: skillHints.length ? skillHints : null,
|
||||
neighbor_before_title: stepA?.exerciseTitle || offer?.from_title || null,
|
||||
neighbor_after_title: stepB?.exerciseTitle || offer?.to_title || null,
|
||||
path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user