200 lines
8.2 KiB
PHP
200 lines
8.2 KiB
PHP
<?php
|
|
// OPUS5 — Knowledge Graph souverain (doctrine 75)
|
|
// Stack : Ollama embeddings (nomic-embed-text ou mxbai-embed-large) + Qdrant + PG relations
|
|
// Pas de Zep (service externe). Qdrant = vector store, PG = graph edges.
|
|
header('Content-Type: application/json');
|
|
$R = ['ts'=>date('c'), 'source'=>'opus5-knowledge-graph'];
|
|
|
|
$OLLAMA = 'http://127.0.0.1:11434';
|
|
$QDRANT = 'http://127.0.0.1:6333';
|
|
$COLLECTION = 'wevia_graph';
|
|
$PG_DSN = 'pgsql:host=10.1.0.3;port=5432;dbname=adx_system;user=admin;password=admin123';
|
|
|
|
$action = $_GET['action'] ?? 'health';
|
|
$raw = file_get_contents('php://input');
|
|
$d = json_decode($raw, true) ?: [];
|
|
|
|
function ollama_embed($text, $OLLAMA) {
|
|
$ch = curl_init("$OLLAMA/api/embeddings");
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode(['model'=>'nomic-embed-text', 'prompt'=>substr($text, 0, 2000)]),
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 15
|
|
]);
|
|
$resp = curl_exec($ch);
|
|
curl_close($ch);
|
|
$d = @json_decode($resp, true);
|
|
return $d['embedding'] ?? null;
|
|
}
|
|
|
|
function qdrant_req($method, $path, $body, $QDRANT) {
|
|
$ch = curl_init("$QDRANT$path");
|
|
$opts = [
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10
|
|
];
|
|
if ($body !== null) $opts[CURLOPT_POSTFIELDS] = json_encode($body);
|
|
curl_setopt_array($ch, $opts);
|
|
$resp = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
return ['code'=>$code, 'body'=>@json_decode($resp, true)];
|
|
}
|
|
|
|
if ($action === 'health') {
|
|
$o_ch = curl_init("$OLLAMA/api/tags");
|
|
curl_setopt_array($o_ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>3]);
|
|
$o_resp = curl_exec($o_ch);
|
|
$o_code = curl_getinfo($o_ch, CURLINFO_HTTP_CODE);
|
|
curl_close($o_ch);
|
|
|
|
$q = qdrant_req('GET', '/collections', null, $QDRANT);
|
|
|
|
$R['ollama_up'] = ($o_code === 200);
|
|
$R['ollama_models'] = array_column(@json_decode($o_resp, true)['models'] ?? [], 'name');
|
|
$R['qdrant_up'] = ($q['code'] === 200);
|
|
$R['qdrant_collections_count'] = count($q['body']['result']['collections'] ?? []);
|
|
$R['wevia_graph_exists'] = in_array($COLLECTION, array_column($q['body']['result']['collections'] ?? [], 'name'));
|
|
|
|
// Check PG connectivity
|
|
try {
|
|
$db = new PDO($PG_DSN, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 3]);
|
|
$R['pg_up'] = true;
|
|
} catch (Throwable $e) { $R['pg_up'] = false; $R['pg_err'] = substr($e->getMessage(), 0, 100); }
|
|
echo json_encode($R, JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'init') {
|
|
// Create wevia_graph collection if missing
|
|
$q = qdrant_req('GET', "/collections/$COLLECTION", null, $QDRANT);
|
|
if ($q['code'] !== 200) {
|
|
// Nomic embed = 768 dimensions
|
|
$create = qdrant_req('PUT', "/collections/$COLLECTION", [
|
|
'vectors' => ['size' => 768, 'distance' => 'Cosine']
|
|
], $QDRANT);
|
|
$R['collection_created'] = $create['code'] === 200;
|
|
$R['create_response'] = $create['body'];
|
|
} else {
|
|
$R['collection_exists'] = true;
|
|
}
|
|
|
|
// Create PG edges table
|
|
try {
|
|
$db = new PDO($PG_DSN, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
$db->exec("CREATE TABLE IF NOT EXISTS admin.wevia_graph_edges (
|
|
id SERIAL PRIMARY KEY,
|
|
src_node VARCHAR(128),
|
|
rel_type VARCHAR(64),
|
|
dst_node VARCHAR(128),
|
|
weight FLOAT DEFAULT 1.0,
|
|
metadata JSONB,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
)");
|
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_graph_src ON admin.wevia_graph_edges(src_node)");
|
|
$db->exec("CREATE INDEX IF NOT EXISTS idx_graph_dst ON admin.wevia_graph_edges(dst_node)");
|
|
$R['pg_table_ready'] = true;
|
|
} catch (Throwable $e) { $R['pg_err'] = substr($e->getMessage(), 0, 200); }
|
|
|
|
$R['doctrine'] = '75 — knowledge graph souverain (Ollama embed + Qdrant vectors + PG edges)';
|
|
echo json_encode($R, JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'add_node') {
|
|
$node_id = (string)($d['node_id'] ?? '');
|
|
$text = (string)($d['text'] ?? '');
|
|
$metadata = $d['metadata'] ?? [];
|
|
if (!$node_id || !$text) { http_response_code(400); echo json_encode(['err'=>'missing node_id or text']); exit; }
|
|
|
|
$vec = ollama_embed($text, $OLLAMA);
|
|
if (!$vec) { http_response_code(503); echo json_encode(['err'=>'ollama_embed_failed']); exit; }
|
|
|
|
$point_id = abs(crc32($node_id));
|
|
$upsert = qdrant_req('PUT', "/collections/$COLLECTION/points", [
|
|
'points' => [[
|
|
'id' => $point_id,
|
|
'vector' => $vec,
|
|
'payload' => array_merge($metadata, ['node_id'=>$node_id, 'text'=>substr($text,0,500)])
|
|
]]
|
|
], $QDRANT);
|
|
$R['upsert_code'] = $upsert['code'];
|
|
$R['node_id'] = $node_id;
|
|
$R['point_id'] = $point_id;
|
|
$R['vec_dim'] = count($vec);
|
|
echo json_encode($R, JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'add_edge') {
|
|
$src = (string)($d['src'] ?? '');
|
|
$rel = (string)($d['rel'] ?? 'related_to');
|
|
$dst = (string)($d['dst'] ?? '');
|
|
if (!$src || !$dst) { http_response_code(400); echo json_encode(['err'=>'missing src or dst']); exit; }
|
|
try {
|
|
$db = new PDO($PG_DSN, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
$stmt = $db->prepare("INSERT INTO admin.wevia_graph_edges (src_node, rel_type, dst_node, weight, metadata) VALUES (?,?,?,?,?)");
|
|
$stmt->execute([$src, $rel, $dst, (float)($d['weight'] ?? 1.0), json_encode($d['metadata'] ?? [])]);
|
|
$R['edge_id'] = $db->lastInsertId();
|
|
} catch (Throwable $e) { http_response_code(500); echo json_encode(['err'=>'pg_err', 'msg'=>$e->getMessage()]); exit; }
|
|
echo json_encode($R, JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'query') {
|
|
$query_text = (string)($d['query'] ?? '');
|
|
$limit = (int)($d['limit'] ?? 5);
|
|
if (!$query_text) { http_response_code(400); echo json_encode(['err'=>'no_query']); exit; }
|
|
|
|
$qvec = ollama_embed($query_text, $OLLAMA);
|
|
if (!$qvec) { http_response_code(503); echo json_encode(['err'=>'ollama_embed_failed']); exit; }
|
|
|
|
$search = qdrant_req('POST', "/collections/$COLLECTION/points/search", [
|
|
'vector' => $qvec,
|
|
'limit' => $limit,
|
|
'with_payload' => true
|
|
], $QDRANT);
|
|
|
|
$results = [];
|
|
foreach (($search['body']['result'] ?? []) as $h) {
|
|
$results[] = [
|
|
'score' => round((float)$h['score'], 4),
|
|
'node_id' => $h['payload']['node_id'] ?? '?',
|
|
'text' => substr($h['payload']['text'] ?? '', 0, 200)
|
|
];
|
|
}
|
|
$R['query'] = $query_text;
|
|
$R['results'] = $results;
|
|
|
|
// Also fetch edges for top result
|
|
if (!empty($results)) {
|
|
try {
|
|
$db = new PDO($PG_DSN, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
$stmt = $db->prepare("SELECT src_node, rel_type, dst_node, weight FROM admin.wevia_graph_edges WHERE src_node=? OR dst_node=? LIMIT 10");
|
|
$stmt->execute([$results[0]['node_id'], $results[0]['node_id']]);
|
|
$R['related_edges'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) { $R['edges_err'] = $e->getMessage(); }
|
|
}
|
|
echo json_encode($R, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'stats') {
|
|
$q = qdrant_req('GET', "/collections/$COLLECTION", null, $QDRANT);
|
|
$R['qdrant'] = $q['body']['result'] ?? null;
|
|
try {
|
|
$db = new PDO($PG_DSN, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
$R['edges_count'] = (int)$db->query("SELECT COUNT(*) FROM admin.wevia_graph_edges")->fetchColumn();
|
|
$R['distinct_nodes'] = (int)$db->query("SELECT COUNT(DISTINCT src_node) + COUNT(DISTINCT dst_node) FROM admin.wevia_graph_edges")->fetchColumn();
|
|
} catch (Throwable $e) { $R['pg_err'] = $e->getMessage(); }
|
|
echo json_encode($R, JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
http_response_code(400);
|
|
echo json_encode(['err'=>'unknown_action', 'available'=>['health', 'init', 'add_node', 'add_edge', 'query', 'stats']]);
|