# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization] # @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog. # @LAYER API # @RELATION DEPENDS_ON -> [AssistantLlmPlanner] # @RELATION DEPENDS_ON -> [AssistantResolvers] # @PRE Assistant routes initialized, user authenticated # @POST Intent planning registered with confirmation gate # @INVARIANT Production deployments always require confirmation. # @SIDE_EFFECT Registers intent planning routes # @DATA_CONTRACT UserIntent -> PlannedAction from __future__ import annotations import json from typing import Any from sqlalchemy.orm import Session from src.core.config_manager import ConfigManager from src.core.logger import logger from src.plugins.llm_analysis.models import LLMProviderType from src.plugins.llm_analysis.service import LLMClient from src.schemas.auth import User from src.services.llm_prompt_templates import ( normalize_llm_settings, ) from src.services.llm_provider import LLMProviderService from ._llm_planner import ( _check_any_permission, _coerce_intent_entities, ) from ._resolvers import ( _is_production_env, _resolve_provider_id, ) from ._tool_registry import get_permission_checks # #region _plan_intent_with_llm [C:2] [TYPE Function] # @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog. # @PRE tools list contains allowed operations for current user. # @POST Returns normalized intent dict when planning succeeds; otherwise None. async def _plan_intent_with_llm( message: str, tools: list[dict[str, Any]], db: Session, config_manager: ConfigManager, ) -> dict[str, Any] | None: 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_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) if not api_key: return None planner = LLMClient( provider_type=LLMProviderType(provider.provider_type), api_key=api_key, base_url=provider.base_url, default_model=planner_model_override or provider.default_model, ) system_instruction = ( "You are a helpful assistant that understands backend tools.\n" "Choose exactly one operation from available_tools or return clarify.\n" "Output strict JSON object:\n" "{" '"domain": string, ' '"operation": string, ' '"entities": object, ' '"confidence": number, ' '"risk_level": "safe"|"guarded"|"dangerous", ' '"requires_confirmation": boolean' "}\n" "Rules:\n" "- Use only operation names from available_tools.\n" '- If input is ambiguous, operation must be "clarify" with low confidence.\n' ' Include "clarification" field with a specific question in Russian ' "asking what exactly is missing or unclear.\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 = { "available_tools": tools, "user_message": message, "known_environments": [ {"id": e.id, "name": e.name} for e in config_manager.get_environments() ], } try: response = await planner.get_json_completion( [ {"role": "system", "content": system_instruction}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, ] ) except Exception as exc: import traceback logger.warning( f"[assistant.planner][fallback] LLM planner unavailable: {exc}\n{traceback.format_exc()}" ) return None if not isinstance(response, dict): return None operation = response.get("operation") valid_ops = {tool["operation"] for tool in tools} if operation == "clarify": clarification = response.get("clarification", "") return { "domain": "unknown", "operation": "clarify", "entities": {}, "confidence": float(response.get("confidence", 0.3)), "risk_level": "safe", "requires_confirmation": False, "clarification": clarification, } if operation not in valid_ops: return None by_operation = {tool["operation"]: tool for tool in tools} selected = by_operation[operation] intent = { "domain": response.get("domain") or selected["domain"], "operation": operation, "entities": response.get("entities", {}), "confidence": float(response.get("confidence", 0.75)), "risk_level": response.get("risk_level") or selected["risk_level"], "requires_confirmation": bool( response.get("requires_confirmation", selected["requires_confirmation"]) ), } intent = _coerce_intent_entities(intent) defaults = selected.get("defaults") or {} for key, value in defaults.items(): if value and not intent["entities"].get(key): intent["entities"][key] = value if operation in {"deploy_dashboard", "execute_migration"}: env_token = intent["entities"].get("environment") or intent["entities"].get( "target_env" ) if _is_production_env(env_token, config_manager): intent["risk_level"] = "dangerous" intent["requires_confirmation"] = True return intent # #endregion _plan_intent_with_llm # #region _authorize_intent [C:2] [TYPE Function] # @BRIEF Validate user permissions for parsed intent before confirmation/dispatch. # @PRE intent.operation is present for known assistant command domains. # @POST Returns if authorized; raises HTTPException(403) when denied. def _authorize_intent(intent: dict[str, Any], current_user: User): operation = intent.get("operation") checks = get_permission_checks().get(operation) if checks: _check_any_permission(current_user, checks) # #endregion _authorize_intent # #endregion AssistantLlmPlannerIntent