From 2b197440f6e0b60ddf6047dcb82c50ce6f4c8294 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Fri, 12 Jun 2026 15:14:39 +0200 Subject: [PATCH] [KB] Script ingest Guida UI V2 nella KB AI (eseguito: 8 chunk, scope SYSTEM) ARIA ora conosce la nuova interfaccia V2 (docs/kb/V2_UI_NAVIGAZIONE.md indicizzato in Qdrant nis2_kb scope SYSTEM). Header documenta il workaround mount (copia sotto application/). Co-Authored-By: Claude Opus 4.8 --- scripts/ingest-v2-ui-doc.php | 137 +++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 scripts/ingest-v2-ui-doc.php diff --git a/scripts/ingest-v2-ui-doc.php b/scripts/ingest-v2-ui-doc.php new file mode 100644 index 0000000..acdde92 --- /dev/null +++ b/scripts/ingest-v2-ui-doc.php @@ -0,0 +1,137 @@ += $len) break; + $start += ($size - $overlap); + } + return $chunks; +} + +function uuid(): string +{ + $b = random_bytes(16); + $b[6] = chr((ord($b[6]) & 0x0f) | 0x40); + $b[8] = chr((ord($b[8]) & 0x3f) | 0x80); + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4)); +} + +logln('=== Ingest Guida UI V2 nella KB (scope SYSTEM) ==='); +if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.'); + +if (!is_file($DOC_FILE)) { logln("ERRORE: doc non trovato {$DOC_FILE}"); exit(1); } +$text = (string) file_get_contents($DOC_FILE); +$text = preg_replace('/[ \t]+/', ' ', $text); +$text = preg_replace('/\n{3,}/', "\n\n", trim($text)); +if (strlen($text) < 200) { logln('ERRORE: testo troppo breve.'); exit(1); } + +$header = "DOCUMENTO: Guida all'interfaccia V2 di NIS2 Agile (Bootstrap Italia).\n" + . "USO: aiutare l'utente a navigare la UI, trovare moduli e funzioni.\n\n"; +$chunks = chunkText($header . $text, 2000, 200); +logln(' testo: ' . strlen($text) . ' char -> ' . count($chunks) . ' chunk'); + +if ($dryRun) { logln('=== DRY-RUN completato ==='); exit(0); } + +$embed = new EmbedService(); +$vector = new VectorService(); +$vector->ensureCollection($embed->dims); + +// Idempotenza: rimuovi i chunk SYSTEM esistenti per questa fonte +try { + $vector->deleteByFilter(['must' => [ + ['key' => 'scope', 'match' => ['value' => 'SYSTEM']], + ['key' => 'source', 'match' => ['value' => $SOURCE]], + ]]); +} catch (Exception $e) { logln(' (warning) delete precedente: ' . $e->getMessage()); } + +$docUuid = uuid(); +$points = []; +foreach ($chunks as $i => $chunk) { + $vec = null; + for ($try = 1; $try <= 5; $try++) { + try { $vec = $embed->embed($chunk); break; } + catch (Throwable $e) { + if ($try === 5) { logln(" ERRORE embed chunk {$i} dopo 5 tentativi: " . $e->getMessage()); throw $e; } + logln(" retry embed chunk {$i} (tentativo {$try})"); + sleep($try); + } + } + $points[] = [ + 'id' => uuid(), + 'vector' => $vec, + 'payload' => [ + 'doc_uuid' => $docUuid, + 'title' => $TITLE . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''), + 'chunk' => $chunk, + 'entity_type' => 'guida_prodotto', + 'source' => $SOURCE, + 'lang' => 'it', + 'scope' => 'SYSTEM', + 'consulting_firm_id' => null, + 'organization_id' => null, + 'shared_with_orgs' => [], + 'uploaded_by' => 0, + ], + ]; +} +foreach (array_chunk($points, 64) as $batch) { + $vector->upsertBatch($batch); +} + +// Tracking MySQL (best-effort) +try { + $stmt = Database::getInstance()->prepare( + "INSERT INTO kb_uploaded_documents + (qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status) + VALUES (?, 'SYSTEM', NULL, NULL, 0, ?, 'guida_prodotto', ?, 'it', ?, '[]', 'ready')" + ); + $stmt->execute([$docUuid, $TITLE, $SOURCE, count($chunks)]); +} catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); } + +logln("=== OK indicizzato: doc_uuid={$docUuid}, " . count($chunks) . " chunk ===");