Files
ss-tools/backend/src/api/routes/assistant/_resolvers.py
2026-05-26 09:30:41 +03:00

386 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# #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