# [DEF:AssistantDispatch:Module] # @COMPLEXITY: 5 # @PURPOSE: Intent dispatch engine, confirmation summary, and clarification text for the assistant API. # @LAYER: API # @RELATION: DEPENDS_ON -> [AssistantSchemas] # @RELATION: DEPENDS_ON -> [AssistantResolvers] # @RELATION: DEPENDS_ON -> [AssistantLlmPlanner] # @RELATION: DEPENDS_ON -> [AssistantDatasetReview] # @INVARIANT: Unsupported operations are rejected via HTTPException(400). from __future__ import annotations from typing import Any, Dict, List, Optional, Tuple from fastapi import HTTPException from sqlalchemy.orm import Session from src.core.logger import belief_scope, logger from src.core.config_manager import ConfigManager from src.core.task_manager import TaskManager from src.schemas.auth import User from src.services.git_service import GitService from src.services.llm_provider import LLMProviderService from src.services.llm_prompt_templates import is_multimodal_model from ._schemas import ( _DATASET_REVIEW_OPS, AssistantAction, ) from ._resolvers import ( _build_task_observability_summary, _extract_result_deep_links, _get_environment_name_by_id, _resolve_dashboard_id_entity, _resolve_env_id, _resolve_provider_id, ) from ._history import _coerce_query_bool from ._llm_planner import _check_any_permission from ._dataset_review import _dispatch_dataset_review_intent git_service = GitService() # [DEF:_clarification_text_for_intent:Function] # @COMPLEXITY: 2 # @PURPOSE: 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: Optional[Dict[str, Any]], 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) # [/DEF:_clarification_text_for_intent:Function] # [DEF:_async_confirmation_summary:Function] # @COMPLEXITY: 4 # @PURPOSE: 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, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'} template = descriptions.get(operation) if not template: logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary') return 'Подтвердите выполнение операции или отмените.' 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') text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id'))) 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.models.dashboard import DashboardSelection from src.core.superset_client import SupersetClient src_token = entities.get('source_env') tgt_token = entities.get('target_env') dashboard_id = _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 = SupersetClient(source_env) target_client = SupersetClient(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}. Подтвердите или отмените.' # [/DEF:_async_confirmation_summary:Function] # [DEF:_dispatch_intent:Function] # @COMPLEXITY: 5 # @PURPOSE: Execute parsed assistant intent via existing task/plugin/git services. # @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]] # @RELATION: DEPENDS_ON -> [_check_any_permission] # @RELATION: DEPENDS_ON -> [_resolve_dashboard_id_entity] # @RELATION: DEPENDS_ON -> [TaskManager] # @RELATION: DEPENDS_ON -> [GitService] # @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state. # @PRE: intent operation is known and actor permissions are validated per operation. # @POST: Returns response text, optional task id, and UI actions for follow-up. # @INVARIANT: unsupported operations are rejected via HTTPException(400). async def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> Tuple[str, Optional[str], List[AssistantAction]]: with belief_scope('_dispatch_intent'): logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent') operation = intent.get('operation') entities = intent.get('entities', {}) if operation in _DATASET_REVIEW_OPS or operation == 'dataset_review_answer_context': logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return await _dispatch_dataset_review_intent(intent, current_user, config_manager, db) if operation == 'show_capabilities': from ._llm_planner import _build_tool_catalog tools_catalog = _build_tool_catalog(current_user, config_manager, db) labels = {'create_branch': 'Git: создание ветки', 'commit_changes': 'Git: коммит', 'deploy_dashboard': 'Git: деплой дашборда', 'execute_migration': 'Миграции: запуск переноса', 'run_backup': 'Бэкапы: запуск резервного копирования', 'run_llm_validation': 'LLM: валидация дашборда', 'run_llm_documentation': 'LLM: генерация документации', 'get_task_status': 'Статус: проверка задачи', 'get_health_summary': 'Здоровье: сводка по дашбордам'} available = [labels[t['operation']] for t in tools_catalog if t['operation'] in labels] if not available: logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return ('Сейчас нет доступных для вас операций ассистента.', None, []) commands = '\n'.join((f'- {item}' for item in available)) text = f'Вот что я могу сделать для вас:\n{commands}\n\nПример: `запусти миграцию с dev на prod для дашборда 42`.' logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (text, None, []) if operation == 'get_health_summary': from src.services.health_service import HealthService env_token = entities.get('environment') env_id = _resolve_env_id(env_token, config_manager) service = HealthService(db) summary = await service.get_health_summary(environment_id=env_id) env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else 'всех окружений' text = f'Сводка здоровья дашбордов для {env_name}:\n- ✅ Прошли проверку: {summary.pass_count}\n- ⚠️ С предупреждениями: {summary.warn_count}\n- ❌ Ошибки валидации: {summary.fail_count}\n- ❓ Неизвестно: {summary.unknown_count}' actions = [AssistantAction(type='open_route', label='Открыть Health Center', target='/dashboards/health')] if summary.fail_count > 0: text += '\n\nОбнаружены ошибки в следующих дашбордах:' for item in summary.items: if item.status == 'FAIL': text += f"\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}" actions.append(AssistantAction(type='open_route', label=f'Отчет {item.dashboard_id}', target=f'/reports/llm/{item.task_id}')) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (text, None, actions[:5]) if operation == 'get_task_status': _check_any_permission(current_user, [('tasks', 'READ')]) task_id = entities.get('task_id') if not task_id: recent = [t for t in task_manager.get_tasks(limit=20, offset=0) if t.user_id == current_user.id] if not recent: logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return ('У вас пока нет задач в истории.', None, []) task = recent[0] actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)] if str(task.status).upper() in {'SUCCESS', 'FAILED'}: actions.extend(_extract_result_deep_links(task, config_manager)) summary_line = _build_task_observability_summary(task, config_manager) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Последняя задача: {task.id}, статус: {task.status}.' + (f'\n{summary_line}' if summary_line else ''), task.id, actions) task = task_manager.get_task(task_id) if not task: raise HTTPException(status_code=404, detail=f'Task {task_id} not found') actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)] if str(task.status).upper() in {'SUCCESS', 'FAILED'}: actions.extend(_extract_result_deep_links(task, config_manager)) summary_line = _build_task_observability_summary(task, config_manager) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Статус задачи {task.id}: {task.status}.' + (f'\n{summary_line}' if summary_line else ''), task.id, actions) if operation == 'create_branch': _check_any_permission(current_user, [('plugin:git', 'EXECUTE')]) dashboard_id = _resolve_dashboard_id_entity(entities, config_manager) branch_name = entities.get('branch_name') if not dashboard_id or not branch_name: raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or branch_name') git_service.create_branch(dashboard_id, branch_name, 'main') logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Ветка `{branch_name}` создана для дашборда {dashboard_id}.', None, []) if operation == 'commit_changes': _check_any_permission(current_user, [('plugin:git', 'EXECUTE')]) dashboard_id = _resolve_dashboard_id_entity(entities, config_manager) commit_message = entities.get('message') if not dashboard_id: raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref') git_service.commit_changes(dashboard_id, commit_message, None) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return ('Коммит выполнен успешно.', None, []) if operation == 'deploy_dashboard': _check_any_permission(current_user, [('plugin:git', 'EXECUTE')]) env_token = entities.get('environment') env_id = _resolve_env_id(env_token, config_manager) dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token) if not dashboard_id or not env_id: raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or environment') task = await task_manager.create_task(plugin_id='git-integration', params={'operation': 'deploy', 'dashboard_id': dashboard_id, 'environment_id': env_id}, user_id=current_user.id) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Деплой запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')]) if operation == 'execute_migration': _check_any_permission(current_user, [('plugin:migration', 'EXECUTE'), ('plugin:superset-migration', 'EXECUTE')]) src_token = entities.get('source_env') dashboard_ref = entities.get('dashboard_ref') dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token) src = _resolve_env_id(src_token, config_manager) tgt = _resolve_env_id(entities.get('target_env'), config_manager) if not src or not tgt: raise HTTPException(status_code=422, detail='Missing source_env/target_env') if not dashboard_id and (not dashboard_ref): raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref') migration_params: Dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))} if dashboard_id: migration_params['selected_ids'] = [dashboard_id] else: migration_params['dashboard_regex'] = str(dashboard_ref) task = await task_manager.create_task(plugin_id='superset-migration', params=migration_params, user_id=current_user.id) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Миграция запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={tgt}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if dashboard_id else [])]) if operation == 'run_backup': _check_any_permission(current_user, [('plugin:superset-backup', 'EXECUTE'), ('plugin:backup', 'EXECUTE')]) env_token = entities.get('environment') env_id = _resolve_env_id(env_token, config_manager) if not env_id: raise HTTPException(status_code=400, detail='Missing or unknown environment') params: Dict[str, Any] = {'environment_id': env_id} if entities.get('dashboard_id') or entities.get('dashboard_ref'): dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token) if not dashboard_id: raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref') params['dashboard_ids'] = [dashboard_id] task = await task_manager.create_task(plugin_id='superset-backup', params=params, user_id=current_user.id) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Бэкап запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={env_id}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if entities.get('dashboard_id') or entities.get('dashboard_ref') else [])]) if operation == 'run_llm_validation': _check_any_permission(current_user, [('plugin:llm_dashboard_validation', 'EXECUTE')]) env_token = entities.get('environment') env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(None, config_manager) dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token) provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='dashboard_validation') if not dashboard_id or not env_id or (not provider_id): raise HTTPException(status_code=422, detail='Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.') provider = LLMProviderService(db).get_provider(provider_id) provider_model = provider.default_model if provider else '' if not is_multimodal_model(provider_model, provider.provider_type if provider else None): raise HTTPException(status_code=422, detail='Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).') task = await task_manager.create_task(plugin_id='llm_dashboard_validation', params={'dashboard_id': str(dashboard_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'LLM-валидация запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')]) if operation == 'run_llm_documentation': _check_any_permission(current_user, [('plugin:llm_documentation', 'EXECUTE')]) dataset_id = entities.get('dataset_id') env_id = _resolve_env_id(entities.get('environment'), config_manager) provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='documentation') if not dataset_id or not env_id or (not provider_id): raise HTTPException(status_code=400, detail='Missing dataset_id/environment/provider') task = await task_manager.create_task(plugin_id='llm_documentation', params={'dataset_id': str(dataset_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id) logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent') return (f'Генерация документации запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')]) raise HTTPException(status_code=400, detail='Unsupported operation') # [/DEF:_dispatch_intent:Function] # [/DEF:AssistantDispatch:Module]