[FEAT] Knowledge Base RAG multi-livello (SYSTEM/FIRM/ORG) + Qdrant + Voyage

- KnowledgeBaseController: ingest, list, firmOrgs, search, delete
- VectorService (Qdrant + buildAuthzFilter), EmbedService (Voyage), RagService (pipeline)
- AIService::askWithRag con fallback graceful
- docker-compose: servizio qdrant + env Voyage (chiave da .env/vault, no hardcoded)
- SQL 012 consulting_firms, 013 firm_assignments + kb_uploaded_documents
- public/kb.html + kb.js (upload, lista, search preview)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-05-29 15:44:13 +02:00
co-authored by Claude Opus 4.8
parent 9b53ca3ba1
commit a7a21faa82
10 changed files with 1164 additions and 0 deletions
+54
View File
@@ -564,4 +564,58 @@ PROMPT;
'model_used' => $this->model,
]);
}
/**
* Migration 012-014: Q&A grounded sulla KB multi-livello.
*
* Esegue una RAG search sui documenti visibili all'utente (SYSTEM/FIRM/ORG)
* e inietta i top-K chunks nel system prompt prima di chiamare Claude.
*
* Se Voyage/Qdrant non sono disponibili, ricade su Claude diretto senza grounding.
*
* @param string $question Domanda dell'utente
* @param array $userContext ['user_id', 'organization_id', 'consulting_firm_id']
* @return array ['answer'=>string, 'sources'=>array, 'rag_used'=>bool]
*/
public function askWithRag(string $question, array $userContext): array
{
$sources = [];
$contextBlock = '';
$ragUsed = false;
// Tenta RAG: se fallisce, prosegui senza grounding (degradazione graceful)
try {
require_once __DIR__ . '/RagService.php';
$rag = new RagService();
$hits = $rag->searchForUser($question, $userContext, 5, 0.28);
if (!empty($hits)) {
$contextBlock = $rag->formatContext($hits);
$sources = array_map(fn($h) => [
'title' => $h['title'],
'scope' => $h['scope'],
'score' => $h['score'],
], $hits);
$ragUsed = true;
}
} catch (Exception $e) {
error_log('[AIService::askWithRag] RAG failed, fallback diretto: ' . $e->getMessage());
}
$systemPrompt = "Sei un esperto consulente di cybersecurity NIS2 (EU 2022/2555) e D.Lgs. 138/2024.\n"
. "Rispondi in modo preciso e cita le fonti del contesto quando rilevanti.\n";
if (!empty($contextBlock)) {
$systemPrompt .= "\n## Contesto documentale (knowledge base)\n" . $contextBlock
. "\n\nQuando rispondi, cita esplicitamente i numeri tra parentesi quadre [1], [2], ... che corrispondono ai documenti del contesto.";
} else {
$systemPrompt .= "\nNon e' disponibile contesto documentale specifico per questa domanda. Rispondi con la tua conoscenza generale e indica esplicitamente che non hai trovato fonti nella knowledge base.";
}
$answer = $this->callAPI($question, $systemPrompt);
return [
'answer' => $answer,
'sources' => $sources,
'rag_used' => $ragUsed,
];
}
}