""" Preset „Nächste aus Kontext“: LLM leitet Erwartungsprofil aus Planungskontext ab. Prompt: planning_exercise_expectation_profile (Migration 074) """ from __future__ import annotations import logging from typing import Any, Dict, Mapping, Optional, Tuple from planning_exercise_intent import ( PlanningQueryIntentParsed, _compact_json, _load_compact_catalog, _load_skills_catalog_compact, parse_planning_query_intent_response, ) from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt from openrouter_chat import ( effective_openrouter_model_for_prompt_row, normalize_openrouter_env, openrouter_chat_completion, ) _logger = logging.getLogger("shinkan.planning_exercise_expectation") def try_build_planning_expectation_from_context( cur, *, heuristic_intent: str, context_summary: Mapping[str, Any], target_profile_summary: Mapping[str, Any], ) -> Tuple[Optional[PlanningQueryIntentParsed], bool]: """ LLM-Erwartungsprofil für preset_next / leere Anfrage mit Planungsbezug. Returns (parsed overlay, applied). """ api_key, _ = normalize_openrouter_env() if not api_key: return None, False variables = { "heuristic_intent": heuristic_intent or "suggest_next", "planning_context_json": _compact_json(dict(context_summary or {})), "target_profile_json": _compact_json(dict(target_profile_summary or {})), "skills_catalog_json": _compact_json(_load_skills_catalog_compact(cur)), "focus_areas_catalog_json": _compact_json(_load_compact_catalog(cur, "focus_areas", "id")), "training_types_catalog_json": _compact_json(_load_compact_catalog(cur, "training_types", "id")), "style_directions_catalog_json": _compact_json(_load_compact_catalog(cur, "style_directions", "id")), "target_groups_catalog_json": _compact_json(_load_compact_catalog(cur, "target_groups", "id")), } try: prow, rendered = load_and_render_ai_prompt(cur, "planning_exercise_expectation_profile", variables) model = effective_openrouter_model_for_prompt_row(prow) raw = openrouter_chat_completion(api_key=api_key, model=model, user_content=rendered.text) parsed = parse_planning_query_intent_response(raw) if parsed.scenario not in ("preset_next", "continue_plan", "free_search"): parsed = parsed.model_copy(update={"scenario": "preset_next"}) return parsed, True except AiPromptUnavailableError: return None, False except Exception as exc: _logger.warning("Planungs-Erwartungsprofil-LLM fehlgeschlagen: %s", exc) return None, False __all__ = ["try_build_planning_expectation_from_context"]