386 lines
14 KiB
Python
386 lines
14 KiB
Python
# #region AssistantResolvers [C:2] [TYPE Module] [SEMANTICS assistant, resolver, lookup, environment, mapper]
|
||
# @BRIEF Environment, dashboard, provider, and task resolution utilities for the assistant API.
|
||
# @LAYER API
|
||
# @RELATION DEPENDS_ON -> [ConfigManager]
|
||
# @RELATION DEPENDS_ON -> [SupersetClient]
|
||
# @INVARIANT Resolution functions never raise; they return None on failure.
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any, cast
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.core.config_manager import ConfigManager
|
||
from src.core.logger import logger
|
||
from src.core.superset_client import SupersetClient
|
||
from src.services.llm_prompt_templates import resolve_bound_provider_id
|
||
from src.services.llm_provider import LLMProviderService
|
||
|
||
logger = cast(Any, logger)
|
||
|
||
|
||
# #region _extract_id [C:2] [TYPE Function]
|
||
# @BRIEF Extract first regex match group from text by ordered pattern list.
|
||
# @PRE patterns contain at least one capture group.
|
||
# @POST Returns first matched token or None.
|
||
def _extract_id(text: str, patterns: list[str]) -> str | None:
|
||
for p in patterns:
|
||
m = re.search(p, text, flags=re.IGNORECASE)
|
||
if m:
|
||
return m.group(1)
|
||
return None
|
||
|
||
|
||
# #endregion _extract_id
|
||
|
||
# #region _resolve_env_id [C:2] [TYPE Function]
|
||
# @BRIEF Resolve environment identifier/name token to canonical environment id.
|
||
# @PRE config_manager provides environment list.
|
||
# @POST Returns matched environment id or None.
|
||
def _resolve_env_id(
|
||
token: str | None, config_manager: ConfigManager
|
||
) -> str | None:
|
||
if not token:
|
||
return None
|
||
|
||
normalized = token.strip().lower()
|
||
envs = config_manager.get_environments()
|
||
for env in envs:
|
||
if env.id.lower() == normalized or env.name.lower() == normalized:
|
||
return env.id
|
||
return None
|
||
|
||
|
||
# #endregion _resolve_env_id
|
||
|
||
# #region _is_production_env [C:2] [TYPE Function]
|
||
# @BRIEF Determine whether environment token resolves to production-like target.
|
||
# @PRE config_manager provides environments or token text is provided.
|
||
# @POST Returns True for production/prod synonyms, else False.
|
||
def _is_production_env(token: str | None, config_manager: ConfigManager) -> bool:
|
||
env_id = _resolve_env_id(token, config_manager)
|
||
if not env_id:
|
||
return (token or "").strip().lower() in {"prod", "production", "прод"}
|
||
|
||
env = next((e for e in config_manager.get_environments() if e.id == env_id), None)
|
||
if not env:
|
||
return False
|
||
target = f"{env.id} {env.name}".lower()
|
||
return "prod" in target or "production" in target or "прод" in target
|
||
|
||
|
||
# #endregion _is_production_env
|
||
|
||
# #region _resolve_provider_id [C:2] [TYPE Function]
|
||
# @BRIEF Resolve provider token to provider id with active/default fallback.
|
||
# @PRE db session can load provider list through LLMProviderService.
|
||
# @POST Returns provider id or None when no providers configured.
|
||
def _resolve_provider_id(
|
||
provider_token: str | None,
|
||
db: Session,
|
||
config_manager: ConfigManager | None = None,
|
||
task_key: str | None = None,
|
||
) -> str | None:
|
||
service = LLMProviderService(db)
|
||
providers = service.get_all_providers()
|
||
if not providers:
|
||
return None
|
||
|
||
if provider_token:
|
||
needle = provider_token.strip().lower()
|
||
for p in providers:
|
||
if p.id.lower() == needle or p.name.lower() == needle:
|
||
return p.id
|
||
|
||
if config_manager and task_key:
|
||
try:
|
||
llm_settings = config_manager.get_config().settings.llm
|
||
bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)
|
||
if bound_provider_id and any(p.id == bound_provider_id for p in providers):
|
||
return bound_provider_id
|
||
except Exception:
|
||
logger.debug("Could not resolve bound provider for task_key=%s", task_key)
|
||
|
||
active = next((p for p in providers if p.is_active), None)
|
||
return active.id if active else providers[0].id
|
||
|
||
|
||
# #endregion _resolve_provider_id
|
||
|
||
# #region _get_default_environment_id [C:2] [TYPE Function]
|
||
# @BRIEF Resolve default environment id from settings or first configured environment.
|
||
# @PRE config_manager returns environments list.
|
||
# @POST Returns default environment id or None when environment list is empty.
|
||
def _get_default_environment_id(config_manager: ConfigManager) -> str | None:
|
||
configured = config_manager.get_environments()
|
||
if not configured:
|
||
return None
|
||
preferred = None
|
||
if hasattr(config_manager, "get_config"):
|
||
try:
|
||
preferred = config_manager.get_config().settings.default_environment_id
|
||
except Exception:
|
||
preferred = None
|
||
if preferred and any(env.id == preferred for env in configured):
|
||
return preferred
|
||
explicit_default = next(
|
||
(env.id for env in configured if getattr(env, "is_default", False)), None
|
||
)
|
||
return explicit_default or configured[0].id
|
||
|
||
|
||
# #endregion _get_default_environment_id
|
||
|
||
# #region _resolve_dashboard_id_by_ref [C:2] [TYPE Function]
|
||
# @BRIEF Resolve dashboard id by title or slug reference in selected environment.
|
||
# @PRE dashboard_ref is a non-empty string-like token.
|
||
# @POST Returns dashboard id when uniquely matched, otherwise None.
|
||
def _resolve_dashboard_id_by_ref(
|
||
dashboard_ref: str | None,
|
||
env_id: str | None,
|
||
config_manager: ConfigManager,
|
||
) -> int | None:
|
||
if not dashboard_ref or not env_id:
|
||
return None
|
||
env = next(
|
||
(item for item in config_manager.get_environments() if item.id == env_id), None
|
||
)
|
||
if not env:
|
||
return None
|
||
|
||
needle = dashboard_ref.strip().lower()
|
||
try:
|
||
client = SupersetClient(env)
|
||
_, dashboards = client.get_dashboards(query={"page_size": 200})
|
||
except Exception as exc:
|
||
logger.warning(
|
||
f"[assistant.dashboard_resolve][failed] ref={dashboard_ref} env={env_id} error={exc}"
|
||
)
|
||
return None
|
||
|
||
exact = next(
|
||
(
|
||
d
|
||
for d in dashboards
|
||
if str(d.get("slug", "")).lower() == needle
|
||
or str(d.get("dashboard_title", "")).lower() == needle
|
||
or str(d.get("title", "")).lower() == needle
|
||
),
|
||
None,
|
||
)
|
||
if exact:
|
||
return int(exact.get("id"))
|
||
|
||
partial = [
|
||
d
|
||
for d in dashboards
|
||
if needle in str(d.get("dashboard_title", d.get("title", ""))).lower()
|
||
]
|
||
if len(partial) == 1 and partial[0].get("id") is not None:
|
||
return int(partial[0]["id"])
|
||
return None
|
||
|
||
|
||
# #endregion _resolve_dashboard_id_by_ref
|
||
|
||
# #region _resolve_dashboard_id_entity [C:2] [TYPE Function]
|
||
# @BRIEF Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.
|
||
# @PRE entities may contain dashboard_id as int/str and optional dashboard_ref.
|
||
# @POST Returns resolved dashboard id or None when ambiguous/unresolvable.
|
||
def _resolve_dashboard_id_entity(
|
||
entities: dict[str, Any],
|
||
config_manager: ConfigManager,
|
||
env_hint: str | None = None,
|
||
) -> int | None:
|
||
raw_dashboard_id = entities.get("dashboard_id")
|
||
dashboard_ref = entities.get("dashboard_ref")
|
||
|
||
if isinstance(raw_dashboard_id, int):
|
||
return raw_dashboard_id
|
||
|
||
if isinstance(raw_dashboard_id, str):
|
||
token = raw_dashboard_id.strip()
|
||
if token.isdigit():
|
||
return int(token)
|
||
if token and not dashboard_ref:
|
||
dashboard_ref = token
|
||
|
||
if not dashboard_ref:
|
||
return None
|
||
|
||
env_token = (
|
||
env_hint
|
||
or entities.get("environment")
|
||
or entities.get("source_env")
|
||
or entities.get("target_env")
|
||
)
|
||
env_id = (
|
||
_resolve_env_id(env_token, config_manager)
|
||
if env_token
|
||
else _get_default_environment_id(config_manager)
|
||
)
|
||
return _resolve_dashboard_id_by_ref(str(dashboard_ref), env_id, config_manager)
|
||
|
||
|
||
# #endregion _resolve_dashboard_id_entity
|
||
|
||
# #region _get_environment_name_by_id [C:2] [TYPE Function]
|
||
# @BRIEF Resolve human-readable environment name by id.
|
||
# @PRE environment id may be None.
|
||
# @POST Returns matching environment name or fallback id.
|
||
def _get_environment_name_by_id(
|
||
env_id: str | None, config_manager: ConfigManager
|
||
) -> str:
|
||
if not env_id:
|
||
return "unknown"
|
||
env = next(
|
||
(item for item in config_manager.get_environments() if item.id == env_id), None
|
||
)
|
||
return env.name if env else env_id
|
||
|
||
|
||
# #endregion _get_environment_name_by_id
|
||
|
||
# #region _extract_result_deep_links [C:2] [TYPE Function]
|
||
# @BRIEF Build deep-link actions to verify task result from assistant chat.
|
||
# @PRE task object is available.
|
||
# @POST Returns zero or more assistant actions for dashboard open/diff.
|
||
def _extract_result_deep_links(
|
||
task: Any, config_manager: ConfigManager
|
||
) -> list:
|
||
from ._schemas import AssistantAction
|
||
|
||
plugin_id = getattr(task, "plugin_id", None)
|
||
params = getattr(task, "params", {}) or {}
|
||
result = getattr(task, "result", {}) or {}
|
||
actions: list = []
|
||
dashboard_id: int | None = None
|
||
env_id: str | None = None
|
||
|
||
if plugin_id == "superset-migration":
|
||
migrated = (
|
||
result.get("migrated_dashboards") if isinstance(result, dict) else None
|
||
)
|
||
if isinstance(migrated, list) and migrated:
|
||
first = migrated[0]
|
||
if isinstance(first, dict) and first.get("id") is not None:
|
||
dashboard_id = int(first.get("id"))
|
||
if (
|
||
dashboard_id is None
|
||
and isinstance(params.get("selected_ids"), list)
|
||
and params["selected_ids"]
|
||
):
|
||
dashboard_id = int(params["selected_ids"][0])
|
||
env_id = params.get("target_env_id")
|
||
elif plugin_id == "superset-backup":
|
||
dashboards = result.get("dashboards") if isinstance(result, dict) else None
|
||
if isinstance(dashboards, list) and dashboards:
|
||
first = dashboards[0]
|
||
if isinstance(first, dict) and first.get("id") is not None:
|
||
dashboard_id = int(first.get("id"))
|
||
if (
|
||
dashboard_id is None
|
||
and isinstance(params.get("dashboard_ids"), list)
|
||
and params["dashboard_ids"]
|
||
):
|
||
dashboard_id = int(params["dashboard_ids"][0])
|
||
env_id = params.get("environment_id") or _resolve_env_id(
|
||
result.get("environment"), config_manager
|
||
)
|
||
elif plugin_id == "llm_dashboard_validation":
|
||
if params.get("dashboard_id") is not None:
|
||
dashboard_id = int(params["dashboard_id"])
|
||
env_id = params.get("environment_id")
|
||
|
||
if dashboard_id is not None and env_id:
|
||
env_name = _get_environment_name_by_id(env_id, config_manager)
|
||
actions.append(
|
||
AssistantAction(
|
||
type="open_route",
|
||
label=f"Открыть дашборд в {env_name}",
|
||
target=f"/dashboards/{dashboard_id}?env_id={env_id}",
|
||
)
|
||
)
|
||
if dashboard_id is not None:
|
||
actions.append(
|
||
AssistantAction(
|
||
type="open_diff",
|
||
label="Показать Diff",
|
||
target=str(dashboard_id),
|
||
)
|
||
)
|
||
return actions
|
||
|
||
|
||
# #endregion _extract_result_deep_links
|
||
|
||
# #region _build_task_observability_summary [C:2] [TYPE Function]
|
||
# @BRIEF Build compact textual summary for completed tasks to reduce "black box" effect.
|
||
# @PRE task may contain plugin-specific result payload.
|
||
# @POST Returns non-empty summary line for known task types or empty string fallback.
|
||
def _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:
|
||
plugin_id = getattr(task, "plugin_id", None)
|
||
status = str(getattr(task, "status", "")).upper()
|
||
params = getattr(task, "params", {}) or {}
|
||
result = getattr(task, "result", {}) or {}
|
||
|
||
if plugin_id == "superset-migration" and isinstance(result, dict):
|
||
migrated = len(result.get("migrated_dashboards") or [])
|
||
failed_rows = result.get("failed_dashboards") or []
|
||
failed = len(failed_rows)
|
||
selected = result.get("selected_dashboards", migrated + failed)
|
||
mappings = result.get("mapping_count", 0)
|
||
target_env_id = params.get("target_env_id")
|
||
target_env_name = _get_environment_name_by_id(target_env_id, config_manager)
|
||
warning = ""
|
||
if failed_rows:
|
||
first = failed_rows[0]
|
||
warning = (
|
||
f" Внимание: {first.get('title') or first.get('id')}: "
|
||
f"{first.get('error') or 'ошибка'}."
|
||
)
|
||
return (
|
||
f"Сводка миграции: выбрано {selected}, перенесено {migrated}, "
|
||
f"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}."
|
||
f"{warning}"
|
||
)
|
||
|
||
if plugin_id == "superset-backup" and isinstance(result, dict):
|
||
total = int(result.get("total_dashboards", 0) or 0)
|
||
ok = int(result.get("backed_up_dashboards", 0) or 0)
|
||
failed = int(result.get("failed_dashboards", 0) or 0)
|
||
env_id = params.get("environment_id") or _resolve_env_id(
|
||
result.get("environment"), config_manager
|
||
)
|
||
env_name = _get_environment_name_by_id(env_id, config_manager)
|
||
failures = result.get("failures") or []
|
||
warning = ""
|
||
if failures:
|
||
first = failures[0]
|
||
warning = (
|
||
f" Внимание: {first.get('title') or first.get('id')}: "
|
||
f"{first.get('error') or 'ошибка'}."
|
||
)
|
||
return (
|
||
f"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, "
|
||
f"с ошибками {failed}. {status}.{warning}"
|
||
)
|
||
|
||
if plugin_id == "llm_dashboard_validation" and isinstance(result, dict):
|
||
report_status = result.get("status") or status
|
||
report_summary = result.get("summary") or "Итог недоступен."
|
||
issues = result.get("issues") or []
|
||
return f"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}"
|
||
|
||
# Fallback for unknown task payloads.
|
||
if status in {"SUCCESS", "FAILED"}:
|
||
return f"Задача завершена со статусом {status}."
|
||
return ""
|
||
|
||
|
||
# #endregion _build_task_observability_summary
|
||
|
||
# #endregion AssistantResolvers
|