Add Structured Roadmap Inputs and Enhance Goal Analysis Features
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced `RoadmapStructuredInput` to encapsulate structured inputs for start situation, target state, and roadmap notes. - Updated `ProgressionPathSuggestRequest` to include new fields for structured roadmap inputs. - Implemented parsing logic for goal queries to extract start and target states, enhancing the goal analysis process. - Enhanced `build_goal_analysis` to utilize structured inputs, improving the clarity and relevance of generated goals. - Updated the `ExerciseProgressionPathBuilder` component to support new structured input fields, enhancing user experience. - Incremented application version to 0.8.210 to reflect these changes.
This commit is contained in:
parent
87f258be38
commit
9dd44ce3ca
|
|
@ -54,6 +54,7 @@ from planning_progression_roadmap import (
|
|||
MajorStep,
|
||||
ProgressionRoadmapContext,
|
||||
RoadmapOverridePayload,
|
||||
RoadmapStructuredInput,
|
||||
StageSpecArtifact,
|
||||
build_roadmap_unfilled_gap_specs,
|
||||
progression_roadmap_to_api_dict,
|
||||
|
|
@ -78,10 +79,26 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
roadmap_first: bool = False
|
||||
roadmap_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)
|
||||
roadmap_notes: Optional[str] = Field(default=None, max_length=2000)
|
||||
progression_graph_id: Optional[int] = Field(default=None, ge=1)
|
||||
exercise_kind_any: Optional[List[str]] = None
|
||||
|
||||
|
||||
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
|
||||
notes = (body.roadmap_notes or "").strip() or None
|
||||
if not any([start, target, notes]):
|
||||
return None
|
||||
return RoadmapStructuredInput(
|
||||
start_situation=start,
|
||||
target_state=target,
|
||||
roadmap_notes=notes,
|
||||
)
|
||||
|
||||
|
||||
def _pick_best_path_hit(
|
||||
hits: List[Dict[str, Any]],
|
||||
used_exercise_ids: Set[int],
|
||||
|
|
@ -502,6 +519,7 @@ def suggest_progression_path(
|
|||
progression_roadmap: Optional[Dict[str, Any]] = None
|
||||
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
||||
roadmap_edited = False
|
||||
roadmap_structured = _roadmap_structured_from_body(body)
|
||||
|
||||
if body.roadmap_override is not None:
|
||||
try:
|
||||
|
|
@ -510,6 +528,7 @@ def suggest_progression_path(
|
|||
max_steps=max_steps,
|
||||
semantic_brief=semantic_brief,
|
||||
override=body.roadmap_override,
|
||||
structured=roadmap_structured,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
|
@ -525,6 +544,7 @@ def suggest_progression_path(
|
|||
semantic_brief=semantic_brief,
|
||||
cur=cur,
|
||||
include_llm_roadmap=body.include_llm_roadmap,
|
||||
structured=roadmap_structured,
|
||||
)
|
||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ class StageSpecArtifact(BaseModel):
|
|||
anti_patterns: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RoadmapStructuredInput(BaseModel):
|
||||
"""Optionale Strukturierung: Start, Ziel, Ergänzungen (Progressionsgraph, kein Gruppen-Tracking)."""
|
||||
|
||||
start_situation: Optional[str] = Field(default=None, max_length=2000)
|
||||
target_state: Optional[str] = Field(default=None, max_length=2000)
|
||||
roadmap_notes: Optional[str] = Field(default=None, max_length=2000)
|
||||
|
||||
|
||||
class RoadmapOverridePayload(BaseModel):
|
||||
"""Vom Trainer bearbeitete Major Steps — steuert Retrieval ohne erneute Roadmap-KI."""
|
||||
|
||||
|
|
@ -116,6 +124,9 @@ class RoadmapOverridePayload(BaseModel):
|
|||
stage_specs: Optional[List[StageSpecArtifact]] = None
|
||||
|
||||
|
||||
_GENERIC_START_MARKER = "Voraussetzungen der Zielgruppe werden im Progressionsgraphen nicht analysiert"
|
||||
|
||||
|
||||
class ProgressionRoadmapContext(BaseModel):
|
||||
goal_query: str
|
||||
max_steps: int = Field(ge=2, le=10, default=5)
|
||||
|
|
@ -268,34 +279,177 @@ def _topic_label(brief: PlanningSemanticBrief) -> str:
|
|||
return (brief.primary_topic or brief.retrieval_query or "Technik").strip()
|
||||
|
||||
|
||||
_PHASE_TOPIC_WORDS = frozenset(
|
||||
{"einstieg", "grundlage", "vertiefung", "anwendung", "perfektion", "technik"}
|
||||
)
|
||||
|
||||
|
||||
def _extract_topic_from_goal_query(goal_query: str, brief: PlanningSemanticBrief) -> str:
|
||||
q = (goal_query or "").strip()
|
||||
m = re.match(r"^(.+?)\s+von\s+(?:der|die|dem|das|einer?|einem)\s+", q, flags=re.IGNORECASE)
|
||||
if m:
|
||||
cand = m.group(1).strip().rstrip(".,;")
|
||||
if len(cand) >= 3:
|
||||
return cand
|
||||
topic = _topic_label(brief)
|
||||
if topic and topic.lower() not in _PHASE_TOPIC_WORDS and len(topic) >= 4:
|
||||
return topic
|
||||
return topic or "Technik"
|
||||
|
||||
|
||||
def parse_start_target_from_goal_query(goal_query: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""„von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y)."""
|
||||
q = (goal_query or "").strip()
|
||||
if not q:
|
||||
return None, None
|
||||
m = re.search(
|
||||
r"\bvon\s+((?:(?:der|die|dem|das|einer?|einem)\s+)?.+?)\s+bis\s+"
|
||||
r"(?:zur?|zum|zu der|zu einem)?\s*(.+?)\s*$",
|
||||
q,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
if not m:
|
||||
return None, None
|
||||
start = m.group(1).strip().rstrip(".,;")
|
||||
target = m.group(2).strip().rstrip(".,;")
|
||||
if len(start) < 4 or len(target) < 4:
|
||||
return None, None
|
||||
return start[:800], target[:800]
|
||||
|
||||
|
||||
def _roadmap_llm_goal_block(
|
||||
goal_query: str,
|
||||
*,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
parsed_start: Optional[str] = None,
|
||||
parsed_target: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Reicher Kontext für Roadmap-LLM ohne zwingend neue Prompt-Migration."""
|
||||
lines = [f"Gesamtanfrage: {(goal_query or '').strip()}"]
|
||||
start = (structured.start_situation if structured else None) or parsed_start
|
||||
target = (structured.target_state if structured else None) or parsed_target
|
||||
notes = structured.roadmap_notes if structured else None
|
||||
if start:
|
||||
lines.append(f"Ausgangslage/Startpunkt: {start.strip()}")
|
||||
if target:
|
||||
lines.append(f"Zielzustand: {target.strip()}")
|
||||
if notes and notes.strip():
|
||||
lines.append(f"Ergänzungen (Fokus, Gruppe, Besonderheiten): {notes.strip()}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_goal_analysis(
|
||||
goal_query: str,
|
||||
brief: PlanningSemanticBrief,
|
||||
*,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
) -> GoalAnalysisArtifact:
|
||||
"""Phase A — deterministisch aus Anfrage + Semantic Brief."""
|
||||
topic = _topic_label(brief)
|
||||
target = goal_query.strip() or f"Entwicklung {topic}"
|
||||
"""Phase A — aus Anfrage, optionalen Feldern und Semantic Brief."""
|
||||
topic = _extract_topic_from_goal_query(goal_query, brief)
|
||||
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||
|
||||
start = (structured.start_situation if structured else None) or parsed_start
|
||||
target = (structured.target_state if structured else None) or parsed_target
|
||||
notes = (structured.roadmap_notes if structured else None) or ""
|
||||
|
||||
if not target:
|
||||
target = goal_query.strip() or f"Entwicklung {topic}"
|
||||
|
||||
arc = list(brief.development_arc or [])
|
||||
start_phase = arc[0] if arc else "grundlage"
|
||||
target_phase = arc[-1] if arc else "perfektion"
|
||||
|
||||
if start:
|
||||
start_assumption = start.strip()
|
||||
else:
|
||||
start_assumption = (
|
||||
f"Einstieg auf Niveau „{start_phase}“ — {_GENERIC_START_MARKER} "
|
||||
"(erst Trainingsplanung)."
|
||||
)
|
||||
|
||||
criteria: List[str] = []
|
||||
if brief.must_phrases:
|
||||
criteria.extend(brief.must_phrases[:3])
|
||||
if topic:
|
||||
criteria.append(f"klarer Bezug zu {topic}")
|
||||
if start and target:
|
||||
criteria.append(f"nachvollziehbarer Übergang von „{start[:80]}“ zu „{target[:80]}“")
|
||||
if notes.strip():
|
||||
criteria.append(f"Berücksichtigung: {notes.strip()[:200]}")
|
||||
|
||||
constraints: Dict[str, Any] = {"partner_required": False, "group_analysis": False}
|
||||
if notes.strip():
|
||||
constraints["trainer_notes"] = notes.strip()[:500]
|
||||
|
||||
return GoalAnalysisArtifact(
|
||||
primary_topic=topic,
|
||||
start_assumption=(
|
||||
f"Einstieg auf Niveau „{start_phase}“ — Voraussetzungen der Zielgruppe werden im "
|
||||
"Progressionsgraphen nicht analysiert (erst Trainingsplanung)."
|
||||
),
|
||||
target_state=target,
|
||||
start_assumption=start_assumption,
|
||||
target_state=target.strip(),
|
||||
success_criteria=criteria or [f"sichere Entwicklung Richtung {target_phase}"],
|
||||
constraints={"partner_required": False, "group_analysis": False},
|
||||
constraints=constraints,
|
||||
)
|
||||
|
||||
|
||||
def _has_specific_start_target(goal_analysis: GoalAnalysisArtifact) -> bool:
|
||||
start = (goal_analysis.start_assumption or "").strip()
|
||||
target = (goal_analysis.target_state or "").strip()
|
||||
if _GENERIC_START_MARKER in start:
|
||||
return False
|
||||
return len(start) >= 6 and len(target) >= 6 and start != target
|
||||
|
||||
|
||||
def _target_facets(target: str) -> List[str]:
|
||||
parts = re.split(r"\s+und\s+|\s+mit\s+|,\s*", target, flags=re.IGNORECASE)
|
||||
out: List[str] = []
|
||||
for p in parts:
|
||||
s = p.strip().rstrip(".,;")
|
||||
if len(s) >= 5 and s.lower() not in {x.lower() for x in out}:
|
||||
out.append(s[:200])
|
||||
return out[:6]
|
||||
|
||||
|
||||
def develop_micro_objectives_from_start_target(
|
||||
goal_analysis: GoalAnalysisArtifact,
|
||||
*,
|
||||
min_count: int,
|
||||
) -> List[MicroObjective]:
|
||||
"""Zwischenziele entlang Start → Ziel (heuristisch, themenspezifisch)."""
|
||||
topic = goal_analysis.primary_topic or "Technik"
|
||||
start = goal_analysis.start_assumption.strip()
|
||||
target = goal_analysis.target_state.strip()
|
||||
facets = _target_facets(target)
|
||||
phases = ["einstieg", "grundlage", "vertiefung", "anwendung", "perfektion"]
|
||||
|
||||
titles: List[str] = [f"{topic}: Ausgang — {start}"]
|
||||
n_middle = max(0, min_count - 2)
|
||||
for i in range(n_middle):
|
||||
if facets and i < len(facets):
|
||||
titles.append(f"{topic}: {facets[i]} — schrittweise einführen")
|
||||
else:
|
||||
titles.append(
|
||||
f"{topic}: Übergangsschritt {i + 1} — Annäherung vom Ausgang zum Ziel"
|
||||
)
|
||||
titles.append(f"{topic}: Ziel — {target}")
|
||||
|
||||
while len(titles) < min_count:
|
||||
titles.insert(max(1, len(titles) - 1), f"{topic}: Vertiefung vor Zielerreichung")
|
||||
|
||||
titles = titles[:max(min_count, 2)]
|
||||
micro: List[MicroObjective] = []
|
||||
for i, title in enumerate(titles):
|
||||
phase = phases[min(i, len(phases) - 1)]
|
||||
micro.append(
|
||||
MicroObjective(
|
||||
id=f"m{i + 1}",
|
||||
phase=phase,
|
||||
title=title,
|
||||
weight=0.9 if i in (0, len(titles) - 1) else 0.85,
|
||||
depends_on=[f"m{i}"] if i > 0 else [],
|
||||
)
|
||||
)
|
||||
return micro
|
||||
|
||||
|
||||
def _micro_title_for_phase(phase: str, topic: str) -> str:
|
||||
p = (phase or "vertiefung").lower()
|
||||
labels = {
|
||||
|
|
@ -314,7 +468,10 @@ def develop_micro_objectives(
|
|||
goal_analysis: GoalAnalysisArtifact,
|
||||
min_count: int = 6,
|
||||
) -> List[MicroObjective]:
|
||||
"""Phase B1 — Zwischenziele (heuristisch aus development_arc)."""
|
||||
"""Phase B1 — Zwischenziele (Start→Ziel oder development_arc-Fallback)."""
|
||||
if _has_specific_start_target(goal_analysis):
|
||||
return develop_micro_objectives_from_start_target(goal_analysis, min_count=min_count)
|
||||
|
||||
topic = goal_analysis.primary_topic or _topic_label(brief)
|
||||
arc = [str(p).lower() for p in (brief.development_arc or []) if str(p).strip()]
|
||||
seen_phases: set = set()
|
||||
|
|
@ -592,11 +749,12 @@ def roadmap_context_from_override(
|
|||
max_steps: int,
|
||||
semantic_brief: PlanningSemanticBrief,
|
||||
override: RoadmapOverridePayload,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
) -> ProgressionRoadmapContext:
|
||||
"""Phase F4: bearbeitete Roadmap → stage_specs → Retrieval (ohne LLM-Roadmap)."""
|
||||
majors = normalize_major_steps_for_override(override.major_steps, max_steps=max_steps)
|
||||
effective_max = len(majors)
|
||||
goal_analysis = build_goal_analysis(goal_query, semantic_brief)
|
||||
goal_analysis = build_goal_analysis(goal_query, semantic_brief, structured=structured)
|
||||
stage_specs: List[StageSpecArtifact]
|
||||
if override.stage_specs and len(override.stage_specs) >= effective_max:
|
||||
stage_specs = []
|
||||
|
|
@ -632,6 +790,31 @@ def roadmap_context_from_override(
|
|||
)
|
||||
|
||||
|
||||
def _merge_structured_into_goal_analysis(
|
||||
llm_ga: GoalAnalysisArtifact,
|
||||
*,
|
||||
goal_query: str,
|
||||
brief: PlanningSemanticBrief,
|
||||
structured: Optional[RoadmapStructuredInput],
|
||||
) -> GoalAnalysisArtifact:
|
||||
ga_struct = build_goal_analysis(goal_query, brief, structured=structured)
|
||||
if not _has_specific_start_target(ga_struct):
|
||||
return llm_ga
|
||||
merged_criteria = list(
|
||||
dict.fromkeys((llm_ga.success_criteria or []) + (ga_struct.success_criteria or []))
|
||||
)[:6]
|
||||
merged_constraints = {**(llm_ga.constraints or {}), **(ga_struct.constraints or {})}
|
||||
return llm_ga.model_copy(
|
||||
update={
|
||||
"primary_topic": ga_struct.primary_topic or llm_ga.primary_topic,
|
||||
"start_assumption": ga_struct.start_assumption,
|
||||
"target_state": ga_struct.target_state,
|
||||
"success_criteria": merged_criteria,
|
||||
"constraints": merged_constraints,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def run_progression_roadmap_pipeline(
|
||||
goal_query: str,
|
||||
*,
|
||||
|
|
@ -639,20 +822,33 @@ def run_progression_roadmap_pipeline(
|
|||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
cur=None,
|
||||
include_llm_roadmap: bool = False,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
) -> ProgressionRoadmapContext:
|
||||
"""Workflow-lite: Phase A → B → C. LLM über ai_prompts-Slugs; deterministischer Fallback."""
|
||||
brief = semantic_brief or build_semantic_brief(goal_query)
|
||||
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||
llm_goal_query = _roadmap_llm_goal_block(
|
||||
goal_query,
|
||||
structured=structured,
|
||||
parsed_start=parsed_start,
|
||||
parsed_target=parsed_target,
|
||||
)
|
||||
ctx = ProgressionRoadmapContext(
|
||||
goal_query=goal_query.strip(),
|
||||
max_steps=max_steps,
|
||||
semantic_brief=brief_to_summary_dict(brief),
|
||||
)
|
||||
|
||||
goal_analysis = build_goal_analysis(goal_query, brief)
|
||||
goal_analysis = build_goal_analysis(goal_query, brief, structured=structured)
|
||||
if include_llm_roadmap and cur is not None:
|
||||
llm_ga, ga_ok = try_llm_goal_analysis(cur, goal_query=goal_query, brief=brief)
|
||||
llm_ga, ga_ok = try_llm_goal_analysis(cur, goal_query=llm_goal_query, brief=brief)
|
||||
if ga_ok and llm_ga:
|
||||
goal_analysis = llm_ga
|
||||
goal_analysis = _merge_structured_into_goal_analysis(
|
||||
llm_ga,
|
||||
goal_query=goal_query,
|
||||
brief=brief,
|
||||
structured=structured,
|
||||
)
|
||||
ctx.llm_goal_analysis_applied = True
|
||||
ctx.prompt_slugs.append(PROMPT_SLUG_GOAL_ANALYSIS)
|
||||
ctx.goal_analysis = goal_analysis
|
||||
|
|
@ -661,7 +857,7 @@ def run_progression_roadmap_pipeline(
|
|||
if include_llm_roadmap and cur is not None:
|
||||
llm_rm, rm_ok = try_llm_roadmap(
|
||||
cur,
|
||||
goal_query=goal_query,
|
||||
goal_query=llm_goal_query,
|
||||
brief=brief,
|
||||
goal_analysis=goal_analysis,
|
||||
max_steps=max_steps,
|
||||
|
|
@ -689,7 +885,7 @@ def run_progression_roadmap_pipeline(
|
|||
if include_llm_roadmap and cur is not None:
|
||||
llm_specs, spec_ok = try_llm_stage_specs(
|
||||
cur,
|
||||
goal_query=goal_query,
|
||||
goal_query=llm_goal_query,
|
||||
goal_analysis=goal_analysis,
|
||||
major_steps=roadmap.major_steps,
|
||||
)
|
||||
|
|
@ -734,7 +930,9 @@ __all__ = [
|
|||
"ProgressionRoadmapContext",
|
||||
"RoadmapArtifact",
|
||||
"RoadmapOverridePayload",
|
||||
"RoadmapStructuredInput",
|
||||
"normalize_major_steps_for_override",
|
||||
"parse_start_target_from_goal_query",
|
||||
"roadmap_context_from_override",
|
||||
"StageSpecArtifact",
|
||||
"build_goal_analysis",
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ from planning_progression_roadmap import (
|
|||
PROMPT_SLUG_ROADMAP,
|
||||
PROMPT_SLUG_STAGE_SPEC,
|
||||
MajorStep,
|
||||
RoadmapStructuredInput,
|
||||
StageSpecArtifact,
|
||||
build_goal_analysis,
|
||||
build_roadmap_unfilled_gap_specs,
|
||||
consolidate_micro_to_major,
|
||||
develop_micro_objectives,
|
||||
parse_start_target_from_goal_query,
|
||||
progression_roadmap_to_api_dict,
|
||||
resolve_step_exercise_kind_filter,
|
||||
run_progression_roadmap_pipeline,
|
||||
|
|
@ -20,6 +22,11 @@ from planning_progression_roadmap import (
|
|||
)
|
||||
from planning_exercise_semantics import build_semantic_brief
|
||||
|
||||
KUMITE_GOAL = (
|
||||
"Kumite Beinarbeit von einer gleichartigen Steppbewegung bis zur dynamischen "
|
||||
"unvorhersehbaren Bewegung mit explosivartigem Angriff und ausweichen"
|
||||
)
|
||||
|
||||
|
||||
def test_run_progression_roadmap_pipeline_major_step_count():
|
||||
ctx = run_progression_roadmap_pipeline(
|
||||
|
|
@ -134,3 +141,54 @@ def test_api_dict_exposes_prompt_slug_catalog():
|
|||
assert api["prompt_slug_catalog"]["roadmap"] == PROMPT_SLUG_ROADMAP
|
||||
assert api["prompt_slug_catalog"]["stage_spec"] == PROMPT_SLUG_STAGE_SPEC
|
||||
assert api["prompt_slugs"] == []
|
||||
|
||||
|
||||
def test_parse_start_target_kumite_beinarbeit():
|
||||
start, target = parse_start_target_from_goal_query(KUMITE_GOAL)
|
||||
assert start is not None
|
||||
assert "Steppbewegung" in start
|
||||
assert target is not None
|
||||
assert "dynamischen" in target
|
||||
assert "Angriff" in target
|
||||
|
||||
|
||||
def test_build_goal_analysis_uses_parsed_start_target():
|
||||
brief = build_semantic_brief(KUMITE_GOAL)
|
||||
ga = build_goal_analysis(KUMITE_GOAL, brief)
|
||||
assert "Kumite Beinarbeit" in ga.primary_topic
|
||||
assert "Steppbewegung" in ga.start_assumption
|
||||
assert "dynamischen" in ga.target_state
|
||||
assert "Voraussetzungen der Zielgruppe werden im Progressionsgraphen nicht analysiert" not in ga.start_assumption
|
||||
|
||||
|
||||
def test_build_goal_analysis_structured_fields_override():
|
||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||
structured = RoadmapStructuredInput(
|
||||
start_situation="statische Vorwärtsbewegung im Partnerdrill",
|
||||
target_state="explosiver Gegenangriff nach unvorhersehbarer Beinarbeit",
|
||||
roadmap_notes="Kindergruppe 10–12 Jahre",
|
||||
)
|
||||
ga = build_goal_analysis("Kumite Beinarbeit", brief, structured=structured)
|
||||
assert ga.start_assumption == structured.start_situation
|
||||
assert ga.target_state == structured.target_state
|
||||
assert any("Kindergruppe" in c for c in ga.success_criteria)
|
||||
|
||||
|
||||
def test_develop_micro_objectives_start_target_kumite():
|
||||
brief = build_semantic_brief(KUMITE_GOAL)
|
||||
ga = build_goal_analysis(KUMITE_GOAL, brief)
|
||||
micro = develop_micro_objectives(brief, goal_analysis=ga, min_count=6)
|
||||
titles = [m.title for m in micro]
|
||||
assert any("Ausgang" in t for t in titles)
|
||||
assert any("Ziel" in t for t in titles)
|
||||
assert not any("Einstieg und Orientierung zum Thema" in t for t in titles)
|
||||
|
||||
|
||||
def test_pipeline_kumite_major_steps_not_generic_templates():
|
||||
ctx = run_progression_roadmap_pipeline(KUMITE_GOAL, max_steps=5, include_llm_roadmap=False)
|
||||
goals = [s.learning_goal for s in ctx.roadmap.major_steps]
|
||||
joined = " ".join(goals).lower()
|
||||
assert "kumite beinarbeit" in joined
|
||||
assert "steppbewegung" in joined or "ausgang" in joined
|
||||
assert "dynamisch" in joined or "ziel" in joined
|
||||
assert not any(g == "Grundstellung und Basisbewegung" for g in goals)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.209"
|
||||
APP_VERSION = "0.8.210"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260606086"
|
||||
|
||||
|
|
@ -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.20.1", # F3-Polish: roadmap_first QA lite (keine Brücken/Reorder)
|
||||
"planning_exercise_suggest": "0.20.2", # Strukturierte Roadmap-Eingaben Start/Ziel + von-bis-Parsing
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -12,6 +12,31 @@ import {
|
|||
} from '../utils/exerciseAiQuickCreate'
|
||||
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
||||
|
||||
/** „von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y). */
|
||||
function parseStartTargetFromGoalQuery(q) {
|
||||
const text = String(q || '').trim()
|
||||
if (!text) return { start: '', target: '' }
|
||||
const m = text.match(
|
||||
/\bvon\s+((?:(?:der|die|dem|das|einer?|einem)\s+)?.+?)\s+bis\s+(?:zur?|zum|zu der|zu einem)?\s*(.+)$/is,
|
||||
)
|
||||
if (!m) return { start: '', target: '' }
|
||||
const start = m[1].trim().replace(/[.,;]+$/, '')
|
||||
const target = m[2].trim().replace(/[.,;]+$/, '')
|
||||
if (start.length < 4 || target.length < 4) return { start: '', target: '' }
|
||||
return { start, target }
|
||||
}
|
||||
|
||||
function roadmapStructuredPayload(startSituation, targetState, roadmapNotes) {
|
||||
const start = (startSituation || '').trim()
|
||||
const target = (targetState || '').trim()
|
||||
const notes = (roadmapNotes || '').trim()
|
||||
const body = {}
|
||||
if (start) body.start_situation = start
|
||||
if (target) body.target_state = target
|
||||
if (notes) body.roadmap_notes = notes
|
||||
return body
|
||||
}
|
||||
|
||||
function emptyPathStep() {
|
||||
return { exerciseId: null, exerciseTitle: '', variantId: null, variants: [], reasons: [] }
|
||||
}
|
||||
|
|
@ -154,6 +179,9 @@ export default function ExerciseProgressionPathBuilder({
|
|||
onSaved,
|
||||
}) {
|
||||
const [goalQuery, setGoalQuery] = useState('')
|
||||
const [startSituation, setStartSituation] = useState('')
|
||||
const [targetState, setTargetState] = useState('')
|
||||
const [roadmapNotes, setRoadmapNotes] = useState('')
|
||||
const [maxSteps, setMaxSteps] = useState(5)
|
||||
const [segmentNotes, setSegmentNotes] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
|
@ -507,6 +535,19 @@ export default function ExerciseProgressionPathBuilder({
|
|||
alert('Zuerst einen Graphen wählen.')
|
||||
return
|
||||
}
|
||||
let start = startSituation.trim()
|
||||
let target = targetState.trim()
|
||||
if (!start || !target) {
|
||||
const parsed = parseStartTargetFromGoalQuery(q)
|
||||
if (parsed.start && !start) {
|
||||
start = parsed.start
|
||||
setStartSituation(parsed.start)
|
||||
}
|
||||
if (parsed.target && !target) {
|
||||
target = parsed.target
|
||||
setTargetState(parsed.target)
|
||||
}
|
||||
}
|
||||
setLoadingRoadmap(true)
|
||||
setError('')
|
||||
try {
|
||||
|
|
@ -522,6 +563,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
include_llm_roadmap: true,
|
||||
roadmap_only: true,
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(start, target, roadmapNotes),
|
||||
})
|
||||
const majors = mapMajorStepsFromApi(res?.progression_roadmap)
|
||||
if (majors.length < 2) {
|
||||
|
|
@ -578,6 +620,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
roadmap_first: true,
|
||||
roadmap_override: override,
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
||||
})
|
||||
applyPathMatchResponse(res, q)
|
||||
setMaxSteps(validSteps.length)
|
||||
|
|
@ -680,6 +723,53 @@ export default function ExerciseProgressionPathBuilder({
|
|||
disabled={disabled || loading || saving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
||||
gap: '10px',
|
||||
marginTop: '10px',
|
||||
}}
|
||||
>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Startpunkt / Ausgangslage</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={startSituation}
|
||||
onChange={(e) => setStartSituation(e.target.value)}
|
||||
placeholder="z. B. gleichartige Steppbewegung, vorhersehbar"
|
||||
disabled={disabled || loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Zielzustand</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={targetState}
|
||||
onChange={(e) => setTargetState(e.target.value)}
|
||||
placeholder="z. B. dynamische, unvorhersehbare Bewegung mit explosivem Angriff und Ausweichen"
|
||||
disabled={disabled || loading || saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">Ergänzungen (Fokus, Gruppe, Besonderheiten)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={roadmapNotes}
|
||||
onChange={(e) => setRoadmapNotes(e.target.value)}
|
||||
placeholder="optional: Altersgruppe, Kumite-Kontext, Trainingsfokus …"
|
||||
disabled={disabled || loading || saving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
||||
Bei „von … bis …“ im Ziel werden Start und Ziel automatisch vorausgefüllt, wenn die Felder leer sind.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
|
|
@ -717,6 +807,50 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</p>
|
||||
) : null}
|
||||
|
||||
{progressionRoadmap?.goal_analysis ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<strong style={{ fontSize: '13px' }}>Zielanalyse</strong>
|
||||
{progressionRoadmap.llm_goal_analysis_applied ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
KI-Zielanalyse
|
||||
</span>
|
||||
) : (
|
||||
<span className="exercise-tag">heuristisch</span>
|
||||
)}
|
||||
{progressionRoadmap.goal_analysis.primary_topic ? (
|
||||
<span className="exercise-tag">Thema: {progressionRoadmap.goal_analysis.primary_topic}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--text2)', lineHeight: 1.5 }}>
|
||||
<div>
|
||||
<span style={{ color: 'var(--text3)' }}>Ausgang: </span>
|
||||
{progressionRoadmap.goal_analysis.start_assumption}
|
||||
</div>
|
||||
<div style={{ marginTop: '6px' }}>
|
||||
<span style={{ color: 'var(--text3)' }}>Ziel: </span>
|
||||
{progressionRoadmap.goal_analysis.target_state}
|
||||
</div>
|
||||
{Array.isArray(progressionRoadmap.goal_analysis.success_criteria) &&
|
||||
progressionRoadmap.goal_analysis.success_criteria.length > 0 ? (
|
||||
<ul style={{ margin: '8px 0 0', paddingLeft: '18px' }}>
|
||||
{progressionRoadmap.goal_analysis.success_criteria.slice(0, 4).map((c) => (
|
||||
<li key={c}>{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(semanticBrief || targetSummary) && pathSteps.length > 0 ? (
|
||||
<div style={{ marginTop: '10px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{semanticBrief?.primary_topic ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user