  # #region llm_prompt_templates [C:2] [TYPE Module] [SEMANTICS llm, prompt, template, normalization]
  # @defgroup Services Module group.
  # @BRIEF Provide default LLM prompt templates and normalization helpers for runtime usage.
  # @LAYER Domain
  # @RELATION DEPENDS_ON -> [ConfigManager]
  # @INVARIANT  All required prompt template keys are always present after normalization.
  
> from __future__ import annotations
  
> from copy import deepcopy
> from typing import Any
> import warnings
  
> from ..core.logger import logger
  
  # #region DEFAULT_LLM_PROMPTS [C:2] [TYPE Constant]
  # @ingroup Services
  # @BRIEF Default prompt templates used by documentation, dashboard validation, and git commit generation.
> DEFAULT_LLM_PROMPTS: dict[str, str] = {
>     "dashboard_validation_prompt": (  # Legacy v1 — deprecated for new tasks, kept for backward compat
>         "Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\n\n"
>         "Logs:\n"
>         "{logs}\n\n"
>         "Provide the analysis in JSON format with the following structure:\n"
>         "{\n"
>         '    "status": "PASS" | "WARN" | "FAIL",\n'
>         '    "summary": "Short summary of findings",\n'
>         '    "issues": [\n'
>         "        {\n"
>         '            "severity": "WARN" | "FAIL",\n'
>         '            "message": "Description of the issue",\n'
>         '            "location": "Optional location info (e.g. chart name)"\n'
>         "        }\n"
>         "    ]\n"
>         "}"
>     ),
>     "dashboard_validation_prompt_multimodal": (  # Path A — image-focused (v2)
>         "Analyze the attached {total_chunks} dashboard tab screenshots and execution logs for visual and data health issues.\n\n"
>         "Screenshots are attached IN ORDER — screenshot 0 is the first tab, screenshot 1 is the second, etc.\n"
>         "Tab order:\n"
>         "{tab_list}\n\n"
>         "Logs:\n"
>         "{logs}\n\n"
>         "Provide the analysis in JSON format. Each issue MUST specify the tab_index field "
>         "(0-based index of the screenshot/tab where the issue appears, or -1 if not tab-specific):\n"
>         "{\n"
>         '    "status": "PASS" | "WARN" | "FAIL",\n'
>         '    "summary": "Short summary of findings",\n'
>         '    "issues": [\n'
>         "        {\n"
>         '            "severity": "WARN" | "FAIL",\n'
>         '            "message": "Description of the issue",\n'
>         '            "location": "Where the issue is located (tab name, chart name, screen region)",\n'
>         '            "tab_index": "0-based tab index (-1 if unknown)"\n'
>         "        }\n"
>         "    ]\n"
>         "}"
>     ),
>     "dashboard_validation_prompt_text": (  # Path B — topology-focused (v2)
>         "Analyze the following dashboard topology, dataset health, and execution logs for "
>         "data consistency and KXD connectivity issues.\n\n"
>         "Dashboard structure (tabs are indicated in the topology below):\n"
>         "{topology}\n\n"
>         "Dataset health:\n"
>         "{dataset_health}\n\n"
>         "Logs:\n"
>         "{logs}\n\n"
>         "Provide the analysis in JSON format. Each issue MUST specify the tab_index field "
>         "(0-based index of the tab where the issue appears, or -1 if not tab-specific):\n"
>         "{\n"
>         '    "status": "PASS" | "WARN" | "FAIL",\n'
>         '    "summary": "Short summary of findings",\n'
>         '    "issues": [\n'
>         "        {\n"
>         '            "severity": "WARN" | "FAIL",\n'
>         '            "message": "Description of the issue",\n'
>         '            "location": "Where the issue is located (tab name, chart name, dataset name)",\n'
>         '            "tab_index": "0-based tab index (-1 if unknown)"\n'
>         "        }\n"
>         "    ]\n"
>         "}"
>     ),
>     "documentation_prompt": (
>         "Generate professional documentation for the following dataset and its columns.\n"
>         "Dataset: {dataset_name}\n"
>         "Columns: {columns_json}\n\n"
>         "Provide the documentation in JSON format:\n"
>         "{\n"
>         '    "dataset_description": "General description of the dataset",\n'
>         '    "column_descriptions": [\n'
>         "        {\n"
>         '            "name": "column_name",\n'
>         '            "description": "Generated description"\n'
>         "        }\n"
>         "    ]\n"
>         "}"
>     ),
>     "git_commit_prompt": (
>         "Generate a concise and professional git commit message based on the following diff and recent history.\n"
>         "Use Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\n\n"
>         "Recent History:\n"
>         "{history}\n\n"
>         "Diff:\n"
>         "{diff}\n\n"
>         "Commit Message:"
>     ),
> }
  # #endregion DEFAULT_LLM_PROMPTS
  
  
  # #region DEFAULT_LLM_PROVIDER_BINDINGS [C:2] [TYPE Constant]
  # @ingroup Services
  # @BRIEF Default provider binding per task domain.
> DEFAULT_LLM_PROVIDER_BINDINGS: dict[str, str] = {
>     "documentation": "",
>     "git_commit": "",
> }
  # #endregion DEFAULT_LLM_PROVIDER_BINDINGS
  
  
  # #region DEFAULT_LLM_ASSISTANT_SETTINGS [C:2] [TYPE Constant]
  # @ingroup Services
  # @BRIEF Default planner settings for assistant chat intent model/provider resolution.
