Progressionsgraph verbessert #54
|
|
@ -6,7 +6,7 @@ Wird als ``planning_context_json`` in Übungs-Prompts (summary, skills, instruct
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, Mapping, Optional
|
from typing import Any, Dict, List, Mapping, Optional
|
||||||
|
|
||||||
_MAX_JSON_CHARS = 6000
|
_MAX_JSON_CHARS = 6000
|
||||||
_MAX_STRING = 800
|
_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(
|
def build_progression_path_gap_planning_context(
|
||||||
*,
|
*,
|
||||||
goal_query: str,
|
goal_query: str,
|
||||||
|
|
@ -97,6 +164,10 @@ def build_progression_path_gap_planning_context(
|
||||||
major_step_count: Optional[int] = None,
|
major_step_count: Optional[int] = None,
|
||||||
roadmap_phase: Optional[str] = None,
|
roadmap_phase: Optional[str] = None,
|
||||||
roadmap_learning_goal: 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]:
|
) -> Dict[str, Any]:
|
||||||
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
|
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
|
||||||
offer = offer or {}
|
offer = offer or {}
|
||||||
|
|
@ -127,10 +198,18 @@ def build_progression_path_gap_planning_context(
|
||||||
"path_step_count": path_step_count,
|
"path_step_count": path_step_count,
|
||||||
"major_step_count": major_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)
|
return sanitize_planning_context_for_ai(ctx)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"build_progression_gap_snapshot",
|
||||||
"build_progression_path_gap_planning_context",
|
"build_progression_path_gap_planning_context",
|
||||||
"compact_planning_context_json",
|
"compact_planning_context_json",
|
||||||
"planning_context_prompt_variables",
|
"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 exercise_ai import strip_html_to_plain
|
||||||
|
|
||||||
from planning_exercise_path_qa import find_step_pair_index
|
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")
|
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
|
||||||
|
|
||||||
|
|
@ -265,27 +266,62 @@ def build_gap_fill_goal_text(
|
||||||
spec: Mapping[str, Any],
|
spec: Mapping[str, Any],
|
||||||
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,
|
||||||
) -> str:
|
) -> 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()
|
topic = (brief.primary_topic or "Technik").strip()
|
||||||
phase = spec.get("phase") or "vertiefung"
|
phase = spec.get("phase") or "vertiefung"
|
||||||
from_title = (step_a or {}).get("title") or spec.get("from_title") or "vorherigem Schritt"
|
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"
|
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"
|
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 = [
|
parts = [
|
||||||
f"Planungsziel (gesamter Pfad): {goal_query}",
|
f"Planungsziel (gesamter Pfad): {goal_query}",
|
||||||
f"Hauptthema: {topic}",
|
f"Hauptthema: {snap.get('primary_topic') or topic}",
|
||||||
f"Entwicklungsphase dieser Übung: {phase}",
|
]
|
||||||
|
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"Erwarteter Entwicklungsbogen: {arc}",
|
||||||
f"Einordnung: didaktische Zwischenstufe zwischen „{from_title}“ und „{to_title}“.",
|
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"):
|
if spec.get("rationale"):
|
||||||
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']}")
|
||||||
parts.append(
|
parts.append(
|
||||||
"Die Übung muss einen klaren, trainierbaren Bezug zum Hauptthema haben — "
|
"Die Übung muss die Stufe didaktisch erfüllen: klare Voraussetzungen, messbares Stufenziel, "
|
||||||
"keine generische Kraftübung ohne Technikbezug. Konkrete Durchführung, Ziel und Trainerhinweise ausformulieren."
|
"Bezug zum Gesamtpfad — keine generische Kraftübung ohne Technikbezug. "
|
||||||
|
"Konkrete Durchführung, Ziel und Trainerhinweise ausformulieren."
|
||||||
)
|
)
|
||||||
return "\n\n".join(parts)[:8000]
|
return "\n\n".join(parts)[:8000]
|
||||||
|
|
||||||
|
|
@ -297,6 +333,7 @@ def build_gap_fill_offer(
|
||||||
goal_query: str = "",
|
goal_query: str = "",
|
||||||
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,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
idx = int(spec.get("insert_after_index") or 0)
|
idx = int(spec.get("insert_after_index") or 0)
|
||||||
offer_id = f"{spec.get('source')}-{idx}-{uuid.uuid4().hex[:8]}"
|
offer_id = f"{spec.get('source')}-{idx}-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
@ -310,6 +347,7 @@ def build_gap_fill_offer(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
step_a=step_a,
|
step_a=step_a,
|
||||||
step_b=step_b,
|
step_b=step_b,
|
||||||
|
roadmap_snapshot=roadmap_snapshot,
|
||||||
)
|
)
|
||||||
offer: Dict[str, Any] = {
|
offer: Dict[str, Any] = {
|
||||||
"offer_id": offer_id,
|
"offer_id": offer_id,
|
||||||
|
|
@ -345,6 +383,7 @@ def apply_gap_fill_after_qa(
|
||||||
include_ai_calls: bool = True,
|
include_ai_calls: bool = True,
|
||||||
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,
|
||||||
) -> 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.
|
||||||
|
|
@ -370,6 +409,7 @@ def apply_gap_fill_after_qa(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
proposal=None,
|
proposal=None,
|
||||||
|
roadmap_snapshot=roadmap_snapshot,
|
||||||
)
|
)
|
||||||
offers.append(offer)
|
offers.append(offer)
|
||||||
continue
|
continue
|
||||||
|
|
@ -397,6 +437,7 @@ def apply_gap_fill_after_qa(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
proposal=proposal,
|
proposal=proposal,
|
||||||
|
roadmap_snapshot=roadmap_snapshot,
|
||||||
)
|
)
|
||||||
offers.append(offer)
|
offers.append(offer)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ planning_progression_roadmap.py und PLANNING_PROGRESSION_ROADMAP_SPEC.md.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
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 fastapi import HTTPException
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
@ -50,6 +50,7 @@ from planning_exercise_suggest import (
|
||||||
_normalize_query,
|
_normalize_query,
|
||||||
resolve_planning_exercise_intent,
|
resolve_planning_exercise_intent,
|
||||||
)
|
)
|
||||||
|
from planning_exercise_form_context import build_progression_gap_snapshot
|
||||||
from planning_progression_roadmap import (
|
from planning_progression_roadmap import (
|
||||||
MajorStep,
|
MajorStep,
|
||||||
ProgressionRoadmapContext,
|
ProgressionRoadmapContext,
|
||||||
|
|
@ -61,6 +62,7 @@ from planning_progression_roadmap import (
|
||||||
resolve_step_exercise_kind_filter,
|
resolve_step_exercise_kind_filter,
|
||||||
roadmap_context_from_override,
|
roadmap_context_from_override,
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
|
run_start_target_resolve_only,
|
||||||
stage_spec_retrieval_query,
|
stage_spec_retrieval_query,
|
||||||
)
|
)
|
||||||
from routers.training_planning import _has_planning_role
|
from routers.training_planning import _has_planning_role
|
||||||
|
|
@ -79,6 +81,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
||||||
include_llm_start_target: bool = True
|
include_llm_start_target: bool = True
|
||||||
roadmap_first: bool = False
|
roadmap_first: bool = False
|
||||||
roadmap_only: bool = False
|
roadmap_only: bool = False
|
||||||
|
start_target_only: bool = False
|
||||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||||
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
||||||
target_state: 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
|
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]:
|
def _roadmap_structured_from_body(body: ProgressionPathSuggestRequest) -> Optional[RoadmapStructuredInput]:
|
||||||
start = (body.start_situation or "").strip() or None
|
start = (body.start_situation or "").strip() or None
|
||||||
target = (body.target_state 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_first = bool(body.roadmap_first)
|
||||||
roadmap_only = bool(body.roadmap_only)
|
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
|
progression_roadmap: Optional[Dict[str, Any]] = None
|
||||||
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
||||||
roadmap_edited = False
|
roadmap_edited = False
|
||||||
|
|
@ -538,6 +582,15 @@ def suggest_progression_path(
|
||||||
roadmap_edited = True
|
roadmap_edited = True
|
||||||
max_steps = int(roadmap_ctx.max_steps)
|
max_steps = int(roadmap_ctx.max_steps)
|
||||||
roadmap_first = True
|
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:
|
elif include_roadmap:
|
||||||
roadmap_ctx = run_progression_roadmap_pipeline(
|
roadmap_ctx = run_progression_roadmap_pipeline(
|
||||||
goal_query,
|
goal_query,
|
||||||
|
|
@ -550,6 +603,28 @@ def suggest_progression_path(
|
||||||
)
|
)
|
||||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
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:
|
if roadmap_only:
|
||||||
return {
|
return {
|
||||||
"goal_query": goal_query,
|
"goal_query": goal_query,
|
||||||
|
|
@ -615,6 +690,8 @@ def suggest_progression_path(
|
||||||
steps=steps,
|
steps=steps,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
goal_query=goal_query,
|
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:
|
for spec in roadmap_gap_specs:
|
||||||
roadmap_gap_offers.append(
|
roadmap_gap_offers.append(
|
||||||
|
|
@ -624,6 +701,9 @@ def suggest_progression_path(
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
proposal=None,
|
proposal=None,
|
||||||
|
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
||||||
|
roadmap_ctx, spec, semantic_brief=semantic_brief
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -760,6 +840,21 @@ def suggest_progression_path(
|
||||||
brief=semantic_brief,
|
brief=semantic_brief,
|
||||||
goal_query=goal_query,
|
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(
|
steps, ai_proposals, gap_fill_offers = apply_gap_fill_after_qa(
|
||||||
cur,
|
cur,
|
||||||
steps,
|
steps,
|
||||||
|
|
@ -769,6 +864,7 @@ def suggest_progression_path(
|
||||||
include_ai_calls=False,
|
include_ai_calls=False,
|
||||||
max_ai_proposals=0,
|
max_ai_proposals=0,
|
||||||
auto_insert_proposals=False,
|
auto_insert_proposals=False,
|
||||||
|
roadmap_snapshot=path_roadmap_snapshot,
|
||||||
)
|
)
|
||||||
|
|
||||||
if roadmap_gap_offers:
|
if roadmap_gap_offers:
|
||||||
|
|
|
||||||
|
|
@ -794,6 +794,8 @@ def build_roadmap_unfilled_gap_specs(
|
||||||
steps: Sequence[Mapping[str, Any]],
|
steps: Sequence[Mapping[str, Any]],
|
||||||
brief: PlanningSemanticBrief,
|
brief: PlanningSemanticBrief,
|
||||||
goal_query: str,
|
goal_query: str,
|
||||||
|
goal_analysis: Optional[GoalAnalysisArtifact] = None,
|
||||||
|
resolved_structured: Optional[RoadmapStructuredInput] = None,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Gap-Fill-Angebote für Roadmap-Stufen ohne Bibliothekstreffer."""
|
"""Gap-Fill-Angebote für Roadmap-Stufen ohne Bibliothekstreffer."""
|
||||||
topic = (brief.primary_topic or "Technik").strip()
|
topic = (brief.primary_topic or "Technik").strip()
|
||||||
|
|
@ -807,8 +809,18 @@ def build_roadmap_unfilled_gap_specs(
|
||||||
f"Planungsziel: {goal_query}",
|
f"Planungsziel: {goal_query}",
|
||||||
f"Roadmap-Stufe {stage_spec.major_step_index + 1} ({phase}): {stage_spec.learning_goal}",
|
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:
|
if stage_spec.success_criteria:
|
||||||
sketch_parts.append(f"Erfolgskriterien: {', '.join(stage_spec.success_criteria[:3])}")
|
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(
|
specs.append(
|
||||||
{
|
{
|
||||||
"source": "roadmap_unfilled",
|
"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(
|
def run_progression_roadmap_pipeline(
|
||||||
goal_query: str,
|
goal_query: str,
|
||||||
*,
|
*,
|
||||||
|
|
@ -1135,6 +1189,7 @@ __all__ = [
|
||||||
"consolidate_micro_to_major",
|
"consolidate_micro_to_major",
|
||||||
"develop_micro_objectives",
|
"develop_micro_objectives",
|
||||||
"progression_roadmap_to_api_dict",
|
"progression_roadmap_to_api_dict",
|
||||||
|
"run_start_target_resolve_only",
|
||||||
"run_progression_roadmap_pipeline",
|
"run_progression_roadmap_pipeline",
|
||||||
"try_llm_start_target_extract",
|
"try_llm_start_target_extract",
|
||||||
"try_llm_goal_analysis",
|
"try_llm_goal_analysis",
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ def post_progression_path_suggest(
|
||||||
or body.include_ai_gap_fill
|
or body.include_ai_gap_fill
|
||||||
or body.include_llm_roadmap
|
or body.include_llm_roadmap
|
||||||
or body.include_llm_start_target
|
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
|
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
||||||
if uses_ai:
|
if uses_ai:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Tests Planungs-KI Phase D — planning_context für suggestExerciseAi."""
|
"""Tests Planungs-KI Phase D — planning_context für suggestExerciseAi."""
|
||||||
from planning_exercise_form_context import (
|
from planning_exercise_form_context import (
|
||||||
|
build_progression_gap_snapshot,
|
||||||
build_progression_path_gap_planning_context,
|
build_progression_path_gap_planning_context,
|
||||||
planning_context_prompt_variables,
|
planning_context_prompt_variables,
|
||||||
sanitize_planning_context_for_ai,
|
sanitize_planning_context_for_ai,
|
||||||
|
|
@ -44,3 +45,36 @@ def test_build_progression_path_gap_context():
|
||||||
def test_sanitize_truncates_long_strings():
|
def test_sanitize_truncates_long_strings():
|
||||||
ctx = sanitize_planning_context_for_ai({"goal_query": "x" * 900})
|
ctx = sanitize_planning_context_for_ai({"goal_query": "x" * 900})
|
||||||
assert len(ctx["goal_query"]) <= 800
|
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 "Mae Geri" in text or "mae geri" in text.lower()
|
||||||
assert "anwendung" in text
|
assert "anwendung" in text
|
||||||
assert "Kihon" 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_roadmap_structured_input,
|
||||||
resolve_step_exercise_kind_filter,
|
resolve_step_exercise_kind_filter,
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
|
run_start_target_resolve_only,
|
||||||
stage_spec_exercise_kind_filter,
|
stage_spec_exercise_kind_filter,
|
||||||
stage_spec_retrieval_query,
|
stage_spec_retrieval_query,
|
||||||
normalize_major_steps_for_override,
|
normalize_major_steps_for_override,
|
||||||
|
|
@ -173,6 +174,15 @@ def test_resolve_structured_regex_fallback_without_llm():
|
||||||
assert "dynamischen" in (resolved.target_state or "")
|
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():
|
def test_resolve_structured_merges_user_and_llm_notes():
|
||||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||||
structured = RoadmapStructuredInput(roadmap_notes="Kindergruppe 10–12")
|
structured = RoadmapStructuredInput(roadmap_notes="Kindergruppe 10–12")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.211"
|
APP_VERSION = "0.8.212"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260607087"
|
DB_SCHEMA_VERSION = "20260607087"
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
"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_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||||
|
|
|
||||||
|
|
@ -201,8 +201,10 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
const [editableMajorSteps, setEditableMajorSteps] = useState([])
|
const [editableMajorSteps, setEditableMajorSteps] = useState([])
|
||||||
const [roadmapDirty, setRoadmapDirty] = useState(false)
|
const [roadmapDirty, setRoadmapDirty] = useState(false)
|
||||||
const [loadingRoadmap, setLoadingRoadmap] = useState(false)
|
const [loadingRoadmap, setLoadingRoadmap] = useState(false)
|
||||||
|
const [loadingStartTarget, setLoadingStartTarget] = useState(false)
|
||||||
const [loadingMatch, setLoadingMatch] = 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 [focusAreas, setFocusAreas] = useState([])
|
||||||
const [skillsCatalog, setSkillsCatalog] = useState([])
|
const [skillsCatalog, setSkillsCatalog] = useState([])
|
||||||
const [generatingOfferId, setGeneratingOfferId] = useState(null)
|
const [generatingOfferId, setGeneratingOfferId] = useState(null)
|
||||||
|
|
@ -397,6 +399,9 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
pathSteps,
|
pathSteps,
|
||||||
editableMajorSteps,
|
editableMajorSteps,
|
||||||
progressionRoadmap,
|
progressionRoadmap,
|
||||||
|
startSituation,
|
||||||
|
targetState,
|
||||||
|
roadmapNotes,
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const aiRes = await api.suggestExerciseAi({
|
const aiRes = await api.suggestExerciseAi({
|
||||||
|
|
@ -531,6 +536,60 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400))
|
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 suggestRoadmap = async () => {
|
||||||
const q = (goalQuery || '').trim()
|
const q = (goalQuery || '').trim()
|
||||||
if (q.length < 3) {
|
if (q.length < 3) {
|
||||||
|
|
@ -541,6 +600,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
alert('Zuerst einen Graphen wählen.')
|
alert('Zuerst einen Graphen wählen.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const fieldsEmpty = !startSituation.trim() && !targetState.trim()
|
||||||
setLoadingRoadmap(true)
|
setLoadingRoadmap(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
|
|
@ -554,7 +614,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
include_ai_gap_fill: false,
|
include_ai_gap_fill: false,
|
||||||
include_roadmap_preview: true,
|
include_roadmap_preview: true,
|
||||||
include_llm_roadmap: true,
|
include_llm_roadmap: true,
|
||||||
include_llm_start_target: true,
|
include_llm_start_target: fieldsEmpty,
|
||||||
roadmap_only: true,
|
roadmap_only: true,
|
||||||
progression_graph_id: Number(graphId),
|
progression_graph_id: Number(graphId),
|
||||||
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
||||||
|
|
@ -567,11 +627,14 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
setMaxSteps(majors.length)
|
setMaxSteps(majors.length)
|
||||||
const roadmap = res?.progression_roadmap || null
|
const roadmap = res?.progression_roadmap || null
|
||||||
setProgressionRoadmap(roadmap)
|
setProgressionRoadmap(roadmap)
|
||||||
|
if (fieldsEmpty) {
|
||||||
applyResolvedStructuredFromRoadmap(roadmap, {
|
applyResolvedStructuredFromRoadmap(roadmap, {
|
||||||
setStartSituation,
|
setStartSituation,
|
||||||
setTargetState,
|
setTargetState,
|
||||||
setRoadmapNotes,
|
setRoadmapNotes,
|
||||||
})
|
})
|
||||||
|
setStartTargetAnalyzed(true)
|
||||||
|
}
|
||||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||||
setPathSteps([])
|
setPathSteps([])
|
||||||
setTargetSummary(null)
|
setTargetSummary(null)
|
||||||
|
|
@ -767,18 +830,37 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
<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
|
Optional zuerst „Start/Ziel analysieren“, anpassen, dann Roadmap-Stufen. Sind Start und Ziel leer,
|
||||||
„von … bis …“). Manuelle Eingaben haben Vorrang.
|
geschieht die Analyse beim Roadmap-Vorschlag automatisch mit. Manuelle Eingaben haben immer Vorrang.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
disabled={disabled || loading || saving || !graphId}
|
disabled={disabled || loading || saving || !graphId}
|
||||||
onClick={suggestRoadmap}
|
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'}
|
{loadingRoadmap ? 'Roadmap …' : 'Roadmap vorschlagen'}
|
||||||
</button>
|
</button>
|
||||||
|
{startTargetAnalyzed && !editableMajorSteps.length ? (
|
||||||
|
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||||
|
Start/Ziel bereit — Roadmap als Nächstes
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
|
|
@ -808,7 +890,8 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{progressionRoadmap?.goal_analysis ? (
|
{(progressionRoadmap?.goal_analysis ||
|
||||||
|
progressionRoadmap?.pipeline_phase === 'start_target_only') ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,18 @@ export function buildPickerPlanningContextForAi({
|
||||||
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== ''))
|
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({
|
export function buildPathGapPlanningContextForAi({
|
||||||
goalQuery = '',
|
goalQuery = '',
|
||||||
semanticBrief = null,
|
semanticBrief = null,
|
||||||
|
|
@ -38,6 +50,9 @@ export function buildPathGapPlanningContextForAi({
|
||||||
pathSteps = [],
|
pathSteps = [],
|
||||||
editableMajorSteps = [],
|
editableMajorSteps = [],
|
||||||
progressionRoadmap = null,
|
progressionRoadmap = null,
|
||||||
|
startSituation = '',
|
||||||
|
targetState = '',
|
||||||
|
roadmapNotes = '',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const afterIdx = Number(offer?.insert_after_index)
|
const afterIdx = Number(offer?.insert_after_index)
|
||||||
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
|
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
|
majorIdxRaw != null && Number.isFinite(Number(majorIdxRaw)) ? Number(majorIdxRaw) : null
|
||||||
const majorStep =
|
const majorStep =
|
||||||
majorIdx != null && editableMajorSteps[majorIdx] ? editableMajorSteps[majorIdx] : null
|
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 = {
|
const ctx = {
|
||||||
source: 'progression_path_gap_fill',
|
source: 'progression_path_gap_fill',
|
||||||
goal_query: (goalQuery || '').trim() || null,
|
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,
|
progression_graph_id: graphId != null ? Number(graphId) : null,
|
||||||
gap_source: offer?.source || null,
|
gap_source: offer?.source || null,
|
||||||
gap_phase: offer?.phase || offer?.gap?.expected_phase || null,
|
gap_phase: offer?.phase || offer?.gap?.expected_phase || null,
|
||||||
roadmap_major_step_index: majorIdx,
|
roadmap_major_step_index: majorIdx,
|
||||||
roadmap_phase: majorStep?.phase || offer?.phase || null,
|
roadmap_phase: majorStep?.phase || stageSpec?.phase || offer?.phase || null,
|
||||||
roadmap_learning_goal:
|
roadmap_learning_goal:
|
||||||
(majorStep?.learning_goal || offer?.title_hint || offer?.gap?.learning_goal || '').trim() ||
|
(majorStep?.learning_goal || offer?.title_hint || offer?.gap?.learning_goal || '').trim() ||
|
||||||
null,
|
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_before_title: stepA?.exerciseTitle || offer?.from_title || null,
|
||||||
neighbor_after_title: stepB?.exerciseTitle || offer?.to_title || null,
|
neighbor_after_title: stepB?.exerciseTitle || offer?.to_title || null,
|
||||||
path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0,
|
path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user