Refactor Planning Exercise Path Logic and Enhance Semantic Gating
All checks were successful
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) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
All checks were successful
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) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Replaced the manual path selection logic with a new `pick_best_path_hit` function to streamline the process of selecting the best exercise based on semantic scores and gating criteria. - Updated the semantic gating logic to apply a soft penalty for off-topic exercises, improving the flexibility of exercise selection. - Enhanced the handling of title, summary, and goal parameters in semantic checks to ensure more accurate relevance assessments. - Incremented version to 0.8.189 and updated changelog to reflect these improvements in planning AI functionality.
This commit is contained in:
parent
5b73d1a1f5
commit
8d1dd59c3c
|
|
@ -28,6 +28,7 @@ from planning_exercise_semantics import (
|
|||
build_semantic_brief,
|
||||
enrich_target_with_semantic_expectations,
|
||||
exercise_passes_path_semantic_gate,
|
||||
pick_best_path_hit,
|
||||
resolve_semantic_skill_weights,
|
||||
step_phase_for_index,
|
||||
step_retrieval_query,
|
||||
|
|
@ -62,25 +63,7 @@ def _pick_best_path_hit(
|
|||
*,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
best: Optional[Dict[str, Any]] = None
|
||||
best_key: Tuple[float, float] = (-1.0, -1.0)
|
||||
for hit in hits:
|
||||
eid = int(hit["id"])
|
||||
if eid in used_exercise_ids:
|
||||
continue
|
||||
sem = float(hit.get("semantic_score") or 0.0)
|
||||
if semantic_brief and not exercise_passes_path_semantic_gate(
|
||||
semantic_score=sem,
|
||||
title=str(hit.get("title") or ""),
|
||||
brief=semantic_brief,
|
||||
):
|
||||
continue
|
||||
score = float(hit.get("score") or 0.0)
|
||||
key = (sem, score)
|
||||
if key > best_key:
|
||||
best_key = key
|
||||
best = hit
|
||||
return best
|
||||
return pick_best_path_hit(hits, used_exercise_ids, semantic_brief=semantic_brief)
|
||||
|
||||
|
||||
def _build_path_target_profile(
|
||||
|
|
@ -327,10 +310,12 @@ def _make_bridge_search_fn(
|
|||
if exercise_passes_path_semantic_gate(
|
||||
semantic_score=float(h.get("semantic_score") or 0.0),
|
||||
title=str(h.get("title") or ""),
|
||||
summary=str(h.get("summary") or ""),
|
||||
brief=semantic_brief,
|
||||
strict=False,
|
||||
)
|
||||
]
|
||||
return gated or hits[:8]
|
||||
return gated or hits[:12]
|
||||
|
||||
return _bridge_search
|
||||
|
||||
|
|
|
|||
|
|
@ -286,10 +286,15 @@ def rank_visible_library_hits(
|
|||
and not exercise_passes_path_semantic_gate(
|
||||
semantic_score=semantic_score,
|
||||
title=str(row.get("title") or ""),
|
||||
summary=str(row.get("summary") or ""),
|
||||
goal=goals_by_ex.get(eid, ""),
|
||||
brief=semantic_brief,
|
||||
strict=True,
|
||||
)
|
||||
):
|
||||
continue
|
||||
score_penalty = 0.42
|
||||
else:
|
||||
score_penalty = 0.0
|
||||
|
||||
score = (
|
||||
weights.get("semantic", 0.0) * semantic_score
|
||||
|
|
@ -300,6 +305,7 @@ def rank_visible_library_hits(
|
|||
+ weights["profile"] * profile_score
|
||||
+ weights["repeat_unit"] * repeat_unit
|
||||
+ weights["repeat_group"] * repeat_group
|
||||
- score_penalty
|
||||
)
|
||||
|
||||
reasons: List[str] = []
|
||||
|
|
|
|||
|
|
@ -421,15 +421,22 @@ def _blob_from_fields(
|
|||
return " ".join(p for p in parts if p).lower()
|
||||
|
||||
|
||||
def _compact_alpha(text: str) -> str:
|
||||
return re.sub(r"[^a-z0-9äöüß]+", "", (text or "").lower())
|
||||
|
||||
|
||||
def _phrase_in_blob(phrase: str, blob: str) -> bool:
|
||||
ph = _normalize_phrase(phrase)
|
||||
if not ph or not blob:
|
||||
return False
|
||||
if ph in blob:
|
||||
low = blob.lower()
|
||||
if ph in low:
|
||||
return True
|
||||
if _compact_alpha(ph) and _compact_alpha(ph) in _compact_alpha(low):
|
||||
return True
|
||||
if " " not in ph:
|
||||
return bool(re.search(rf"\b{re.escape(ph)}\b", blob))
|
||||
return ph in blob
|
||||
return bool(re.search(rf"\b{re.escape(ph)}\b", low))
|
||||
return ph in low
|
||||
|
||||
|
||||
def score_exercise_semantic_relevance(
|
||||
|
|
@ -602,17 +609,95 @@ def exercise_passes_path_semantic_gate(
|
|||
semantic_score: float,
|
||||
title: str,
|
||||
brief: PlanningSemanticBrief,
|
||||
summary: str = "",
|
||||
goal: str = "",
|
||||
strict: bool = True,
|
||||
) -> bool:
|
||||
if brief.semantic_strength < 0.55:
|
||||
return True
|
||||
if semantic_score >= 0.20:
|
||||
|
||||
blob = _blob_from_fields(title, summary, goal, [])
|
||||
min_score = 0.18 if strict else 0.06
|
||||
if semantic_score >= min_score:
|
||||
return True
|
||||
|
||||
topic = brief.primary_topic or ""
|
||||
if topic and _phrase_in_blob(topic, (title or "").lower()):
|
||||
if topic and _phrase_in_blob(topic, blob):
|
||||
return True
|
||||
|
||||
if not strict:
|
||||
# Mae Geri oft im Fließtext, nicht im Titel
|
||||
if semantic_score >= 0.04 and topic and _phrase_in_blob(topic, blob):
|
||||
return True
|
||||
parts = topic.split()
|
||||
if len(parts) >= 2 and all(_phrase_in_blob(p, blob) for p in parts):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def pick_best_path_hit(
|
||||
hits: List[Dict[str, Any]],
|
||||
used_exercise_ids: Set[int],
|
||||
*,
|
||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Gestufte Auswahl: strikt → relaxed → bester Semantik-Score."""
|
||||
if not hits:
|
||||
return None
|
||||
|
||||
def _scan(*, strict: bool) -> Optional[Dict[str, Any]]:
|
||||
best: Optional[Dict[str, Any]] = None
|
||||
best_key: Tuple[float, float] = (-1.0, -1.0)
|
||||
for hit in hits:
|
||||
eid = int(hit["id"])
|
||||
if eid in used_exercise_ids:
|
||||
continue
|
||||
sem = float(hit.get("semantic_score") or 0.0)
|
||||
if semantic_brief and not exercise_passes_path_semantic_gate(
|
||||
semantic_score=sem,
|
||||
title=str(hit.get("title") or ""),
|
||||
summary=str(hit.get("summary") or ""),
|
||||
goal="",
|
||||
brief=semantic_brief,
|
||||
strict=strict,
|
||||
):
|
||||
continue
|
||||
score = float(hit.get("score") or 0.0)
|
||||
key = (sem, score)
|
||||
if key > best_key:
|
||||
best_key = key
|
||||
best = hit
|
||||
return best
|
||||
|
||||
chosen = _scan(strict=True)
|
||||
if chosen:
|
||||
return chosen
|
||||
chosen = _scan(strict=False)
|
||||
if chosen:
|
||||
return chosen
|
||||
|
||||
# Notfall: bester verbleibender Treffer mit Semantik > 0 (Thema trotzdem priorisieren)
|
||||
fallback: Optional[Dict[str, Any]] = None
|
||||
fallback_key: Tuple[float, float] = (-1.0, -1.0)
|
||||
for hit in hits:
|
||||
eid = int(hit["id"])
|
||||
if eid in used_exercise_ids:
|
||||
continue
|
||||
sem = float(hit.get("semantic_score") or 0.0)
|
||||
score = float(hit.get("score") or 0.0)
|
||||
if sem <= 0 and semantic_brief and semantic_brief.primary_topic:
|
||||
topic = semantic_brief.primary_topic
|
||||
blob = (str(hit.get("title") or "") + " " + str(hit.get("summary") or "")).lower()
|
||||
if not _phrase_in_blob(topic, blob):
|
||||
continue
|
||||
key = (sem, score)
|
||||
if key > fallback_key:
|
||||
fallback_key = key
|
||||
fallback = hit
|
||||
return fallback
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PlanningSemanticBrief",
|
||||
"apply_dynamic_retrieval_weights",
|
||||
|
|
@ -622,6 +707,7 @@ __all__ = [
|
|||
"enrich_target_with_semantic_expectations",
|
||||
"exercise_passes_path_semantic_gate",
|
||||
"merge_semantic_brief_llm",
|
||||
"pick_best_path_hit",
|
||||
"resolve_semantic_skill_weights",
|
||||
"score_exercise_semantic_relevance",
|
||||
"semantic_core_phrases",
|
||||
|
|
|
|||
|
|
@ -14,10 +14,36 @@ def test_pick_best_path_hit_prefers_semantic_score():
|
|||
assert chosen["id"] == 2
|
||||
|
||||
|
||||
def test_pick_best_path_hit_skips_off_topic_when_gate():
|
||||
brief = build_semantic_brief("Mae Geri")
|
||||
hits = [{"id": 1, "title": "Kumite Grundstellung", "score": 0.9, "semantic_score": 0.05}]
|
||||
assert _pick_best_path_hit(hits, set(), semantic_brief=brief) is None
|
||||
def test_phrase_compact_match_maegeri():
|
||||
from planning_exercise_semantics import _phrase_in_blob
|
||||
|
||||
assert _phrase_in_blob("mae geri", "Erlernen des Mae-Geri aus Einzelbewegungen")
|
||||
assert _phrase_in_blob("mae geri", "Maegeri Kihon")
|
||||
|
||||
|
||||
def test_pick_best_path_hit_fallback_title_only_in_summary():
|
||||
from planning_exercise_semantics import pick_best_path_hit
|
||||
|
||||
brief = build_semantic_brief("Mae Geri Perfektion")
|
||||
hits = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Kumite Stellungen",
|
||||
"summary": "",
|
||||
"score": 0.9,
|
||||
"semantic_score": 0.02,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Einzelbewegungen",
|
||||
"summary": "Schrittweise Erlernen des Mae Geri",
|
||||
"score": 0.5,
|
||||
"semantic_score": 0.08,
|
||||
},
|
||||
]
|
||||
chosen = pick_best_path_hit(hits, set(), semantic_brief=brief)
|
||||
assert chosen is not None
|
||||
assert int(chosen["id"]) == 2
|
||||
|
||||
|
||||
def test_pick_best_path_hit_skips_used():
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.188"
|
||||
APP_VERSION = "0.8.189"
|
||||
BUILD_DATE = "2026-05-23"
|
||||
DB_SCHEMA_VERSION = "20260531074"
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ MODULE_VERSIONS = {
|
|||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.37.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay
|
||||
"planning_exercise_suggest": "0.15.1", # Pfad: fixes Semantik-Gate + Skill-Erwartungsprofil
|
||||
"planning_exercise_suggest": "0.15.2", # Pfad-Gate: soft penalty + gestufter Fallback
|
||||
"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
|
||||
|
|
@ -44,6 +44,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.189",
|
||||
"date": "2026-05-23",
|
||||
"changes": [
|
||||
"Fix Pfad-Builder: Semantik-Gate zu strikt — soft penalty statt Hard-Filter, gestufter Fallback.",
|
||||
"Mae-Geri-Matching: Titel/Summary/Goal, Mae-Geri/Maegeri-Schreibweisen.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.188",
|
||||
"date": "2026-05-23",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user