Enhance Progression Path Features with LLM Start/Target Extraction
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added `include_llm_start_target` option to `ProgressionPathSuggestRequest` for improved roadmap suggestions. - Introduced new classes `StartTargetExtractArtifact` and `StartTargetResolveMeta` to handle LLM extraction results and metadata. - Implemented `try_llm_start_target_extract` function to extract start and target states from goal queries using LLM. - Updated `resolve_roadmap_structured_input` to prioritize user inputs, LLM extractions, and regex parsing for start/target resolution. - Enhanced `ExerciseProgressionPathBuilder` to utilize new structured inputs and display extraction sources. - Incremented application version to 0.8.211 to reflect these changes.
This commit is contained in:
parent
9dd44ce3ca
commit
fad1058d54
|
|
@ -0,0 +1,52 @@
|
|||
-- Migration 087: Planungs-KI — LLM Start/Ziel-Extraktion aus Trainer-Anfrage (Alternative zu Regex)
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_progression_start_target',
|
||||
'Progressions-Roadmap Start/Ziel-Extraktion',
|
||||
'Versteht die Trainer-Anfrage und formuliert dedizierte Ausgangslage, Zielzustand und Ergänzungen (ohne Gruppen-Tracking).',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen didaktischen Progressionsgraphen.
|
||||
|
||||
Trainer-Anfrage (Ursprungstext):
|
||||
{{goal_query}}
|
||||
|
||||
Semantic Brief (heuristisch): {{semantic_brief_json}}
|
||||
|
||||
Bereits vom Trainer eingegebene Ergänzungen (falls vorhanden): {{user_notes}}
|
||||
|
||||
Aufgabe:
|
||||
1. **primary_topic** — Kern-Thema/Technik in kurzer, präziser Bezeichnung (z. B. „Kumite Beinarbeit“, „Mae Geri“).
|
||||
2. **start_situation** — Ausgangslage in eigenen Worten: Was kann der Athlet/die Gruppe *jetzt* (laut Anfrage oder sinnvoll ableitbar)? Konkret, beobachtbar, ohne Gruppenanalyse aus der Datenbank.
|
||||
3. **target_state** — Zielzustand in eigenen Worten: Was soll am Ende der Progression erreicht sein? Konkret, didaktisch nutzbar.
|
||||
4. **roadmap_notes** — Ergänzungen aus dem Ursprungstext: Fokus, Kontext (z. B. Kumite), besondere Anforderungen, Einschränkungen, die der Trainer erwähnt hat oder die für die Roadmap relevant sind. Nicht wiederholen, was bereits in start_situation/target_state steht.
|
||||
5. **extraction_notes** — Kurz (1–2 Sätze): Was war explizit vs. abgeleitet? Wo war die Anfrage unklar?
|
||||
|
||||
Regeln:
|
||||
- Keine Gruppenanalyse — nur das, was aus dem Text hervorgeht oder didaktisch naheliegend formuliert ist.
|
||||
- Formuliere start_situation und target_state **eigenständig und verständlich**, nicht nur Textfragmente kopieren.
|
||||
- Bei „von … bis …“: Start und Ziel aus diesem Bogen schärfen und präzise beschreiben.
|
||||
- Bei nur einem Thema ohne Bogen: start_situation und target_state didaktisch sinnvoll formulieren oder leer lassen, wenn nicht ableitbar — dann in extraction_notes erklären.
|
||||
- Antworte NUR mit JSON.
|
||||
|
||||
{
|
||||
"primary_topic": "…",
|
||||
"start_situation": "…",
|
||||
"target_state": "…",
|
||||
"roadmap_notes": "…",
|
||||
"extraction_notes": "…"
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","properties":{"primary_topic":{"type":"string"},"start_situation":{"type":"string"},"target_state":{"type":"string"},"roadmap_notes":{"type":"string"},"extraction_notes":{"type":"string"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
13
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_start_target');
|
||||
|
||||
UPDATE ai_prompts SET default_template = template
|
||||
WHERE slug = 'planning_progression_start_target'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
|
|
@ -76,6 +76,7 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
include_ai_gap_fill: bool = True
|
||||
include_roadmap_preview: bool = False
|
||||
include_llm_roadmap: bool = True
|
||||
include_llm_start_target: bool = True
|
||||
roadmap_first: bool = False
|
||||
roadmap_only: bool = False
|
||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||
|
|
@ -544,6 +545,7 @@ def suggest_progression_path(
|
|||
semantic_brief=semantic_brief,
|
||||
cur=cur,
|
||||
include_llm_roadmap=body.include_llm_roadmap,
|
||||
include_llm_start_target=body.include_llm_start_target,
|
||||
structured=roadmap_structured,
|
||||
)
|
||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from planning_exercise_semantics import (
|
|||
_logger = logging.getLogger("shinkan.planning_progression_roadmap")
|
||||
|
||||
# Nur Slugs — Templates in DB (ai_prompts), bearbeitbar im Admin.
|
||||
PROMPT_SLUG_START_TARGET = "planning_progression_start_target"
|
||||
PROMPT_SLUG_GOAL_ANALYSIS = "planning_progression_goal_analysis"
|
||||
PROMPT_SLUG_ROADMAP = "planning_progression_roadmap"
|
||||
PROMPT_SLUG_STAGE_SPEC = "planning_progression_stage_spec"
|
||||
|
|
@ -117,6 +118,26 @@ class RoadmapStructuredInput(BaseModel):
|
|||
roadmap_notes: Optional[str] = Field(default=None, max_length=2000)
|
||||
|
||||
|
||||
class StartTargetExtractArtifact(BaseModel):
|
||||
"""LLM-Ergebnis: dedizierte Beschreibung von Ausgang, Ziel und Ergänzungen."""
|
||||
|
||||
primary_topic: str = ""
|
||||
start_situation: str = ""
|
||||
target_state: str = ""
|
||||
roadmap_notes: str = ""
|
||||
extraction_notes: str = ""
|
||||
|
||||
|
||||
class StartTargetResolveMeta(BaseModel):
|
||||
"""Herkunft der aufgelösten Felder (user > llm > regex)."""
|
||||
|
||||
start_source: str = "none"
|
||||
target_source: str = "none"
|
||||
notes_source: str = "none"
|
||||
topic_source: str = "none"
|
||||
llm_start_target_applied: bool = False
|
||||
|
||||
|
||||
class RoadmapOverridePayload(BaseModel):
|
||||
"""Vom Trainer bearbeitete Major Steps — steuert Retrieval ohne erneute Roadmap-KI."""
|
||||
|
||||
|
|
@ -131,10 +152,14 @@ class ProgressionRoadmapContext(BaseModel):
|
|||
goal_query: str
|
||||
max_steps: int = Field(ge=2, le=10, default=5)
|
||||
semantic_brief: Optional[Dict[str, Any]] = None
|
||||
resolved_structured: Optional[RoadmapStructuredInput] = None
|
||||
start_target_extract: Optional[StartTargetExtractArtifact] = None
|
||||
start_target_resolve: Optional[StartTargetResolveMeta] = None
|
||||
goal_analysis: Optional[GoalAnalysisArtifact] = None
|
||||
roadmap: Optional[RoadmapArtifact] = None
|
||||
stage_specs: List[StageSpecArtifact] = Field(default_factory=list)
|
||||
pipeline_phase: str = "roadmap_v1"
|
||||
llm_start_target_applied: bool = False
|
||||
llm_goal_analysis_applied: bool = False
|
||||
llm_roadmap_applied: bool = False
|
||||
llm_stage_spec_applied: bool = False
|
||||
|
|
@ -177,6 +202,31 @@ def _run_prompt_json(
|
|||
return None
|
||||
|
||||
|
||||
def try_llm_start_target_extract(
|
||||
cur,
|
||||
*,
|
||||
goal_query: str,
|
||||
brief: PlanningSemanticBrief,
|
||||
user_notes: str = "",
|
||||
) -> Tuple[Optional[StartTargetExtractArtifact], bool]:
|
||||
obj = _run_prompt_json(
|
||||
cur,
|
||||
PROMPT_SLUG_START_TARGET,
|
||||
{
|
||||
"goal_query": goal_query or "",
|
||||
"semantic_brief_json": json.dumps(brief_to_summary_dict(brief), ensure_ascii=False),
|
||||
"user_notes": (user_notes or "").strip(),
|
||||
},
|
||||
)
|
||||
if not obj:
|
||||
return None, False
|
||||
try:
|
||||
return StartTargetExtractArtifact.model_validate(obj), True
|
||||
except ValidationError as exc:
|
||||
_logger.warning("Start/Ziel-Extraktion JSON ungültig: %s", exc)
|
||||
return None, False
|
||||
|
||||
|
||||
def try_llm_goal_analysis(
|
||||
cur,
|
||||
*,
|
||||
|
|
@ -297,6 +347,100 @@ def _extract_topic_from_goal_query(goal_query: str, brief: PlanningSemanticBrief
|
|||
return topic or "Technik"
|
||||
|
||||
|
||||
def _merge_roadmap_notes(*parts: Optional[str]) -> Optional[str]:
|
||||
seen: set[str] = set()
|
||||
lines: List[str] = []
|
||||
for raw in parts:
|
||||
s = (raw or "").strip()
|
||||
if not s:
|
||||
continue
|
||||
key = s.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
lines.append(s)
|
||||
return "\n".join(lines) if lines else None
|
||||
|
||||
|
||||
def resolve_roadmap_structured_input(
|
||||
goal_query: str,
|
||||
structured: Optional[RoadmapStructuredInput],
|
||||
*,
|
||||
brief: PlanningSemanticBrief,
|
||||
cur=None,
|
||||
include_llm: bool = False,
|
||||
) -> Tuple[RoadmapStructuredInput, StartTargetResolveMeta, Optional[StartTargetExtractArtifact]]:
|
||||
"""Priorität je Feld: Trainer-Eingabe > LLM-Extraktion > Regex (von … bis …)."""
|
||||
user = structured or RoadmapStructuredInput()
|
||||
user_start = (user.start_situation or "").strip()
|
||||
user_target = (user.target_state or "").strip()
|
||||
user_notes = (user.roadmap_notes or "").strip()
|
||||
|
||||
llm_extract: Optional[StartTargetExtractArtifact] = None
|
||||
llm_ok = False
|
||||
if include_llm and cur is not None:
|
||||
llm_extract, llm_ok = try_llm_start_target_extract(
|
||||
cur,
|
||||
goal_query=goal_query,
|
||||
brief=brief,
|
||||
user_notes=user_notes,
|
||||
)
|
||||
|
||||
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||
|
||||
meta = StartTargetResolveMeta(llm_start_target_applied=llm_ok)
|
||||
|
||||
if user_start:
|
||||
start = user_start
|
||||
meta.start_source = "user"
|
||||
elif llm_ok and (llm_extract.start_situation or "").strip():
|
||||
start = llm_extract.start_situation.strip()
|
||||
meta.start_source = "llm"
|
||||
elif parsed_start:
|
||||
start = parsed_start
|
||||
meta.start_source = "regex"
|
||||
else:
|
||||
start = ""
|
||||
|
||||
if user_target:
|
||||
target = user_target
|
||||
meta.target_source = "user"
|
||||
elif llm_ok and (llm_extract.target_state or "").strip():
|
||||
target = llm_extract.target_state.strip()
|
||||
meta.target_source = "llm"
|
||||
elif parsed_target:
|
||||
target = parsed_target
|
||||
meta.target_source = "regex"
|
||||
else:
|
||||
target = ""
|
||||
|
||||
llm_notes = (llm_extract.roadmap_notes or "").strip() if llm_ok and llm_extract else ""
|
||||
if user_notes and llm_notes:
|
||||
notes = _merge_roadmap_notes(user_notes, llm_notes) or ""
|
||||
meta.notes_source = "merged"
|
||||
elif user_notes:
|
||||
notes = user_notes
|
||||
meta.notes_source = "user"
|
||||
elif llm_notes:
|
||||
notes = llm_notes
|
||||
meta.notes_source = "llm"
|
||||
else:
|
||||
notes = ""
|
||||
meta.notes_source = "none"
|
||||
|
||||
if llm_ok and (llm_extract.primary_topic or "").strip():
|
||||
meta.topic_source = "llm"
|
||||
else:
|
||||
meta.topic_source = "heuristic"
|
||||
|
||||
resolved = RoadmapStructuredInput(
|
||||
start_situation=start or None,
|
||||
target_state=target or None,
|
||||
roadmap_notes=notes or None,
|
||||
)
|
||||
return resolved, meta, llm_extract if llm_ok else None
|
||||
|
||||
|
||||
def parse_start_target_from_goal_query(goal_query: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""„von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y)."""
|
||||
q = (goal_query or "").strip()
|
||||
|
|
@ -343,9 +487,10 @@ def build_goal_analysis(
|
|||
brief: PlanningSemanticBrief,
|
||||
*,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
topic_override: Optional[str] = None,
|
||||
) -> GoalAnalysisArtifact:
|
||||
"""Phase A — aus Anfrage, optionalen Feldern und Semantic Brief."""
|
||||
topic = _extract_topic_from_goal_query(goal_query, brief)
|
||||
topic = (topic_override or "").strip() or _extract_topic_from_goal_query(goal_query, brief)
|
||||
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||
|
||||
start = (structured.start_situation if structured else None) or parsed_start
|
||||
|
|
@ -797,7 +942,7 @@ def _merge_structured_into_goal_analysis(
|
|||
brief: PlanningSemanticBrief,
|
||||
structured: Optional[RoadmapStructuredInput],
|
||||
) -> GoalAnalysisArtifact:
|
||||
ga_struct = build_goal_analysis(goal_query, brief, structured=structured)
|
||||
ga_struct = build_goal_analysis(goal_query, brief, structured=structured, topic_override=None)
|
||||
if not _has_specific_start_target(ga_struct):
|
||||
return llm_ga
|
||||
merged_criteria = list(
|
||||
|
|
@ -822,14 +967,22 @@ def run_progression_roadmap_pipeline(
|
|||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||
cur=None,
|
||||
include_llm_roadmap: bool = False,
|
||||
include_llm_start_target: bool = False,
|
||||
structured: Optional[RoadmapStructuredInput] = None,
|
||||
) -> ProgressionRoadmapContext:
|
||||
"""Workflow-lite: Phase A → B → C. LLM über ai_prompts-Slugs; deterministischer Fallback."""
|
||||
brief = semantic_brief or build_semantic_brief(goal_query)
|
||||
resolved, resolve_meta, llm_extract = resolve_roadmap_structured_input(
|
||||
goal_query,
|
||||
structured,
|
||||
brief=brief,
|
||||
cur=cur,
|
||||
include_llm=include_llm_start_target,
|
||||
)
|
||||
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||
llm_goal_query = _roadmap_llm_goal_block(
|
||||
goal_query,
|
||||
structured=structured,
|
||||
structured=resolved,
|
||||
parsed_start=parsed_start,
|
||||
parsed_target=parsed_target,
|
||||
)
|
||||
|
|
@ -837,9 +990,24 @@ def run_progression_roadmap_pipeline(
|
|||
goal_query=goal_query.strip(),
|
||||
max_steps=max_steps,
|
||||
semantic_brief=brief_to_summary_dict(brief),
|
||||
resolved_structured=resolved,
|
||||
start_target_extract=llm_extract,
|
||||
start_target_resolve=resolve_meta,
|
||||
llm_start_target_applied=resolve_meta.llm_start_target_applied,
|
||||
)
|
||||
if resolve_meta.llm_start_target_applied:
|
||||
ctx.prompt_slugs.append(PROMPT_SLUG_START_TARGET)
|
||||
|
||||
goal_analysis = build_goal_analysis(goal_query, brief, structured=structured)
|
||||
topic_override = None
|
||||
if llm_extract and (llm_extract.primary_topic or "").strip():
|
||||
topic_override = llm_extract.primary_topic.strip()
|
||||
|
||||
goal_analysis = build_goal_analysis(
|
||||
goal_query,
|
||||
brief,
|
||||
structured=resolved,
|
||||
topic_override=topic_override,
|
||||
)
|
||||
if include_llm_roadmap and cur is not None:
|
||||
llm_ga, ga_ok = try_llm_goal_analysis(cur, goal_query=llm_goal_query, brief=brief)
|
||||
if ga_ok and llm_ga:
|
||||
|
|
@ -847,7 +1015,7 @@ def run_progression_roadmap_pipeline(
|
|||
llm_ga,
|
||||
goal_query=goal_query,
|
||||
brief=brief,
|
||||
structured=structured,
|
||||
structured=resolved,
|
||||
)
|
||||
ctx.llm_goal_analysis_applied = True
|
||||
ctx.prompt_slugs.append(PROMPT_SLUG_GOAL_ANALYSIS)
|
||||
|
|
@ -901,18 +1069,37 @@ def run_progression_roadmap_pipeline(
|
|||
|
||||
|
||||
def progression_roadmap_to_api_dict(ctx: ProgressionRoadmapContext) -> Dict[str, Any]:
|
||||
resolve = ctx.start_target_resolve
|
||||
return {
|
||||
"goal_analysis": ctx.goal_analysis.model_dump() if ctx.goal_analysis else None,
|
||||
"resolved_structured": (
|
||||
ctx.resolved_structured.model_dump() if ctx.resolved_structured else None
|
||||
),
|
||||
"start_target_extract": (
|
||||
ctx.start_target_extract.model_dump() if ctx.start_target_extract else None
|
||||
),
|
||||
"start_target_sources": (
|
||||
{
|
||||
"start": resolve.start_source,
|
||||
"target": resolve.target_source,
|
||||
"notes": resolve.notes_source,
|
||||
"topic": resolve.topic_source,
|
||||
}
|
||||
if resolve
|
||||
else None
|
||||
),
|
||||
"roadmap": ctx.roadmap.model_dump() if ctx.roadmap else None,
|
||||
"stage_specs": [s.model_dump() for s in ctx.stage_specs],
|
||||
"pipeline_phase": ctx.pipeline_phase,
|
||||
"major_step_count": len(ctx.roadmap.major_steps) if ctx.roadmap else 0,
|
||||
"micro_objective_count": len(ctx.roadmap.micro_objectives) if ctx.roadmap else 0,
|
||||
"llm_start_target_applied": ctx.llm_start_target_applied,
|
||||
"llm_goal_analysis_applied": ctx.llm_goal_analysis_applied,
|
||||
"llm_roadmap_applied": ctx.llm_roadmap_applied,
|
||||
"llm_stage_spec_applied": ctx.llm_stage_spec_applied,
|
||||
"prompt_slugs": list(ctx.prompt_slugs),
|
||||
"prompt_slug_catalog": {
|
||||
"start_target": PROMPT_SLUG_START_TARGET,
|
||||
"goal_analysis": PROMPT_SLUG_GOAL_ANALYSIS,
|
||||
"roadmap": PROMPT_SLUG_ROADMAP,
|
||||
"stage_spec": PROMPT_SLUG_STAGE_SPEC,
|
||||
|
|
@ -921,6 +1108,7 @@ def progression_roadmap_to_api_dict(ctx: ProgressionRoadmapContext) -> Dict[str,
|
|||
|
||||
|
||||
__all__ = [
|
||||
"PROMPT_SLUG_START_TARGET",
|
||||
"PROMPT_SLUG_GOAL_ANALYSIS",
|
||||
"PROMPT_SLUG_ROADMAP",
|
||||
"PROMPT_SLUG_STAGE_SPEC",
|
||||
|
|
@ -931,8 +1119,11 @@ __all__ = [
|
|||
"RoadmapArtifact",
|
||||
"RoadmapOverridePayload",
|
||||
"RoadmapStructuredInput",
|
||||
"StartTargetExtractArtifact",
|
||||
"StartTargetResolveMeta",
|
||||
"normalize_major_steps_for_override",
|
||||
"parse_start_target_from_goal_query",
|
||||
"resolve_roadmap_structured_input",
|
||||
"roadmap_context_from_override",
|
||||
"StageSpecArtifact",
|
||||
"build_goal_analysis",
|
||||
|
|
@ -945,6 +1136,7 @@ __all__ = [
|
|||
"develop_micro_objectives",
|
||||
"progression_roadmap_to_api_dict",
|
||||
"run_progression_roadmap_pipeline",
|
||||
"try_llm_start_target_extract",
|
||||
"try_llm_goal_analysis",
|
||||
"try_llm_roadmap",
|
||||
"try_llm_stage_specs",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ def post_progression_path_suggest(
|
|||
or body.include_llm_path_qa
|
||||
or body.include_ai_gap_fill
|
||||
or body.include_llm_roadmap
|
||||
or body.include_llm_start_target
|
||||
)
|
||||
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
||||
if uses_ai:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from planning_progression_roadmap import (
|
|||
PROMPT_SLUG_GOAL_ANALYSIS,
|
||||
PROMPT_SLUG_ROADMAP,
|
||||
PROMPT_SLUG_STAGE_SPEC,
|
||||
PROMPT_SLUG_START_TARGET,
|
||||
MajorStep,
|
||||
RoadmapStructuredInput,
|
||||
StageSpecArtifact,
|
||||
|
|
@ -12,6 +13,7 @@ from planning_progression_roadmap import (
|
|||
develop_micro_objectives,
|
||||
parse_start_target_from_goal_query,
|
||||
progression_roadmap_to_api_dict,
|
||||
resolve_roadmap_structured_input,
|
||||
resolve_step_exercise_kind_filter,
|
||||
run_progression_roadmap_pipeline,
|
||||
stage_spec_exercise_kind_filter,
|
||||
|
|
@ -137,12 +139,53 @@ def test_roadmap_context_from_override():
|
|||
def test_api_dict_exposes_prompt_slug_catalog():
|
||||
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
||||
api = progression_roadmap_to_api_dict(ctx)
|
||||
assert api["prompt_slug_catalog"]["start_target"] == PROMPT_SLUG_START_TARGET
|
||||
assert api["prompt_slug_catalog"]["goal_analysis"] == PROMPT_SLUG_GOAL_ANALYSIS
|
||||
assert api["prompt_slug_catalog"]["roadmap"] == PROMPT_SLUG_ROADMAP
|
||||
assert api["prompt_slug_catalog"]["stage_spec"] == PROMPT_SLUG_STAGE_SPEC
|
||||
assert api["prompt_slugs"] == []
|
||||
|
||||
|
||||
def test_resolve_structured_user_overrides_regex():
|
||||
brief = build_semantic_brief(KUMITE_GOAL)
|
||||
structured = RoadmapStructuredInput(
|
||||
start_situation="Trainer-Start explizit",
|
||||
target_state="Trainer-Ziel explizit",
|
||||
)
|
||||
resolved, meta, llm_raw = resolve_roadmap_structured_input(
|
||||
KUMITE_GOAL, structured, brief=brief, include_llm=False
|
||||
)
|
||||
assert llm_raw is None
|
||||
assert resolved.start_situation == "Trainer-Start explizit"
|
||||
assert resolved.target_state == "Trainer-Ziel explizit"
|
||||
assert meta.start_source == "user"
|
||||
assert meta.target_source == "user"
|
||||
|
||||
|
||||
def test_resolve_structured_regex_fallback_without_llm():
|
||||
brief = build_semantic_brief(KUMITE_GOAL)
|
||||
resolved, meta, _ = resolve_roadmap_structured_input(
|
||||
KUMITE_GOAL, None, brief=brief, include_llm=False
|
||||
)
|
||||
assert meta.start_source == "regex"
|
||||
assert meta.target_source == "regex"
|
||||
assert "Steppbewegung" in (resolved.start_situation or "")
|
||||
assert "dynamischen" in (resolved.target_state or "")
|
||||
|
||||
|
||||
def test_resolve_structured_merges_user_and_llm_notes():
|
||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||
structured = RoadmapStructuredInput(roadmap_notes="Kindergruppe 10–12")
|
||||
resolved, meta, _ = resolve_roadmap_structured_input(
|
||||
"Kumite Beinarbeit",
|
||||
structured,
|
||||
brief=brief,
|
||||
include_llm=False,
|
||||
)
|
||||
assert resolved.roadmap_notes == "Kindergruppe 10–12"
|
||||
assert meta.notes_source == "user"
|
||||
|
||||
|
||||
def test_parse_start_target_kumite_beinarbeit():
|
||||
start, target = parse_start_target_from_goal_query(KUMITE_GOAL)
|
||||
assert start is not None
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.210"
|
||||
APP_VERSION = "0.8.211"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260606086"
|
||||
DB_SCHEMA_VERSION = "20260607087"
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"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
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||
"planning_exercise_suggest": "0.20.2", # Strukturierte Roadmap-Eingaben Start/Ziel + von-bis-Parsing
|
||||
"planning_exercise_suggest": "0.21.0", # LLM Start/Ziel-Extraktion (planning_progression_start_target)
|
||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||
|
|
|
|||
|
|
@ -12,18 +12,24 @@ import {
|
|||
} from '../utils/exerciseAiQuickCreate'
|
||||
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
||||
|
||||
/** „von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y). */
|
||||
function parseStartTargetFromGoalQuery(q) {
|
||||
const text = String(q || '').trim()
|
||||
if (!text) return { start: '', target: '' }
|
||||
const m = text.match(
|
||||
/\bvon\s+((?:(?:der|die|dem|das|einer?|einem)\s+)?.+?)\s+bis\s+(?:zur?|zum|zu der|zu einem)?\s*(.+)$/is,
|
||||
)
|
||||
if (!m) return { start: '', target: '' }
|
||||
const start = m[1].trim().replace(/[.,;]+$/, '')
|
||||
const target = m[2].trim().replace(/[.,;]+$/, '')
|
||||
if (start.length < 4 || target.length < 4) return { start: '', target: '' }
|
||||
return { start, target }
|
||||
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
|
||||
const rs = progressionRoadmap?.resolved_structured
|
||||
if (!rs) return
|
||||
if (rs.start_situation) setters.setStartSituation(String(rs.start_situation))
|
||||
if (rs.target_state) setters.setTargetState(String(rs.target_state))
|
||||
if (rs.roadmap_notes) setters.setRoadmapNotes(String(rs.roadmap_notes))
|
||||
}
|
||||
|
||||
function sourceLabel(source) {
|
||||
const map = {
|
||||
user: 'manuell',
|
||||
llm: 'KI-Extraktion',
|
||||
regex: 'Muster (von … bis …)',
|
||||
merged: 'manuell + KI',
|
||||
heuristic: 'heuristisch',
|
||||
none: '—',
|
||||
}
|
||||
return map[source] || source || '—'
|
||||
}
|
||||
|
||||
function roadmapStructuredPayload(startSituation, targetState, roadmapNotes) {
|
||||
|
|
@ -535,19 +541,6 @@ export default function ExerciseProgressionPathBuilder({
|
|||
alert('Zuerst einen Graphen wählen.')
|
||||
return
|
||||
}
|
||||
let start = startSituation.trim()
|
||||
let target = targetState.trim()
|
||||
if (!start || !target) {
|
||||
const parsed = parseStartTargetFromGoalQuery(q)
|
||||
if (parsed.start && !start) {
|
||||
start = parsed.start
|
||||
setStartSituation(parsed.start)
|
||||
}
|
||||
if (parsed.target && !target) {
|
||||
target = parsed.target
|
||||
setTargetState(parsed.target)
|
||||
}
|
||||
}
|
||||
setLoadingRoadmap(true)
|
||||
setError('')
|
||||
try {
|
||||
|
|
@ -561,9 +554,10 @@ export default function ExerciseProgressionPathBuilder({
|
|||
include_ai_gap_fill: false,
|
||||
include_roadmap_preview: true,
|
||||
include_llm_roadmap: true,
|
||||
include_llm_start_target: true,
|
||||
roadmap_only: true,
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(start, target, roadmapNotes),
|
||||
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
||||
})
|
||||
const majors = mapMajorStepsFromApi(res?.progression_roadmap)
|
||||
if (majors.length < 2) {
|
||||
|
|
@ -571,7 +565,13 @@ export default function ExerciseProgressionPathBuilder({
|
|||
}
|
||||
setEditableMajorSteps(majors)
|
||||
setMaxSteps(majors.length)
|
||||
setProgressionRoadmap(res?.progression_roadmap || null)
|
||||
const roadmap = res?.progression_roadmap || null
|
||||
setProgressionRoadmap(roadmap)
|
||||
applyResolvedStructuredFromRoadmap(roadmap, {
|
||||
setStartSituation,
|
||||
setTargetState,
|
||||
setRoadmapNotes,
|
||||
})
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setPathSteps([])
|
||||
setTargetSummary(null)
|
||||
|
|
@ -767,7 +767,8 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
||||
Bei „von … bis …“ im Ziel werden Start und Ziel automatisch vorausgefüllt, wenn die Felder leer sind.
|
||||
Leer gelassen: Start/Ziel werden per KI aus dem Zieltext verstanden und formuliert (Fallback: Muster
|
||||
„von … bis …“). Manuelle Eingaben haben Vorrang.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
||||
<button
|
||||
|
|
@ -819,6 +820,11 @@ export default function ExerciseProgressionPathBuilder({
|
|||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<strong style={{ fontSize: '13px' }}>Zielanalyse</strong>
|
||||
{progressionRoadmap.llm_start_target_applied ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
KI Start/Ziel
|
||||
</span>
|
||||
) : null}
|
||||
{progressionRoadmap.llm_goal_analysis_applied ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
KI-Zielanalyse
|
||||
|
|
@ -826,6 +832,12 @@ export default function ExerciseProgressionPathBuilder({
|
|||
) : (
|
||||
<span className="exercise-tag">heuristisch</span>
|
||||
)}
|
||||
{progressionRoadmap.start_target_sources ? (
|
||||
<span className="exercise-tag" style={{ fontSize: '11px' }}>
|
||||
Start: {sourceLabel(progressionRoadmap.start_target_sources.start)} · Ziel:{' '}
|
||||
{sourceLabel(progressionRoadmap.start_target_sources.target)}
|
||||
</span>
|
||||
) : null}
|
||||
{progressionRoadmap.goal_analysis.primary_topic ? (
|
||||
<span className="exercise-tag">Thema: {progressionRoadmap.goal_analysis.primary_topic}</span>
|
||||
) : null}
|
||||
|
|
@ -847,6 +859,11 @@ export default function ExerciseProgressionPathBuilder({
|
|||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{progressionRoadmap.start_target_extract?.extraction_notes ? (
|
||||
<p style={{ margin: '8px 0 0', fontSize: '11px', color: 'var(--text3)' }}>
|
||||
{progressionRoadmap.start_target_extract.extraction_notes}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user