fix(assistant): resolve dashboard refs via LLM entities and remove deterministic parser fallback

This commit is contained in:
2026-02-24 13:32:25 +03:00
parent 2e93f5ca63
commit 3d42a487f7

View File

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