Enhance Gap Fill Goal Text and Skill Expectations Integration
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 48s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 48s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Updated `build_gap_fill_goal_text` to include expected skills in the generated text, improving clarity for users. - Enhanced `_roadmap_gap_snapshot_for_spec` to incorporate skill expectations from the progression stage, enriching the roadmap context. - Modified `_annotate_roadmap_step` to append skill expectations to the step reasons, providing additional insights. - Updated tests to verify the inclusion of expected skills in the gap fill goal text. - Incremented application version to 0.8.215 to reflect these changes.
This commit is contained in:
parent
0adf20c9e1
commit
1e7941f57b
|
|
@ -314,6 +314,17 @@ def build_gap_fill_goal_text(
|
|||
"Fähigkeiten-/Fokus-Hinweise: "
|
||||
+ "; ".join(str(x) for x in snap["skill_hints"][:4])
|
||||
)
|
||||
expected = snap.get("expected_skills") or []
|
||||
if expected:
|
||||
names = [
|
||||
str(s.get("skill_name") or "").strip()
|
||||
for s in expected[:5]
|
||||
if str(s.get("skill_name") or "").strip()
|
||||
]
|
||||
if names:
|
||||
parts.append(
|
||||
"Erwartete Fähigkeiten (Scoring): " + ", ".join(names)
|
||||
)
|
||||
if spec.get("rationale"):
|
||||
parts.append(f"Qualitätsprüfung: {spec['rationale']}")
|
||||
if spec.get("sketch"):
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ from planning_exercise_suggest import (
|
|||
resolve_planning_exercise_intent,
|
||||
)
|
||||
from planning_exercise_form_context import build_progression_gap_snapshot
|
||||
from planning_skill_expectations import (
|
||||
apply_expectations_to_target,
|
||||
build_planning_skill_expectations,
|
||||
expectation_input_from_progression_path,
|
||||
expectation_input_from_progression_stage,
|
||||
)
|
||||
from planning_progression_roadmap import (
|
||||
MajorStep,
|
||||
ProgressionRoadmapContext,
|
||||
|
|
@ -91,14 +97,17 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
|
||||
|
||||
def _roadmap_gap_snapshot_for_spec(
|
||||
cur,
|
||||
roadmap_ctx: Optional[ProgressionRoadmapContext],
|
||||
spec: Mapping[str, Any],
|
||||
*,
|
||||
goal_query: str,
|
||||
semantic_brief: PlanningSemanticBrief,
|
||||
) -> Dict[str, Any]:
|
||||
"""Roadmap-Kontext für KI-Lücken-Übung (Start, Ziel, Stufenspec)."""
|
||||
"""Roadmap-Kontext für KI-Lücken-Übung (Start, Ziel, Stufenspec, Fähigkeiten)."""
|
||||
major_idx = spec.get("roadmap_major_step_index")
|
||||
stage_spec_dict: Optional[Dict[str, Any]] = None
|
||||
major_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):
|
||||
|
|
@ -107,6 +116,7 @@ def _roadmap_gap_snapshot_for_spec(
|
|||
for m in roadmap_ctx.roadmap.major_steps:
|
||||
if m.index == int(major_idx):
|
||||
stage_spec_dict["phase"] = m.phase
|
||||
major_dict = m.model_dump()
|
||||
break
|
||||
break
|
||||
ga = roadmap_ctx.goal_analysis.model_dump() if roadmap_ctx and roadmap_ctx.goal_analysis else None
|
||||
|
|
@ -120,12 +130,25 @@ def _roadmap_gap_snapshot_for_spec(
|
|||
if roadmap_ctx and roadmap_ctx.semantic_brief
|
||||
else brief_to_summary_dict(semantic_brief)
|
||||
)
|
||||
return build_progression_gap_snapshot(
|
||||
snap = build_progression_gap_snapshot(
|
||||
goal_analysis=ga,
|
||||
resolved_structured=rs,
|
||||
stage_spec=stage_spec_dict,
|
||||
semantic_brief=brief_summary,
|
||||
)
|
||||
inp = expectation_input_from_progression_stage(
|
||||
goal_query=goal_query,
|
||||
goal_analysis=ga,
|
||||
resolved_structured=rs,
|
||||
stage_spec=stage_spec_dict,
|
||||
semantic_brief_summary=brief_summary,
|
||||
major_step=major_dict,
|
||||
)
|
||||
exp = build_planning_skill_expectations(cur, inp, semantic_brief=semantic_brief)
|
||||
if exp.items:
|
||||
snap["expected_skills"] = exp.to_api_dict()["expected_skills"]
|
||||
snap["skill_expectation_sources"] = exp.sources
|
||||
return snap
|
||||
|
||||
|
||||
def _roadmap_structured_from_body(body: ProgressionPathSuggestRequest) -> Optional[RoadmapStructuredInput]:
|
||||
|
|
@ -244,6 +267,7 @@ def _run_path_step_retrieval(
|
|||
path_intent: Optional[str] = None,
|
||||
step_query_override: Optional[str] = None,
|
||||
step_phase_override: Optional[str] = None,
|
||||
step_target_profile_override: Optional[PlanningTargetProfile] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], PlanningTargetProfile, Dict[str, Any], str]:
|
||||
step_query = step_query_override or step_retrieval_query(
|
||||
semantic_brief, goal_query, step_index, max_steps
|
||||
|
|
@ -311,7 +335,11 @@ def _run_path_step_retrieval(
|
|||
"expectation_mode": "query_only" if step_index == 0 and not planned_ids else "planning_hybrid",
|
||||
}
|
||||
|
||||
if path_target_profile is not None:
|
||||
if step_target_profile_override is not None:
|
||||
target_profile = step_target_profile_override
|
||||
intent = path_intent or resolve_planning_exercise_intent(goal_query, "free_search")
|
||||
query_intent_summary = {}
|
||||
elif path_target_profile is not None:
|
||||
target_profile = path_target_profile
|
||||
intent = path_intent or resolve_planning_exercise_intent(goal_query, "free_search")
|
||||
query_intent_summary = {}
|
||||
|
|
@ -414,6 +442,7 @@ def _annotate_roadmap_step(
|
|||
*,
|
||||
stage_spec: StageSpecArtifact,
|
||||
major_step: Optional[MajorStep],
|
||||
skill_expectations: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
reasons = list(step.get("reasons") or [])
|
||||
learning_goal = (stage_spec.learning_goal or "").strip()
|
||||
|
|
@ -421,11 +450,23 @@ def _annotate_roadmap_step(
|
|||
roadmap_reason = f"Roadmap: {learning_goal[:120]}"
|
||||
if roadmap_reason not in reasons:
|
||||
reasons.insert(0, roadmap_reason)
|
||||
if skill_expectations and skill_expectations.get("expected_skills"):
|
||||
names = [
|
||||
str(s.get("skill_name") or "").strip()
|
||||
for s in skill_expectations["expected_skills"][:3]
|
||||
if str(s.get("skill_name") or "").strip()
|
||||
]
|
||||
if names:
|
||||
skill_reason = f"Fähigkeiten: {', '.join(names)}"
|
||||
if skill_reason not in reasons:
|
||||
reasons.append(skill_reason)
|
||||
step["reasons"] = reasons[:4]
|
||||
step["roadmap_major_step_index"] = stage_spec.major_step_index
|
||||
step["roadmap_phase"] = major_step.phase if major_step else None
|
||||
step["roadmap_learning_goal"] = learning_goal or None
|
||||
step["roadmap_match_source"] = "stage_spec"
|
||||
if skill_expectations:
|
||||
step["skill_expectations"] = skill_expectations
|
||||
return step
|
||||
|
||||
|
||||
|
|
@ -463,8 +504,37 @@ def _build_steps_roadmap_first(
|
|||
anchor_variant_id: Optional[int] = None
|
||||
unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||
|
||||
ga_dump = (
|
||||
roadmap_ctx.goal_analysis.model_dump() if roadmap_ctx.goal_analysis else None
|
||||
)
|
||||
rs_dump = (
|
||||
roadmap_ctx.resolved_structured.model_dump()
|
||||
if roadmap_ctx.resolved_structured
|
||||
else None
|
||||
)
|
||||
brief_summary = (
|
||||
roadmap_ctx.semantic_brief
|
||||
if roadmap_ctx.semantic_brief
|
||||
else brief_to_summary_dict(semantic_brief)
|
||||
)
|
||||
|
||||
for step_index, stage_spec in enumerate(stage_specs):
|
||||
major = major_by_index.get(stage_spec.major_step_index)
|
||||
stage_spec_dict = stage_spec.model_dump()
|
||||
if major:
|
||||
stage_spec_dict["phase"] = major.phase
|
||||
stage_inp = expectation_input_from_progression_stage(
|
||||
goal_query=goal_query,
|
||||
goal_analysis=ga_dump,
|
||||
resolved_structured=rs_dump,
|
||||
stage_spec=stage_spec_dict,
|
||||
semantic_brief_summary=brief_summary,
|
||||
major_step=major.model_dump() if major else None,
|
||||
)
|
||||
stage_exp = build_planning_skill_expectations(cur, stage_inp, semantic_brief=semantic_brief)
|
||||
step_target = apply_expectations_to_target(path_target_profile, stage_exp)
|
||||
skill_exp_api = stage_exp.to_api_dict() if stage_exp.items else None
|
||||
|
||||
step_query = stage_spec_retrieval_query(
|
||||
semantic_brief=semantic_brief,
|
||||
goal_query=goal_query,
|
||||
|
|
@ -490,6 +560,7 @@ def _build_steps_roadmap_first(
|
|||
path_intent=path_intent,
|
||||
step_query_override=step_query,
|
||||
step_phase_override=major.phase if major else None,
|
||||
step_target_profile_override=step_target,
|
||||
)
|
||||
|
||||
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
|
||||
|
|
@ -511,6 +582,7 @@ def _build_steps_roadmap_first(
|
|||
path_intent=path_intent,
|
||||
step_query_override=goal_query,
|
||||
step_phase_override=major.phase if major else None,
|
||||
step_target_profile_override=step_target,
|
||||
)
|
||||
hit = _pick_best_path_hit(hits, used, semantic_brief=semantic_brief)
|
||||
|
||||
|
|
@ -522,6 +594,7 @@ def _build_steps_roadmap_first(
|
|||
_hit_to_path_step(hit),
|
||||
stage_spec=stage_spec,
|
||||
major_step=major,
|
||||
skill_expectations=skill_exp_api,
|
||||
)
|
||||
steps.append(step)
|
||||
eid = int(step["exercise_id"])
|
||||
|
|
@ -652,6 +725,26 @@ def suggest_progression_path(
|
|||
semantic_brief=semantic_brief,
|
||||
include_llm_intent=body.include_llm_intent,
|
||||
)
|
||||
path_skill_expectations: Optional[Dict[str, Any]] = None
|
||||
if roadmap_ctx and roadmap_ctx.goal_analysis:
|
||||
path_inp = expectation_input_from_progression_path(
|
||||
goal_query=goal_query,
|
||||
goal_analysis=roadmap_ctx.goal_analysis.model_dump(),
|
||||
resolved_structured=(
|
||||
roadmap_ctx.resolved_structured.model_dump()
|
||||
if roadmap_ctx.resolved_structured
|
||||
else None
|
||||
),
|
||||
semantic_brief_summary=(
|
||||
roadmap_ctx.semantic_brief
|
||||
if roadmap_ctx.semantic_brief
|
||||
else brief_to_summary_dict(semantic_brief)
|
||||
),
|
||||
)
|
||||
path_exp = build_planning_skill_expectations(cur, path_inp, semantic_brief=semantic_brief)
|
||||
if path_exp.items:
|
||||
path_target_profile = apply_expectations_to_target(path_target_profile, path_exp)
|
||||
path_skill_expectations = path_exp.to_api_dict()
|
||||
|
||||
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||
roadmap_gap_offers: List[Dict[str, Any]] = []
|
||||
|
|
@ -702,7 +795,11 @@ def suggest_progression_path(
|
|||
brief=semantic_brief,
|
||||
proposal=None,
|
||||
roadmap_snapshot=_roadmap_gap_snapshot_for_spec(
|
||||
roadmap_ctx, spec, semantic_brief=semantic_brief
|
||||
cur,
|
||||
roadmap_ctx,
|
||||
spec,
|
||||
goal_query=goal_query,
|
||||
semantic_brief=semantic_brief,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -927,6 +1024,7 @@ def suggest_progression_path(
|
|||
"roadmap_only": False,
|
||||
"roadmap_edited": roadmap_edited,
|
||||
"roadmap_unfilled_count": len(roadmap_unfilled),
|
||||
"path_skill_expectations": path_skill_expectations,
|
||||
"retrieval_phase": "+".join(retrieval_parts),
|
||||
}
|
||||
|
||||
|
|
|
|||
334
backend/planning_skill_expectations.py
Normal file
334
backend/planning_skill_expectations.py
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
"""
|
||||
Wiederverwendbare Fähigkeiten-Erwartungen für Planungs-KI.
|
||||
|
||||
Domänen-Scopes (gleiches Input-/Output-Modell):
|
||||
- ``progression_stage`` — ein Major Step / stage_spec im Progressionsgraphen
|
||||
- ``progression_path`` — gesamter Pfad (Ziel + Start/Ziel)
|
||||
- ``training_section`` — Abschnitt einer Trainingseinheit (Phase G, später)
|
||||
- ``framework_slot`` — Rahmen-Session-Slot (Phase G, später)
|
||||
|
||||
Konsumenten mergen ``skill_weights`` in ``PlanningTargetProfile`` (Retrieval)
|
||||
oder liefern ``expected_skills`` an Übungs-KI (planning_context).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
from planning_exercise_profiles import _merge_weight_maps, _normalize_weight_map
|
||||
from planning_exercise_semantics import PlanningSemanticBrief, resolve_semantic_skill_weights
|
||||
from planning_exercise_text_signals import _load_skills_for_text_match, _match_skills_in_text
|
||||
|
||||
# Scope-Strings — stabil für API/UI und spätere Trainingsplanung
|
||||
SCOPE_PROGRESSION_STAGE = "progression_stage"
|
||||
SCOPE_PROGRESSION_PATH = "progression_path"
|
||||
SCOPE_TRAINING_SECTION = "training_section"
|
||||
SCOPE_FRAMEWORK_SLOT = "framework_slot"
|
||||
|
||||
_LOAD_PROFILE_SKILL_TERMS: Dict[str, Tuple[str, ...]] = {
|
||||
"koordination": ("Koordination",),
|
||||
"präzision": ("Präzision",),
|
||||
"praezision": ("Präzision",),
|
||||
"kraft": ("Kraft", "Kime"),
|
||||
"geschwindigkeit": ("Geschwindigkeit", "Schnelligkeit"),
|
||||
"schnelligkeit": ("Schnelligkeit", "Geschwindigkeit"),
|
||||
"timing": ("Timing", "Reaktion"),
|
||||
"reaktion": ("Reaktion", "Timing"),
|
||||
"distanz": ("Distanz",),
|
||||
"raum": ("Raum", "Distanz"),
|
||||
"gleichgewicht": ("Gleichgewicht",),
|
||||
"kime": ("Kime",),
|
||||
"ausdauer": ("Ausdauer",),
|
||||
"beweglichkeit": ("Beweglichkeit",),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanningSkillExpectationInput:
|
||||
scope: str = SCOPE_PROGRESSION_STAGE
|
||||
primary_topic: str = ""
|
||||
goal_query: str = ""
|
||||
start_situation: str = ""
|
||||
target_state: str = ""
|
||||
stage_learning_goal: str = ""
|
||||
roadmap_notes: str = ""
|
||||
load_profile: List[str] = field(default_factory=list)
|
||||
phase: str = ""
|
||||
skill_hints: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanningSkillExpectationItem:
|
||||
skill_id: int
|
||||
skill_name: str
|
||||
weight: float
|
||||
source: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanningSkillExpectations:
|
||||
scope: str
|
||||
skill_weights: Dict[int, float]
|
||||
items: List[PlanningSkillExpectationItem]
|
||||
sources: List[str]
|
||||
|
||||
def to_api_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"scope": self.scope,
|
||||
"sources": list(self.sources),
|
||||
"expected_skills": [
|
||||
{
|
||||
"skill_id": it.skill_id,
|
||||
"skill_name": it.skill_name,
|
||||
"weight": round(it.weight, 4),
|
||||
"source": it.source,
|
||||
}
|
||||
for it in self.items
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _norm_load_key(s: str) -> str:
|
||||
return (s or "").strip().lower().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue")
|
||||
|
||||
|
||||
def _text_blob(inp: PlanningSkillExpectationInput) -> str:
|
||||
parts = [
|
||||
inp.primary_topic,
|
||||
inp.goal_query,
|
||||
inp.start_situation,
|
||||
inp.target_state,
|
||||
inp.stage_learning_goal,
|
||||
inp.roadmap_notes,
|
||||
inp.phase,
|
||||
" ".join(inp.load_profile or []),
|
||||
" ".join(inp.skill_hints or []),
|
||||
]
|
||||
return "\n".join(p for p in parts if (p or "").strip()).lower()
|
||||
|
||||
|
||||
def _resolve_skills_by_name_terms(
|
||||
cur,
|
||||
terms: Sequence[str],
|
||||
*,
|
||||
weight: float = 0.9,
|
||||
source: str,
|
||||
weights: Dict[int, float],
|
||||
items: Dict[int, PlanningSkillExpectationItem],
|
||||
) -> bool:
|
||||
found = False
|
||||
for name in terms:
|
||||
if not name:
|
||||
continue
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name FROM skills
|
||||
WHERE (status IS NULL OR status = 'active')
|
||||
AND LOWER(name) LIKE %s
|
||||
ORDER BY CASE WHEN LOWER(name) = %s THEN 0 WHEN LOWER(name) LIKE %s THEN 1 ELSE 2 END,
|
||||
LENGTH(name) ASC
|
||||
LIMIT 1
|
||||
""",
|
||||
(f"%{name.lower()}%", name.lower(), f"{name.lower()}%"),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
continue
|
||||
sid = int(row["id"])
|
||||
w = max(weights.get(sid, 0.0), weight)
|
||||
weights[sid] = w
|
||||
items[sid] = PlanningSkillExpectationItem(
|
||||
skill_id=sid,
|
||||
skill_name=str(row.get("name") or "").strip(),
|
||||
weight=w,
|
||||
source=source,
|
||||
)
|
||||
found = True
|
||||
return found
|
||||
|
||||
|
||||
def _merge_weights_into(
|
||||
weights: Dict[int, float],
|
||||
items: Dict[int, PlanningSkillExpectationItem],
|
||||
incoming: Dict[int, float],
|
||||
*,
|
||||
source: str,
|
||||
skill_rows: Sequence[Tuple[int, str, int]],
|
||||
) -> None:
|
||||
name_by_id = {sid: name for sid, name, _ in skill_rows}
|
||||
for sid, w in incoming.items():
|
||||
if w <= 0:
|
||||
continue
|
||||
merged = max(weights.get(sid, 0.0), float(w))
|
||||
weights[sid] = merged
|
||||
items[sid] = PlanningSkillExpectationItem(
|
||||
skill_id=sid,
|
||||
skill_name=name_by_id.get(sid, f"Skill #{sid}"),
|
||||
weight=merged,
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
def build_planning_skill_expectations(
|
||||
cur,
|
||||
inp: PlanningSkillExpectationInput,
|
||||
*,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
) -> PlanningSkillExpectations:
|
||||
"""Deterministisch: Thema + Text + load_profile → skill_weights."""
|
||||
weights: Dict[int, float] = {}
|
||||
items: Dict[int, PlanningSkillExpectationItem] = {}
|
||||
sources: List[str] = []
|
||||
|
||||
skill_rows = _load_skills_for_text_match(cur)
|
||||
|
||||
if semantic_brief is not None:
|
||||
topic_weights = resolve_semantic_skill_weights(cur, semantic_brief)
|
||||
if topic_weights:
|
||||
sources.append("semantic_topic")
|
||||
_merge_weights_into(
|
||||
weights, items, topic_weights, source="semantic_topic", skill_rows=skill_rows
|
||||
)
|
||||
|
||||
blob = _text_blob(inp)
|
||||
if blob:
|
||||
text_weights = _match_skills_in_text(blob, skill_rows)
|
||||
if text_weights:
|
||||
sources.append("text_match")
|
||||
_merge_weights_into(
|
||||
weights, items, text_weights, source="text_match", skill_rows=skill_rows
|
||||
)
|
||||
|
||||
load_found = False
|
||||
for raw in inp.load_profile or []:
|
||||
key = _norm_load_key(str(raw))
|
||||
terms = _LOAD_PROFILE_SKILL_TERMS.get(key, (str(raw).strip(),) if key else ())
|
||||
if _resolve_skills_by_name_terms(
|
||||
cur, terms, weight=0.88, source="load_profile", weights=weights, items=items
|
||||
):
|
||||
load_found = True
|
||||
if load_found and "load_profile" not in sources:
|
||||
sources.append("load_profile")
|
||||
|
||||
normalized = _normalize_weight_map(weights)
|
||||
out_items = sorted(
|
||||
[
|
||||
PlanningSkillExpectationItem(
|
||||
skill_id=sid,
|
||||
skill_name=items[sid].skill_name,
|
||||
weight=normalized[sid],
|
||||
source=items[sid].source,
|
||||
)
|
||||
for sid in normalized
|
||||
],
|
||||
key=lambda x: (-x.weight, x.skill_name.lower()),
|
||||
)[:8]
|
||||
|
||||
return PlanningSkillExpectations(
|
||||
scope=inp.scope or SCOPE_PROGRESSION_STAGE,
|
||||
skill_weights=normalized,
|
||||
items=out_items,
|
||||
sources=sources,
|
||||
)
|
||||
|
||||
|
||||
def expectation_input_from_progression_stage(
|
||||
*,
|
||||
goal_query: str,
|
||||
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||
resolved_structured: Optional[Mapping[str, Any]] = None,
|
||||
stage_spec: Optional[Mapping[str, Any]] = None,
|
||||
semantic_brief_summary: Optional[Mapping[str, Any]] = None,
|
||||
major_step: Optional[Mapping[str, Any]] = None,
|
||||
) -> PlanningSkillExpectationInput:
|
||||
"""Roadmap-Stufe → PlanningSkillExpectationInput (Progressionsgraph)."""
|
||||
ga = dict(goal_analysis or {})
|
||||
rs = dict(resolved_structured or {})
|
||||
spec = dict(stage_spec or {})
|
||||
brief = dict(semantic_brief_summary or {})
|
||||
major = dict(major_step or {})
|
||||
|
||||
skill_hints: List[str] = []
|
||||
for item in (brief.get("must_phrases") or [])[:4]:
|
||||
s = str(item or "").strip()
|
||||
if s:
|
||||
skill_hints.append(s)
|
||||
|
||||
return PlanningSkillExpectationInput(
|
||||
scope=SCOPE_PROGRESSION_STAGE,
|
||||
primary_topic=str(ga.get("primary_topic") or brief.get("primary_topic") or "").strip(),
|
||||
goal_query=(goal_query or "").strip(),
|
||||
start_situation=str(rs.get("start_situation") or ga.get("start_assumption") or "").strip(),
|
||||
target_state=str(rs.get("target_state") or ga.get("target_state") or "").strip(),
|
||||
stage_learning_goal=str(
|
||||
spec.get("learning_goal") or major.get("learning_goal") or ""
|
||||
).strip(),
|
||||
roadmap_notes=str(rs.get("roadmap_notes") or "").strip(),
|
||||
load_profile=[str(x).strip() for x in (spec.get("load_profile") or []) if str(x).strip()],
|
||||
phase=str(spec.get("phase") or major.get("phase") or "").strip(),
|
||||
skill_hints=skill_hints,
|
||||
)
|
||||
|
||||
|
||||
def expectation_input_from_progression_path(
|
||||
*,
|
||||
goal_query: str,
|
||||
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||
resolved_structured: Optional[Mapping[str, Any]] = None,
|
||||
semantic_brief_summary: Optional[Mapping[str, Any]] = None,
|
||||
) -> PlanningSkillExpectationInput:
|
||||
"""Gesamtpfad-Kontext (z. B. einmaliges Pfad-Profil)."""
|
||||
ga = dict(goal_analysis or {})
|
||||
rs = dict(resolved_structured or {})
|
||||
brief = dict(semantic_brief_summary or {})
|
||||
skill_hints: List[str] = []
|
||||
for item in (brief.get("must_phrases") or [])[:4]:
|
||||
s = str(item or "").strip()
|
||||
if s:
|
||||
skill_hints.append(s)
|
||||
return PlanningSkillExpectationInput(
|
||||
scope=SCOPE_PROGRESSION_PATH,
|
||||
primary_topic=str(ga.get("primary_topic") or brief.get("primary_topic") or "").strip(),
|
||||
goal_query=(goal_query or "").strip(),
|
||||
start_situation=str(rs.get("start_situation") or ga.get("start_assumption") or "").strip(),
|
||||
target_state=str(rs.get("target_state") or ga.get("target_state") or "").strip(),
|
||||
roadmap_notes=str(rs.get("roadmap_notes") or "").strip(),
|
||||
skill_hints=skill_hints,
|
||||
)
|
||||
|
||||
|
||||
def apply_expectations_to_target(target, expectations: PlanningSkillExpectations):
|
||||
"""Merge Erwartungs-Skills in PlanningTargetProfile (Retrieval)."""
|
||||
from planning_exercise_semantics import enrich_target_with_semantic_expectations
|
||||
|
||||
if not expectations.skill_weights:
|
||||
return target
|
||||
return enrich_target_with_semantic_expectations(
|
||||
target, skill_weights=dict(expectations.skill_weights)
|
||||
)
|
||||
|
||||
|
||||
def merge_expectation_skill_weights(
|
||||
base: Dict[int, float],
|
||||
extra: Dict[int, float],
|
||||
*,
|
||||
extra_scale: float = 1.0,
|
||||
) -> Dict[int, float]:
|
||||
merged = _merge_weight_maps(base, extra, scale=extra_scale)
|
||||
return _normalize_weight_map(merged)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SCOPE_FRAMEWORK_SLOT",
|
||||
"SCOPE_PROGRESSION_PATH",
|
||||
"SCOPE_PROGRESSION_STAGE",
|
||||
"SCOPE_TRAINING_SECTION",
|
||||
"PlanningSkillExpectationInput",
|
||||
"PlanningSkillExpectationItem",
|
||||
"PlanningSkillExpectations",
|
||||
"apply_expectations_to_target",
|
||||
"build_planning_skill_expectations",
|
||||
"expectation_input_from_progression_path",
|
||||
"expectation_input_from_progression_stage",
|
||||
"merge_expectation_skill_weights",
|
||||
]
|
||||
|
|
@ -119,6 +119,23 @@ def test_build_gap_fill_goal_text_includes_roadmap_snapshot():
|
|||
assert "timing" in text
|
||||
|
||||
|
||||
def test_build_gap_fill_goal_text_includes_expected_skills():
|
||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||
text = build_gap_fill_goal_text(
|
||||
goal_query="Kumite Beinarbeit",
|
||||
brief=brief,
|
||||
spec={"phase": "vertiefung", "title_hint": "Rhythmen"},
|
||||
roadmap_snapshot={
|
||||
"expected_skills": [
|
||||
{"skill_name": "Timing", "weight": 0.9},
|
||||
{"skill_name": "Distanz", "weight": 0.8},
|
||||
],
|
||||
},
|
||||
)
|
||||
assert "Erwartete Fähigkeiten" in text
|
||||
assert "Timing" in text
|
||||
|
||||
|
||||
def test_build_gap_fill_offer_exposes_context_preview():
|
||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||
offer = build_gap_fill_offer(
|
||||
|
|
|
|||
121
backend/tests/test_planning_skill_expectations.py
Normal file
121
backend/tests/test_planning_skill_expectations.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""Tests wiederverwendbare Fähigkeiten-Erwartungen (Progressionsgraph + später Planung)."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from planning_skill_expectations import (
|
||||
SCOPE_PROGRESSION_PATH,
|
||||
SCOPE_PROGRESSION_STAGE,
|
||||
PlanningSkillExpectationInput,
|
||||
apply_expectations_to_target,
|
||||
build_planning_skill_expectations,
|
||||
expectation_input_from_progression_path,
|
||||
expectation_input_from_progression_stage,
|
||||
)
|
||||
|
||||
|
||||
class _FakeCursor:
|
||||
def __init__(self, skills):
|
||||
self._skills = skills
|
||||
|
||||
def execute(self, query, params=None):
|
||||
del query
|
||||
if params and len(params) >= 2:
|
||||
needle = str(params[0]).strip("%").lower()
|
||||
exact = str(params[1]).lower()
|
||||
matches = [
|
||||
s
|
||||
for s in self._skills
|
||||
if needle in s["name"].lower() or s["name"].lower() == exact
|
||||
]
|
||||
self._row = matches[0] if matches else None
|
||||
else:
|
||||
self._row = None
|
||||
|
||||
def fetchone(self):
|
||||
return self._row
|
||||
|
||||
|
||||
def _fake_skill_rows():
|
||||
return [
|
||||
(1, "Koordination", 0),
|
||||
(2, "Timing", 0),
|
||||
(3, "Distanz", 0),
|
||||
(4, "Kime", 0),
|
||||
]
|
||||
|
||||
|
||||
def test_expectation_input_from_progression_stage_merges_sources(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"planning_skill_expectations._load_skills_for_text_match",
|
||||
lambda cur: _fake_skill_rows(),
|
||||
)
|
||||
inp = expectation_input_from_progression_stage(
|
||||
goal_query="Kumite Beinarbeit",
|
||||
goal_analysis={
|
||||
"primary_topic": "Kumite",
|
||||
"start_assumption": "gleichförmige Steppbewegung",
|
||||
"target_state": "explosiver Angriff",
|
||||
},
|
||||
resolved_structured={"roadmap_notes": "Kindergruppe"},
|
||||
stage_spec={
|
||||
"learning_goal": "variable Rhythmen",
|
||||
"load_profile": ["timing", "distanz"],
|
||||
"phase": "vertiefung",
|
||||
},
|
||||
semantic_brief_summary={"must_phrases": ["Beinarbeit"], "primary_topic": "Kumite"},
|
||||
major_step={"phase": "vertiefung", "learning_goal": "Major-Ziel"},
|
||||
)
|
||||
assert inp.scope == SCOPE_PROGRESSION_STAGE
|
||||
assert inp.primary_topic == "Kumite"
|
||||
assert inp.start_situation == "gleichförmige Steppbewegung"
|
||||
assert inp.load_profile == ["timing", "distanz"]
|
||||
assert "Beinarbeit" in inp.skill_hints
|
||||
|
||||
|
||||
def test_build_planning_skill_expectations_load_profile_and_text(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"planning_skill_expectations._load_skills_for_text_match",
|
||||
lambda cur: _fake_skill_rows(),
|
||||
)
|
||||
cur = _FakeCursor(
|
||||
[
|
||||
{"id": 2, "name": "Timing"},
|
||||
{"id": 3, "name": "Distanz"},
|
||||
]
|
||||
)
|
||||
inp = PlanningSkillExpectationInput(
|
||||
scope=SCOPE_PROGRESSION_STAGE,
|
||||
primary_topic="Kumite",
|
||||
goal_query="Kumite Beinarbeit mit Timing",
|
||||
load_profile=["timing", "distanz"],
|
||||
)
|
||||
exp = build_planning_skill_expectations(cur, inp)
|
||||
assert exp.scope == SCOPE_PROGRESSION_STAGE
|
||||
assert "load_profile" in exp.sources or "text_match" in exp.sources
|
||||
names = {it.skill_name for it in exp.items}
|
||||
assert "Timing" in names or "Distanz" in names
|
||||
assert exp.skill_weights
|
||||
|
||||
|
||||
def test_expectation_input_from_progression_path_scope():
|
||||
inp = expectation_input_from_progression_path(
|
||||
goal_query="Mae Geri Perfektion",
|
||||
goal_analysis={"primary_topic": "Mae Geri"},
|
||||
resolved_structured={"start_situation": "Grundstellung", "target_state": "freier Kick"},
|
||||
)
|
||||
assert inp.scope == SCOPE_PROGRESSION_PATH
|
||||
assert inp.start_situation == "Grundstellung"
|
||||
assert inp.target_state == "freier Kick"
|
||||
|
||||
|
||||
def test_apply_expectations_to_target_noop_without_weights():
|
||||
class _Target:
|
||||
skill_weights = {}
|
||||
|
||||
def to_summary_dict(self, cur):
|
||||
return {}
|
||||
|
||||
target = _Target()
|
||||
from planning_skill_expectations import PlanningSkillExpectations
|
||||
|
||||
empty = PlanningSkillExpectations(scope=SCOPE_PROGRESSION_STAGE, skill_weights={}, items=[], sources=[])
|
||||
assert apply_expectations_to_target(target, empty) is target
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.214"
|
||||
APP_VERSION = "0.8.215"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260607087"
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,13 @@ export function gapOfferContextDisplayLines(offer, fallbackParams = null) {
|
|||
if (Array.isArray(raw.skill_hints) && raw.skill_hints.length) {
|
||||
push('Fähigkeiten-Fokus', raw.skill_hints.slice(0, 3).join(' · '))
|
||||
}
|
||||
if (Array.isArray(raw.expected_skills) && raw.expected_skills.length) {
|
||||
const names = raw.expected_skills
|
||||
.map((s) => String(s?.skill_name || '').trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 5)
|
||||
if (names.length) push('Erwartete Fähigkeiten', names.join(' · '))
|
||||
}
|
||||
push('Trainer-Ergänzungen', raw.gap_trainer_supplements)
|
||||
return lines
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user