feat(assistant): add multi-dialog UX, task-aware llm settings, and i18n cleanup
This commit is contained in:
@@ -218,6 +218,29 @@ def test_unknown_command_returns_needs_clarification():
|
||||
|
||||
|
||||
# [/DEF:test_unknown_command_returns_needs_clarification:Function]
|
||||
|
||||
|
||||
# [DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
# @PURPOSE: Capability query should return deterministic help response, not clarification.
|
||||
# @PRE: User sends natural-language "what can you do" style query.
|
||||
# @POST: Response is successful and includes capabilities summary.
|
||||
def test_capabilities_question_returns_successful_help():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(message="Что ты умеешь?"),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "success"
|
||||
assert "Вот что я могу сделать" in response.text
|
||||
assert "Миграции" in response.text or "Git" in response.text
|
||||
|
||||
|
||||
# [/DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
# [DEF:test_non_admin_command_returns_denied:Function]
|
||||
# @PURPOSE: Non-admin user must receive denied state for privileged command.
|
||||
# @PRE: Limited principal executes privileged git branch command.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -25,7 +25,11 @@ from src.api.routes.git_schemas import (
|
||||
)
|
||||
from src.services.git_service import GitService
|
||||
from src.core.logger import logger, belief_scope
|
||||
from ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, normalize_llm_settings
|
||||
from ...services.llm_prompt_templates import (
|
||||
DEFAULT_LLM_PROMPTS,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["git"])
|
||||
git_service = GitService()
|
||||
@@ -431,7 +435,11 @@ async def generate_commit_message(
|
||||
|
||||
llm_service = LLMProviderService(db)
|
||||
providers = llm_service.get_all_providers()
|
||||
provider = next((p for p in providers if p.is_active), None)
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
bound_provider_id = resolve_bound_provider_id(llm_settings, "git_commit")
|
||||
provider = next((p for p in providers if p.id == bound_provider_id), None)
|
||||
if not provider:
|
||||
provider = next((p for p in providers if p.is_active), None)
|
||||
|
||||
if not provider:
|
||||
raise HTTPException(status_code=400, detail="No active LLM provider found")
|
||||
@@ -447,7 +455,6 @@ async def generate_commit_message(
|
||||
# 4. Generate Message
|
||||
from ...plugins.git.llm_extension import GitLLMExtension
|
||||
extension = GitLLMExtension(client)
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
git_prompt = llm_settings["prompts"].get(
|
||||
"git_commit_prompt",
|
||||
DEFAULT_LLM_PROMPTS["git_commit_prompt"],
|
||||
|
||||
@@ -9,9 +9,15 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from pydantic import BaseModel
|
||||
from ...core.logger import belief_scope
|
||||
|
||||
from ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry
|
||||
from ...core.task_manager.models import LogFilter, LogStats
|
||||
from ...dependencies import get_task_manager, has_permission, get_current_user
|
||||
from ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry
|
||||
from ...core.task_manager.models import LogFilter, LogStats
|
||||
from ...dependencies import get_task_manager, has_permission, get_current_user, get_config_manager
|
||||
from ...core.config_manager import ConfigManager
|
||||
from ...services.llm_prompt_templates import (
|
||||
is_multimodal_model,
|
||||
normalize_llm_settings,
|
||||
resolve_bound_provider_id,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -39,32 +45,50 @@ class ResumeTaskRequest(BaseModel):
|
||||
# @PRE: plugin_id must exist and params must be valid for that plugin.
|
||||
# @POST: A new task is created and started.
|
||||
# @RETURN: Task - The created task instance.
|
||||
async def create_task(
|
||||
request: CreateTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
async def create_task(
|
||||
request: CreateTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
current_user = Depends(get_current_user),
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
):
|
||||
# Dynamic permission check based on plugin_id
|
||||
has_permission(f"plugin:{request.plugin_id}", "EXECUTE")(current_user)
|
||||
"""
|
||||
Create and start a new task for a given plugin.
|
||||
"""
|
||||
with belief_scope("create_task"):
|
||||
try:
|
||||
# Special handling for validation task to include provider config
|
||||
if request.plugin_id == "llm_dashboard_validation":
|
||||
from ...core.database import SessionLocal
|
||||
from ...services.llm_provider import LLMProviderService
|
||||
db = SessionLocal()
|
||||
try:
|
||||
llm_service = LLMProviderService(db)
|
||||
provider_id = request.params.get("provider_id")
|
||||
if provider_id:
|
||||
db_provider = llm_service.get_provider(provider_id)
|
||||
if not db_provider:
|
||||
raise ValueError(f"LLM Provider {provider_id} not found")
|
||||
finally:
|
||||
db.close()
|
||||
try:
|
||||
# Special handling for LLM tasks to resolve provider config by task binding.
|
||||
if request.plugin_id in {"llm_dashboard_validation", "llm_documentation"}:
|
||||
from ...core.database import SessionLocal
|
||||
from ...services.llm_provider import LLMProviderService
|
||||
db = SessionLocal()
|
||||
try:
|
||||
llm_service = LLMProviderService(db)
|
||||
provider_id = request.params.get("provider_id")
|
||||
if not provider_id:
|
||||
llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)
|
||||
binding_key = "dashboard_validation" if request.plugin_id == "llm_dashboard_validation" else "documentation"
|
||||
provider_id = resolve_bound_provider_id(llm_settings, binding_key)
|
||||
if provider_id:
|
||||
request.params["provider_id"] = provider_id
|
||||
if not provider_id:
|
||||
providers = llm_service.get_all_providers()
|
||||
active_provider = next((p for p in providers if p.is_active), None)
|
||||
if active_provider:
|
||||
provider_id = active_provider.id
|
||||
request.params["provider_id"] = provider_id
|
||||
|
||||
if provider_id:
|
||||
db_provider = llm_service.get_provider(provider_id)
|
||||
if not db_provider:
|
||||
raise ValueError(f"LLM Provider {provider_id} not found")
|
||||
if request.plugin_id == "llm_dashboard_validation" and not is_multimodal_model(db_provider.default_model):
|
||||
raise ValueError(
|
||||
"Selected provider model is not multimodal for dashboard validation"
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
task = await task_manager.create_task(
|
||||
plugin_id=request.plugin_id,
|
||||
|
||||
Reference in New Issue
Block a user