Update Dockerfile and requirements for improved dependency management
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 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
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 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
- Added `tzdata` installation in the Dockerfile to support time zone handling in Linux environments. - Increased `PIP_DEFAULT_TIMEOUT` and added retry logic for pip installations to enhance reliability during dependency installation. - Updated `requirements.txt` to conditionally include `tzdata` for Windows platforms, ensuring compatibility across different operating systems.
This commit is contained in:
parent
8f1dad53ab
commit
480890d0c6
|
|
@ -2,14 +2,16 @@ FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies (tzdata für zoneinfo/ZoneInfo unter Linux)
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
|
tzdata \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements and install dependencies
|
# Copy requirements and install dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
ENV PIP_DEFAULT_TIMEOUT=120
|
||||||
|
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
|
||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Wird als ``planning_context_json`` in Übungs-Prompts (summary, skills, instruct
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List, Mapping, Optional
|
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
_MAX_JSON_CHARS = 6000
|
_MAX_JSON_CHARS = 6000
|
||||||
_MAX_STRING = 800
|
_MAX_STRING = 800
|
||||||
|
|
@ -85,6 +85,163 @@ def planning_context_prompt_variables(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _major_index_from_step(step: Mapping[str, Any]) -> Optional[int]:
|
||||||
|
for key in ("roadmap_major_step_index", "major_step_index"):
|
||||||
|
raw = step.get(key)
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def prior_path_steps_before_major(
|
||||||
|
steps: Sequence[Mapping[str, Any]],
|
||||||
|
major_idx: int,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Pfadschritte mit kleinerem roadmap_major_step_index, sortiert."""
|
||||||
|
prior: List[Dict[str, Any]] = []
|
||||||
|
for step in steps:
|
||||||
|
mi = _major_index_from_step(step)
|
||||||
|
if mi is not None and mi < major_idx:
|
||||||
|
prior.append(dict(step))
|
||||||
|
prior.sort(key=lambda s: _major_index_from_step(s) or 0)
|
||||||
|
return prior
|
||||||
|
|
||||||
|
|
||||||
|
def _step_display_fields(step: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
|
title = _trim_str(
|
||||||
|
step.get("title") or step.get("exercise_title"),
|
||||||
|
limit=200,
|
||||||
|
)
|
||||||
|
learning_goal = _trim_str(
|
||||||
|
step.get("roadmap_learning_goal") or step.get("learning_goal"),
|
||||||
|
limit=500,
|
||||||
|
)
|
||||||
|
summary = _trim_str(step.get("summary"), limit=400)
|
||||||
|
start_state = _trim_str(step.get("roadmap_start_state") or step.get("start_state"))
|
||||||
|
target_state = _trim_str(step.get("roadmap_target_state") or step.get("target_state"))
|
||||||
|
phase = _trim_str(step.get("roadmap_phase") or step.get("phase"))
|
||||||
|
criteria_raw = step.get("stage_success_criteria") or step.get("success_criteria") or []
|
||||||
|
criteria = [
|
||||||
|
t
|
||||||
|
for x in criteria_raw
|
||||||
|
if (t := _trim_str(x, limit=200))
|
||||||
|
][:4]
|
||||||
|
out: Dict[str, Any] = {
|
||||||
|
"title": title,
|
||||||
|
"learning_goal": learning_goal,
|
||||||
|
"summary": summary,
|
||||||
|
"start_state": start_state,
|
||||||
|
"target_state": target_state,
|
||||||
|
"phase": phase,
|
||||||
|
"success_criteria": criteria or None,
|
||||||
|
"major_step_index": _major_index_from_step(step),
|
||||||
|
}
|
||||||
|
return {k: v for k, v in out.items() if v is not None and v != "" and v != []}
|
||||||
|
|
||||||
|
|
||||||
|
def build_progression_entry_state(
|
||||||
|
*,
|
||||||
|
major_step_index: Optional[int] = None,
|
||||||
|
prior_steps: Sequence[Mapping[str, Any]] = (),
|
||||||
|
start_situation: Optional[str] = None,
|
||||||
|
current_stage_start: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Eingangszustand für eine Roadmap-Stufe: erreichte Voraussetzungen aus Vorstufen.
|
||||||
|
"""
|
||||||
|
prior_compact = [_step_display_fields(s) for s in prior_steps]
|
||||||
|
prior_compact = [
|
||||||
|
p
|
||||||
|
for p in prior_compact
|
||||||
|
if any(p.get(k) for k in ("title", "learning_goal", "summary", "success_criteria"))
|
||||||
|
]
|
||||||
|
|
||||||
|
achievements: List[str] = []
|
||||||
|
detail_lines: List[str] = []
|
||||||
|
for p in prior_compact:
|
||||||
|
if p.get("success_criteria"):
|
||||||
|
achievements.extend(p["success_criteria"])
|
||||||
|
elif p.get("learning_goal"):
|
||||||
|
achievements.append(p["learning_goal"])
|
||||||
|
|
||||||
|
label_parts: List[str] = []
|
||||||
|
if p.get("major_step_index") is not None:
|
||||||
|
label_parts.append(f"Stufe {int(p['major_step_index']) + 1}")
|
||||||
|
if p.get("phase"):
|
||||||
|
label_parts.append(f"({p['phase']})")
|
||||||
|
if p.get("title"):
|
||||||
|
label_parts.append(f"„{p['title']}\"")
|
||||||
|
prefix = " ".join(label_parts) if label_parts else "Vorstufe"
|
||||||
|
achieved = ""
|
||||||
|
if p.get("target_state"):
|
||||||
|
achieved = p["target_state"]
|
||||||
|
elif p.get("success_criteria"):
|
||||||
|
achieved = "; ".join(p["success_criteria"])
|
||||||
|
elif p.get("learning_goal"):
|
||||||
|
achieved = p["learning_goal"]
|
||||||
|
elif p.get("summary"):
|
||||||
|
achieved = p["summary"]
|
||||||
|
if achieved:
|
||||||
|
detail_lines.append(f"{prefix}: erreicht — {achieved}")
|
||||||
|
|
||||||
|
immediate_entry: Optional[str] = _trim_str(current_stage_start)
|
||||||
|
if not immediate_entry and prior_compact:
|
||||||
|
immediate = prior_compact[-1]
|
||||||
|
if immediate.get("target_state"):
|
||||||
|
immediate_entry = immediate["target_state"]
|
||||||
|
elif immediate.get("success_criteria"):
|
||||||
|
immediate_entry = "; ".join(immediate["success_criteria"])
|
||||||
|
elif immediate.get("learning_goal"):
|
||||||
|
immediate_entry = immediate["learning_goal"]
|
||||||
|
elif immediate.get("summary"):
|
||||||
|
immediate_entry = immediate["summary"]
|
||||||
|
elif not immediate_entry and start_situation:
|
||||||
|
immediate_entry = start_situation
|
||||||
|
|
||||||
|
entry_state = immediate_entry or start_situation
|
||||||
|
if prior_compact and start_situation and not immediate_entry:
|
||||||
|
detail_lines.insert(0, f"Ausgangsbasis Pfad: {start_situation}")
|
||||||
|
|
||||||
|
out: Dict[str, Any] = {}
|
||||||
|
if entry_state:
|
||||||
|
out["entry_state"] = _trim_str(entry_state, limit=1200)
|
||||||
|
if detail_lines:
|
||||||
|
out["entry_state_detail"] = _trim_str("\n".join(detail_lines), limit=2000)
|
||||||
|
if prior_compact:
|
||||||
|
out["prior_steps"] = prior_compact[:6]
|
||||||
|
if achievements:
|
||||||
|
out["prior_achievements"] = list(dict.fromkeys(achievements))[:8]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_gap_snapshot_with_entry_state(
|
||||||
|
snapshot: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
steps: Sequence[Mapping[str, Any]],
|
||||||
|
major_step_index: Optional[int],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
snap = dict(snapshot)
|
||||||
|
if major_step_index is None:
|
||||||
|
return snap
|
||||||
|
try:
|
||||||
|
mi = int(major_step_index)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return snap
|
||||||
|
prior = prior_path_steps_before_major(steps, mi)
|
||||||
|
entry = build_progression_entry_state(
|
||||||
|
major_step_index=mi,
|
||||||
|
prior_steps=prior,
|
||||||
|
start_situation=snap.get("start_situation"),
|
||||||
|
current_stage_start=snap.get("stage_start_state"),
|
||||||
|
)
|
||||||
|
snap.update(entry)
|
||||||
|
return snap
|
||||||
|
|
||||||
|
|
||||||
def build_progression_gap_snapshot(
|
def build_progression_gap_snapshot(
|
||||||
*,
|
*,
|
||||||
goal_analysis: Optional[Mapping[str, Any]] = None,
|
goal_analysis: Optional[Mapping[str, Any]] = None,
|
||||||
|
|
@ -141,6 +298,8 @@ def build_progression_gap_snapshot(
|
||||||
"stage_learning_goal": _trim_str(
|
"stage_learning_goal": _trim_str(
|
||||||
spec.get("learning_goal"), limit=1200
|
spec.get("learning_goal"), limit=1200
|
||||||
),
|
),
|
||||||
|
"stage_start_state": _trim_str(spec.get("start_state")),
|
||||||
|
"stage_target_state": _trim_str(spec.get("target_state")),
|
||||||
"stage_phase": _trim_str(spec.get("phase")),
|
"stage_phase": _trim_str(spec.get("phase")),
|
||||||
"stage_exercise_type": _trim_str(spec.get("exercise_type")),
|
"stage_exercise_type": _trim_str(spec.get("exercise_type")),
|
||||||
"stage_load_profile": load_profile or None,
|
"stage_load_profile": load_profile or None,
|
||||||
|
|
@ -160,6 +319,7 @@ def build_progression_path_gap_planning_context(
|
||||||
offer: Optional[Mapping[str, Any]] = None,
|
offer: Optional[Mapping[str, Any]] = None,
|
||||||
neighbor_before: Optional[Mapping[str, Any]] = None,
|
neighbor_before: Optional[Mapping[str, Any]] = None,
|
||||||
neighbor_after: Optional[Mapping[str, Any]] = None,
|
neighbor_after: Optional[Mapping[str, Any]] = None,
|
||||||
|
prior_path_steps: Optional[Sequence[Mapping[str, Any]]] = None,
|
||||||
path_step_count: int = 0,
|
path_step_count: int = 0,
|
||||||
major_step_count: Optional[int] = None,
|
major_step_count: Optional[int] = None,
|
||||||
roadmap_phase: Optional[str] = None,
|
roadmap_phase: Optional[str] = None,
|
||||||
|
|
@ -207,6 +367,14 @@ def build_progression_path_gap_planning_context(
|
||||||
semantic_brief=semantic_brief,
|
semantic_brief=semantic_brief,
|
||||||
)
|
)
|
||||||
ctx.update(snap)
|
ctx.update(snap)
|
||||||
|
if major_idx is not None and prior_path_steps:
|
||||||
|
ctx.update(
|
||||||
|
build_progression_entry_state(
|
||||||
|
major_step_index=major_idx,
|
||||||
|
prior_steps=list(prior_path_steps),
|
||||||
|
start_situation=ctx.get("start_situation"),
|
||||||
|
)
|
||||||
|
)
|
||||||
if stage_learning_goal_override and stage_learning_goal_override.strip():
|
if stage_learning_goal_override and stage_learning_goal_override.strip():
|
||||||
ctx["stage_learning_goal"] = _trim_str(stage_learning_goal_override, limit=1200)
|
ctx["stage_learning_goal"] = _trim_str(stage_learning_goal_override, limit=1200)
|
||||||
ctx["roadmap_learning_goal"] = ctx["stage_learning_goal"]
|
ctx["roadmap_learning_goal"] = ctx["stage_learning_goal"]
|
||||||
|
|
@ -216,8 +384,11 @@ def build_progression_path_gap_planning_context(
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"build_progression_entry_state",
|
||||||
"build_progression_gap_snapshot",
|
"build_progression_gap_snapshot",
|
||||||
"build_progression_path_gap_planning_context",
|
"build_progression_path_gap_planning_context",
|
||||||
|
"enrich_gap_snapshot_with_entry_state",
|
||||||
|
"prior_path_steps_before_major",
|
||||||
"compact_planning_context_json",
|
"compact_planning_context_json",
|
||||||
"planning_context_prompt_variables",
|
"planning_context_prompt_variables",
|
||||||
"sanitize_planning_context_for_ai",
|
"sanitize_planning_context_for_ai",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,12 @@ from ai_prompt_job import run_exercise_form_ai_suggestion
|
||||||
from exercise_ai import strip_html_to_plain
|
from exercise_ai import strip_html_to_plain
|
||||||
|
|
||||||
from planning_exercise_path_qa import find_step_pair_index
|
from planning_exercise_path_qa import find_step_pair_index
|
||||||
from planning_exercise_form_context import build_progression_gap_snapshot
|
from planning_exercise_form_context import (
|
||||||
|
build_progression_entry_state,
|
||||||
|
build_progression_gap_snapshot,
|
||||||
|
enrich_gap_snapshot_with_entry_state,
|
||||||
|
prior_path_steps_before_major,
|
||||||
|
)
|
||||||
from planning_exercise_semantics import PlanningSemanticBrief, brief_to_summary_dict
|
from planning_exercise_semantics import PlanningSemanticBrief, brief_to_summary_dict
|
||||||
|
|
||||||
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
|
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
|
||||||
|
|
@ -47,6 +52,8 @@ def _build_stage_ai_context(
|
||||||
spec: Mapping[str, Any],
|
spec: Mapping[str, Any],
|
||||||
step_before: Optional[Mapping[str, Any]] = None,
|
step_before: Optional[Mapping[str, Any]] = None,
|
||||||
step_after: Optional[Mapping[str, Any]] = None,
|
step_after: Optional[Mapping[str, Any]] = None,
|
||||||
|
prior_steps: Optional[Sequence[Mapping[str, Any]]] = None,
|
||||||
|
start_situation: Optional[str] = None,
|
||||||
) -> ExerciseFormAiPromptContext:
|
) -> ExerciseFormAiPromptContext:
|
||||||
"""KI-Kontext für unbesetzte Roadmap-Stufe (keine Brücke zwischen falschen Array-Indizes)."""
|
"""KI-Kontext für unbesetzte Roadmap-Stufe (keine Brücke zwischen falschen Array-Indizes)."""
|
||||||
gap = dict(spec.get("gap") or {})
|
gap = dict(spec.get("gap") or {})
|
||||||
|
|
@ -59,11 +66,26 @@ def _build_stage_ai_context(
|
||||||
or ""
|
or ""
|
||||||
).strip()
|
).strip()
|
||||||
title = (spec.get("title_hint") or f"{topic} — {phase}").strip()[:280]
|
title = (spec.get("title_hint") or f"{topic} — {phase}").strip()[:280]
|
||||||
|
major_idx = spec.get("roadmap_major_step_index")
|
||||||
|
entry: Dict[str, Any] = {}
|
||||||
|
if prior_steps is not None and major_idx is not None:
|
||||||
|
entry = build_progression_entry_state(
|
||||||
|
major_step_index=major_idx,
|
||||||
|
prior_steps=prior_steps,
|
||||||
|
start_situation=start_situation,
|
||||||
|
)
|
||||||
|
|
||||||
goal_parts = [
|
goal_parts = [
|
||||||
f"Planungsziel: {goal_query}",
|
f"Planungsziel: {goal_query}",
|
||||||
f"Roadmap-Stufe ({phase}): {learning_goal}",
|
f"Roadmap-Stufe ({phase}): {learning_goal}",
|
||||||
"Erstelle eine Übung, die dieses Stufen-Lernziel erfüllt — keine generische Brücken-Übung.",
|
"Erstelle eine Übung, die dieses Stufen-Lernziel erfüllt — keine generische Brücken-Übung.",
|
||||||
]
|
]
|
||||||
|
if entry.get("entry_state"):
|
||||||
|
goal_parts.append(
|
||||||
|
f"Eingangszustand (erreichte Voraussetzungen): {entry['entry_state']}"
|
||||||
|
)
|
||||||
|
if entry.get("entry_state_detail") and entry.get("entry_state_detail") != entry.get("entry_state"):
|
||||||
|
goal_parts.append(f"Bisheriger Pfad:\n{entry['entry_state_detail']}")
|
||||||
if step_before:
|
if step_before:
|
||||||
goal_parts.append(
|
goal_parts.append(
|
||||||
f"Vorherige Stufe im Pfad: „{(step_before.get('title') or '').strip()}“"
|
f"Vorherige Stufe im Pfad: „{(step_before.get('title') or '').strip()}“"
|
||||||
|
|
@ -106,6 +128,7 @@ def try_suggest_ai_stage_step(
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
step_before, step_after = _resolve_neighbor_steps_by_major_index(steps, mi)
|
step_before, step_after = _resolve_neighbor_steps_by_major_index(steps, mi)
|
||||||
|
prior_steps = prior_path_steps_before_major(steps, mi)
|
||||||
gap = dict(spec.get("gap") or {})
|
gap = dict(spec.get("gap") or {})
|
||||||
if not gap.get("expected_phase"):
|
if not gap.get("expected_phase"):
|
||||||
gap["expected_phase"] = spec.get("phase") or "vertiefung"
|
gap["expected_phase"] = spec.get("phase") or "vertiefung"
|
||||||
|
|
@ -119,6 +142,7 @@ def try_suggest_ai_stage_step(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
step_before=step_before,
|
step_before=step_before,
|
||||||
step_after=step_after,
|
step_after=step_after,
|
||||||
|
prior_steps=prior_steps,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
ai_payload = run_exercise_form_ai_suggestion(cur, ctx=ctx)
|
ai_payload = run_exercise_form_ai_suggestion(cur, ctx=ctx)
|
||||||
|
|
@ -440,8 +464,16 @@ def build_gap_fill_goal_text(
|
||||||
f"Planungsziel (gesamter Pfad): {goal_query}",
|
f"Planungsziel (gesamter Pfad): {goal_query}",
|
||||||
f"Hauptthema: {snap.get('primary_topic') or topic}",
|
f"Hauptthema: {snap.get('primary_topic') or topic}",
|
||||||
]
|
]
|
||||||
if snap.get("start_situation"):
|
if snap.get("entry_state"):
|
||||||
|
parts.append(
|
||||||
|
f"Eingangszustand (erreichte Voraussetzungen aus Vorstufen): {snap['entry_state']}"
|
||||||
|
)
|
||||||
|
if snap.get("entry_state_detail") and snap.get("entry_state_detail") != snap.get("entry_state"):
|
||||||
|
parts.append(f"Bisheriger Pfad:\n{snap['entry_state_detail']}")
|
||||||
|
if snap.get("start_situation") and not snap.get("entry_state"):
|
||||||
parts.append(f"Voraussetzung / Ausgangslage (Progression): {snap['start_situation']}")
|
parts.append(f"Voraussetzung / Ausgangslage (Progression): {snap['start_situation']}")
|
||||||
|
elif snap.get("start_situation") and snap.get("prior_steps"):
|
||||||
|
parts.append(f"Ausgangsbasis des gesamten Pfads: {snap['start_situation']}")
|
||||||
if snap.get("target_state"):
|
if snap.get("target_state"):
|
||||||
parts.append(f"Gesamtziel der Progression: {snap['target_state']}")
|
parts.append(f"Gesamtziel der Progression: {snap['target_state']}")
|
||||||
if snap.get("roadmap_notes"):
|
if snap.get("roadmap_notes"):
|
||||||
|
|
@ -525,6 +557,14 @@ def build_gap_fill_offer(
|
||||||
step_a = steps[idx] if idx < len(steps) else None
|
step_a = steps[idx] if idx < len(steps) else None
|
||||||
step_b = steps[idx + 1] if idx + 1 < len(steps) else None
|
step_b = steps[idx + 1] if idx + 1 < len(steps) else None
|
||||||
offer_id = f"{spec.get('source')}-{idx}-{uuid.uuid4().hex[:8]}"
|
offer_id = f"{spec.get('source')}-{idx}-{uuid.uuid4().hex[:8]}"
|
||||||
|
enriched_snapshot = dict(roadmap_snapshot) if roadmap_snapshot else {}
|
||||||
|
major_raw = spec.get("roadmap_major_step_index")
|
||||||
|
if major_raw is not None:
|
||||||
|
enriched_snapshot = enrich_gap_snapshot_with_entry_state(
|
||||||
|
enriched_snapshot,
|
||||||
|
steps=steps,
|
||||||
|
major_step_index=major_raw,
|
||||||
|
)
|
||||||
goal_for_ai = ""
|
goal_for_ai = ""
|
||||||
if brief and goal_query:
|
if brief and goal_query:
|
||||||
goal_for_ai = build_gap_fill_goal_text(
|
goal_for_ai = build_gap_fill_goal_text(
|
||||||
|
|
@ -533,9 +573,9 @@ def build_gap_fill_offer(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
step_a=step_a,
|
step_a=step_a,
|
||||||
step_b=step_b,
|
step_b=step_b,
|
||||||
roadmap_snapshot=roadmap_snapshot,
|
roadmap_snapshot=enriched_snapshot or None,
|
||||||
)
|
)
|
||||||
ctx_preview = dict(roadmap_snapshot) if roadmap_snapshot else None
|
ctx_preview = enriched_snapshot or None
|
||||||
offer: Dict[str, Any] = {
|
offer: Dict[str, Any] = {
|
||||||
"offer_id": offer_id,
|
"offer_id": offer_id,
|
||||||
"source": spec.get("source"),
|
"source": spec.get("source"),
|
||||||
|
|
|
||||||
|
|
@ -535,6 +535,9 @@ def _annotate_roadmap_step(
|
||||||
step["roadmap_start_state"] = stage_spec.start_state.strip()
|
step["roadmap_start_state"] = stage_spec.start_state.strip()
|
||||||
if (stage_spec.target_state or "").strip():
|
if (stage_spec.target_state or "").strip():
|
||||||
step["roadmap_target_state"] = stage_spec.target_state.strip()
|
step["roadmap_target_state"] = stage_spec.target_state.strip()
|
||||||
|
if stage_spec.success_criteria:
|
||||||
|
step["success_criteria"] = list(stage_spec.success_criteria)
|
||||||
|
step["stage_success_criteria"] = list(stage_spec.success_criteria)
|
||||||
step["roadmap_match_source"] = "stage_spec"
|
step["roadmap_match_source"] = "stage_spec"
|
||||||
if skill_expectations:
|
if skill_expectations:
|
||||||
step["skill_expectations"] = skill_expectations
|
step["skill_expectations"] = skill_expectations
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@ bcrypt==4.1.3
|
||||||
slowapi==0.1.9
|
slowapi==0.1.9
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
python-dateutil==2.9.0
|
python-dateutil==2.9.0
|
||||||
tzdata>=2024.1 # ZoneInfo (Europe/Berlin) auch unter Windows
|
tzdata>=2024.1; sys_platform == "win32" # ZoneInfo lokal; Linux/Docker: apt tzdata
|
||||||
sqlparse>=0.5.0 # Migrationen: Statements splitten (Fallback ohne psql)
|
sqlparse>=0.5.0 # Migrationen: Statements splitten (Fallback ohne psql)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Tests Planungs-KI Phase D — planning_context für suggestExerciseAi."""
|
"""Tests Planungs-KI Phase D — planning_context für suggestExerciseAi."""
|
||||||
from planning_exercise_form_context import (
|
from planning_exercise_form_context import (
|
||||||
|
build_progression_entry_state,
|
||||||
build_progression_gap_snapshot,
|
build_progression_gap_snapshot,
|
||||||
build_progression_path_gap_planning_context,
|
build_progression_path_gap_planning_context,
|
||||||
|
enrich_gap_snapshot_with_entry_state,
|
||||||
planning_context_prompt_variables,
|
planning_context_prompt_variables,
|
||||||
sanitize_planning_context_for_ai,
|
sanitize_planning_context_for_ai,
|
||||||
)
|
)
|
||||||
|
|
@ -80,6 +82,47 @@ def test_gap_planning_context_carries_snapshot_fields():
|
||||||
assert ctx["stage_learning_goal"] == "Stufenziel"
|
assert ctx["stage_learning_goal"] == "Stufenziel"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_progression_entry_state_from_prior_steps():
|
||||||
|
entry = build_progression_entry_state(
|
||||||
|
major_step_index=2,
|
||||||
|
prior_steps=[
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 0,
|
||||||
|
"title": "Schritt-Stand",
|
||||||
|
"roadmap_phase": "einstieg",
|
||||||
|
"success_criteria": ["stabile Grundstellung"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 1,
|
||||||
|
"title": "Mawashi Vorbereitung",
|
||||||
|
"roadmap_target_state": "Hüfte dreht vor dem Knie",
|
||||||
|
"roadmap_phase": "grundlage",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
start_situation="Anfänger ohne Kumite-Erfahrung",
|
||||||
|
current_stage_start="Hüfte dreht vor dem Knie, sicherer Stand",
|
||||||
|
)
|
||||||
|
assert entry["entry_state"] == "Hüfte dreht vor dem Knie, sicherer Stand"
|
||||||
|
assert "Mawashi Vorbereitung" in entry["entry_state_detail"]
|
||||||
|
assert "stabile Grundstellung" in entry["prior_achievements"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_enrich_gap_snapshot_with_entry_state():
|
||||||
|
snap = enrich_gap_snapshot_with_entry_state(
|
||||||
|
{"start_situation": "Basis", "stage_learning_goal": "Rhythmen"},
|
||||||
|
steps=[
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 0,
|
||||||
|
"title": "A",
|
||||||
|
"success_criteria": ["Timing erkannt"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
major_step_index=1,
|
||||||
|
)
|
||||||
|
assert snap["entry_state"] == "Timing erkannt"
|
||||||
|
assert snap["prior_steps"][0]["title"] == "A"
|
||||||
|
|
||||||
|
|
||||||
def test_gap_planning_context_trainer_supplements_and_stage_override():
|
def test_gap_planning_context_trainer_supplements_and_stage_override():
|
||||||
ctx = build_progression_path_gap_planning_context(
|
ctx = build_progression_path_gap_planning_context(
|
||||||
goal_query="Kumite",
|
goal_query="Kumite",
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,36 @@ def test_build_gap_fill_offer_roadmap_unfilled_uses_major_step_neighbors():
|
||||||
assert "Stufen-Lernziel" in offer["goal_for_ai"] or "Roadmap-Stufe" in offer["goal_for_ai"]
|
assert "Stufen-Lernziel" in offer["goal_for_ai"] or "Roadmap-Stufe" in offer["goal_for_ai"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_gap_fill_offer_includes_entry_state_from_prior_steps():
|
||||||
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||||
|
steps = [
|
||||||
|
{
|
||||||
|
"roadmap_major_step_index": 0,
|
||||||
|
"title": "Schritt A",
|
||||||
|
"roadmap_target_state": "gleichmäßige Distanz",
|
||||||
|
"success_criteria": ["Partnerabstand stabil"],
|
||||||
|
},
|
||||||
|
{"roadmap_major_step_index": 2, "title": "Schritt C"},
|
||||||
|
]
|
||||||
|
offer = build_gap_fill_offer(
|
||||||
|
spec={
|
||||||
|
"source": "roadmap_unfilled",
|
||||||
|
"phase": "vertiefung",
|
||||||
|
"title_hint": "Rhythmen",
|
||||||
|
"roadmap_major_step_index": 1,
|
||||||
|
},
|
||||||
|
steps=steps,
|
||||||
|
goal_query="Kumite Beinarbeit",
|
||||||
|
brief=brief,
|
||||||
|
roadmap_snapshot={
|
||||||
|
"start_situation": "Steppbewegung",
|
||||||
|
"stage_learning_goal": "variable Rhythmen",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert offer["context_preview"]["entry_state"] == "gleichmäßige Distanz"
|
||||||
|
assert "Eingangszustand" in offer["goal_for_ai"]
|
||||||
|
|
||||||
|
|
||||||
def test_build_gap_fill_offer_exposes_context_preview():
|
def test_build_gap_fill_offer_exposes_context_preview():
|
||||||
brief = build_semantic_brief("Kumite Beinarbeit")
|
brief = build_semantic_brief("Kumite Beinarbeit")
|
||||||
offer = build_gap_fill_offer(
|
offer = build_gap_fill_offer(
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from '../utils/exerciseAiQuickCreate'
|
} from '../utils/exerciseAiQuickCreate'
|
||||||
import {
|
import {
|
||||||
buildPathGapPlanningContextForAi,
|
buildPathGapPlanningContextForAi,
|
||||||
|
buildSlotGapGoalForAi,
|
||||||
gapOfferContextDisplayLines,
|
gapOfferContextDisplayLines,
|
||||||
initialStageLearningGoalFromOffer,
|
initialStageLearningGoalFromOffer,
|
||||||
} from '../utils/planningContextForExerciseAi'
|
} from '../utils/planningContextForExerciseAi'
|
||||||
|
|
@ -527,15 +528,23 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
|
|
||||||
const slotOfferContext = (slotIndex) => {
|
const slotOfferContext = (slotIndex) => {
|
||||||
const slot = draft?.slots?.[slotIndex]
|
const slot = draft?.slots?.[slotIndex]
|
||||||
if (!slot) return null
|
if (!draft || !slot) return null
|
||||||
|
const goalForAi =
|
||||||
|
buildSlotGapGoalForAi(draft, slotIndex, { goalQuery: draft.goalQuery }) ||
|
||||||
|
slot.learning_goal
|
||||||
|
const priorSlot =
|
||||||
|
slotIndex > 0 && draft.slots[slotIndex - 1]
|
||||||
|
? draft.slots[slotIndex - 1]
|
||||||
|
: null
|
||||||
return {
|
return {
|
||||||
offer_id: `slot-${slotIndex}`,
|
offer_id: `slot-${slotIndex}`,
|
||||||
title_hint: slot.primary?.exerciseTitle || slot.learning_goal,
|
title_hint: slot.primary?.exerciseTitle || slot.learning_goal,
|
||||||
roadmap_major_step_index: slot.majorStepIndex,
|
roadmap_major_step_index: slot.majorStepIndex,
|
||||||
phase: slot.phase,
|
phase: slot.phase,
|
||||||
source: 'roadmap_unfilled',
|
source: 'roadmap_unfilled',
|
||||||
goal_for_ai: slot.learning_goal,
|
goal_for_ai: goalForAi,
|
||||||
sketch: slot.learning_goal,
|
sketch: goalForAi,
|
||||||
|
from_title: priorSlot?.primary?.exerciseTitle || null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
* Planungs-KI Phase D: strukturierter Kontext für suggestExerciseAi.
|
* Planungs-KI Phase D: strukturierter Kontext für suggestExerciseAi.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { slotsAsPathStepRows } from './progressionGraphDraft.js'
|
||||||
|
|
||||||
export function buildPickerPlanningContextForAi({
|
export function buildPickerPlanningContextForAi({
|
||||||
planningContextSummary = null,
|
planningContextSummary = null,
|
||||||
planningContext = null,
|
planningContext = null,
|
||||||
|
|
@ -30,6 +32,109 @@ export function buildPickerPlanningContextForAi({
|
||||||
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== ''))
|
return Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null && v !== ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function majorIndexFromStep(step) {
|
||||||
|
const raw = step?.roadmap_major_step_index ?? step?.roadmapMajorStepIndex
|
||||||
|
if (raw == null || !Number.isFinite(Number(raw))) return null
|
||||||
|
return Number(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
function priorPathStepsBeforeMajor(pathSteps, majorIdx) {
|
||||||
|
if (majorIdx == null || !Number.isFinite(Number(majorIdx))) return []
|
||||||
|
const mi = Number(majorIdx)
|
||||||
|
return (pathSteps || [])
|
||||||
|
.filter((s) => {
|
||||||
|
const idx = majorIndexFromStep(s)
|
||||||
|
return idx != null && idx < mi
|
||||||
|
})
|
||||||
|
.sort((a, b) => (majorIndexFromStep(a) || 0) - (majorIndexFromStep(b) || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepDisplayFields(step) {
|
||||||
|
if (!step) return null
|
||||||
|
const title = String(step.title || step.exerciseTitle || '').trim()
|
||||||
|
const learningGoal = String(
|
||||||
|
step.roadmap_learning_goal || step.roadmapLearningGoal || step.learning_goal || '',
|
||||||
|
).trim()
|
||||||
|
const phase = String(step.roadmap_phase || step.roadmapPhase || step.phase || '').trim()
|
||||||
|
const startState = String(step.roadmap_start_state || step.start_state || '').trim()
|
||||||
|
const targetState = String(step.roadmap_target_state || step.target_state || '').trim()
|
||||||
|
const criteria = Array.isArray(step.success_criteria)
|
||||||
|
? step.success_criteria.map((x) => String(x || '').trim()).filter(Boolean).slice(0, 4)
|
||||||
|
: []
|
||||||
|
const majorStepIndex = majorIndexFromStep(step)
|
||||||
|
const out = {
|
||||||
|
title: title || null,
|
||||||
|
learning_goal: learningGoal || null,
|
||||||
|
start_state: startState || null,
|
||||||
|
target_state: targetState || null,
|
||||||
|
phase: phase || null,
|
||||||
|
success_criteria: criteria.length ? criteria : null,
|
||||||
|
major_step_index: majorStepIndex,
|
||||||
|
}
|
||||||
|
const hasData = Object.values(out).some((v) => v != null && v !== '')
|
||||||
|
return hasData ? out : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildProgressionEntryState({
|
||||||
|
majorStepIndex = null,
|
||||||
|
priorSteps = [],
|
||||||
|
startSituation = '',
|
||||||
|
currentStageStart = '',
|
||||||
|
} = {}) {
|
||||||
|
const priorCompact = (priorSteps || [])
|
||||||
|
.map(stepDisplayFields)
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const achievements = []
|
||||||
|
const detailLines = []
|
||||||
|
for (const p of priorCompact) {
|
||||||
|
if (Array.isArray(p.success_criteria) && p.success_criteria.length) {
|
||||||
|
achievements.push(...p.success_criteria)
|
||||||
|
} else if (p.learning_goal) {
|
||||||
|
achievements.push(p.learning_goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelParts = []
|
||||||
|
if (p.major_step_index != null) labelParts.push(`Stufe ${p.major_step_index + 1}`)
|
||||||
|
if (p.phase) labelParts.push(`(${p.phase})`)
|
||||||
|
if (p.title) labelParts.push(`„${p.title}"`)
|
||||||
|
const prefix = labelParts.length ? labelParts.join(' ') : 'Vorstufe'
|
||||||
|
const achieved =
|
||||||
|
p.target_state ||
|
||||||
|
(Array.isArray(p.success_criteria) && p.success_criteria.length
|
||||||
|
? p.success_criteria.join('; ')
|
||||||
|
: '') ||
|
||||||
|
p.learning_goal ||
|
||||||
|
''
|
||||||
|
if (achieved) detailLines.push(`${prefix}: erreicht — ${achieved}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let entryState = (currentStageStart || '').trim()
|
||||||
|
if (!entryState && priorCompact.length) {
|
||||||
|
const immediate = priorCompact[priorCompact.length - 1]
|
||||||
|
entryState =
|
||||||
|
immediate.target_state ||
|
||||||
|
(Array.isArray(immediate.success_criteria) && immediate.success_criteria.length
|
||||||
|
? immediate.success_criteria.join('; ')
|
||||||
|
: '') ||
|
||||||
|
immediate.learning_goal ||
|
||||||
|
''
|
||||||
|
} else if (!entryState && (startSituation || '').trim()) {
|
||||||
|
entryState = startSituation.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priorCompact.length && (startSituation || '').trim() && !entryState) {
|
||||||
|
detailLines.unshift(`Ausgangsbasis Pfad: ${startSituation.trim()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = {}
|
||||||
|
if (entryState) out.entry_state = entryState
|
||||||
|
if (detailLines.length) out.entry_state_detail = detailLines.join('\n')
|
||||||
|
if (priorCompact.length) out.prior_steps = priorCompact.slice(0, 6)
|
||||||
|
if (achievements.length) out.prior_achievements = [...new Set(achievements)].slice(0, 8)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
function stageSpecForMajorIndex(progressionRoadmap, majorIdx) {
|
function stageSpecForMajorIndex(progressionRoadmap, majorIdx) {
|
||||||
if (majorIdx == null || !progressionRoadmap) return null
|
if (majorIdx == null || !progressionRoadmap) return null
|
||||||
const specs = progressionRoadmap?.stage_specs
|
const specs = progressionRoadmap?.stage_specs
|
||||||
|
|
@ -56,14 +161,25 @@ export function buildPathGapPlanningContextForAi({
|
||||||
stageLearningGoalOverride = '',
|
stageLearningGoalOverride = '',
|
||||||
gapTrainerSupplements = '',
|
gapTrainerSupplements = '',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const afterIdx = Number(offer?.insert_after_index)
|
|
||||||
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
|
|
||||||
const stepB =
|
|
||||||
Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx + 1] : null
|
|
||||||
const majorIdxRaw =
|
const majorIdxRaw =
|
||||||
offer?.roadmap_major_step_index ?? offer?.gap?.roadmap_major_step_index
|
offer?.roadmap_major_step_index ?? offer?.gap?.roadmap_major_step_index
|
||||||
const majorIdx =
|
const majorIdx =
|
||||||
majorIdxRaw != null && Number.isFinite(Number(majorIdxRaw)) ? Number(majorIdxRaw) : null
|
majorIdxRaw != null && Number.isFinite(Number(majorIdxRaw)) ? Number(majorIdxRaw) : null
|
||||||
|
const priorSteps = majorIdx != null ? priorPathStepsBeforeMajor(pathSteps, majorIdx) : []
|
||||||
|
const afterIdx = Number(offer?.insert_after_index)
|
||||||
|
const stepA =
|
||||||
|
priorSteps.length > 0
|
||||||
|
? priorSteps[priorSteps.length - 1]
|
||||||
|
: Number.isFinite(afterIdx) && afterIdx >= 0
|
||||||
|
? pathSteps[afterIdx]
|
||||||
|
: null
|
||||||
|
const stepB =
|
||||||
|
majorIdx != null
|
||||||
|
? (pathSteps || []).find((s) => majorIndexFromStep(s) === majorIdx + 1) ||
|
||||||
|
(Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx + 1] : null)
|
||||||
|
: Number.isFinite(afterIdx) && afterIdx >= 0
|
||||||
|
? pathSteps[afterIdx + 1]
|
||||||
|
: null
|
||||||
const majorStep =
|
const majorStep =
|
||||||
majorIdx != null && editableMajorSteps[majorIdx] ? editableMajorSteps[majorIdx] : null
|
majorIdx != null && editableMajorSteps[majorIdx] ? editableMajorSteps[majorIdx] : null
|
||||||
const stageSpec = stageSpecForMajorIndex(progressionRoadmap, majorIdx)
|
const stageSpec = stageSpecForMajorIndex(progressionRoadmap, majorIdx)
|
||||||
|
|
@ -92,6 +208,13 @@ export function buildPathGapPlanningContextForAi({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entryState = buildProgressionEntryState({
|
||||||
|
majorStepIndex: majorIdx,
|
||||||
|
priorSteps,
|
||||||
|
startSituation: start,
|
||||||
|
currentStageStart: stageSpec?.start_state || '',
|
||||||
|
})
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
source: 'progression_path_gap_fill',
|
source: 'progression_path_gap_fill',
|
||||||
goal_query: (goalQuery || '').trim() || null,
|
goal_query: (goalQuery || '').trim() || null,
|
||||||
|
|
@ -109,6 +232,8 @@ export function buildPathGapPlanningContextForAi({
|
||||||
roadmap_notes: notes,
|
roadmap_notes: notes,
|
||||||
stage_learning_goal:
|
stage_learning_goal:
|
||||||
(stageLearningGoalOverride || '').trim() || stageSpec?.learning_goal || null,
|
(stageLearningGoalOverride || '').trim() || stageSpec?.learning_goal || null,
|
||||||
|
stage_start_state: stageSpec?.start_state || null,
|
||||||
|
stage_target_state: stageSpec?.target_state || null,
|
||||||
gap_trainer_supplements: (gapTrainerSupplements || '').trim() || null,
|
gap_trainer_supplements: (gapTrainerSupplements || '').trim() || null,
|
||||||
stage_phase: stageSpec?.phase || majorStep?.phase || null,
|
stage_phase: stageSpec?.phase || majorStep?.phase || null,
|
||||||
stage_exercise_type: stageSpec?.exercise_type || null,
|
stage_exercise_type: stageSpec?.exercise_type || null,
|
||||||
|
|
@ -125,8 +250,9 @@ export function buildPathGapPlanningContextForAi({
|
||||||
? ga.success_criteria.slice(0, 4)
|
? ga.success_criteria.slice(0, 4)
|
||||||
: null,
|
: null,
|
||||||
skill_hints: skillHints.length ? skillHints : null,
|
skill_hints: skillHints.length ? skillHints : null,
|
||||||
neighbor_before_title: stepA?.exerciseTitle || offer?.from_title || null,
|
neighbor_before_title: stepA?.exerciseTitle || stepA?.title || offer?.from_title || null,
|
||||||
neighbor_after_title: stepB?.exerciseTitle || offer?.to_title || null,
|
neighbor_after_title: stepB?.exerciseTitle || stepB?.title || offer?.to_title || null,
|
||||||
|
...entryState,
|
||||||
path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0,
|
path_step_count: Array.isArray(pathSteps) ? pathSteps.length : 0,
|
||||||
major_step_count:
|
major_step_count:
|
||||||
editableMajorSteps?.length ||
|
editableMajorSteps?.length ||
|
||||||
|
|
@ -148,7 +274,14 @@ export function gapOfferContextDisplayLines(offer, fallbackParams = null) {
|
||||||
const v = String(value || '').trim()
|
const v = String(value || '').trim()
|
||||||
if (v) lines.push({ label, value: v })
|
if (v) lines.push({ label, value: v })
|
||||||
}
|
}
|
||||||
push('Ausgangslage (Pfad)', raw.start_situation)
|
push('Eingangszustand (Vorstufen)', raw.entry_state)
|
||||||
|
if (raw.entry_state_detail && raw.entry_state_detail !== raw.entry_state) {
|
||||||
|
push('Bisheriger Pfad', raw.entry_state_detail)
|
||||||
|
}
|
||||||
|
if (Array.isArray(raw.prior_achievements) && raw.prior_achievements.length) {
|
||||||
|
push('Erreichte Voraussetzungen', raw.prior_achievements.slice(0, 6).join(' · '))
|
||||||
|
}
|
||||||
|
push('Ausgangslage (gesamter Pfad)', raw.start_situation)
|
||||||
push('Gesamtziel (Pfad)', raw.target_state)
|
push('Gesamtziel (Pfad)', raw.target_state)
|
||||||
push('Ergänzungen', raw.roadmap_notes)
|
push('Ergänzungen', raw.roadmap_notes)
|
||||||
push('Stufen-Lernziel', raw.stage_learning_goal || raw.roadmap_learning_goal)
|
push('Stufen-Lernziel', raw.stage_learning_goal || raw.roadmap_learning_goal)
|
||||||
|
|
@ -173,6 +306,43 @@ export function gapOfferContextDisplayLines(offer, fallbackParams = null) {
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Zieltext für KI aus Slot-Kontext (Graph-Editor ohne API-Offer). */
|
||||||
|
export function buildSlotGapGoalForAi(draft, slotIndex, { goalQuery = '' } = {}) {
|
||||||
|
const slot = draft?.slots?.[slotIndex]
|
||||||
|
if (!slot) return ''
|
||||||
|
const pathSteps = slotsAsPathStepRows(draft)
|
||||||
|
const majorIdx = slot.majorStepIndex
|
||||||
|
const priorSteps = priorPathStepsBeforeMajor(pathSteps, majorIdx)
|
||||||
|
const start = (draft.startSituation || '').trim()
|
||||||
|
const stageSpec =
|
||||||
|
majorIdx != null && draft.progressionRoadmap
|
||||||
|
? stageSpecForMajorIndex(draft.progressionRoadmap, majorIdx)
|
||||||
|
: null
|
||||||
|
const entry = buildProgressionEntryState({
|
||||||
|
majorStepIndex: majorIdx,
|
||||||
|
priorSteps,
|
||||||
|
startSituation: start,
|
||||||
|
currentStageStart: stageSpec?.start_state || '',
|
||||||
|
})
|
||||||
|
const parts = [
|
||||||
|
goalQuery ? `Planungsziel (gesamter Pfad): ${goalQuery}` : '',
|
||||||
|
entry.entry_state
|
||||||
|
? `Eingangszustand (erreichte Voraussetzungen): ${entry.entry_state}`
|
||||||
|
: start
|
||||||
|
? `Ausgangslage (Pfad): ${start}`
|
||||||
|
: '',
|
||||||
|
entry.entry_state_detail && entry.entry_state_detail !== entry.entry_state
|
||||||
|
? `Bisheriger Pfad:\n${entry.entry_state_detail}`
|
||||||
|
: '',
|
||||||
|
(slot.learning_goal || '').trim()
|
||||||
|
? `Lernziel dieser Roadmap-Stufe: ${(slot.learning_goal || '').trim()}`
|
||||||
|
: '',
|
||||||
|
(slot.phase || '').trim() ? `Entwicklungsphase: ${slot.phase}` : '',
|
||||||
|
'Die Übung baut didaktisch auf den Vorstufen auf — Voraussetzungen explizit benennen, messbares Stufenziel.',
|
||||||
|
].filter(Boolean)
|
||||||
|
return parts.join('\n\n').trim()
|
||||||
|
}
|
||||||
|
|
||||||
export function initialStageLearningGoalFromOffer(offer, fallbackParams = null) {
|
export function initialStageLearningGoalFromOffer(offer, fallbackParams = null) {
|
||||||
const lines = gapOfferContextDisplayLines(offer, fallbackParams)
|
const lines = gapOfferContextDisplayLines(offer, fallbackParams)
|
||||||
const hit = lines.find((l) => l.label === 'Stufen-Lernziel')
|
const hit = lines.find((l) => l.label === 'Stufen-Lernziel')
|
||||||
|
|
|
||||||
|
|
@ -280,9 +280,15 @@ export function slotsAsPathStepRows(draft) {
|
||||||
return (draft.slots || []).map((slot) => ({
|
return (draft.slots || []).map((slot) => ({
|
||||||
exerciseId: slot.primary?.exerciseId ?? null,
|
exerciseId: slot.primary?.exerciseId ?? null,
|
||||||
exerciseTitle: slot.primary?.exerciseTitle || '',
|
exerciseTitle: slot.primary?.exerciseTitle || '',
|
||||||
|
title: slot.primary?.exerciseTitle || '',
|
||||||
|
roadmap_major_step_index: slot.majorStepIndex,
|
||||||
roadmapMajorStepIndex: slot.majorStepIndex,
|
roadmapMajorStepIndex: slot.majorStepIndex,
|
||||||
|
roadmap_phase: slot.phase,
|
||||||
roadmapPhase: slot.phase,
|
roadmapPhase: slot.phase,
|
||||||
|
roadmap_learning_goal: slot.learning_goal,
|
||||||
roadmapLearningGoal: slot.learning_goal,
|
roadmapLearningGoal: slot.learning_goal,
|
||||||
|
learning_goal: slot.learning_goal,
|
||||||
|
success_criteria: Array.isArray(slot.success_criteria) ? slot.success_criteria : [],
|
||||||
isAiProposal: slot.primary?.kind === 'proposal',
|
isAiProposal: slot.primary?.kind === 'proposal',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user