Enhance Planning Intent Context and Stage Specification Finalization
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `intent_context` and `semantic_brief` parameters in `try_llm_stage_specs` to improve context handling for stage specifications. - Updated `build_goal_analysis` to extract explicit exclusions from goal queries, enhancing constraint management. - Enhanced `roadmap_context_from_override` to enrich semantic briefs with path constraints and finalize stage specifications with intent context. - Incremented application version to reflect these updates.
This commit is contained in:
parent
3c12363b8f
commit
4ef3f00e6b
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- Migration 089: Planungs-Intent — Zielanalyse + Stufenspecs (anti_patterns, success_criteria)
|
||||||
|
|
||||||
|
UPDATE ai_prompts SET
|
||||||
|
description = 'Phase A: Ist-/Soll, Erfolgskriterien und explizite Ausschlüsse (ohne Gruppenkontext).',
|
||||||
|
template = $t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen.
|
||||||
|
|
||||||
|
Anfrage: {{goal_query}}
|
||||||
|
Semantic Brief: {{semantic_brief_json}}
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
- Keine Gruppenanalyse — nur didaktischer Pfad für die Technik/das Thema.
|
||||||
|
- Explizite Negationen aus der Anfrage (ohne/kein/nicht …) in constraints.excluded_themes übernehmen — nicht raten.
|
||||||
|
- success_criteria: messbar, für späteres Übungs-Matching (Titel + Kurzbeschreibung + Übungsziel).
|
||||||
|
|
||||||
|
Antworte NUR mit JSON:
|
||||||
|
{
|
||||||
|
"primary_topic": "Hauptthema",
|
||||||
|
"start_assumption": "Voraussetzungen für den Einstieg",
|
||||||
|
"target_state": "Konkreter Zielzustand der Progression",
|
||||||
|
"success_criteria": ["messbare Kriterien entlang des Pfads"],
|
||||||
|
"constraints": {
|
||||||
|
"partner_required": false,
|
||||||
|
"excluded_themes": ["wörtliche Negationen, z. B. keine Kumite-Anwendung"],
|
||||||
|
"trainer_notes": "optional: Fokus aus Ergänzungen"
|
||||||
|
}
|
||||||
|
}$t$,
|
||||||
|
default_template = template
|
||||||
|
WHERE slug = 'planning_progression_goal_analysis';
|
||||||
|
|
||||||
|
UPDATE ai_prompts SET
|
||||||
|
description = 'Phase C: Belastung, Übungstyp, Erfolgskriterien und anti_patterns je Major Step — für Retrieval-Matching.',
|
||||||
|
template = $t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
|
||||||
|
|
||||||
|
Anfrage: {{goal_query}}
|
||||||
|
Zielanalyse: {{goal_analysis_json}}
|
||||||
|
Major Steps: {{major_steps_json}}
|
||||||
|
Planungs-Intent (Pfadweite Regeln): {{intent_context_json}}
|
||||||
|
Semantic Brief: {{semantic_brief_json}}
|
||||||
|
|
||||||
|
Aufgabe je Major Step — Felder für automatisches Übungs-Matching (nicht nur Titel):
|
||||||
|
- learning_goal: messbares Stufen-Lernziel (was die Übung bringen soll)
|
||||||
|
- load_profile: z. B. koordination, präzision, kraft, athletik
|
||||||
|
- exercise_type: kihon_einzel | partner_drill | kombination | kraft_auxiliary
|
||||||
|
- success_criteria: 2–4 prüfbare Kriterien an Kurzbeschreibung + Übungsziel (nicht nur Technikname im Titel)
|
||||||
|
- anti_patterns: 2–5 Dinge, die für diese Stufe unpassend sind
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
1. Jede explicit_exclusions / excluded_themes aus intent_context und Zielanalyse MUSS in anti_patterns jeder Stufe vorkommen (umformuliert ok).
|
||||||
|
2. Keine neuen Ausschlüsse erfinden, die nicht in Anfrage/Intent/Zielanalyse stehen.
|
||||||
|
3. success_criteria Pfad-weit + stufenspezifisch kombinieren.
|
||||||
|
4. partner_drill nur wenn Partner/Kumite nicht ausgeschlossen ist.
|
||||||
|
|
||||||
|
Antworte NUR mit JSON:
|
||||||
|
{
|
||||||
|
"stage_specs": [
|
||||||
|
{
|
||||||
|
"major_step_index": 0,
|
||||||
|
"learning_goal": "…",
|
||||||
|
"load_profile": ["koordination"],
|
||||||
|
"exercise_type": "kihon_einzel",
|
||||||
|
"success_criteria": ["…"],
|
||||||
|
"anti_patterns": ["…"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}$t$,
|
||||||
|
default_template = template
|
||||||
|
WHERE slug = 'planning_progression_stage_spec';
|
||||||
216
backend/planning_intent_context.py
Normal file
216
backend/planning_intent_context.py
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
"""
|
||||||
|
Gemeinsame Intent-Anreicherung für Planungs-Retrieval.
|
||||||
|
|
||||||
|
Progressionsgraph (Roadmap stage_specs) und später Trainingsplanung
|
||||||
|
(Abschnitt/Slot) nutzen dieselben Bausteine:
|
||||||
|
Intent-Kontext bauen → Specs finalisieren → Matching-Gates.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
|
from planning_exercise_semantics import (
|
||||||
|
PlanningSemanticBrief,
|
||||||
|
resolve_path_anti_patterns,
|
||||||
|
)
|
||||||
|
|
||||||
|
_NEGATION_CLAUSE_RE = re.compile(
|
||||||
|
r"\b(?:ohne|kein(?:e|en|er|em)?|nicht)\s+[^,.;\n]+",
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_explicit_exclusions(*texts: Optional[str]) -> List[str]:
|
||||||
|
"""Lesbare Negationsklauseln aus Freitext (ohne Themen-Raten)."""
|
||||||
|
out: List[str] = []
|
||||||
|
for raw in texts:
|
||||||
|
s = (raw or "").strip()
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
for m in _NEGATION_CLAUSE_RE.finditer(s):
|
||||||
|
clause = m.group(0).strip().rstrip(".,;")
|
||||||
|
if clause and clause.lower() not in {x.lower() for x in out}:
|
||||||
|
out.append(clause[:220])
|
||||||
|
return out[:12]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanningIntentContext:
|
||||||
|
"""Pfad-/Abschnittsweiter Planungs-Intent — domänenneutral."""
|
||||||
|
|
||||||
|
source_query: str = ""
|
||||||
|
primary_topic: str = ""
|
||||||
|
path_anti_patterns: List[str] = field(default_factory=list)
|
||||||
|
path_success_criteria: List[str] = field(default_factory=list)
|
||||||
|
explicit_exclusions: List[str] = field(default_factory=list)
|
||||||
|
context_notes: str = ""
|
||||||
|
|
||||||
|
def to_api_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"source_query": self.source_query,
|
||||||
|
"primary_topic": self.primary_topic,
|
||||||
|
"path_anti_patterns": self.path_anti_patterns[:16],
|
||||||
|
"path_success_criteria": self.path_success_criteria[:10],
|
||||||
|
"explicit_exclusions": self.explicit_exclusions[:10],
|
||||||
|
"context_notes": self.context_notes[:1200] or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_planning_intent_context(
|
||||||
|
goal_query: str,
|
||||||
|
*,
|
||||||
|
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||||
|
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||||
|
extra_context: Optional[str] = None,
|
||||||
|
primary_topic: Optional[str] = None,
|
||||||
|
) -> PlanningIntentContext:
|
||||||
|
"""Intent aus Anfrage, Zielanalyse und optionalem Kontext — ohne Sonderregeln pro Thema."""
|
||||||
|
ga = dict(goal_analysis or {})
|
||||||
|
notes_parts = [extra_context or ""]
|
||||||
|
constraints = ga.get("constraints") if isinstance(ga.get("constraints"), dict) else {}
|
||||||
|
if isinstance(constraints, dict):
|
||||||
|
trainer_notes = str(constraints.get("trainer_notes") or "").strip()
|
||||||
|
if trainer_notes:
|
||||||
|
notes_parts.append(trainer_notes)
|
||||||
|
|
||||||
|
combined_notes = " ".join(p.strip() for p in notes_parts if p and p.strip())
|
||||||
|
explicit = extract_explicit_exclusions(goal_query, combined_notes or None)
|
||||||
|
ga_excluded = constraints.get("excluded_themes") if isinstance(constraints, dict) else None
|
||||||
|
if isinstance(ga_excluded, list):
|
||||||
|
for item in ga_excluded:
|
||||||
|
s = str(item or "").strip()
|
||||||
|
if s and s.lower() not in {x.lower() for x in explicit}:
|
||||||
|
explicit.append(s[:220])
|
||||||
|
|
||||||
|
path_anti = resolve_path_anti_patterns(
|
||||||
|
goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
extra_context=combined_notes or None,
|
||||||
|
)
|
||||||
|
path_success: List[str] = []
|
||||||
|
for item in ga.get("success_criteria") or []:
|
||||||
|
s = str(item or "").strip()
|
||||||
|
if s and s not in path_success:
|
||||||
|
path_success.append(s[:240])
|
||||||
|
target = str(ga.get("target_state") or "").strip()
|
||||||
|
if target and len(target) >= 8:
|
||||||
|
line = f"Zielzustand erreichbar: {target[:200]}"
|
||||||
|
if line not in path_success:
|
||||||
|
path_success.append(line)
|
||||||
|
|
||||||
|
topic = (primary_topic or ga.get("primary_topic") or "").strip()
|
||||||
|
if semantic_brief and not topic:
|
||||||
|
topic = (semantic_brief.primary_topic or "").strip()
|
||||||
|
|
||||||
|
return PlanningIntentContext(
|
||||||
|
source_query=(goal_query or "").strip(),
|
||||||
|
primary_topic=topic,
|
||||||
|
path_anti_patterns=path_anti,
|
||||||
|
path_success_criteria=path_success,
|
||||||
|
explicit_exclusions=explicit,
|
||||||
|
context_notes=combined_notes[:1200],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_preserve(items: Sequence[str], *, limit: int = 14) -> List[str]:
|
||||||
|
out: List[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw in items:
|
||||||
|
s = str(raw or "").strip()
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
key = s.lower()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(s[:240])
|
||||||
|
if len(out) >= limit:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def finalize_stage_spec_artifact(
|
||||||
|
spec: "StageSpecArtifact",
|
||||||
|
*,
|
||||||
|
major_step: Optional["MajorStep"] = None,
|
||||||
|
intent: PlanningIntentContext,
|
||||||
|
) -> "StageSpecArtifact":
|
||||||
|
"""Pfad-Intent in eine Stufenspezifikation mergen (LLM oder heuristisch)."""
|
||||||
|
from planning_progression_roadmap import MajorStep, StageSpecArtifact
|
||||||
|
|
||||||
|
learning_goal = (spec.learning_goal or (major_step.learning_goal if major_step else "")).strip()
|
||||||
|
phase = (major_step.phase if major_step else "").strip().lower()
|
||||||
|
|
||||||
|
anti = _dedupe_preserve(
|
||||||
|
[
|
||||||
|
*(spec.anti_patterns or []),
|
||||||
|
*intent.explicit_exclusions,
|
||||||
|
*intent.path_anti_patterns,
|
||||||
|
],
|
||||||
|
limit=14,
|
||||||
|
)
|
||||||
|
success = _dedupe_preserve(
|
||||||
|
[
|
||||||
|
*(spec.success_criteria or []),
|
||||||
|
*intent.path_success_criteria,
|
||||||
|
(
|
||||||
|
f"Übung liefert messbar: {learning_goal[:160]}"
|
||||||
|
if learning_goal
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
(
|
||||||
|
f"Kurzbeschreibung und Übungsziel passen zur Phase {phase}"
|
||||||
|
if phase
|
||||||
|
else "Kurzbeschreibung und Übungsziel passen zum Stufen-Lernziel"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
limit=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
idx = spec.major_step_index
|
||||||
|
if major_step is not None:
|
||||||
|
idx = major_step.index
|
||||||
|
|
||||||
|
return StageSpecArtifact(
|
||||||
|
major_step_index=idx,
|
||||||
|
learning_goal=learning_goal,
|
||||||
|
load_profile=list(spec.load_profile or []),
|
||||||
|
exercise_type=(spec.exercise_type or "").strip(),
|
||||||
|
success_criteria=success,
|
||||||
|
anti_patterns=anti,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def finalize_stage_specs_with_intent(
|
||||||
|
specs: Sequence["StageSpecArtifact"],
|
||||||
|
major_steps: Sequence["MajorStep"],
|
||||||
|
*,
|
||||||
|
intent: PlanningIntentContext,
|
||||||
|
fallback_specs: Optional[Sequence["StageSpecArtifact"]] = None,
|
||||||
|
) -> List["StageSpecArtifact"]:
|
||||||
|
"""Alle Stufen mit gleichem Pfad-Intent anreichern; fehlende Indizes aus Fallback."""
|
||||||
|
from planning_progression_roadmap import MajorStep, StageSpecArtifact
|
||||||
|
|
||||||
|
by_idx = {int(s.major_step_index): s for s in specs}
|
||||||
|
fallback_by_idx = {int(s.major_step_index): s for s in (fallback_specs or [])}
|
||||||
|
out: List[StageSpecArtifact] = []
|
||||||
|
for major in major_steps:
|
||||||
|
raw = by_idx.get(major.index) or fallback_by_idx.get(major.index)
|
||||||
|
if raw is None:
|
||||||
|
raw = StageSpecArtifact(
|
||||||
|
major_step_index=major.index,
|
||||||
|
learning_goal=major.learning_goal,
|
||||||
|
)
|
||||||
|
out.append(finalize_stage_spec_artifact(raw, major_step=major, intent=intent))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PlanningIntentContext",
|
||||||
|
"build_planning_intent_context",
|
||||||
|
"extract_explicit_exclusions",
|
||||||
|
"finalize_stage_spec_artifact",
|
||||||
|
"finalize_stage_specs_with_intent",
|
||||||
|
]
|
||||||
|
|
@ -298,6 +298,8 @@ def try_llm_stage_specs(
|
||||||
goal_query: str,
|
goal_query: str,
|
||||||
goal_analysis: GoalAnalysisArtifact,
|
goal_analysis: GoalAnalysisArtifact,
|
||||||
major_steps: Sequence[MajorStep],
|
major_steps: Sequence[MajorStep],
|
||||||
|
intent_context: Optional[Mapping[str, Any]] = None,
|
||||||
|
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||||
) -> Tuple[Optional[List[StageSpecArtifact]], bool]:
|
) -> Tuple[Optional[List[StageSpecArtifact]], bool]:
|
||||||
obj = _run_prompt_json(
|
obj = _run_prompt_json(
|
||||||
cur,
|
cur,
|
||||||
|
|
@ -306,6 +308,11 @@ def try_llm_stage_specs(
|
||||||
"goal_query": goal_query or "",
|
"goal_query": goal_query or "",
|
||||||
"goal_analysis_json": json.dumps(goal_analysis.model_dump(), ensure_ascii=False),
|
"goal_analysis_json": json.dumps(goal_analysis.model_dump(), ensure_ascii=False),
|
||||||
"major_steps_json": json.dumps([m.model_dump() for m in major_steps], ensure_ascii=False),
|
"major_steps_json": json.dumps([m.model_dump() for m in major_steps], ensure_ascii=False),
|
||||||
|
"intent_context_json": json.dumps(dict(intent_context or {}), ensure_ascii=False),
|
||||||
|
"semantic_brief_json": json.dumps(
|
||||||
|
brief_to_summary_dict(semantic_brief) if semantic_brief else {},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not obj:
|
if not obj:
|
||||||
|
|
@ -522,9 +529,14 @@ def build_goal_analysis(
|
||||||
if notes.strip():
|
if notes.strip():
|
||||||
criteria.append(f"Berücksichtigung: {notes.strip()[:200]}")
|
criteria.append(f"Berücksichtigung: {notes.strip()[:200]}")
|
||||||
|
|
||||||
|
from planning_intent_context import extract_explicit_exclusions
|
||||||
|
|
||||||
constraints: Dict[str, Any] = {"partner_required": False, "group_analysis": False}
|
constraints: Dict[str, Any] = {"partner_required": False, "group_analysis": False}
|
||||||
if notes.strip():
|
if notes.strip():
|
||||||
constraints["trainer_notes"] = notes.strip()[:500]
|
constraints["trainer_notes"] = notes.strip()[:500]
|
||||||
|
excluded = extract_explicit_exclusions(goal_query, notes or None)
|
||||||
|
if excluded:
|
||||||
|
constraints["excluded_themes"] = excluded
|
||||||
|
|
||||||
return GoalAnalysisArtifact(
|
return GoalAnalysisArtifact(
|
||||||
primary_topic=topic,
|
primary_topic=topic,
|
||||||
|
|
@ -955,6 +967,44 @@ def roadmap_context_from_override(
|
||||||
semantic_brief=semantic_brief,
|
semantic_brief=semantic_brief,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from planning_exercise_semantics import enrich_brief_with_path_constraints
|
||||||
|
from planning_intent_context import (
|
||||||
|
build_planning_intent_context,
|
||||||
|
finalize_stage_specs_with_intent,
|
||||||
|
)
|
||||||
|
|
||||||
|
enriched_brief = enrich_brief_with_path_constraints(
|
||||||
|
semantic_brief,
|
||||||
|
goal_query.strip(),
|
||||||
|
extra_context=_merge_roadmap_notes(
|
||||||
|
structured.roadmap_notes if structured else None,
|
||||||
|
structured.start_situation if structured else None,
|
||||||
|
structured.target_state if structured else None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
intent = build_planning_intent_context(
|
||||||
|
goal_query.strip(),
|
||||||
|
semantic_brief=enriched_brief,
|
||||||
|
goal_analysis=goal_analysis.model_dump(),
|
||||||
|
extra_context=_merge_roadmap_notes(
|
||||||
|
structured.roadmap_notes if structured else None,
|
||||||
|
structured.start_situation if structured else None,
|
||||||
|
structured.target_state if structured else None,
|
||||||
|
),
|
||||||
|
primary_topic=goal_analysis.primary_topic,
|
||||||
|
)
|
||||||
|
stage_specs = finalize_stage_specs_with_intent(
|
||||||
|
stage_specs,
|
||||||
|
majors,
|
||||||
|
intent=intent,
|
||||||
|
fallback_specs=build_stage_specs(
|
||||||
|
majors,
|
||||||
|
goal_analysis=goal_analysis,
|
||||||
|
goal_query=goal_query.strip(),
|
||||||
|
semantic_brief=enriched_brief,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return ProgressionRoadmapContext(
|
return ProgressionRoadmapContext(
|
||||||
goal_query=goal_query.strip(),
|
goal_query=goal_query.strip(),
|
||||||
max_steps=effective_max,
|
max_steps=effective_max,
|
||||||
|
|
@ -1122,24 +1172,59 @@ def run_progression_roadmap_pipeline(
|
||||||
)
|
)
|
||||||
ctx.roadmap = roadmap
|
ctx.roadmap = roadmap
|
||||||
|
|
||||||
stage_specs = build_stage_specs(
|
from planning_exercise_semantics import enrich_brief_with_path_constraints
|
||||||
|
from planning_intent_context import (
|
||||||
|
build_planning_intent_context,
|
||||||
|
finalize_stage_specs_with_intent,
|
||||||
|
)
|
||||||
|
|
||||||
|
brief = enrich_brief_with_path_constraints(
|
||||||
|
brief,
|
||||||
|
goal_query,
|
||||||
|
extra_context=_merge_roadmap_notes(
|
||||||
|
resolved.roadmap_notes,
|
||||||
|
resolved.start_situation,
|
||||||
|
resolved.target_state,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
intent = build_planning_intent_context(
|
||||||
|
goal_query,
|
||||||
|
semantic_brief=brief,
|
||||||
|
goal_analysis=goal_analysis.model_dump(),
|
||||||
|
extra_context=_merge_roadmap_notes(
|
||||||
|
resolved.roadmap_notes,
|
||||||
|
resolved.start_situation,
|
||||||
|
resolved.target_state,
|
||||||
|
),
|
||||||
|
primary_topic=goal_analysis.primary_topic,
|
||||||
|
)
|
||||||
|
|
||||||
|
heuristic_specs = build_stage_specs(
|
||||||
roadmap.major_steps,
|
roadmap.major_steps,
|
||||||
goal_analysis=goal_analysis,
|
goal_analysis=goal_analysis,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
semantic_brief=brief,
|
semantic_brief=brief,
|
||||||
)
|
)
|
||||||
|
stage_specs = list(heuristic_specs)
|
||||||
if include_llm_roadmap and cur is not None:
|
if include_llm_roadmap and cur is not None:
|
||||||
llm_specs, spec_ok = try_llm_stage_specs(
|
llm_specs, spec_ok = try_llm_stage_specs(
|
||||||
cur,
|
cur,
|
||||||
goal_query=llm_goal_query,
|
goal_query=llm_goal_query,
|
||||||
goal_analysis=goal_analysis,
|
goal_analysis=goal_analysis,
|
||||||
major_steps=roadmap.major_steps,
|
major_steps=roadmap.major_steps,
|
||||||
|
intent_context=intent.to_api_dict(),
|
||||||
|
semantic_brief=brief,
|
||||||
)
|
)
|
||||||
if spec_ok and llm_specs:
|
if spec_ok and llm_specs:
|
||||||
stage_specs = llm_specs
|
stage_specs = list(llm_specs)
|
||||||
ctx.llm_stage_spec_applied = True
|
ctx.llm_stage_spec_applied = True
|
||||||
ctx.prompt_slugs.append(PROMPT_SLUG_STAGE_SPEC)
|
ctx.prompt_slugs.append(PROMPT_SLUG_STAGE_SPEC)
|
||||||
ctx.stage_specs = stage_specs
|
ctx.stage_specs = finalize_stage_specs_with_intent(
|
||||||
|
stage_specs,
|
||||||
|
roadmap.major_steps,
|
||||||
|
intent=intent,
|
||||||
|
fallback_specs=heuristic_specs,
|
||||||
|
)
|
||||||
|
|
||||||
if ctx.llm_goal_analysis_applied or ctx.llm_roadmap_applied or ctx.llm_stage_spec_applied:
|
if ctx.llm_goal_analysis_applied or ctx.llm_roadmap_applied or ctx.llm_stage_spec_applied:
|
||||||
ctx.pipeline_phase = "roadmap_v1_llm"
|
ctx.pipeline_phase = "roadmap_v1_llm"
|
||||||
|
|
|
||||||
58
backend/tests/test_planning_intent_context.py
Normal file
58
backend/tests/test_planning_intent_context.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""Tests gemeinsames Planungs-Intent-Modul (Progressionsgraph → Trainingsplanung)."""
|
||||||
|
from planning_intent_context import (
|
||||||
|
build_planning_intent_context,
|
||||||
|
extract_explicit_exclusions,
|
||||||
|
finalize_stage_specs_with_intent,
|
||||||
|
)
|
||||||
|
from planning_progression_roadmap import (
|
||||||
|
MajorStep,
|
||||||
|
StageSpecArtifact,
|
||||||
|
build_goal_analysis,
|
||||||
|
build_stage_specs,
|
||||||
|
run_progression_roadmap_pipeline,
|
||||||
|
)
|
||||||
|
from planning_exercise_semantics import build_semantic_brief, enrich_brief_with_path_constraints
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_explicit_exclusions_parses_negations():
|
||||||
|
q = "gesprungener Mawashi Geri, keine Kumite-Anwendung gewünscht"
|
||||||
|
out = extract_explicit_exclusions(q)
|
||||||
|
assert any("kumite" in x.lower() for x in out)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_planning_intent_context_includes_path_anti():
|
||||||
|
q = "Mawashi Geri Sprungkraft, keine Kumite-Anwendung"
|
||||||
|
brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q)
|
||||||
|
ga = build_goal_analysis(q, brief)
|
||||||
|
intent = build_planning_intent_context(q, semantic_brief=brief, goal_analysis=ga.model_dump())
|
||||||
|
assert intent.explicit_exclusions
|
||||||
|
assert any("kumite" in a for a in intent.path_anti_patterns)
|
||||||
|
|
||||||
|
|
||||||
|
def test_finalize_stage_specs_merges_intent_into_each_stage():
|
||||||
|
q = "gesprungener Mawashi Geri, keine Kumite-Anwendung"
|
||||||
|
brief = enrich_brief_with_path_constraints(build_semantic_brief(q), q)
|
||||||
|
ga = build_goal_analysis(q, brief)
|
||||||
|
intent = build_planning_intent_context(q, semantic_brief=brief, goal_analysis=ga.model_dump())
|
||||||
|
majors = [
|
||||||
|
MajorStep(index=0, phase="grundlage", learning_goal="Grundtechnik Mawashi", consolidates=["m1"]),
|
||||||
|
MajorStep(index=1, phase="vertiefung", learning_goal="Sprungkoordination", consolidates=["m2"]),
|
||||||
|
]
|
||||||
|
raw_specs = [
|
||||||
|
StageSpecArtifact(major_step_index=0, learning_goal=majors[0].learning_goal),
|
||||||
|
StageSpecArtifact(major_step_index=1, learning_goal=majors[1].learning_goal),
|
||||||
|
]
|
||||||
|
finalized = finalize_stage_specs_with_intent(raw_specs, majors, intent=intent)
|
||||||
|
assert len(finalized) == 2
|
||||||
|
for spec in finalized:
|
||||||
|
assert spec.success_criteria
|
||||||
|
assert any("kumite" in a.lower() for a in spec.anti_patterns)
|
||||||
|
assert any("messbar" in c.lower() or "übungsziel" in c.lower() for c in spec.success_criteria)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pipeline_stage_specs_carry_exclusions_without_llm():
|
||||||
|
q = "gesprungener Mawashi Geri Sprungphase, keine Kumite-Anwendung"
|
||||||
|
ctx = run_progression_roadmap_pipeline(q, max_steps=3, include_llm_roadmap=False)
|
||||||
|
assert len(ctx.stage_specs) == 3
|
||||||
|
for spec in ctx.stage_specs:
|
||||||
|
assert any("kumite" in a.lower() for a in (spec.anti_patterns or []))
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.222"
|
APP_VERSION = "0.8.223"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260607088"
|
DB_SCHEMA_VERSION = "20260607089"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
||||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||||
"planning_exercise_suggest": "0.22.0", # skill_expectations, planning_roadmap persist (088), stage_specs override
|
"planning_exercise_suggest": "0.23.0", # planning_intent_context, finalize stage_specs, Prompt 089
|
||||||
"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
|
||||||
|
|
@ -53,6 +53,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.223",
|
||||||
|
"date": "2026-06-07",
|
||||||
|
"changes": [
|
||||||
|
"planning_intent_context: gemeinsamer Intent für anti_patterns/success_criteria (Phase G-ready).",
|
||||||
|
"Migration 089: LLM-Prompts Zielanalyse + stage_spec mit Intent-Kontext; finalize nach Pipeline.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.222",
|
"version": "0.8.222",
|
||||||
"date": "2026-06-07",
|
"date": "2026-06-07",
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,23 @@ Validierung: `progression_graph_planning_artifact.py` · Tests: `test_progressio
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Fähigkeiten-Scoring-Anbindung
|
## 8. Planungs-Intent (gemeinsam mit Trainingsplanung)
|
||||||
|
|
||||||
|
Modul: **`planning_intent_context.py`** — domänenneutral, wiederverwendbar in Phase G.
|
||||||
|
|
||||||
|
| Baustein | Progressionsgraph heute | Trainingsplanung (Phase G) |
|
||||||
|
|----------|-------------------------|----------------------------|
|
||||||
|
| `build_planning_intent_context` | Aus `goal_query`, Zielanalyse, Notizen | Aus `section_guidance`, Slot, Einheit |
|
||||||
|
| `explicit_exclusions` | Negationen (`ohne/kein/nicht …`) | gleich |
|
||||||
|
| `path_anti_patterns` | → jede `stage_spec.anti_patterns` | → Abschnitts-/Slot-Brief |
|
||||||
|
| `path_success_criteria` | → `success_criteria` + Matching | → Slot-Erwartung |
|
||||||
|
| `finalize_stage_specs_with_intent` | Nach heuristischer/LLM-Roadmap | Analog für Sektionen |
|
||||||
|
|
||||||
|
LLM: Migration **089** — Prompts `planning_progression_goal_analysis` + `planning_progression_stage_spec` erhalten `intent_context_json`; Ausschlüsse nicht erfinden, nur aus Anfrage übernehmen.
|
||||||
|
|
||||||
|
Matching: `anti_patterns` + `success_criteria` → `build_stage_match_brief` → Retrieval-Gate (Titel + Summary + Goal).
|
||||||
|
|
||||||
|
## 9. Fähigkeiten-Scoring-Anbindung
|
||||||
|
|
||||||
Modul: `planning_skill_expectations.py`
|
Modul: `planning_skill_expectations.py`
|
||||||
|
|
||||||
|
|
@ -245,7 +261,7 @@ Integration:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. KI-Lücken (Gap-Fill)
|
## 10. KI-Lücken (Gap-Fill)
|
||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
1. `roadmap_unfilled` / QA-Lücken → `gap_fill_offers`
|
1. `roadmap_unfilled` / QA-Lücken → `gap_fill_offers`
|
||||||
|
|
@ -257,7 +273,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Implementierungsstände (Phasen)
|
## 11. Implementierungsstände (Phasen)
|
||||||
|
|
||||||
| Phase | Inhalt | Status | Version |
|
| Phase | Inhalt | Status | Version |
|
||||||
|-------|--------|--------|---------|
|
|-------|--------|--------|---------|
|
||||||
|
|
@ -275,7 +291,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Offenes Backlog (priorisiert)
|
## 12. Offenes Backlog (priorisiert)
|
||||||
|
|
||||||
1. **UI-Überarbeitung** — Wizard mit 4 Schritten, progressive disclosure (Briefing unten)
|
1. **UI-Überarbeitung** — Wizard mit 4 Schritten, progressive disclosure (Briefing unten)
|
||||||
2. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz
|
2. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz
|
||||||
|
|
@ -292,13 +308,14 @@ Kern: Wizard ① Ziel & Start/Ziel → ② Roadmap → ③ Match → ④ Lücken
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Tests
|
## 13. Tests
|
||||||
|
|
||||||
| Datei | Abdeckung |
|
| Datei | Abdeckung |
|
||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| `test_planning_progression_roadmap.py` | Roadmap-Pipeline, Override, Start/Ziel |
|
| `test_planning_progression_roadmap.py` | Roadmap-Pipeline, Override, Start/Ziel |
|
||||||
| `test_planning_exercise_path_builder.py` | Pfad-Annotierung, Skill-Expectations auf Steps |
|
| `test_planning_exercise_path_builder.py` | Pfad-Annotierung, Skill-Expectations auf Steps |
|
||||||
| `test_planning_skill_expectations.py` | Skill-Erwartungen, Scopes |
|
| `test_planning_skill_expectations.py` | Skill-Erwartungen, Scopes |
|
||||||
|
| `test_planning_intent_context.py` | Intent-Kontext, finalize stage_specs |
|
||||||
| `test_planning_exercise_path_ai_fill.py` | Gap-Fill, `expected_skills` in goal text |
|
| `test_planning_exercise_path_ai_fill.py` | Gap-Fill, `expected_skills` in goal text |
|
||||||
| `test_planning_exercise_form_context.py` | `planning_context`, Gap-Snapshot |
|
| `test_planning_exercise_form_context.py` | `planning_context`, Gap-Snapshot |
|
||||||
| `test_progression_graph_planning_artifact.py` | JSONB-Artefakt-Validierung |
|
| `test_progression_graph_planning_artifact.py` | JSONB-Artefakt-Validierung |
|
||||||
|
|
@ -306,7 +323,7 @@ Kern: Wizard ① Ziel & Start/Ziel → ② Roadmap → ③ Match → ④ Lücken
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Dokumenten-Index (Drift vermeiden)
|
## 14. Dokumenten-Index (Drift vermeiden)
|
||||||
|
|
||||||
| Frage | Primäre Quelle |
|
| Frage | Primäre Quelle |
|
||||||
|-------|----------------|
|
|-------|----------------|
|
||||||
|
|
@ -322,7 +339,7 @@ Kern: Wizard ① Ziel & Start/Ziel → ② Roadmap → ③ Match → ④ Lücken
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Changelog (Dokument)
|
## 15. Changelog (Dokument)
|
||||||
|
|
||||||
| Datum | Änderung |
|
| Datum | Änderung |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user