[FEAT] AiController /api/ai/ask (ARIA) -> askWithRag + fix DNS Qdrant php-fpm
Il FAB ARIA (common.js) chiamava POST /api/ai/ask ma il controller non esisteva (assistente AI rotto). Creato AiController::ask -> AIService::askWithRag con RAG su KB + grounding fonti certe. Verificato in produzione: rag_used=True, cita Ambiti NIS2 / Determina ACN. Fix DNS Qdrant: nei worker php-fpm (musl) getenv e gethostbyname NON funzionano per hostname Docker single-label; funziona solo un IP letterale. VectorService fallback -> 172.21.0.3 (fpm-safe); QDRANT_URL compose resta hostname per CLI. Vedi nota drift in VectorService. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
94d7867cea
commit
d5d83bb3b9
@@ -0,0 +1,75 @@
|
||||
<?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() ?? [];
|
||||
$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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user