230 lines
8.7 KiB
PHP
230 lines
8.7 KiB
PHP
<?php
|
|
/**
|
|
* WEVIA Skills Explorer API · multi-source skills discovery
|
|
* Sources: /skills/ disk + Qdrant weval_skills + tool-registry + arena
|
|
* Opus Yacine · 19avr2026
|
|
*/
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
$action = $_GET['action'] ?? 'sources';
|
|
$QDRANT = 'http://127.0.0.1:6333';
|
|
|
|
function qdrant_call($url, $method = 'GET', $data = null) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
]);
|
|
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
$r = curl_exec($ch);
|
|
curl_close($ch);
|
|
return json_decode($r ?: '{}', true);
|
|
}
|
|
|
|
$result = ['ok' => true, 'action' => $action, 'ts' => date('c')];
|
|
|
|
if ($action === 'sources') {
|
|
// Source 1: Disk /skills/ (with and without SKILL.md)
|
|
$all_dirs = glob('/var/www/html/skills/*', GLOB_ONLYDIR);
|
|
$disk_skills = [];
|
|
$big_catalogs = [];
|
|
foreach ($all_dirs as $dir) {
|
|
$name = basename($dir);
|
|
$skill_md = "$dir/SKILL.md";
|
|
$has_md = file_exists($skill_md);
|
|
$desc = '';
|
|
$sub_count = 0;
|
|
|
|
if ($has_md) {
|
|
$content = @file_get_contents($skill_md);
|
|
if ($content) {
|
|
foreach (explode("\n", $content) as $line) {
|
|
$line = trim($line);
|
|
if ($line && !str_starts_with($line, '#') && !str_starts_with($line, '---')) {
|
|
$desc = substr($line, 0, 200);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$disk_skills[] = [
|
|
'name' => $name,
|
|
'type' => 'skill',
|
|
'desc' => $desc,
|
|
'path' => "/skills/$name/SKILL.md",
|
|
];
|
|
} else {
|
|
// Count sub-skills inside catalog folder
|
|
$sub_skills = glob("$dir/*", GLOB_ONLYDIR);
|
|
$sub_count = count($sub_skills);
|
|
$readme = @file_get_contents("$dir/README.md") ?: @file_get_contents("$dir/CATALOG.md");
|
|
$readme_desc = '';
|
|
if ($readme) {
|
|
foreach (explode("\n", $readme) as $line) {
|
|
$line = trim($line);
|
|
if ($line && !str_starts_with($line, '#')) {
|
|
$readme_desc = substr($line, 0, 200);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$big_catalogs[] = [
|
|
'name' => $name,
|
|
'type' => 'catalog',
|
|
'sub_count' => $sub_count,
|
|
'desc' => $readme_desc ?: "Catalog avec $sub_count sub-items",
|
|
'path' => "/skills/$name/",
|
|
'samples' => array_slice(array_map('basename', $sub_skills), 0, 5),
|
|
];
|
|
}
|
|
}
|
|
|
|
// Source 2: Qdrant weval_skills
|
|
$qdrant_info = qdrant_call("$QDRANT/collections/weval_skills");
|
|
$qdrant_points = $qdrant_info['result']['points_count'] ?? 0;
|
|
|
|
// Source 3: Tools registry
|
|
$reg = @json_decode(@file_get_contents('/var/www/html/api/wevia-tool-registry.json'), true);
|
|
$tools_count = isset($reg['tools']) && is_array($reg['tools']) ? count($reg['tools']) : 0;
|
|
$tools_sample = [];
|
|
if ($tools_count) {
|
|
foreach (array_slice($reg['tools'], 0, 10) as $t) {
|
|
$tools_sample[] = ['name' => $t['name'] ?? $t['id'] ?? '?', 'desc' => substr($t['desc'] ?? $t['description'] ?? $t['kw'] ?? $t['cmd'] ?? '', 0, 150)];
|
|
}
|
|
}
|
|
|
|
// Source 4: Arena declared
|
|
$arena = @json_decode(@file_get_contents('/var/www/html/api/arena-intent-registry.json'), true);
|
|
$arena_skills = $arena['total_skills'] ?? 0;
|
|
|
|
$total = count($disk_skills) + $qdrant_points + $tools_count + $arena_skills;
|
|
|
|
$result['sources'] = [
|
|
'source_1_disk_skill_md' => [
|
|
'label' => '/skills/ dossiers avec SKILL.md · vitrine visible',
|
|
'count' => count($disk_skills),
|
|
'url' => '/skills/',
|
|
'items' => $disk_skills,
|
|
],
|
|
'source_2_disk_big_catalogs' => [
|
|
'label' => '/skills/ catalogs avec sub-skills · deer-flow/paperclip/etc',
|
|
'count' => count($big_catalogs),
|
|
'total_sub_items' => array_sum(array_column($big_catalogs, 'sub_count')),
|
|
'items' => $big_catalogs,
|
|
],
|
|
'source_3_qdrant_vectorized' => [
|
|
'label' => 'Qdrant weval_skills · vectorisés pour semantic search',
|
|
'count' => $qdrant_points,
|
|
'url' => '/api/skills-explorer-api.php?action=qdrant_sample',
|
|
'note' => 'Indexés depuis /opt/deer-flow/skills/ · searchable via embeddings',
|
|
],
|
|
'source_4_tools_registry' => [
|
|
'label' => 'wevia-tool-registry.json · tools callable',
|
|
'count' => $tools_count,
|
|
'url' => '/api/wevia-tool-registry.json',
|
|
'samples' => $tools_sample,
|
|
],
|
|
'source_5_arena_declared' => [
|
|
'label' => 'Arena registry · skills déclarés (intents associés)',
|
|
'count' => $arena_skills,
|
|
'url' => '/api/arena-intent-registry.json',
|
|
],
|
|
'TOTAL_ALL_SOURCES' => $total,
|
|
];
|
|
}
|
|
|
|
if ($action === 'qdrant_sample') {
|
|
$limit = (int)($_GET['limit'] ?? 50);
|
|
$limit = min(100, max(10, $limit));
|
|
$offset = $_GET['offset'] ?? null;
|
|
|
|
$payload = ['limit' => $limit, 'with_payload' => true];
|
|
if ($offset) $payload['offset'] = $offset;
|
|
|
|
$res = qdrant_call("$QDRANT/collections/weval_skills/points/scroll", 'POST', $payload);
|
|
$points = $res['result']['points'] ?? [];
|
|
|
|
$skills = [];
|
|
foreach ($points as $p) {
|
|
$pl = $p['payload'] ?? [];
|
|
$skills[] = [
|
|
'id' => $p['id'] ?? '?',
|
|
'name' => $pl['name'] ?? '?',
|
|
'desc' => substr($pl['desc'] ?? '', 0, 250),
|
|
'source' => $pl['source'] ?? '?',
|
|
'path' => $pl['path'] ?? '',
|
|
];
|
|
}
|
|
$result['qdrant_skills'] = $skills;
|
|
$result['count'] = count($skills);
|
|
$result['next_offset'] = $res['result']['next_page_offset'] ?? null;
|
|
}
|
|
|
|
if ($action === 'search' && !empty($_GET['q'])) {
|
|
$q = strtolower($_GET['q']);
|
|
|
|
// Scroll Qdrant with payload filter
|
|
$matches = [];
|
|
$offset = null;
|
|
$scanned = 0;
|
|
$max_scans = 5; // scroll 5 pages max
|
|
for ($i = 0; $i < $max_scans; $i++) {
|
|
$payload = ['limit' => 200, 'with_payload' => true];
|
|
if ($offset) $payload['offset'] = $offset;
|
|
$res = qdrant_call("$QDRANT/collections/weval_skills/points/scroll", 'POST', $payload);
|
|
$points = $res['result']['points'] ?? [];
|
|
$scanned += count($points);
|
|
|
|
foreach ($points as $p) {
|
|
$pl = $p['payload'] ?? [];
|
|
$name = strtolower($pl['name'] ?? '');
|
|
$desc = strtolower($pl['desc'] ?? '');
|
|
if (strpos($name, $q) !== false || strpos($desc, $q) !== false) {
|
|
$matches[] = [
|
|
'id' => $p['id'],
|
|
'name' => $pl['name'] ?? '?',
|
|
'desc' => substr($pl['desc'] ?? '', 0, 250),
|
|
'source' => $pl['source'] ?? '?',
|
|
'path' => $pl['path'] ?? '',
|
|
];
|
|
if (count($matches) >= 50) break 2;
|
|
}
|
|
}
|
|
$offset = $res['result']['next_page_offset'] ?? null;
|
|
if (!$offset) break;
|
|
}
|
|
|
|
$result['query'] = $q;
|
|
$result['scanned'] = $scanned;
|
|
$result['matches_count'] = count($matches);
|
|
$result['matches'] = $matches;
|
|
}
|
|
|
|
if ($action === 'tools') {
|
|
$reg = @json_decode(@file_get_contents('/var/www/html/api/wevia-tool-registry.json'), true);
|
|
$tools_list = [];
|
|
if (isset($reg['tools']) && is_array($reg['tools'])) {
|
|
$lim = (int)($_GET['limit'] ?? 0);
|
|
$off = (int)($_GET['offset'] ?? 0);
|
|
$all = $reg['tools'];
|
|
$sliced = $lim > 0 ? array_slice($all, $off, $lim) : $all;
|
|
foreach ($sliced as $t) {
|
|
$tools_list[] = [
|
|
'id' => $t['id'] ?? $t['name'] ?? '',
|
|
'name' => $t['name'] ?? $t['id'] ?? '',
|
|
'kw' => $t['kw'] ?? '',
|
|
'desc' => substr((string)($t['desc'] ?? $t['description'] ?? $t['kw'] ?? $t['cmd'] ?? ''), 0, 200),
|
|
'cmd' => isset($t['cmd']) ? substr((string)$t['cmd'], 0, 100) : '',
|
|
'category' => $t['category'] ?? $t['cat'] ?? 'general',
|
|
];
|
|
}
|
|
}
|
|
$result['total'] = isset($reg['tools']) ? count($reg['tools']) : 0;
|
|
$result['returned'] = count($tools_list);
|
|
$result['tools'] = $tools_list;
|
|
}
|
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|