From 5c882985e0217a1df16b3755c6e0447a7006c622 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 22 May 2026 23:08:53 +0200 Subject: [PATCH] Enhance Planning Exercise Functionality and LLM Integration - Added support for the new planning exercise expectation profile slug in the AI prompt runtime. - Refactored SQL parameter handling in the planning exercise retrieval process to ensure correct binding for full-text search. - Updated the planning exercise suggestion logic to incorporate LLM expectation handling, improving the accuracy of exercise recommendations. - Introduced new functions to determine when to run the LLM expectation pipeline, enhancing the decision-making process for exercise suggestions. - Incremented version to 0.8.176 and updated changelog to reflect these enhancements in planning AI capabilities. --- backend/ai_prompt_runtime.py | 1 + ..._planning_exercise_expectation_profile.sql | 70 ++++++++++++++++++ backend/planning_exercise_expectation.py | 69 ++++++++++++++++++ backend/planning_exercise_retrieval.py | 7 +- backend/planning_exercise_suggest.py | 7 ++ backend/planning_exercise_target_pipeline.py | 72 +++++++++++++++++-- .../tests/test_planning_exercise_retrieval.py | 43 +++++++++++ .../tests/test_planning_exercise_suggest.py | 43 +++++++++++ backend/version.py | 24 +++++-- .../src/components/ExercisePickerModal.jsx | 11 ++- 10 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 backend/migrations/074_ai_prompt_planning_exercise_expectation_profile.sql create mode 100644 backend/planning_exercise_expectation.py create mode 100644 backend/tests/test_planning_exercise_retrieval.py diff --git a/backend/ai_prompt_runtime.py b/backend/ai_prompt_runtime.py index f942ad1..5944f36 100644 --- a/backend/ai_prompt_runtime.py +++ b/backend/ai_prompt_runtime.py @@ -15,6 +15,7 @@ _PLANNING_AI_SLUGS = frozenset( { "planning_exercise_search_rank", "planning_exercise_search_intent", + "planning_exercise_expectation_profile", } ) diff --git a/backend/migrations/074_ai_prompt_planning_exercise_expectation_profile.sql b/backend/migrations/074_ai_prompt_planning_exercise_expectation_profile.sql new file mode 100644 index 0000000..ed8fa19 --- /dev/null +++ b/backend/migrations/074_ai_prompt_planning_exercise_expectation_profile.sql @@ -0,0 +1,70 @@ +-- Migration 074: KI-Prompt Planungs-Übungssuche — Erwartungsprofil aus Planungskontext (Preset) +-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §16 + +INSERT INTO ai_prompts ( + slug, display_name, description, template, + category, output_format, output_schema, is_system_default, default_template, active, sort_order +) +SELECT + 'planning_exercise_expectation_profile', + 'Planungs-Übungssuche Erwartungsprofil', + 'Leitet aus Einheit, Abschnitt, Anker und bisherigem Plan ein Erwartungsprofil für die nächste Übung ab (ohne Freitext-Anfrage).', + $t$Du bist Assistent für Kampfsport-Trainer in der Trainingsplanung. +Der Trainer wählt „nächste Übung aus Kontext“ — es gibt KEINE zusätzliche Freitext-Suchanfrage. + +Deine Aufgabe: Aus dem Planungskontext und dem deterministischen Basis-Zielprofil ein präzises Erwartungsprofil ableiten: +- Was soll die nächste Übung fachlich leisten (Fortsetzen, Vertiefen, Lücke schließen, Abwechslung)? +- Welche Fähigkeiten, Fokus-Bereiche, Trainingsstile passen dazu? +- Berücksichtige: Rahmen/Einheit, Abschnittsziel (guidance_notes), letzte Übung im Abschnitt, Anker-Übung, Skill-Profile Einheit vs. Abschnitt, Skill-Lücken im Basisprofil. + +Intent (intent): meist suggest_next oder continue_plan_goal; progression_next nur wenn Progressionsgraph/Anker klar nahelegt; deepen_exercise nur bei klarer Vertiefungslage. + +continuation (optional, Kurzlabel): +- build_on_section: nahtlos an Abschnitt/letzte Übung anknüpfen +- close_skill_gap: fehlende Fähigkeiten aus Plan/Rahmen nachziehen +- deepen_anchor: Anker-Übung vertiefen +- variety: bewusst variieren nach bisherigem Block +- balance_load: Belastung ausgleichen / Tempo wechseln + +Nutze skill_hints/focus_hints etc. mit Namen aus den Katalog-JSONs (beste Übereinstimmung). +emphasis: fast immer additive (baut auf Basisprofil auf), nur replace wenn Kontext eindeutig neuen Schwerpunkt verlangt. + +Eingabe: +Heuristik-Intent: {{heuristic_intent}} +Planungskontext: {{planning_context_json}} +Basis-Zielprofil (deterministisch): {{target_profile_json}} + +Kataloge (Auszug — nur diese Namen/IDs verwenden): +Skills: {{skills_catalog_json}} +Fokus: {{focus_areas_catalog_json}} +Trainingsstil: {{training_types_catalog_json}} +Stilrichtung: {{style_directions_catalog_json}} +Zielgruppe: {{target_groups_catalog_json}} + +Antworte NUR mit JSON: +{ + "intent": "suggest_next", + "scenario": "preset_next", + "continuation": "build_on_section", + "skill_hints": [{"name": "Kime", "weight": 0.9}], + "focus_hints": [], + "style_hints": [], + "training_type_hints": [], + "target_group_hints": [], + "requires_partner": null, + "emphasis": "additive", + "rationale": "Kurz auf Deutsch, 1–2 Sätze: warum diese nächste Übung sinnvoll ist" +}$t$, + 'training', + 'json', + '{"type":"object","required":["intent","scenario","rationale"],"properties":{"intent":{"type":"string"},"scenario":{"type":"string"},"continuation":{"type":"string"},"skill_hints":{"type":"array"},"emphasis":{"type":"string"},"rationale":{"type":"string"}}}'::jsonb, + true, + NULL, + true, + 12 +WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_expectation_profile'); + +UPDATE ai_prompts +SET default_template = template +WHERE slug = 'planning_exercise_expectation_profile' + AND (default_template IS NULL OR TRIM(default_template) = ''); diff --git a/backend/planning_exercise_expectation.py b/backend/planning_exercise_expectation.py new file mode 100644 index 0000000..15d42d8 --- /dev/null +++ b/backend/planning_exercise_expectation.py @@ -0,0 +1,69 @@ +""" +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"] diff --git a/backend/planning_exercise_retrieval.py b/backend/planning_exercise_retrieval.py index 217c51a..4800eae 100644 --- a/backend/planning_exercise_retrieval.py +++ b/backend/planning_exercise_retrieval.py @@ -62,15 +62,18 @@ def fetch_retrieval_candidate_rows( ) -> List[Dict[str, Any]]: """S1b-0: Profil-geführter Kandidaten-Pool.""" where = [vis_sql, "COALESCE(e.status, '') <> %s"] - params: List[Any] = list(vis_params) - params.append("archived") + params: List[Any] = [] if query: ft_select = "ts_rank_cd(e.search_vector, plainto_tsquery('german', %s)) AS ft_rank" + # SELECT-Platzhalter steht im SQL vor WHERE — Query zuerst binden. params.append(query) else: ft_select = "0.0::float AS ft_rank" + params.extend(vis_params) + params.append("archived") + ek_filtered: List[str] = [] if exercise_kind_any: for raw in exercise_kind_any: diff --git a/backend/planning_exercise_suggest.py b/backend/planning_exercise_suggest.py index ffb53a0..1b25080 100644 --- a/backend/planning_exercise_suggest.py +++ b/backend/planning_exercise_suggest.py @@ -617,6 +617,8 @@ def suggest_planning_exercises( weights = _intent_weights(intent) target_profile_summary = target_profile.to_summary_dict(cur) query_intent_applied = bool(query_intent_summary.get("llm_applied")) + llm_expectation_applied = bool(query_intent_summary.get("llm_expectation_applied")) + profile_llm_applied = bool(query_intent_summary.get("profile_llm_applied")) profile_id = tenant.profile_id role = tenant.global_role @@ -645,6 +647,7 @@ def suggest_planning_exercises( retrieval_phase = compose_retrieval_phase( profile_preselect=profile_preselect_applied, query_intent=query_intent_applied, + llm_expectation=llm_expectation_applied, llm_rank=False, ) run_llm_rank = should_run_llm_rank_pipeline( @@ -652,6 +655,7 @@ def suggest_planning_exercises( scenario_kind, include_llm_rank=body.include_llm_rank, query_intent_applied=query_intent_applied, + llm_expectation_applied=llm_expectation_applied, hits=hits, ) if run_llm_rank: @@ -678,6 +682,7 @@ def suggest_planning_exercises( retrieval_phase = compose_retrieval_phase( profile_preselect=profile_preselect_applied, query_intent=query_intent_applied, + llm_expectation=llm_expectation_applied, llm_rank=True, ) tail = hits[pre_limit:] @@ -716,6 +721,8 @@ def suggest_planning_exercises( "profile_preselect_applied": profile_preselect_applied, "llm_rank_applied": llm_rank_applied, "llm_intent_applied": query_intent_applied, + "llm_expectation_applied": llm_expectation_applied, + "profile_llm_applied": profile_llm_applied, "intent_resolved": intent, "intent_heuristic": heuristic_intent, "query_normalized": query or None, diff --git a/backend/planning_exercise_target_pipeline.py b/backend/planning_exercise_target_pipeline.py index 182c90d..0026d16 100644 --- a/backend/planning_exercise_target_pipeline.py +++ b/backend/planning_exercise_target_pipeline.py @@ -12,6 +12,7 @@ from __future__ import annotations import re from typing import Any, Dict, List, Mapping, Optional, Tuple +from planning_exercise_expectation import try_build_planning_expectation_from_context from planning_exercise_intent import ( PlanningQueryIntentParsed, resolve_query_intent_catalog_ids, @@ -37,6 +38,7 @@ _SIMPLE_PRESET_PATTERNS = ( r"^(vorschlag|vorschlagen|empfehl\w*)\s*(für|fuer)?\s*(die\s+)?(n[aä]chste|naechste)?\s*(übung|uebung)?\.?$", r"^n[aä]chste\s+übung$", r"^n[aä]chste\s+uebung$", + r"^(n[aä]chste|naechste)\s+(übung|uebung)\s+planen\.?$", ) _ADDITIVE_MARKERS = ( @@ -89,6 +91,20 @@ def classify_planning_scenario( return SCENARIO_FREE_SEARCH +def should_run_llm_expectation_pipeline( + scenario: str, + *, + include_llm_intent: bool, + has_planning_reference: bool, +) -> bool: + """Preset/leere Anfrage mit Planungsbezug → LLM-Erwartungsprofil statt Query-Intent.""" + if not include_llm_intent: + return False + if not has_planning_reference: + return False + return scenario == SCENARIO_PRESET_NEXT + + def should_run_llm_intent_pipeline( query: Optional[str], scenario: str, @@ -125,15 +141,16 @@ def should_run_llm_rank_pipeline( *, include_llm_rank: bool, query_intent_applied: bool, + llm_expectation_applied: bool = False, hits: Sequence[Mapping[str, Any]], ) -> bool: """ - Maximal ein LLM-Call pro Request: wenn Intent-LLM lief, kein Rerank. + Maximal ein LLM-Call pro Request: wenn Intent- oder Erwartungs-LLM lief, kein Rerank. Rerank nur bei längerer, komplexer Anfrage und unklarem Hybrid-Ranking. """ if not include_llm_rank: return False - if query_intent_applied: + if query_intent_applied or llm_expectation_applied: return False if scenario == SCENARIO_PRESET_NEXT: return False @@ -241,7 +258,9 @@ def build_planning_target_with_query_pipeline( scenario = classify_planning_scenario(query, heuristic_intent) resolved_intent = heuristic_intent llm_applied = False + llm_expectation_applied = False parsed: Optional[PlanningQueryIntentParsed] = None + expectation_parsed: Optional[PlanningQueryIntentParsed] = None resolved_skills: List[Dict[str, Any]] = [] if has_planning_reference: @@ -257,8 +276,44 @@ def build_planning_target_with_query_pipeline( base = PlanningTargetProfile(sources=["query_only"]) base_summary = base.to_summary_dict(cur) + target = base - if should_run_llm_intent_pipeline(query, scenario, include_llm_intent=include_llm_intent): + if should_run_llm_expectation_pipeline( + scenario, + include_llm_intent=include_llm_intent, + has_planning_reference=has_planning_reference, + ): + expectation_parsed, llm_expectation_applied = try_build_planning_expectation_from_context( + cur, + heuristic_intent=heuristic_intent, + context_summary=context_summary, + target_profile_summary=base_summary, + ) + parsed = expectation_parsed + if parsed and llm_expectation_applied: + if parsed.intent in { + "suggest_next", + "progression_next", + "deepen_exercise", + "continue_plan_goal", + "free_search", + }: + resolved_intent = parsed.intent + focus, style, tt, tg, skills, resolved_skills = resolve_query_intent_catalog_ids(cur, parsed) + if focus or style or tt or tg or skills or parsed.rationale: + target = merge_query_overlay_into_target( + base, + focus=focus, + style=style, + tt=tt, + tg=tg, + skills=skills, + emphasis=parsed.emphasis or "additive", + scenario=SCENARIO_PRESET_NEXT, + ) + if "context_expectation" not in target.sources: + target.sources.append("context_expectation") + elif should_run_llm_intent_pipeline(query, scenario, include_llm_intent=include_llm_intent): parsed, llm_applied = try_parse_planning_query_intent( cur, query=_normalize_query(query), @@ -268,8 +323,7 @@ def build_planning_target_with_query_pipeline( target_profile_summary=base_summary, ) - target = base - if parsed and llm_applied: + if parsed and llm_applied and not llm_expectation_applied: if parsed.intent in { "suggest_next", "progression_next", @@ -307,6 +361,8 @@ def build_planning_target_with_query_pipeline( "intent": resolved_intent, "heuristic_intent": heuristic_intent, "llm_applied": llm_applied, + "llm_expectation_applied": llm_expectation_applied, + "profile_llm_applied": llm_applied or llm_expectation_applied, "emphasis": parsed.emphasis if parsed else None, "rationale": (parsed.rationale if parsed else None), "skill_hints_resolved": resolved_skills, @@ -331,12 +387,15 @@ def compose_retrieval_phase( *, profile_preselect: bool = False, query_intent: bool = False, + llm_expectation: bool = False, llm_rank: bool = False, ) -> str: parts = ["profile_v1"] if profile_preselect: parts.append("profile_preselect") - if query_intent: + if llm_expectation: + parts.append("llm_expectation") + elif query_intent: parts.append("query_intent") if llm_rank: parts.append("llm_rank") @@ -351,6 +410,7 @@ __all__ = [ "compose_retrieval_phase", "is_simple_preset_query", "merge_query_overlay_into_target", + "should_run_llm_expectation_pipeline", "should_run_llm_intent_pipeline", "should_run_llm_rank_pipeline", "deterministic_rank_confident", diff --git a/backend/tests/test_planning_exercise_retrieval.py b/backend/tests/test_planning_exercise_retrieval.py new file mode 100644 index 0000000..8e4b834 --- /dev/null +++ b/backend/tests/test_planning_exercise_retrieval.py @@ -0,0 +1,43 @@ +"""Tests Planungs-Retrieval SQL-Parameter.""" +from planning_exercise_retrieval import fetch_retrieval_candidate_rows + + +def test_fetch_retrieval_binds_query_before_visibility_params(): + captured = {} + + class _Cur: + def execute(self, sql, params): + captured["sql"] = sql + captured["params"] = list(params) + + def fetchall(self): + return [ + { + "id": 1, + "title": "Test", + "summary": "", + "primary_focus_name": None, + "ft_rank": 0.2, + } + ] + + fetch_retrieval_candidate_rows( + _Cur(), + vis_sql="(e.visibility = 'official' OR (e.visibility = 'private' AND e.created_by = %s))", + vis_params=[42], + query="nächste Übung planen", + exercise_kind_any=None, + target=__import__( + "planning_exercise_profiles", fromlist=["PlanningTargetProfile"] + ).PlanningTargetProfile(), + progression_successor_ids=set(), + anchor_skill_ids={7}, + raw_pool_limit=10, + ) + + params = captured["params"] + assert params[0] == "nächste Übung planen" + assert params[1] == 42 + assert params[2] == "archived" + assert params[-2] == "nächste Übung planen" + assert params[-1] == 10 diff --git a/backend/tests/test_planning_exercise_suggest.py b/backend/tests/test_planning_exercise_suggest.py index 93c6a5d..5c6e194 100644 --- a/backend/tests/test_planning_exercise_suggest.py +++ b/backend/tests/test_planning_exercise_suggest.py @@ -25,8 +25,10 @@ def test_resolve_planning_exercise_intent_keywords(): def test_classify_planning_scenario_preset(): assert is_simple_preset_query("Schlage mir die nächste Übung vor") + assert is_simple_preset_query("nächste Übung planen") assert classify_planning_scenario("", "suggest_next") == SCENARIO_PRESET_NEXT assert classify_planning_scenario("nächste übung", "suggest_next") == SCENARIO_PRESET_NEXT + assert classify_planning_scenario("nächste Übung planen", "suggest_next") == SCENARIO_PRESET_NEXT def test_classify_planning_scenario_additive(): @@ -78,6 +80,47 @@ def test_compose_retrieval_phase(): ) +def test_should_run_llm_expectation_for_preset_with_planning_ref(): + from planning_exercise_target_pipeline import should_run_llm_expectation_pipeline + + assert should_run_llm_expectation_pipeline( + SCENARIO_PRESET_NEXT, + include_llm_intent=True, + has_planning_reference=True, + ) + assert not should_run_llm_expectation_pipeline( + SCENARIO_PRESET_NEXT, + include_llm_intent=False, + has_planning_reference=True, + ) + assert not should_run_llm_expectation_pipeline( + SCENARIO_ADDITIVE, + include_llm_intent=True, + has_planning_reference=True, + ) + + +def test_should_skip_llm_rank_when_expectation_applied(): + from planning_exercise_target_pipeline import SCENARIO_PRESET_NEXT, should_run_llm_rank_pipeline + + hits = [{"score": 0.5}, {"score": 0.48}, {"score": 0.47}, {"score": 0.46}] + assert not should_run_llm_rank_pipeline( + "", + SCENARIO_PRESET_NEXT, + include_llm_rank=True, + query_intent_applied=False, + llm_expectation_applied=True, + hits=hits, + ) + + +def test_compose_retrieval_phase_llm_expectation(): + assert ( + compose_retrieval_phase(llm_expectation=True) + == "profile_v1+llm_expectation" + ) + + def test_query_only_expectation_without_planning_reference(): from planning_exercise_profiles import PlanningTargetProfile from planning_exercise_target_pipeline import build_planning_target_with_query_pipeline diff --git a/backend/version.py b/backend/version.py index f4beeaa..3f6c0ae 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,8 +1,8 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.174" +APP_VERSION = "0.8.176" BUILD_DATE = "2026-05-22" -DB_SCHEMA_VERSION = "20260531073" +DB_SCHEMA_VERSION = "20260531074" MODULE_VERSIONS = { "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) @@ -22,13 +22,13 @@ MODULE_VERSIONS = { "admin_ai_prompts": "1.0.3", # Migration 070: openrouter_model; PUT/Liste/Detail "ai_prompt_job": "0.2.1", # want_instructions; run_exercise_form_ai_suggestion "ai_prompt_context": "0.2.0", # preparation/trainer_notes; has_instruction_source_text - "ai_prompt_runtime": "0.2.1", # Kontext-Art planning_exercise_search; load_and_render_ai_prompt + "ai_prompt_runtime": "0.2.2", # Slug planning_exercise_expectation_profile "groups": "0.1.0", "skills": "0.1.1", # DB 065 karate_relevance + relevance_level; CRUD unterstützt Felder "skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions "methods": "0.1.0", "exercises": "2.37.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay - "planning_exercise_suggest": "0.6.0", # Abschnitts-/Skill-Kontext; expectation_mode hybrid|query_only + "planning_exercise_suggest": "0.7.0", # LLM-Erwartungsprofil aus Kontext (preset); Migration 074 "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 @@ -43,6 +43,22 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.176", + "date": "2026-05-22", + "changes": [ + "Fix: Planungs-Übungssuche mit Suchtext — SQL-Parameter für ts_rank/plainto_tsquery korrekt gebunden (500).", + "Preset-Erkennung: „nächste Übung planen“.", + ], + }, + { + "version": "0.8.175", + "date": "2026-05-22", + "changes": [ + "Planungs-KI: „Nächste aus Kontext“ — LLM leitet Erwartungsprofil aus Planungskontext ab (Prompt 074).", + "API: llm_expectation_applied, profile_llm_applied; Retrieval-Phase llm_expectation; max. 1 LLM-Call.", + ], + }, { "version": "0.8.174", "date": "2026-05-22", diff --git a/frontend/src/components/ExercisePickerModal.jsx b/frontend/src/components/ExercisePickerModal.jsx index 7770f56..70736ae 100644 --- a/frontend/src/components/ExercisePickerModal.jsx +++ b/frontend/src/components/ExercisePickerModal.jsx @@ -419,7 +419,8 @@ export default function ExercisePickerModal({ .map((x) => Number(x)) .filter((x) => Number.isFinite(x) && x > 0) : undefined, - include_llm_intent: query.length >= PLANNING_LLM_INTENT_MIN_CHARS, + include_llm_intent: + query.length >= PLANNING_LLM_INTENT_MIN_CHARS || !(query || '').trim(), include_llm_rank: query.length >= PLANNING_LLM_RANK_MIN_CHARS, query, intent_hint: @@ -452,7 +453,7 @@ export default function ExercisePickerModal({ setPlanningContextSummary(res?.context_summary || null) setPlanningTargetProfileSummary(res?.target_profile_summary || null) setPlanningLlmRankApplied(Boolean(res?.llm_rank_applied)) - setPlanningLlmIntentApplied(Boolean(res?.llm_intent_applied)) + setPlanningLlmIntentApplied(Boolean(res?.profile_llm_applied ?? res?.llm_intent_applied)) setPlanningRetrievalPhase(res?.retrieval_phase || '') setPlanningQueryIntentSummary(res?.query_intent_summary || null) setPlanningIntentResolved(res?.intent_resolved || null) @@ -748,7 +749,11 @@ export default function ExercisePickerModal({ ? ` · ${String(planningQueryIntentSummary.scenario).replace(/_/g, ' ')}` : null} {planningLlmRankApplied ? ' · KI-Ranking aktiv' : null} - {planningLlmIntentApplied ? ' · KI-Intent aktiv' : null} + {planningLlmIntentApplied + ? planningQueryIntentSummary?.llm_expectation_applied + ? ' · KI-Erwartungsprofil aktiv' + : ' · KI-Intent aktiv' + : null} {!planningLlmRankApplied && !planningLlmIntentApplied && usePlanningSearch ? ' · ohne LLM (Profil/Hybrid)' : null}