RAG su Knowledge Base (incl. fonti normative * certe ingerite, scope SYSTEM) + grounding con citazioni. */ require_once __DIR__ . '/BaseController.php'; require_once __DIR__ . '/../services/AIService.php'; require_once __DIR__ . '/../services/RateLimitService.php'; class AiController extends BaseController { /** * POST /api/ai/ask * Body: { question: string, history?: array } * Risposta: { answer, sources, rag_used } */ public function ask(): void { $this->requireAuth(); $question = trim((string) $this->getParam('question', '')); if ($question === '') { $this->jsonError('Domanda mancante', 422, 'QUESTION_REQUIRED'); } if (mb_strlen($question) > 2000) { $this->jsonError('Domanda troppo lunga (max 2000 caratteri)', 422, 'QUESTION_TOO_LONG'); } // Rate limit per utente (riusa la soglia AI configurata) $userId = $this->getCurrentUserId(); $rlKey = "ai_ask:{$userId}"; if (defined('RATE_LIMIT_AI')) { RateLimitService::check($rlKey, RATE_LIMIT_AI); RateLimitService::increment($rlKey); } $user = $this->getCurrentUser() ?? []; $userContext = [ 'user_id' => $userId, 'organization_id' => $this->getCurrentOrgId(), // può essere null 'consulting_firm_id' => $user['consulting_firm_id'] ?? null, ]; try { $aiService = new AIService(); $result = $aiService->askWithRag($question, $userContext); // Audit best-effort (non blocca la risposta) try { $aiService->logInteraction( (int) ($userContext['organization_id'] ?? 0), (int) ($userId ?? 0), 'qa', mb_substr($question, 0, 200), mb_substr((string) ($result['answer'] ?? ''), 0, 500), ); } catch (Throwable $e) { error_log('[AiController::ask] logInteraction failed: ' . $e->getMessage()); } $this->jsonSuccess([ 'answer' => $result['answer'] ?? '', 'sources' => $result['sources'] ?? [], 'rag_used' => $result['rag_used'] ?? false, ]); } catch (Throwable $e) { error_log('[AiController::ask] ' . $e->getMessage()); $this->jsonError('Assistente AI non disponibile in questo momento', 503, 'AI_UNAVAILABLE'); } } }