feat(assistant): add multi-dialog UX, task-aware llm settings, and i18n cleanup
This commit is contained in:
@@ -27,6 +27,11 @@ from ...core.config_manager import ConfigManager
|
||||
from ...core.database import get_db
|
||||
from ...services.git_service import GitService
|
||||
from ...services.llm_provider import LLMProviderService
|
||||
from ...services.llm_prompt_templates import (
|
||||
is_multimodal_model,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
)
|
||||
from ...core.superset_client import SupersetClient
|
||||
from ...plugins.llm_analysis.service import LLMClient
|
||||
from ...plugins.llm_analysis.models import LLMProviderType
|
||||
@@ -449,7 +454,12 @@ def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> b
|
||||
# @PURPOSE: Resolve provider token to provider id with active/default fallback.
|
||||
# @PRE: db session can load provider list through LLMProviderService.
|
||||
# @POST: Returns provider id or None when no providers configured.
|
||||
def _resolve_provider_id(provider_token: Optional[str], db: Session) -> Optional[str]:
|
||||
def _resolve_provider_id(
|
||||
provider_token: Optional[str],
|
||||
db: Session,
|
||||
config_manager: Optional[ConfigManager] = None,
|
||||
task_key: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
service = LLMProviderService(db)
|
||||
providers = service.get_all_providers()
|
||||
if not providers:
|
||||
@@ -461,6 +471,15 @@ def _resolve_provider_id(provider_token: Optional[str], db: Session) -> Optional
|
||||
if p.id.lower() == needle or p.name.lower() == needle:
|
||||
return p.id
|
||||
|
||||
if config_manager and task_key:
|
||||
try:
|
||||
llm_settings = config_manager.get_config().settings.llm
|
||||
bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)
|
||||
if bound_provider_id and any(p.id == bound_provider_id for p in providers):
|
||||
return bound_provider_id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
active = next((p for p in providers if p.is_active), None)
|
||||
return active.id if active else providers[0].id
|
||||
# [/DEF:_resolve_provider_id:Function]
|
||||
@@ -537,6 +556,27 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
|
||||
text = message.strip()
|
||||
lower = text.lower()
|
||||
|
||||
if any(
|
||||
phrase in lower
|
||||
for phrase in [
|
||||
"что ты умеешь",
|
||||
"что умеешь",
|
||||
"что ты можешь",
|
||||
"help",
|
||||
"помощь",
|
||||
"доступные команды",
|
||||
"какие команды",
|
||||
]
|
||||
):
|
||||
return {
|
||||
"domain": "assistant",
|
||||
"operation": "show_capabilities",
|
||||
"entities": {},
|
||||
"confidence": 0.98,
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
}
|
||||
|
||||
dashboard_id = _extract_id(lower, [r"(?:дашборд\w*|dashboard)\s*(?:id\s*)?(\d+)"])
|
||||
dashboard_ref = _extract_id(
|
||||
lower,
|
||||
@@ -721,10 +761,26 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
envs = config_manager.get_environments()
|
||||
default_env_id = _get_default_environment_id(config_manager)
|
||||
providers = LLMProviderService(db).get_all_providers()
|
||||
llm_settings = {}
|
||||
try:
|
||||
llm_settings = config_manager.get_config().settings.llm
|
||||
except Exception:
|
||||
llm_settings = {}
|
||||
active_provider = next((p.id for p in providers if p.is_active), None)
|
||||
fallback_provider = active_provider or (providers[0].id if providers else None)
|
||||
validation_provider = resolve_bound_provider_id(llm_settings, "dashboard_validation") or fallback_provider
|
||||
documentation_provider = resolve_bound_provider_id(llm_settings, "documentation") or fallback_provider
|
||||
|
||||
candidates: List[Dict[str, Any]] = [
|
||||
{
|
||||
"operation": "show_capabilities",
|
||||
"domain": "assistant",
|
||||
"description": "Show available assistant commands and examples",
|
||||
"required_entities": [],
|
||||
"optional_entities": [],
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
{
|
||||
"operation": "get_task_status",
|
||||
"domain": "status",
|
||||
@@ -785,7 +841,7 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
"description": "Run LLM dashboard validation",
|
||||
"required_entities": ["dashboard_id"],
|
||||
"optional_entities": ["dashboard_ref", "environment", "provider"],
|
||||
"defaults": {"environment": default_env_id, "provider": fallback_provider},
|
||||
"defaults": {"environment": default_env_id, "provider": validation_provider},
|
||||
"risk_level": "guarded",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
@@ -795,7 +851,7 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
"description": "Generate dataset documentation via LLM",
|
||||
"required_entities": ["dataset_id"],
|
||||
"optional_entities": ["environment", "provider"],
|
||||
"defaults": {"environment": default_env_id, "provider": fallback_provider},
|
||||
"defaults": {"environment": default_env_id, "provider": documentation_provider},
|
||||
"risk_level": "guarded",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
@@ -867,9 +923,13 @@ async def _plan_intent_with_llm(
|
||||
if not tools:
|
||||
return None
|
||||
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
planner_provider_token = llm_settings.get("assistant_planner_provider")
|
||||
planner_model_override = llm_settings.get("assistant_planner_model")
|
||||
llm_service = LLMProviderService(db)
|
||||
providers = llm_service.get_all_providers()
|
||||
provider = next((p for p in providers if p.is_active), None)
|
||||
provider_id = _resolve_provider_id(planner_provider_token, db)
|
||||
provider = next((p for p in providers if p.id == provider_id), None)
|
||||
if not provider:
|
||||
return None
|
||||
api_key = llm_service.get_decrypted_api_key(provider.id)
|
||||
@@ -880,7 +940,7 @@ async def _plan_intent_with_llm(
|
||||
provider_type=LLMProviderType(provider.provider_type),
|
||||
api_key=api_key,
|
||||
base_url=provider.base_url,
|
||||
default_model=provider.default_model,
|
||||
default_model=planner_model_override or provider.default_model,
|
||||
)
|
||||
|
||||
system_instruction = (
|
||||
@@ -983,6 +1043,29 @@ async def _dispatch_intent(
|
||||
operation = intent.get("operation")
|
||||
entities = intent.get("entities", {})
|
||||
|
||||
if operation == "show_capabilities":
|
||||
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": "Статус: проверка задачи",
|
||||
}
|
||||
available = [labels[t["operation"]] for t in tools_catalog if t["operation"] in labels]
|
||||
if not available:
|
||||
return "Сейчас нет доступных для вас операций ассистента.", None, []
|
||||
commands = "\n".join(f"- {item}" for item in available)
|
||||
text = (
|
||||
"Вот что я могу сделать для вас:\n"
|
||||
f"{commands}\n\n"
|
||||
"Пример: `запусти миграцию с dev на prod для дашборда 42`."
|
||||
)
|
||||
return text, None, []
|
||||
|
||||
if operation == "get_task_status":
|
||||
_check_any_permission(current_user, [("tasks", "READ")])
|
||||
task_id = entities.get("task_id")
|
||||
@@ -1111,12 +1194,27 @@ async def _dispatch_intent(
|
||||
env_id,
|
||||
config_manager,
|
||||
)
|
||||
provider_id = _resolve_provider_id(entities.get("provider"), db)
|
||||
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):
|
||||
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",
|
||||
@@ -1140,7 +1238,12 @@ async def _dispatch_intent(
|
||||
_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)
|
||||
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")
|
||||
|
||||
@@ -1301,6 +1404,7 @@ async def send_message(
|
||||
is_clarification_error = exc.status_code in (400, 422) and (
|
||||
detail_text.lower().startswith("missing")
|
||||
or "укажите" in detail_text.lower()
|
||||
or "выберите" in detail_text.lower()
|
||||
)
|
||||
if exc.status_code == status.HTTP_403_FORBIDDEN:
|
||||
state = "denied"
|
||||
|
||||
Reference in New Issue
Block a user