> DEFAULT_LLM_ASSISTANT_SETTINGS: dict[str, str] = {
>     "assistant_planner_provider": "",
>     "assistant_planner_model": "",
> }
  # #endregion DEFAULT_LLM_ASSISTANT_SETTINGS
  
  
  # #region normalize_llm_settings [C:3] [TYPE Function]
  # @ingroup Services
  # @BRIEF Ensure llm settings contain stable schema with prompts section and default templates.
  # @PRE  llm_settings is dictionary-like value or None.
  # @POST  Returned dict contains prompts with all required template keys.
  # @RELATION DEPENDS_ON -> LLMProviderService
> def normalize_llm_settings(llm_settings: Any) -> dict[str, Any]:
>     normalized: dict[str, Any] = {
>         "providers": [],
>         "default_provider": "",
>         "prompts": {},
>         "provider_bindings": {},
>         **DEFAULT_LLM_ASSISTANT_SETTINGS,
>     }
>     if isinstance(llm_settings, dict):
>         normalized.update(
>             {
>                 k: v
>                 for k, v in llm_settings.items()
>                 if k
>                 in (
>                     "providers",
>                     "default_provider",
>                     "prompts",
>                     "provider_bindings",
>                     "assistant_planner_provider",
>                     "assistant_planner_model",
>                 )
>             }
>         )
>     prompts = normalized.get("prompts") if isinstance(normalized.get("prompts"), dict) else {}
>     merged_prompts = deepcopy(DEFAULT_LLM_PROMPTS)
>     merged_prompts.update({k: v for k, v in prompts.items() if isinstance(v, str) and v.strip()})
>     normalized["prompts"] = merged_prompts
>     bindings = normalized.get("provider_bindings") if isinstance(normalized.get("provider_bindings"), dict) else {}
>     merged_bindings = deepcopy(DEFAULT_LLM_PROVIDER_BINDINGS)
>     merged_bindings.update({k: v for k, v in bindings.items() if isinstance(v, str)})
>     normalized["provider_bindings"] = merged_bindings
>     for key, default_value in DEFAULT_LLM_ASSISTANT_SETTINGS.items():
>         value = normalized.get(key, default_value)
>         normalized[key] = value.strip() if isinstance(value, str) else default_value
>     return normalized
  # #endregion normalize_llm_settings
  
  # #region is_multimodal_model [C:3] [TYPE Function]
  # @ingroup Services
  # @BRIEF Heuristically determine whether model supports image input required for dashboard validation.
  # @DEPRECATED Use the explicit `db_provider.is_multimodal` flag instead (see migration 9f8e7d6c5b4a).
  # @RATIONALE Added import warnings + warnings.warn(DeprecationWarning) to is_multimodal_model as a deprecation shim.
  
  #             Replaced by an explicit boolean flag on `LLMProvider` that users control via checkbox in UI.
  #             This function is retained only as a backward-compatibility shim for the Alembic migration
  #             backfill and must not be imported in new production code.
  # @REJECTED Keeping the function as a backward-compatibility shim; do not use for new validation.
  # @PRE model_name may be empty or mixed-case.
  # @POST Returns True when model likely supports multimodal input.
  # @RELATION DEPENDS_ON -> LLMProviderService
> def is_multimodal_model(model_name: str, provider_type: str | None = None) -> bool:
>     warnings.warn(
>         "is_multimodal_model is deprecated; use LLMProvider.is_multimodal instead",
>         DeprecationWarning,
>         stacklevel=2,
>     )
>     token = (model_name or "").strip().lower()
>     if not token:
>         return False
>     text_only_markers = (
>         "text-only",
>         "embedding",
>         "rerank",
>         "whisper",
>         "tts",
>         "transcribe",
>     )
>     if any(marker in token for marker in text_only_markers):
>         return False
>     multimodal_markers = (
>         "gpt-4o",
>         "gpt-4.1",
>         "vision",
>         "vl",
>         "gemini",
>         "claude-3",
>         "claude-sonnet-4",
>         "omni",
>         "multimodal",
>         "pixtral",
>         "llava",
>         "internvl",
>         "qwen-vl",
>         "qwen2-vl",
>     )
>     return any(marker in token for marker in multimodal_markers)
  # #endregion is_multimodal_model
  
  
  # #region resolve_bound_provider_id [C:3] [TYPE Function]
  # @ingroup Services
  # @BRIEF Resolve provider id configured for a task binding with fallback to default provider.
  # @PRE  llm_settings is normalized or raw dict from config.
  # @POST  Returns configured provider id or fallback id/empty string when not defined.
  # @RELATION DEPENDS_ON -> LLMProviderService
> def resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:
>     normalized = normalize_llm_settings(llm_settings)
>     bindings = normalized.get("provider_bindings", {})
>     bound = bindings.get(task_key)
>     if isinstance(bound, str) and bound.strip():
>         return bound.strip()
>     default_provider = normalized.get("default_provider", "")
>     return default_provider.strip() if isinstance(default_provider, str) else ""
  # #endregion resolve_bound_provider_id
  
  
  # #region render_prompt [C:3] [TYPE Function]
  # @ingroup Services
  # @BRIEF Render prompt template using deterministic placeholder replacement with graceful fallback.
  # @PRE  template is a string and variables values are already stringifiable.
  # @POST  Returns rendered prompt text with known placeholders substituted. Warns about unfilled placeholders.
  # @RELATION DEPENDS_ON -> LLMProviderService
> def render_prompt(template: str, variables: dict[str, Any]) -> str:
>     rendered = template
>     for key, value in variables.items():
>         rendered = rendered.replace("{" + key + "}", str(value))
  
      # Warn about unfilled placeholders that would be sent to LLM
>     import re
>     unfilled = re.findall(r'\{(\w+)\}', rendered)
>     if unfilled:
>         logger.warning(
>             f"[render_prompt] Unfilled placeholders in rendered prompt: {unfilled}"
>         )
  
>     return rendered
  # #endregion render_prompt
  
  
  # #endregion llm_prompt_templates
