Implement Phase B Enhancements for Planning Exercise Profiles
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Added support for section guidance notes and titles in the planning target profile, enabling richer context for exercise suggestions. - Introduced deterministic text-to-catalog signal mapping, allowing for improved integration of planning text signals into the exercise retrieval process. - Implemented a partner-related filter in exercise retrieval, enhancing the relevance of suggested exercises based on user intent. - Updated the retrieval phase to account for text signals, improving the accuracy of exercise recommendations. - Incremented version to 0.8.181 and updated changelog to reflect these significant enhancements in planning AI capabilities.
This commit is contained in:
parent
46fae3da33
commit
a0a891e550
|
|
@ -8,6 +8,10 @@ from __future__ import annotations
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
|
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
|
||||||
|
|
||||||
|
from planning_exercise_text_signals import (
|
||||||
|
load_framework_planning_text_parts,
|
||||||
|
resolve_planning_text_to_catalog_weights,
|
||||||
|
)
|
||||||
from skill_scoring import (
|
from skill_scoring import (
|
||||||
ExerciseOccurrence,
|
ExerciseOccurrence,
|
||||||
collect_unit_exercise_occurrences,
|
collect_unit_exercise_occurrences,
|
||||||
|
|
@ -339,6 +343,8 @@ def build_planning_target_profile(
|
||||||
section_planned_exercise_ids: Optional[Sequence[int]] = None,
|
section_planned_exercise_ids: Optional[Sequence[int]] = None,
|
||||||
anchor_exercise_id: Optional[int],
|
anchor_exercise_id: Optional[int],
|
||||||
intent: str,
|
intent: str,
|
||||||
|
section_guidance_notes: Optional[str] = None,
|
||||||
|
section_title: Optional[str] = None,
|
||||||
) -> PlanningTargetProfile:
|
) -> PlanningTargetProfile:
|
||||||
sources: List[str] = []
|
sources: List[str] = []
|
||||||
focus: Dict[int, float] = {}
|
focus: Dict[int, float] = {}
|
||||||
|
|
@ -414,6 +420,30 @@ def build_planning_target_profile(
|
||||||
tg = _merge_weight_maps(tg, ap.target_group_ids, scale=0.75)
|
tg = _merge_weight_maps(tg, ap.target_group_ids, scale=0.75)
|
||||||
sources.append("anchor_exercise")
|
sources.append("anchor_exercise")
|
||||||
|
|
||||||
|
text_parts: List[str] = []
|
||||||
|
if (section_title or "").strip():
|
||||||
|
text_parts.append(str(section_title).strip())
|
||||||
|
if (section_guidance_notes or "").strip():
|
||||||
|
text_parts.append(str(section_guidance_notes).strip())
|
||||||
|
if fw:
|
||||||
|
text_parts.extend(
|
||||||
|
load_framework_planning_text_parts(
|
||||||
|
cur,
|
||||||
|
int(fw["framework_program_id"]),
|
||||||
|
slot_id=int(fw["slot_id"]) if fw.get("slot_id") else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if text_parts:
|
||||||
|
blob = "\n".join(text_parts)
|
||||||
|
tf, ts, ttt, ttg, tsk = resolve_planning_text_to_catalog_weights(cur, blob)
|
||||||
|
if tf or ts or ttt or ttg or tsk:
|
||||||
|
focus = _merge_weight_maps(focus, tf, scale=0.88)
|
||||||
|
style = _merge_weight_maps(style, ts, scale=0.8)
|
||||||
|
tt = _merge_weight_maps(tt, ttt, scale=0.8)
|
||||||
|
tg = _merge_weight_maps(tg, ttg, scale=0.8)
|
||||||
|
skill_target = _merge_weight_maps(skill_target, tsk, scale=0.92)
|
||||||
|
sources.append("planning_text_signals")
|
||||||
|
|
||||||
skill_target = _normalize_weight_map(skill_target)
|
skill_target = _normalize_weight_map(skill_target)
|
||||||
skill_plan_norm = _normalize_weight_map(skill_plan)
|
skill_plan_norm = _normalize_weight_map(skill_plan)
|
||||||
skill_gap: Dict[int, float] = {}
|
skill_gap: Dict[int, float] = {}
|
||||||
|
|
@ -470,6 +500,8 @@ def score_exercise_against_target(
|
||||||
reasons.append("Deckt Skill-Lücke im bisherigen Plan")
|
reasons.append("Deckt Skill-Lücke im bisherigen Plan")
|
||||||
if "query_intent" in (target.sources or []):
|
if "query_intent" in (target.sources or []):
|
||||||
reasons.append("Passt zur KI-interpretierten Suchanfrage")
|
reasons.append("Passt zur KI-interpretierten Suchanfrage")
|
||||||
|
if "planning_text_signals" in (target.sources or []):
|
||||||
|
reasons.append("Passt zu Abschnitts- oder Rahmen-Zieltext")
|
||||||
|
|
||||||
# Intent-gewichtete Dimensionen (Summe = 1.0)
|
# Intent-gewichtete Dimensionen (Summe = 1.0)
|
||||||
if intent == INTENT_FREE_SEARCH:
|
if intent == INTENT_FREE_SEARCH:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,17 @@ from planning_exercise_profiles import (
|
||||||
|
|
||||||
_MAX_LIBRARY_ROWS = 8000
|
_MAX_LIBRARY_ROWS = 8000
|
||||||
_PROFILE_LOAD_BATCH = 400
|
_PROFILE_LOAD_BATCH = 400
|
||||||
|
_PARTNER_TEXT_MARKERS = ("partner", "paar", "paarweise", "zu zweit")
|
||||||
|
|
||||||
|
|
||||||
|
def _exercise_looks_partner_related(row: Mapping[str, Any]) -> bool:
|
||||||
|
parts = [
|
||||||
|
str(row.get("method_archetype") or ""),
|
||||||
|
str(row.get("title") or ""),
|
||||||
|
str(row.get("summary") or ""),
|
||||||
|
]
|
||||||
|
blob = " ".join(parts).lower()
|
||||||
|
return any(m in blob for m in _PARTNER_TEXT_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
def _skill_jaccard(a: Set[int], b: Set[int]) -> float:
|
def _skill_jaccard(a: Set[int], b: Set[int]) -> float:
|
||||||
|
|
@ -72,7 +83,7 @@ def fetch_all_visible_exercise_rows(
|
||||||
params.extend(ek_filtered)
|
params.extend(ek_filtered)
|
||||||
|
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT e.id, e.title, e.summary,
|
SELECT e.id, e.title, e.summary, e.method_archetype,
|
||||||
(
|
(
|
||||||
SELECT fa.name FROM exercise_focus_areas efa
|
SELECT fa.name FROM exercise_focus_areas efa
|
||||||
JOIN focus_areas fa ON fa.id = efa.focus_area_id
|
JOIN focus_areas fa ON fa.id = efa.focus_area_id
|
||||||
|
|
@ -139,6 +150,7 @@ def rank_visible_library_hits(
|
||||||
anchor_skills = set(pack.get("anchor_skill_ids") or [])
|
anchor_skills = set(pack.get("anchor_skill_ids") or [])
|
||||||
anchor_id = pack.get("anchor_exercise_id")
|
anchor_id = pack.get("anchor_exercise_id")
|
||||||
progression_notes = pack.get("progression_edge_notes") or {}
|
progression_notes = pack.get("progression_edge_notes") or {}
|
||||||
|
requires_partner = pack.get("requires_partner")
|
||||||
|
|
||||||
last_planned_skills: Set[int] = set()
|
last_planned_skills: Set[int] = set()
|
||||||
planned_ids = pack.get("planned_exercise_ids") or []
|
planned_ids = pack.get("planned_exercise_ids") or []
|
||||||
|
|
@ -154,6 +166,10 @@ def rank_visible_library_hits(
|
||||||
eid = int(row["id"])
|
eid = int(row["id"])
|
||||||
if anchor_id and eid == int(anchor_id):
|
if anchor_id and eid == int(anchor_id):
|
||||||
continue
|
continue
|
||||||
|
if requires_partner is True and not _exercise_looks_partner_related(row):
|
||||||
|
continue
|
||||||
|
if requires_partner is False and _exercise_looks_partner_related(row):
|
||||||
|
continue
|
||||||
cand_rows.append(row)
|
cand_rows.append(row)
|
||||||
|
|
||||||
cand_ids = [int(r["id"]) for r in cand_rows]
|
cand_ids = [int(r["id"]) for r in cand_rows]
|
||||||
|
|
|
||||||
|
|
@ -638,14 +638,20 @@ def suggest_planning_exercises(
|
||||||
target=target_profile,
|
target=target_profile,
|
||||||
intent=intent,
|
intent=intent,
|
||||||
intent_weights=weights,
|
intent_weights=weights,
|
||||||
pack=pack,
|
pack={
|
||||||
|
**pack,
|
||||||
|
"requires_partner": query_intent_summary.get("requires_partner"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
text_signals_applied = "planning_text_signals" in (target_profile.sources or [])
|
||||||
|
|
||||||
planned_set = set(pack["planned_exercise_ids"])
|
planned_set = set(pack["planned_exercise_ids"])
|
||||||
|
|
||||||
llm_rank_applied = False
|
llm_rank_applied = False
|
||||||
retrieval_phase = compose_retrieval_phase(
|
retrieval_phase = compose_retrieval_phase(
|
||||||
full_library=full_library_ranked,
|
full_library=full_library_ranked,
|
||||||
|
text_signals=text_signals_applied,
|
||||||
query_intent=query_intent_applied,
|
query_intent=query_intent_applied,
|
||||||
llm_expectation=llm_expectation_applied,
|
llm_expectation=llm_expectation_applied,
|
||||||
llm_rank=False,
|
llm_rank=False,
|
||||||
|
|
@ -681,6 +687,7 @@ def suggest_planning_exercises(
|
||||||
if llm_rank_applied:
|
if llm_rank_applied:
|
||||||
retrieval_phase = compose_retrieval_phase(
|
retrieval_phase = compose_retrieval_phase(
|
||||||
full_library=full_library_ranked,
|
full_library=full_library_ranked,
|
||||||
|
text_signals=text_signals_applied,
|
||||||
query_intent=query_intent_applied,
|
query_intent=query_intent_applied,
|
||||||
llm_expectation=llm_expectation_applied,
|
llm_expectation=llm_expectation_applied,
|
||||||
llm_rank=True,
|
llm_rank=True,
|
||||||
|
|
@ -719,6 +726,7 @@ def suggest_planning_exercises(
|
||||||
"query_intent_summary": query_intent_summary,
|
"query_intent_summary": query_intent_summary,
|
||||||
"retrieval_phase": retrieval_phase,
|
"retrieval_phase": retrieval_phase,
|
||||||
"full_library_ranked": full_library_ranked,
|
"full_library_ranked": full_library_ranked,
|
||||||
|
"text_signals_applied": text_signals_applied,
|
||||||
"profile_preselect_applied": False,
|
"profile_preselect_applied": False,
|
||||||
"llm_rank_applied": llm_rank_applied,
|
"llm_rank_applied": llm_rank_applied,
|
||||||
"llm_intent_applied": query_intent_applied,
|
"llm_intent_applied": query_intent_applied,
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,8 @@ def build_planning_target_with_query_pipeline(
|
||||||
section_planned_exercise_ids=section_planned_exercise_ids or [],
|
section_planned_exercise_ids=section_planned_exercise_ids or [],
|
||||||
anchor_exercise_id=anchor_exercise_id,
|
anchor_exercise_id=anchor_exercise_id,
|
||||||
intent=heuristic_intent,
|
intent=heuristic_intent,
|
||||||
|
section_guidance_notes=(context_summary.get("section_guidance_notes") or None),
|
||||||
|
section_title=(context_summary.get("section_title") or None),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
base = PlanningTargetProfile(sources=["query_only"])
|
base = PlanningTargetProfile(sources=["query_only"])
|
||||||
|
|
@ -387,6 +389,7 @@ def compose_retrieval_phase(
|
||||||
*,
|
*,
|
||||||
full_library: bool = False,
|
full_library: bool = False,
|
||||||
profile_preselect: bool = False,
|
profile_preselect: bool = False,
|
||||||
|
text_signals: bool = False,
|
||||||
query_intent: bool = False,
|
query_intent: bool = False,
|
||||||
llm_expectation: bool = False,
|
llm_expectation: bool = False,
|
||||||
llm_rank: bool = False,
|
llm_rank: bool = False,
|
||||||
|
|
@ -394,6 +397,8 @@ def compose_retrieval_phase(
|
||||||
parts = ["profile_v1"]
|
parts = ["profile_v1"]
|
||||||
if full_library or profile_preselect:
|
if full_library or profile_preselect:
|
||||||
parts.append("full_library")
|
parts.append("full_library")
|
||||||
|
if text_signals:
|
||||||
|
parts.append("text_signals")
|
||||||
if llm_expectation:
|
if llm_expectation:
|
||||||
parts.append("llm_expectation")
|
parts.append("llm_expectation")
|
||||||
elif query_intent:
|
elif query_intent:
|
||||||
|
|
|
||||||
201
backend/planning_exercise_text_signals.py
Normal file
201
backend/planning_exercise_text_signals.py
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
"""
|
||||||
|
Phase B: Deterministische Text→Katalog-Signale für PlanningTargetProfile.
|
||||||
|
|
||||||
|
Mappt Abschnitts-guidance, Rahmen-Ziele/-Notizen und Programmbeschreibung
|
||||||
|
auf Skill-/Katalog-Gewichte (ohne LLM).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
_MIN_SKILL_NAME_LEN = 3
|
||||||
|
_MAX_SKILL_MATCHES = 12
|
||||||
|
_MAX_CATALOG_MATCHES = 6
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_text_blob(*parts: Optional[str]) -> str:
|
||||||
|
chunks: List[str] = []
|
||||||
|
for p in parts:
|
||||||
|
s = (p or "").strip()
|
||||||
|
if s:
|
||||||
|
chunks.append(s)
|
||||||
|
return "\n".join(chunks).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_skills_for_text_match(cur) -> List[Tuple[int, str, int]]:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, name FROM skills
|
||||||
|
WHERE (status IS NULL OR status = 'active')
|
||||||
|
AND name IS NOT NULL AND TRIM(name) <> ''
|
||||||
|
ORDER BY LENGTH(name) DESC, name ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
out: List[Tuple[int, str, int]] = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
name = str(row.get("name") or "").strip()
|
||||||
|
if len(name) < _MIN_SKILL_NAME_LEN:
|
||||||
|
continue
|
||||||
|
out.append((int(row["id"]), name.lower(), len(name)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _load_catalog_names(cur, table: str, id_col: str = "id", name_col: str = "name") -> List[Tuple[int, str, int]]:
|
||||||
|
cur.execute(
|
||||||
|
f"""
|
||||||
|
SELECT {id_col} AS id, {name_col} AS name
|
||||||
|
FROM {table}
|
||||||
|
WHERE {name_col} IS NOT NULL AND TRIM({name_col}) <> ''
|
||||||
|
ORDER BY LENGTH({name_col}) DESC, {name_col} ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
out: List[Tuple[int, str, int]] = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
name = str(row.get("name") or "").strip()
|
||||||
|
if len(name) < 2:
|
||||||
|
continue
|
||||||
|
out.append((int(row["id"]), name.lower(), len(name)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _match_catalog_names_in_text(
|
||||||
|
text: str,
|
||||||
|
catalog_rows: Sequence[Tuple[int, str, int]],
|
||||||
|
*,
|
||||||
|
weight: float = 0.85,
|
||||||
|
limit: int = _MAX_CATALOG_MATCHES,
|
||||||
|
) -> Dict[int, float]:
|
||||||
|
if not text or not catalog_rows:
|
||||||
|
return {}
|
||||||
|
out: Dict[int, float] = {}
|
||||||
|
for cid, name_lower, _ in catalog_rows:
|
||||||
|
if len(out) >= limit:
|
||||||
|
break
|
||||||
|
if len(name_lower) < 2:
|
||||||
|
continue
|
||||||
|
if name_lower in text:
|
||||||
|
out[cid] = max(out.get(cid, 0.0), weight)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _match_skills_in_text(
|
||||||
|
text: str,
|
||||||
|
skill_rows: Sequence[Tuple[int, str, int]],
|
||||||
|
*,
|
||||||
|
limit: int = _MAX_SKILL_MATCHES,
|
||||||
|
) -> Dict[int, float]:
|
||||||
|
if not text or not skill_rows:
|
||||||
|
return {}
|
||||||
|
out: Dict[int, float] = {}
|
||||||
|
for sid, name_lower, name_len in skill_rows:
|
||||||
|
if len(out) >= limit:
|
||||||
|
break
|
||||||
|
if name_len < _MIN_SKILL_NAME_LEN:
|
||||||
|
continue
|
||||||
|
if name_lower in text:
|
||||||
|
w = min(1.0, 0.72 + min(name_len, 20) * 0.012)
|
||||||
|
out[sid] = max(out.get(sid, 0.0), w)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def load_framework_planning_text_parts(
|
||||||
|
cur,
|
||||||
|
framework_program_id: int,
|
||||||
|
*,
|
||||||
|
slot_id: Optional[int] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
"""Sammelt Rahmen-Texte für Text-Signal-Matching."""
|
||||||
|
parts: List[str] = []
|
||||||
|
cur.execute(
|
||||||
|
"SELECT description FROM training_framework_programs WHERE id = %s",
|
||||||
|
(int(framework_program_id),),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row and (row.get("description") or "").strip():
|
||||||
|
parts.append(str(row["description"]).strip())
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT title, notes FROM training_framework_goals
|
||||||
|
WHERE framework_program_id = %s
|
||||||
|
ORDER BY sort_order ASC
|
||||||
|
""",
|
||||||
|
(int(framework_program_id),),
|
||||||
|
)
|
||||||
|
for g in cur.fetchall():
|
||||||
|
t = (g.get("title") or "").strip()
|
||||||
|
n = (g.get("notes") or "").strip()
|
||||||
|
if t:
|
||||||
|
parts.append(t)
|
||||||
|
if n:
|
||||||
|
parts.append(n)
|
||||||
|
|
||||||
|
if slot_id:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT title, notes FROM training_framework_slots WHERE id = %s",
|
||||||
|
(int(slot_id),),
|
||||||
|
)
|
||||||
|
srow = cur.fetchone()
|
||||||
|
if srow:
|
||||||
|
st = (srow.get("title") or "").strip()
|
||||||
|
sn = (srow.get("notes") or "").strip()
|
||||||
|
if st:
|
||||||
|
parts.append(st)
|
||||||
|
if sn:
|
||||||
|
parts.append(sn)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_planning_text_to_catalog_weights(
|
||||||
|
cur,
|
||||||
|
text_blob: str,
|
||||||
|
) -> Tuple[Dict[int, float], Dict[int, float], Dict[int, float], Dict[int, float], Dict[int, float]]:
|
||||||
|
"""
|
||||||
|
Returns: focus, style, training_type, target_group, skill weight maps.
|
||||||
|
"""
|
||||||
|
text = _normalize_text_blob(text_blob)
|
||||||
|
if not text or len(text) < 3:
|
||||||
|
return {}, {}, {}, {}, {}
|
||||||
|
|
||||||
|
skill_rows = _load_skills_for_text_match(cur)
|
||||||
|
focus_rows = _load_catalog_names(cur, "focus_areas")
|
||||||
|
style_rows = _load_catalog_names(cur, "style_directions")
|
||||||
|
tt_rows = _load_catalog_names(cur, "training_types")
|
||||||
|
tg_rows = _load_catalog_names(cur, "target_groups")
|
||||||
|
|
||||||
|
skills = _match_skills_in_text(text, skill_rows)
|
||||||
|
focus = _match_catalog_names_in_text(text, focus_rows, weight=0.88)
|
||||||
|
style = _match_catalog_names_in_text(text, style_rows, weight=0.82)
|
||||||
|
tt = _match_catalog_names_in_text(text, tt_rows, weight=0.82)
|
||||||
|
tg = _match_catalog_names_in_text(text, tg_rows, weight=0.8)
|
||||||
|
|
||||||
|
if re.search(r"\bpartner\b|\bpaar\b|\bpaarweise\b|\bzu zweit\b", text):
|
||||||
|
for gid, name_lower, _ in tg_rows:
|
||||||
|
if "partner" in name_lower or "paar" in name_lower:
|
||||||
|
tg[gid] = max(tg.get(gid, 0.0), 0.9)
|
||||||
|
break
|
||||||
|
|
||||||
|
return focus, style, tt, tg, skills
|
||||||
|
|
||||||
|
|
||||||
|
def merge_text_signal_summary(
|
||||||
|
summary: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
text_sources: Sequence[str],
|
||||||
|
matched_skills: Sequence[Mapping[str, Any]],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
out = dict(summary)
|
||||||
|
if text_sources:
|
||||||
|
out["text_signal_sources"] = list(text_sources)
|
||||||
|
if matched_skills:
|
||||||
|
out["text_signal_skills"] = list(matched_skills)[:8]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"load_framework_planning_text_parts",
|
||||||
|
"merge_text_signal_summary",
|
||||||
|
"resolve_planning_text_to_catalog_weights",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""Tests Partner-Filter im Planungs-Retrieval."""
|
||||||
|
from planning_exercise_retrieval import _exercise_looks_partner_related
|
||||||
|
|
||||||
|
|
||||||
|
def test_exercise_partner_heuristic():
|
||||||
|
assert _exercise_looks_partner_related({"title": "Partner-Fangspiel", "summary": ""})
|
||||||
|
assert not _exercise_looks_partner_related({"title": "Kihon Solo", "summary": "Allein"})
|
||||||
47
backend/tests/test_planning_exercise_text_signals.py
Normal file
47
backend/tests/test_planning_exercise_text_signals.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""Tests Phase B: Text-Signale für PlanningTargetProfile."""
|
||||||
|
from planning_exercise_text_signals import resolve_planning_text_to_catalog_weights
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_planning_text_matches_skill_and_partner_hint():
|
||||||
|
class _Cur:
|
||||||
|
def execute(self, sql, params=None):
|
||||||
|
self._sql = sql
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
sql = getattr(self, "_sql", "")
|
||||||
|
if "FROM skills" in sql:
|
||||||
|
return [
|
||||||
|
{"id": 5, "name": "Kime"},
|
||||||
|
{"id": 8, "name": "Schnellkraft"},
|
||||||
|
{"id": 2, "name": "Ab"},
|
||||||
|
]
|
||||||
|
if "FROM focus_areas" in sql:
|
||||||
|
return [{"id": 10, "name": "Karate Technik"}]
|
||||||
|
if "FROM style_directions" in sql:
|
||||||
|
return []
|
||||||
|
if "FROM training_types" in sql:
|
||||||
|
return []
|
||||||
|
if "FROM target_groups" in sql:
|
||||||
|
return [{"id": 3, "name": "Partnerübung"}]
|
||||||
|
return []
|
||||||
|
|
||||||
|
focus, style, tt, tg, skills = resolve_planning_text_to_catalog_weights(
|
||||||
|
_Cur(),
|
||||||
|
"Abschnitt: Kime vertiefen mit Partnerübung",
|
||||||
|
)
|
||||||
|
assert skills.get(5, 0) > 0
|
||||||
|
assert 8 not in skills
|
||||||
|
assert tg.get(3, 0) > 0
|
||||||
|
assert not style and not tt
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_planning_text_empty():
|
||||||
|
class _Cur:
|
||||||
|
def execute(self, sql, params=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fetchall(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
focus, style, tt, tg, skills = resolve_planning_text_to_catalog_weights(_Cur(), " ")
|
||||||
|
assert not focus and not skills
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.180"
|
APP_VERSION = "0.8.181"
|
||||||
BUILD_DATE = "2026-05-23"
|
BUILD_DATE = "2026-05-23"
|
||||||
DB_SCHEMA_VERSION = "20260531074"
|
DB_SCHEMA_VERSION = "20260531074"
|
||||||
|
|
||||||
|
|
@ -29,7 +29,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.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay
|
"exercises": "2.37.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay
|
||||||
"planning_exercise_suggest": "0.8.0", # Phase A: Voll-Library-Ranking gegen Erwartungsprofil
|
"planning_exercise_suggest": "0.9.0", # Phase B: Text-Signale guidance/Rahmen-Ziele; requires_partner-Filter
|
||||||
"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
|
||||||
|
|
@ -44,6 +44,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.181",
|
||||||
|
"date": "2026-05-23",
|
||||||
|
"changes": [
|
||||||
|
"Planungs-KI Phase B: guidance_notes + Rahmen-Ziele/Notizen → Text-Signale im Erwartungsprofil (planning_text_signals).",
|
||||||
|
"requires_partner aus Intent filtert Übungen; retrieval_phase +text_signals; Grund „Passt zu Abschnitts-/Rahmen-Zieltext“.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.180",
|
"version": "0.8.180",
|
||||||
"date": "2026-05-23",
|
"date": "2026-05-23",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user