nis2-agile/application/services/EmbedService.php
DevEnv nis2-agile a7a21faa82 [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>
2026-05-29 15:44:13 +02:00

67 lines
2.2 KiB
PHP

<?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;
}
}