diff --git a/backend/src/api/routes/assistant.py b/backend/src/api/routes/assistant.py index e54b2a7..3fbf70c 100644 --- a/backend/src/api/routes/assistant.py +++ b/backend/src/api/routes/assistant.py @@ -548,6 +548,37 @@ def _resolve_dashboard_id_by_ref( # [/DEF:_resolve_dashboard_id_by_ref:Function] +# [DEF:_resolve_dashboard_id_entity:Function] +# @PURPOSE: 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: Optional[str] = None, +) -> Optional[int]: + 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) +# [/DEF:_resolve_dashboard_id_entity:Function] + + # [DEF:_parse_command:Function] # @PURPOSE: Deterministically parse RU/EN command text into intent payload. # @PRE: message contains raw user text and config manager resolves environments. @@ -793,53 +824,53 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S { "operation": "create_branch", "domain": "git", - "description": "Create git branch for dashboard", - "required_entities": ["dashboard_id", "branch_name"], - "optional_entities": [], + "description": "Create git branch for dashboard by id/slug/title", + "required_entities": ["branch_name"], + "optional_entities": ["dashboard_id", "dashboard_ref"], "risk_level": "guarded", "requires_confirmation": False, }, { "operation": "commit_changes", "domain": "git", - "description": "Commit dashboard repository changes", - "required_entities": ["dashboard_id"], - "optional_entities": ["message"], + "description": "Commit dashboard repository changes by dashboard id/slug/title", + "required_entities": [], + "optional_entities": ["dashboard_id", "dashboard_ref", "message"], "risk_level": "guarded", "requires_confirmation": False, }, { "operation": "deploy_dashboard", "domain": "git", - "description": "Deploy dashboard to target environment", - "required_entities": ["dashboard_id", "environment"], - "optional_entities": [], + "description": "Deploy dashboard (id/slug/title) to target environment", + "required_entities": ["environment"], + "optional_entities": ["dashboard_id", "dashboard_ref"], "risk_level": "guarded", "requires_confirmation": False, }, { "operation": "execute_migration", "domain": "migration", - "description": "Run dashboard migration between environments", - "required_entities": ["dashboard_id", "source_env", "target_env"], - "optional_entities": [], + "description": "Run dashboard migration (id/slug/title) between environments", + "required_entities": ["source_env", "target_env"], + "optional_entities": ["dashboard_id", "dashboard_ref"], "risk_level": "guarded", "requires_confirmation": False, }, { "operation": "run_backup", "domain": "backup", - "description": "Run backup for environment or specific dashboard", + "description": "Run backup for environment or specific dashboard by id/slug/title", "required_entities": ["environment"], - "optional_entities": ["dashboard_id"], + "optional_entities": ["dashboard_id", "dashboard_ref"], "risk_level": "guarded", "requires_confirmation": False, }, { "operation": "run_llm_validation", "domain": "llm", - "description": "Run LLM dashboard validation", - "required_entities": ["dashboard_id"], + "description": "Run LLM dashboard validation by dashboard id/slug/title", + "required_entities": [], "optional_entities": ["dashboard_ref", "environment", "provider"], "defaults": {"environment": default_env_id, "provider": validation_provider}, "risk_level": "guarded", @@ -900,11 +931,11 @@ def _clarification_text_for_intent(intent: Optional[Dict[str, Any]], detail_text "run_llm_documentation": ( "Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM." ), - "create_branch": "Нужно уточнение: укажите dashboard_id и имя ветки.", - "commit_changes": "Нужно уточнение: укажите dashboard_id для коммита.", - "deploy_dashboard": "Нужно уточнение: укажите dashboard_id и целевое окружение.", - "execute_migration": "Нужно уточнение: укажите dashboard_id, source_env и target_env.", - "run_backup": "Нужно уточнение: укажите окружение для бэкапа.", + "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] @@ -958,6 +989,7 @@ async def _plan_intent_with_llm( "Rules:\n" "- Use only operation names from available_tools.\n" "- If input is ambiguous, operation must be \"clarify\" with low confidence.\n" + "- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\n" "- Keep entities minimal and factual.\n" ) payload = { @@ -1091,35 +1123,35 @@ async def _dispatch_intent( if operation == "create_branch": _check_any_permission(current_user, [("plugin:git", "EXECUTE")]) - dashboard_id = entities.get("dashboard_id") + 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=400, detail="Missing dashboard_id or branch_name") - git_service.create_branch(int(dashboard_id), branch_name, "main") + raise HTTPException(status_code=422, detail="Missing dashboard_id/dashboard_ref or branch_name") + git_service.create_branch(dashboard_id, branch_name, "main") return f"Ветка `{branch_name}` создана для дашборда {dashboard_id}.", None, [] if operation == "commit_changes": _check_any_permission(current_user, [("plugin:git", "EXECUTE")]) - dashboard_id = entities.get("dashboard_id") + dashboard_id = _resolve_dashboard_id_entity(entities, config_manager) commit_message = entities.get("message") if not dashboard_id: - raise HTTPException(status_code=400, detail="Missing dashboard_id") - git_service.commit_changes(int(dashboard_id), commit_message, None) + raise HTTPException(status_code=422, detail="Missing dashboard_id/dashboard_ref") + git_service.commit_changes(dashboard_id, commit_message, None) return "Коммит выполнен успешно.", None, [] if operation == "deploy_dashboard": _check_any_permission(current_user, [("plugin:git", "EXECUTE")]) - dashboard_id = entities.get("dashboard_id") 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=400, detail="Missing dashboard_id or environment") + 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": int(dashboard_id), + "dashboard_id": dashboard_id, "environment_id": env_id, }, user_id=current_user.id, @@ -1135,16 +1167,17 @@ async def _dispatch_intent( if operation == "execute_migration": _check_any_permission(current_user, [("plugin:migration", "EXECUTE"), ("plugin:superset-migration", "EXECUTE")]) - dashboard_id = entities.get("dashboard_id") - src = _resolve_env_id(entities.get("source_env"), config_manager) + src_token = entities.get("source_env") + 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 dashboard_id or not src or not tgt: - raise HTTPException(status_code=400, detail="Missing dashboard_id/source_env/target_env") + raise HTTPException(status_code=422, detail="Missing dashboard_id/dashboard_ref/source_env/target_env") task = await task_manager.create_task( plugin_id="superset-migration", params={ - "selected_ids": [int(dashboard_id)], + "selected_ids": [dashboard_id], "source_env_id": src, "target_env_id": tgt, "replace_db_config": False, @@ -1162,13 +1195,17 @@ async def _dispatch_intent( if operation == "run_backup": _check_any_permission(current_user, [("plugin:superset-backup", "EXECUTE"), ("plugin:backup", "EXECUTE")]) - env_id = _resolve_env_id(entities.get("environment"), config_manager) + 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"): - params["dashboard_ids"] = [int(entities["dashboard_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", @@ -1186,14 +1223,9 @@ async def _dispatch_intent( if operation == "run_llm_validation": _check_any_permission(current_user, [("plugin:llm_dashboard_validation", "EXECUTE")]) - env_id = _resolve_env_id(entities.get("environment"), config_manager) or _get_default_environment_id(config_manager) - dashboard_id = entities.get("dashboard_id") - if not dashboard_id: - dashboard_id = _resolve_dashboard_id_by_ref( - entities.get("dashboard_ref"), - env_id, - config_manager, - ) + env_token = entities.get("environment") + env_id = _resolve_env_id(env_token, config_manager) or _get_default_environment_id(config_manager) + dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token) provider_id = _resolve_provider_id( entities.get("provider"), db, @@ -1296,7 +1328,14 @@ async def send_message( except Exception as exc: logger.warning(f"[assistant.planner][fallback] Planner error: {exc}") if not intent: - intent = _parse_command(request.message, config_manager) + intent = { + "domain": "unknown", + "operation": "clarify", + "entities": {}, + "confidence": 0.0, + "risk_level": "safe", + "requires_confirmation": False, + } confidence = float(intent.get("confidence", 0.0)) if intent.get("domain") == "unknown" or confidence < 0.6: