semantic cleanup
This commit is contained in:
309
backend/src/api/routes/assistant/_dispatch.py
Normal file
309
backend/src/api/routes/assistant/_dispatch.py
Normal file
@@ -0,0 +1,309 @@
|
||||
# [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]
|
||||
Reference in New Issue
Block a user