# #region AssistantDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dispatch, confirm, execution, orchestration] # @defgroup AssistantApi Module group. # @BRIEF Intent dispatch engine and backward-compat wrapper around the central tool registry. # @LAYER API # @RELATION DEPENDS_ON -> [AssistantToolRegistry] # @RELATION DEPENDS_ON -> [AssistantSchemas] # @INVARIANT Unsupported operations are rejected via HTTPException(400). # @INVARIANT All tool handlers live in _tool_registry — _dispatch is a thin wrapper. from __future__ import annotations from typing import Any from fastapi import HTTPException from sqlalchemy.orm import Session from src.core.config_manager import ConfigManager from src.core.logger import belief_scope, logger from src.core.task_manager import TaskManager from src.schemas.auth import User from src.services.git_service import GitService from ._history import _coerce_query_bool from ._resolvers import ( _get_environment_name_by_id, _resolve_dashboard_id_entity, _resolve_env_id, ) from ._schemas import AssistantAction from ._tool_registry import dispatch as _registry_dispatch # #region _get_git_service [TYPE Function] # @BRIEF Lazy-init GitService singleton to avoid crash at module import time when /app/storage/ is unavailable. # @POST Returns GitService instance (created once, cached). # @SIDE_EFFECT May attempt to create /app/storage/repositories on first call. _git_service_instance: GitService | None = None def _get_git_service() -> GitService: global _git_service_instance if _git_service_instance is None: _git_service_instance = GitService() return _git_service_instance # #endregion _get_git_service # #region _clarification_text_for_intent [C:2] [TYPE Function] # @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts. # @PRE state was classified as needs_clarification for current intent/error combination. # @POST Returned text is human-readable and actionable for target operation. def _clarification_text_for_intent( intent: dict[str, Any] | None, detail_text: str ) -> str: operation = (intent or {}).get("operation") guidance_by_operation: dict[str, str] = { "run_llm_validation": ( "Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM." ), "run_llm_documentation": ( "Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM." ), "create_branch": "Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.", "commit_changes": "Нужно уточнение: укажите дашборд (id/slug/title) для коммита.", "deploy_dashboard": "Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.", "execute_migration": "Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.", "run_backup": "Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).", } return guidance_by_operation.get(operation, detail_text) # #endregion _clarification_text_for_intent # #region _async_confirmation_summary [C:4] [TYPE Function] # @BRIEF Build human-readable confirmation prompt for an intent before execution. # @PRE actions is a non-empty list of planned review actions. # @POST Returns a formatted summary string suitable for display to the user. # @SIDE_EFFECT None - pure formatting function. async def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str: with belief_scope('_confirmation_summary'): logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary') operation = intent.get('operation', '') entities = intent.get('entities', {}) descriptions: dict[str, tuple[str, str]] = { 'create_branch': ( '📝 Создание ветки в Git', 'Будет создана ветка **{branch}** для дашборда **{dashboard}**. ' 'Изменения можно будет коммитить в эту ветку.' ), 'commit_changes': ( '📝 Коммит изменений в Git', 'Изменения дашборда **{dashboard}** будут закоммичены в Git-репозиторий. ' 'После коммита их можно будет развернуть на других окружениях.' ), 'deploy_dashboard': ( '🚀 Деплой дашборда', 'Дашборд **{dashboard}** будет развёрнут в окружение **{env}**. ' 'Изменения станут доступны на целевом окружении.' ), 'execute_migration': ( '🔄 Миграция дашборда', 'Дашборд **{dashboard}** будет перенесён с **{src}** на **{tgt}**.' ), 'run_backup': ( '💾 Бэкап', 'Будет запущено резервное копирование окружения **{env}**{dashboard}.' ), 'run_llm_validation': ( '🤖 LLM-валидация дашборда', 'Будет запущена LLM-валидация дашборда **{dashboard}**{env}. ' 'Это займёт некоторое время.' ), 'run_llm_documentation': ( '📄 Генерация документации', 'Будет сгенерирована документация для датасета **{dataset}**{env}.' ), 'start_maintenance': ( '🛠 Обслуживание', 'Будет запущено окно обслуживания для таблиц **{tables}**.' ), 'end_maintenance': ( '✅ Завершение обслуживания', 'Обслуживание будет завершено{event}.' ), } entry = descriptions.get(operation) if not entry: logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary') return 'Подтвердите выполнение операции или отмените.' title, desc = entry def _label(value: Any, prefix: str=' ') -> str: logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary') return f'{prefix}{value}' if value else '' dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref') env_val = entities.get('environment') or entities.get('target_env') dataset_val = entities.get('dataset_id') event_val = entities.get('event_id') tables_val = entities.get('tables') tables_str = ', '.join(tables_val[:5]) if isinstance(tables_val, list) else str(tables_val or '') text = desc.format( branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(env_val), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(dataset_val), tables=tables_str if tables_str else '?', event=_label(event_val), ) text = f'**{title}**\n\n{text}' if operation == 'execute_migration': flags = [] flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ')) flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ')) dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False)) flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ')) text += f" ({', '.join(flags)})" if dry_run_enabled: try: from src.core.migration.dry_run_orchestrator import MigrationDryRunService from src.core.async_superset_client import AsyncSupersetClient from src.models.dashboard import DashboardSelection src_token = entities.get('source_env') tgt_token = entities.get('target_env') dashboard_id = await _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token) if dashboard_id and src_token and tgt_token: src_env_id = _resolve_env_id(src_token, config_manager) tgt_env_id = _resolve_env_id(tgt_token, config_manager) if src_env_id and tgt_env_id: env_map = {env.id: env for env in config_manager.get_environments()} source_env = env_map.get(src_env_id) target_env = env_map.get(tgt_env_id) if source_env and target_env and (source_env.id != target_env.id): selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], replace_db_config=_coerce_query_bool(entities.get('replace_db_config', False)), fix_cross_filters=_coerce_query_bool(entities.get('fix_cross_filters', True))) service = MigrationDryRunService() source_client = AsyncSupersetClient(source_env) target_client = AsyncSupersetClient(target_env) report = service.run(selection, source_client, target_client, db) s = report.get('summary', {}) dash_s = s.get('dashboards', {}) charts_s = s.get('charts', {}) ds_s = s.get('datasets', {}) creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0) updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0) deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0) text += f'\n\nОтчет dry-run:\n- Будет создано новых объектов: {creates}\n- Будет обновлено: {updates}\n- Будет удалено: {deletes}' else: text += '\n\n(Не удалось загрузить отчет dry-run: неверные окружения).' except Exception as e: import traceback logger.warning('[assistant.dry_run_summary][failed] Exception: %s\n%s', e, traceback.format_exc()) text += f'\n\n(Не удалось загрузить отчет dry-run: {e}).' logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary') return f'Выполнить: {text}. Подтвердите или отмените.' # #endregion _async_confirmation_summary # #region _dispatch_intent [C:1] [TYPE Function] # @BRIEF Backward-compat wrapper around the central tool registry dispatch. # @DEPRECATED Use `dispatch()` from `_tool_registry` directly. async def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]: """Backward-compat wrapper around the central tool registry dispatch.""" operation = intent.get('operation') return await _registry_dispatch(operation, intent, current_user, task_manager, config_manager, db) # #endregion _dispatch_intent # #endregion AssistantDispatch