""" Planungs-KI Phase E2: KI-Neuanlage-Vorschläge für unüberbrückbare Pfad-Lücken. """ from __future__ import annotations import logging from typing import Any, Dict, Mapping, Optional import uuid from ai_prompt_context import ExerciseFormAiPromptContext from ai_prompt_job import run_exercise_form_ai_suggestion from exercise_ai import strip_html_to_plain from planning_exercise_semantics import PlanningSemanticBrief _logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill") def _build_gap_ai_context( *, goal_query: str, brief: PlanningSemanticBrief, step_a: Mapping[str, Any], step_b: Mapping[str, Any], gap: Mapping[str, Any], ) -> ExerciseFormAiPromptContext: topic = (brief.primary_topic or "Technik").strip() phase = gap.get("expected_phase") or "vertiefung" from_title = (step_a.get("title") or f"Übung #{step_a.get('exercise_id')}").strip() to_title = (step_b.get("title") or f"Übung #{step_b.get('exercise_id')}").strip() title = f"Brücke {topic} ({phase})" goal = ( f"Planungsziel: {goal_query}\n\n" f"Didaktische Brücken-Übung zwischen „{from_title}“ und „{to_title}“.\n" f"Phase: {phase}. Thema: {topic}. " f"Die Übung schließt die Lücke im Progressionspfad und bereitet sinnvoll auf den nächsten Schritt vor." ) focus_hint = topic if brief.topic_type == "technique" else None if brief.must_phrases: focus_hint = ", ".join(brief.must_phrases[:2]) return ExerciseFormAiPromptContext( title=title[:280], goal=goal[:8000], execution=None, focus_hint=focus_hint, ) def ai_proposal_to_path_step( *, ai_payload: Mapping[str, Any], ctx_title: str, gap: Mapping[str, Any], step_a: Mapping[str, Any], step_b: Mapping[str, Any], ) -> Dict[str, Any]: summary_text = "" summary_obj = ai_payload.get("summary") if isinstance(summary_obj, dict): summary_text = str(summary_obj.get("text") or "").strip() elif isinstance(summary_obj, str): summary_text = summary_obj.strip() proposal_key = f"ai-{uuid.uuid4().hex[:10]}" title = (ctx_title or "").strip() or "KI-Vorschlag (Brücke)" reasons = ["KI-Neuanlage-Vorschlag — Lücke ohne passende Bibliotheks-Übung"] return { "exercise_id": None, "proposal_key": proposal_key, "variant_id": None, "title": title, "summary": summary_text or None, "score": None, "semantic_score": None, "reasons": reasons, "variants": [], "is_bridge": True, "is_ai_proposal": True, "ai_suggestion": dict(ai_payload), "bridge_for_gap": { "from_exercise_id": int(step_a["exercise_id"]), "to_exercise_id": int(step_b["exercise_id"]), "gap_score": gap.get("gap_score"), "expected_phase": gap.get("expected_phase"), }, } def try_suggest_ai_bridge_step( cur, *, goal_query: str, brief: PlanningSemanticBrief, step_a: Mapping[str, Any], step_b: Mapping[str, Any], gap: Mapping[str, Any], ) -> Optional[Dict[str, Any]]: """Ruft exercise AI suggest auf — kein Speichern in DB.""" ctx = _build_gap_ai_context( goal_query=goal_query, brief=brief, step_a=step_a, step_b=step_b, gap=gap, ) g_plain = strip_html_to_plain(ctx.goal) if not g_plain.strip() and not (ctx.title or "").strip(): return None try: payload = run_exercise_form_ai_suggestion( cur, ctx, want_summary=True, want_skills=True, want_instructions=False, ) except Exception as exc: _logger.warning("KI-Lückenfüller fehlgeschlagen: %s", exc) return None if not payload: return None return ai_proposal_to_path_step( ai_payload=payload, ctx_title=ctx.title or "", gap=gap, step_a=step_a, step_b=step_b, ) def insert_ai_proposals_for_gaps( cur, steps: list, unfilled_gaps: list, *, goal_query: str, brief: PlanningSemanticBrief, max_proposals: int = 2, ) -> tuple[list, list]: """Fügt KI-Vorschläge für Lücken ein, wenn Bibliotheks-Brücke fehlte.""" if not unfilled_gaps: return steps, [] out = list(steps) proposals: list = [] gap_by_pair = { (int(g["from_exercise_id"]), int(g["to_exercise_id"])): g for g in unfilled_gaps } i = 0 while i < len(out) - 1 and len(proposals) < max_proposals: a = out[i] b = out[i + 1] if a.get("is_ai_proposal") or b.get("is_ai_proposal"): i += 1 continue key = (int(a["exercise_id"]), int(b["exercise_id"])) gap = gap_by_pair.get(key) if not gap: i += 1 continue proposal = try_suggest_ai_bridge_step( cur, goal_query=goal_query, brief=brief, step_a=a, step_b=b, gap=gap, ) if not proposal: i += 1 continue out.insert(i + 1, proposal) proposals.append( { "inserted_after_index": i, "proposal_key": proposal.get("proposal_key"), "proposal_title": proposal.get("title"), "gap": gap, } ) i += 2 return out, proposals __all__ = [ "insert_ai_proposals_for_gaps", "try_suggest_ai_bridge_step", ]