Lamentela: "AI non allineata con help online e contesto". Causa: ARIA e Help erano due fonti separate (Help=help.js, ARIA=KB) → divergevano. Fix (fonte unica = help.js, zero duplicazione): - help.js: getContextText(pageId) (testo piano dell'help della pagina) + detectPageId(). - common.js: invia page_id (canonico) + page_help (stesso testo del pannello "?") a /api/ai/ask. - AiController: passa page_id/page_help in userContext. - AIService::askWithRag: inietta "GUIDA CONTESTUALE DELLA PAGINA" nel system prompt (ARIA si allinea all'help mostrato all'utente, non lo contraddice) + usa page_id per il trigger relevanceGrid (più robusto del title). Smoke: ARIA cita "registro dei rischi" dall'help + "Gestione > Rischi". Cache-buster common.js/help.js -> 20260624e (39 pagine). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
3.3 KiB
PHP
85 lines
3.3 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile - AI Assistant Controller (ARIA)
|
|
*
|
|
* Endpoint dell'assistente conversazionale AI usato dal FAB "ARIA" (common.js).
|
|
* Usa AIService::askWithRag() -> 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() ?? [];
|
|
// Pagina corrente (facoltativa) inviata dal frontend, per dare ad ARIA
|
|
// contesto sulla schermata da cui l'utente sta scrivendo (ticket #424).
|
|
$page = mb_substr(trim((string) $this->getParam('page', '')), 0, 120);
|
|
// pageId canonico + testo dell'help "?" della pagina (allineamento ARIA↔Help).
|
|
$pageId = mb_substr(trim((string) $this->getParam('page_id', '')), 0, 40);
|
|
$pageHelp = mb_substr(trim((string) $this->getParam('page_help', '')), 0, 2500);
|
|
$userContext = [
|
|
'user_id' => $userId,
|
|
'organization_id' => $this->getCurrentOrgId(), // può essere null
|
|
'consulting_firm_id' => $user['consulting_firm_id'] ?? null,
|
|
'page' => $page,
|
|
'page_id' => $pageId,
|
|
'page_help' => $pageHelp,
|
|
];
|
|
|
|
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');
|
|
}
|
|
}
|
|
}
|