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']]);