- 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>
67 lines
2.2 KiB
PHP
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;
|
|
}
|
|
}
|