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_ai_gap_fill: bool = True
|
||||||
include_roadmap_preview: bool = False
|
include_roadmap_preview: bool = False
|
||||||
include_llm_roadmap: bool = True
|
include_llm_roadmap: bool = True
|
||||||
|
include_llm_start_target: bool = True
|
||||||
roadmap_first: bool = False
|
roadmap_first: bool = False
|
||||||
roadmap_only: bool = False
|
roadmap_only: bool = False
|
||||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||||
|
|
@ -544,6 +545,7 @@ def suggest_progression_path(
|
||||||
semantic_brief=semantic_brief,
|
semantic_brief=semantic_brief,
|
||||||
cur=cur,
|
cur=cur,
|
||||||
include_llm_roadmap=body.include_llm_roadmap,
|
include_llm_roadmap=body.include_llm_roadmap,
|
||||||
|
include_llm_start_target=body.include_llm_start_target,
|
||||||
structured=roadmap_structured,
|
structured=roadmap_structured,
|
||||||
)
|
)
|
||||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
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")
|
_logger = logging.getLogger("shinkan.planning_progression_roadmap")
|
||||||
|
|
||||||
# Nur Slugs — Templates in DB (ai_prompts), bearbeitbar im Admin.
|
# 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_GOAL_ANALYSIS = "planning_progression_goal_analysis"
|
||||||
PROMPT_SLUG_ROADMAP = "planning_progression_roadmap"
|
PROMPT_SLUG_ROADMAP = "planning_progression_roadmap"
|
||||||
PROMPT_SLUG_STAGE_SPEC = "planning_progression_stage_spec"
|
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)
|
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):
|
class RoadmapOverridePayload(BaseModel):
|
||||||
"""Vom Trainer bearbeitete Major Steps — steuert Retrieval ohne erneute Roadmap-KI."""
|
"""Vom Trainer bearbeitete Major Steps — steuert Retrieval ohne erneute Roadmap-KI."""
|
||||||
|
|
||||||
|
|
@ -131,10 +152,14 @@ class ProgressionRoadmapContext(BaseModel):
|
||||||
goal_query: str
|
goal_query: str
|
||||||
max_steps: int = Field(ge=2, le=10, default=5)
|
max_steps: int = Field(ge=2, le=10, default=5)
|
||||||
semantic_brief: Optional[Dict[str, Any]] = None
|
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
|
goal_analysis: Optional[GoalAnalysisArtifact] = None
|
||||||
roadmap: Optional[RoadmapArtifact] = None
|
roadmap: Optional[RoadmapArtifact] = None
|
||||||
stage_specs: List[StageSpecArtifact] = Field(default_factory=list)
|
stage_specs: List[StageSpecArtifact] = Field(default_factory=list)
|
||||||
pipeline_phase: str = "roadmap_v1"
|
pipeline_phase: str = "roadmap_v1"
|
||||||
|
llm_start_target_applied: bool = False
|
||||||
llm_goal_analysis_applied: bool = False
|
llm_goal_analysis_applied: bool = False
|
||||||
llm_roadmap_applied: bool = False
|
llm_roadmap_applied: bool = False
|
||||||
llm_stage_spec_applied: bool = False
|
llm_stage_spec_applied: bool = False
|
||||||
|
|
@ -177,6 +202,31 @@ def _run_prompt_json(
|
||||||
return None
|
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(
|
def try_llm_goal_analysis(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
|
|
@ -297,6 +347,100 @@ def _extract_topic_from_goal_query(goal_query: str, brief: PlanningSemanticBrief
|
||||||
return topic or "Technik"
|
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]]:
|
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)."""
|
"""„von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y)."""
|
||||||
q = (goal_query or "").strip()
|
q = (goal_query or "").strip()
|
||||||
|
|
@ -343,9 +487,10 @@ def build_goal_analysis(
|
||||||
brief: PlanningSemanticBrief,
|
brief: PlanningSemanticBrief,
|
||||||
*,
|
*,
|
||||||
structured: Optional[RoadmapStructuredInput] = None,
|
structured: Optional[RoadmapStructuredInput] = None,
|
||||||
|
topic_override: Optional[str] = None,
|
||||||
) -> GoalAnalysisArtifact:
|
) -> GoalAnalysisArtifact:
|
||||||
"""Phase A — aus Anfrage, optionalen Feldern und Semantic Brief."""
|
"""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)
|
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||||
|
|
||||||
start = (structured.start_situation if structured else None) or parsed_start
|
start = (structured.start_situation if structured else None) or parsed_start
|
||||||
|
|
@ -797,7 +942,7 @@ def _merge_structured_into_goal_analysis(
|
||||||
brief: PlanningSemanticBrief,
|
brief: PlanningSemanticBrief,
|
||||||
structured: Optional[RoadmapStructuredInput],
|
structured: Optional[RoadmapStructuredInput],
|
||||||
) -> GoalAnalysisArtifact:
|
) -> 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):
|
if not _has_specific_start_target(ga_struct):
|
||||||
return llm_ga
|
return llm_ga
|
||||||
merged_criteria = list(
|
merged_criteria = list(
|
||||||
|
|
@ -822,14 +967,22 @@ def run_progression_roadmap_pipeline(
|
||||||
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
semantic_brief: Optional[PlanningSemanticBrief] = None,
|
||||||
cur=None,
|
cur=None,
|
||||||
include_llm_roadmap: bool = False,
|
include_llm_roadmap: bool = False,
|
||||||
|
include_llm_start_target: bool = False,
|
||||||
structured: Optional[RoadmapStructuredInput] = None,
|
structured: Optional[RoadmapStructuredInput] = None,
|
||||||
) -> ProgressionRoadmapContext:
|
) -> ProgressionRoadmapContext:
|
||||||
"""Workflow-lite: Phase A → B → C. LLM über ai_prompts-Slugs; deterministischer Fallback."""
|
"""Workflow-lite: Phase A → B → C. LLM über ai_prompts-Slugs; deterministischer Fallback."""
|
||||||
brief = semantic_brief or build_semantic_brief(goal_query)
|
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)
|
parsed_start, parsed_target = parse_start_target_from_goal_query(goal_query)
|
||||||
llm_goal_query = _roadmap_llm_goal_block(
|
llm_goal_query = _roadmap_llm_goal_block(
|
||||||
goal_query,
|
goal_query,
|
||||||
structured=structured,
|
structured=resolved,
|
||||||
parsed_start=parsed_start,
|
parsed_start=parsed_start,
|
||||||
parsed_target=parsed_target,
|
parsed_target=parsed_target,
|
||||||
)
|
)
|
||||||
|
|
@ -837,9 +990,24 @@ def run_progression_roadmap_pipeline(
|
||||||
goal_query=goal_query.strip(),
|
goal_query=goal_query.strip(),
|
||||||
max_steps=max_steps,
|
max_steps=max_steps,
|
||||||
semantic_brief=brief_to_summary_dict(brief),
|
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:
|
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)
|
llm_ga, ga_ok = try_llm_goal_analysis(cur, goal_query=llm_goal_query, brief=brief)
|
||||||
if ga_ok and llm_ga:
|
if ga_ok and llm_ga:
|
||||||
|
|
@ -847,7 +1015,7 @@ def run_progression_roadmap_pipeline(
|
||||||
llm_ga,
|
llm_ga,
|
||||||
goal_query=goal_query,
|
goal_query=goal_query,
|
||||||
brief=brief,
|
brief=brief,
|
||||||
structured=structured,
|
structured=resolved,
|
||||||
)
|
)
|
||||||
ctx.llm_goal_analysis_applied = True
|
ctx.llm_goal_analysis_applied = True
|
||||||
ctx.prompt_slugs.append(PROMPT_SLUG_GOAL_ANALYSIS)
|
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]:
|
def progression_roadmap_to_api_dict(ctx: ProgressionRoadmapContext) -> Dict[str, Any]:
|
||||||
|
resolve = ctx.start_target_resolve
|
||||||
return {
|
return {
|
||||||
"goal_analysis": ctx.goal_analysis.model_dump() if ctx.goal_analysis else None,
|
"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,
|
"roadmap": ctx.roadmap.model_dump() if ctx.roadmap else None,
|
||||||
"stage_specs": [s.model_dump() for s in ctx.stage_specs],
|
"stage_specs": [s.model_dump() for s in ctx.stage_specs],
|
||||||
"pipeline_phase": ctx.pipeline_phase,
|
"pipeline_phase": ctx.pipeline_phase,
|
||||||
"major_step_count": len(ctx.roadmap.major_steps) if ctx.roadmap else 0,
|
"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,
|
"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_goal_analysis_applied": ctx.llm_goal_analysis_applied,
|
||||||
"llm_roadmap_applied": ctx.llm_roadmap_applied,
|
"llm_roadmap_applied": ctx.llm_roadmap_applied,
|
||||||
"llm_stage_spec_applied": ctx.llm_stage_spec_applied,
|
"llm_stage_spec_applied": ctx.llm_stage_spec_applied,
|
||||||
"prompt_slugs": list(ctx.prompt_slugs),
|
"prompt_slugs": list(ctx.prompt_slugs),
|
||||||
"prompt_slug_catalog": {
|
"prompt_slug_catalog": {
|
||||||
|
"start_target": PROMPT_SLUG_START_TARGET,
|
||||||
"goal_analysis": PROMPT_SLUG_GOAL_ANALYSIS,
|
"goal_analysis": PROMPT_SLUG_GOAL_ANALYSIS,
|
||||||
"roadmap": PROMPT_SLUG_ROADMAP,
|
"roadmap": PROMPT_SLUG_ROADMAP,
|
||||||
"stage_spec": PROMPT_SLUG_STAGE_SPEC,
|
"stage_spec": PROMPT_SLUG_STAGE_SPEC,
|
||||||
|
|
@ -921,6 +1108,7 @@ def progression_roadmap_to_api_dict(ctx: ProgressionRoadmapContext) -> Dict[str,
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"PROMPT_SLUG_START_TARGET",
|
||||||
"PROMPT_SLUG_GOAL_ANALYSIS",
|
"PROMPT_SLUG_GOAL_ANALYSIS",
|
||||||
"PROMPT_SLUG_ROADMAP",
|
"PROMPT_SLUG_ROADMAP",
|
||||||
"PROMPT_SLUG_STAGE_SPEC",
|
"PROMPT_SLUG_STAGE_SPEC",
|
||||||
|
|
@ -931,8 +1119,11 @@ __all__ = [
|
||||||
"RoadmapArtifact",
|
"RoadmapArtifact",
|
||||||
"RoadmapOverridePayload",
|
"RoadmapOverridePayload",
|
||||||
"RoadmapStructuredInput",
|
"RoadmapStructuredInput",
|
||||||
|
"StartTargetExtractArtifact",
|
||||||
|
"StartTargetResolveMeta",
|
||||||
"normalize_major_steps_for_override",
|
"normalize_major_steps_for_override",
|
||||||
"parse_start_target_from_goal_query",
|
"parse_start_target_from_goal_query",
|
||||||
|
"resolve_roadmap_structured_input",
|
||||||
"roadmap_context_from_override",
|
"roadmap_context_from_override",
|
||||||
"StageSpecArtifact",
|
"StageSpecArtifact",
|
||||||
"build_goal_analysis",
|
"build_goal_analysis",
|
||||||
|
|
@ -945,6 +1136,7 @@ __all__ = [
|
||||||
"develop_micro_objectives",
|
"develop_micro_objectives",
|
||||||
"progression_roadmap_to_api_dict",
|
"progression_roadmap_to_api_dict",
|
||||||
"run_progression_roadmap_pipeline",
|
"run_progression_roadmap_pipeline",
|
||||||
|
"try_llm_start_target_extract",
|
||||||
"try_llm_goal_analysis",
|
"try_llm_goal_analysis",
|
||||||
"try_llm_roadmap",
|
"try_llm_roadmap",
|
||||||
"try_llm_stage_specs",
|
"try_llm_stage_specs",
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ def post_progression_path_suggest(
|
||||||
or body.include_llm_path_qa
|
or body.include_llm_path_qa
|
||||||
or body.include_ai_gap_fill
|
or body.include_ai_gap_fill
|
||||||
or body.include_llm_roadmap
|
or body.include_llm_roadmap
|
||||||
|
or body.include_llm_start_target
|
||||||
)
|
)
|
||||||
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
||||||
if uses_ai:
|
if uses_ai:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from planning_progression_roadmap import (
|
||||||
PROMPT_SLUG_GOAL_ANALYSIS,
|
PROMPT_SLUG_GOAL_ANALYSIS,
|
||||||
PROMPT_SLUG_ROADMAP,
|
PROMPT_SLUG_ROADMAP,
|
||||||
PROMPT_SLUG_STAGE_SPEC,
|
PROMPT_SLUG_STAGE_SPEC,
|
||||||
|
PROMPT_SLUG_START_TARGET,
|
||||||
MajorStep,
|
MajorStep,
|
||||||
RoadmapStructuredInput,
|
RoadmapStructuredInput,
|
||||||
StageSpecArtifact,
|
StageSpecArtifact,
|
||||||
|
|
@ -12,6 +13,7 @@ from planning_progression_roadmap import (
|
||||||
develop_micro_objectives,
|
develop_micro_objectives,
|
||||||
parse_start_target_from_goal_query,
|
parse_start_target_from_goal_query,
|
||||||
progression_roadmap_to_api_dict,
|
progression_roadmap_to_api_dict,
|
||||||
|
resolve_roadmap_structured_input,
|
||||||
resolve_step_exercise_kind_filter,
|
resolve_step_exercise_kind_filter,
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
stage_spec_exercise_kind_filter,
|
stage_spec_exercise_kind_filter,
|
||||||
|
|
@ -137,12 +139,53 @@ def test_roadmap_context_from_override():
|
||||||
def test_api_dict_exposes_prompt_slug_catalog():
|
def test_api_dict_exposes_prompt_slug_catalog():
|
||||||
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
||||||
api = progression_roadmap_to_api_dict(ctx)
|
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"]["goal_analysis"] == PROMPT_SLUG_GOAL_ANALYSIS
|
||||||
assert api["prompt_slug_catalog"]["roadmap"] == PROMPT_SLUG_ROADMAP
|
assert api["prompt_slug_catalog"]["roadmap"] == PROMPT_SLUG_ROADMAP
|
||||||
assert api["prompt_slug_catalog"]["stage_spec"] == PROMPT_SLUG_STAGE_SPEC
|
assert api["prompt_slug_catalog"]["stage_spec"] == PROMPT_SLUG_STAGE_SPEC
|
||||||
assert api["prompt_slugs"] == []
|
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():
|
def test_parse_start_target_kumite_beinarbeit():
|
||||||
start, target = parse_start_target_from_goal_query(KUMITE_GOAL)
|
start, target = parse_start_target_from_goal_query(KUMITE_GOAL)
|
||||||
assert start is not None
|
assert start is not None
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.210"
|
APP_VERSION = "0.8.211"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260606086"
|
DB_SCHEMA_VERSION = "20260607087"
|
||||||
|
|
||||||
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.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_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
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,24 @@ import {
|
||||||
} from '../utils/exerciseAiQuickCreate'
|
} from '../utils/exerciseAiQuickCreate'
|
||||||
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
import { buildPathGapPlanningContextForAi } from '../utils/planningContextForExerciseAi'
|
||||||
|
|
||||||
/** „von … bis …“ aus Freitext (z. B. Kumite Beinarbeit von X bis Y). */
|
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
|
||||||
function parseStartTargetFromGoalQuery(q) {
|
const rs = progressionRoadmap?.resolved_structured
|
||||||
const text = String(q || '').trim()
|
if (!rs) return
|
||||||
if (!text) return { start: '', target: '' }
|
if (rs.start_situation) setters.setStartSituation(String(rs.start_situation))
|
||||||
const m = text.match(
|
if (rs.target_state) setters.setTargetState(String(rs.target_state))
|
||||||
/\bvon\s+((?:(?:der|die|dem|das|einer?|einem)\s+)?.+?)\s+bis\s+(?:zur?|zum|zu der|zu einem)?\s*(.+)$/is,
|
if (rs.roadmap_notes) setters.setRoadmapNotes(String(rs.roadmap_notes))
|
||||||
)
|
}
|
||||||
if (!m) return { start: '', target: '' }
|
|
||||||
const start = m[1].trim().replace(/[.,;]+$/, '')
|
function sourceLabel(source) {
|
||||||
const target = m[2].trim().replace(/[.,;]+$/, '')
|
const map = {
|
||||||
if (start.length < 4 || target.length < 4) return { start: '', target: '' }
|
user: 'manuell',
|
||||||
return { start, target }
|
llm: 'KI-Extraktion',
|
||||||
|
regex: 'Muster (von … bis …)',
|
||||||
|
merged: 'manuell + KI',
|
||||||
|
heuristic: 'heuristisch',
|
||||||
|
none: '—',
|
||||||
|
}
|
||||||
|
return map[source] || source || '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
function roadmapStructuredPayload(startSituation, targetState, roadmapNotes) {
|
function roadmapStructuredPayload(startSituation, targetState, roadmapNotes) {
|
||||||
|
|
@ -535,19 +541,6 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
alert('Zuerst einen Graphen wählen.')
|
alert('Zuerst einen Graphen wählen.')
|
||||||
return
|
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)
|
setLoadingRoadmap(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
|
|
@ -561,9 +554,10 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
include_ai_gap_fill: false,
|
include_ai_gap_fill: false,
|
||||||
include_roadmap_preview: true,
|
include_roadmap_preview: true,
|
||||||
include_llm_roadmap: true,
|
include_llm_roadmap: true,
|
||||||
|
include_llm_start_target: true,
|
||||||
roadmap_only: true,
|
roadmap_only: true,
|
||||||
progression_graph_id: Number(graphId),
|
progression_graph_id: Number(graphId),
|
||||||
...roadmapStructuredPayload(start, target, roadmapNotes),
|
...roadmapStructuredPayload(startSituation, targetState, roadmapNotes),
|
||||||
})
|
})
|
||||||
const majors = mapMajorStepsFromApi(res?.progression_roadmap)
|
const majors = mapMajorStepsFromApi(res?.progression_roadmap)
|
||||||
if (majors.length < 2) {
|
if (majors.length < 2) {
|
||||||
|
|
@ -571,7 +565,13 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
}
|
}
|
||||||
setEditableMajorSteps(majors)
|
setEditableMajorSteps(majors)
|
||||||
setMaxSteps(majors.length)
|
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)
|
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||||
setPathSteps([])
|
setPathSteps([])
|
||||||
setTargetSummary(null)
|
setTargetSummary(null)
|
||||||
|
|
@ -767,7 +767,8 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
<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>
|
</p>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -819,6 +820,11 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||||
<strong style={{ fontSize: '13px' }}>Zielanalyse</strong>
|
<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 ? (
|
{progressionRoadmap.llm_goal_analysis_applied ? (
|
||||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||||
KI-Zielanalyse
|
KI-Zielanalyse
|
||||||
|
|
@ -826,6 +832,12 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
) : (
|
) : (
|
||||||
<span className="exercise-tag">heuristisch</span>
|
<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 ? (
|
{progressionRoadmap.goal_analysis.primary_topic ? (
|
||||||
<span className="exercise-tag">Thema: {progressionRoadmap.goal_analysis.primary_topic}</span>
|
<span className="exercise-tag">Thema: {progressionRoadmap.goal_analysis.primary_topic}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -847,6 +859,11 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : 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>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user