[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:
co-authored by
Claude Opus 4.8
parent
9b53ca3ba1
commit
a7a21faa82
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - EmbedService
|
||||
*
|
||||
* Client minimale per Voyage AI embeddings (voyage-3-lite, 1024 dim).
|
||||
*/
|
||||
|
||||
class EmbedService
|
||||
{
|
||||
public int $dims = 512;
|
||||
private string $apiKey;
|
||||
private string $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// PHP-FPM Alpine non popola env via getenv() (clear_env). Multi-source lookup.
|
||||
// La chiave vive in .env (gitignored) + vault-steward; nessun segreto hardcoded.
|
||||
$this->apiKey = getenv('VOYAGE_API_KEY')
|
||||
?: ($_SERVER['VOYAGE_API_KEY'] ?? '')
|
||||
?: ($_ENV['VOYAGE_API_KEY'] ?? '')
|
||||
?: (class_exists('Env') ? Env::get('VOYAGE_API_KEY', '') : '');
|
||||
$this->model = getenv('VOYAGE_MODEL')
|
||||
?: ($_SERVER['VOYAGE_MODEL'] ?? null)
|
||||
?: ($_ENV['VOYAGE_MODEL'] ?? null)
|
||||
?: 'voyage-3-lite';
|
||||
if (empty($this->apiKey)) {
|
||||
throw new RuntimeException('VOYAGE_API_KEY non configurata');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float[] Vettore embedding 1024-dim
|
||||
*/
|
||||
public function embed(string $text): array
|
||||
{
|
||||
$ch = curl_init('https://api.voyageai.com/v1/embeddings');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->apiKey,
|
||||
],
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'input' => [$text],
|
||||
'model' => $this->model,
|
||||
'input_type' => 'document',
|
||||
'output_dimension' => 512,
|
||||
]),
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$raw = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($status !== 200 || !$raw) {
|
||||
throw new RuntimeException("Voyage embed failed (HTTP $status): " . substr((string)$raw, 0, 200));
|
||||
}
|
||||
$data = json_decode($raw, true);
|
||||
$vec = $data['data'][0]['embedding'] ?? null;
|
||||
if (!is_array($vec)) {
|
||||
throw new RuntimeException('Voyage response without embedding: ' . substr($raw, 0, 200));
|
||||
}
|
||||
return $vec;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user