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_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: