@team-semicolon/semo-cli 3.14.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/bots.d.ts +11 -0
- package/dist/commands/bots.js +382 -0
- package/dist/commands/context.d.ts +8 -0
- package/dist/commands/context.js +302 -0
- package/dist/commands/get.d.ts +12 -0
- package/dist/commands/get.js +433 -0
- package/dist/database.d.ts +2 -0
- package/dist/database.js +26 -10
- package/dist/index.d.ts +6 -9
- package/dist/index.js +750 -1298
- package/dist/kb.d.ts +134 -0
- package/dist/kb.js +627 -0
- package/package.json +1 -1
package/dist/kb.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SEMO KB/Ontology Module
|
|
4
|
+
*
|
|
5
|
+
* Knowledge Base and Ontology management for SEMO bot ecosystem.
|
|
6
|
+
* Uses the team's core PostgreSQL database as Single Source of Truth.
|
|
7
|
+
*
|
|
8
|
+
* v3.15.0: Initial implementation
|
|
9
|
+
* v3.15.1: pgvector embedding integration
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.generateEmbedding = generateEmbedding;
|
|
46
|
+
exports.generateEmbeddings = generateEmbeddings;
|
|
47
|
+
exports.kbPull = kbPull;
|
|
48
|
+
exports.kbPush = kbPush;
|
|
49
|
+
exports.kbStatus = kbStatus;
|
|
50
|
+
exports.kbList = kbList;
|
|
51
|
+
exports.kbDiff = kbDiff;
|
|
52
|
+
exports.kbSearch = kbSearch;
|
|
53
|
+
exports.ontoList = ontoList;
|
|
54
|
+
exports.ontoShow = ontoShow;
|
|
55
|
+
exports.ontoValidate = ontoValidate;
|
|
56
|
+
exports.ontoPullToLocal = ontoPullToLocal;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
// ============================================================
|
|
60
|
+
// Embedding
|
|
61
|
+
// ============================================================
|
|
62
|
+
const EMBEDDING_MODEL = "voyage-3";
|
|
63
|
+
const EMBEDDING_DIMENSIONS = 1024;
|
|
64
|
+
/**
|
|
65
|
+
* Generate embedding vector for text using OpenAI API
|
|
66
|
+
* Requires OPENAI_API_KEY environment variable
|
|
67
|
+
*/
|
|
68
|
+
async function generateEmbedding(text) {
|
|
69
|
+
const apiKey = process.env.VOYAGE_API_KEY;
|
|
70
|
+
if (!apiKey)
|
|
71
|
+
return null;
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch("https://api.voyageai.com/v1/embeddings", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
model: EMBEDDING_MODEL,
|
|
81
|
+
input: text.substring(0, 8000), // truncate to avoid token limit
|
|
82
|
+
dimensions: EMBEDDING_DIMENSIONS,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const err = await response.text();
|
|
87
|
+
console.error(`Embedding API error: ${response.status} ${err}`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
return data.data?.[0]?.embedding || null;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(`Embedding error: ${err}`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate embeddings for multiple texts (batched)
|
|
100
|
+
*/
|
|
101
|
+
async function generateEmbeddings(texts) {
|
|
102
|
+
const apiKey = process.env.VOYAGE_API_KEY;
|
|
103
|
+
if (!apiKey)
|
|
104
|
+
return texts.map(() => null);
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch("https://api.voyageai.com/v1/embeddings", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
model: EMBEDDING_MODEL,
|
|
114
|
+
input: texts.map(t => t.substring(0, 16000)),
|
|
115
|
+
output_dimension: EMBEDDING_DIMENSIONS,
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok)
|
|
119
|
+
return texts.map(() => null);
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
return data.data?.map((d) => d.embedding) || texts.map(() => null);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return texts.map(() => null);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ============================================================
|
|
128
|
+
// KB Directory Management
|
|
129
|
+
// ============================================================
|
|
130
|
+
const KB_DIR = ".kb";
|
|
131
|
+
const SYNC_STATE_FILE = ".sync-state.json";
|
|
132
|
+
function getKBDir(cwd) {
|
|
133
|
+
return path.join(cwd, KB_DIR);
|
|
134
|
+
}
|
|
135
|
+
function ensureKBDir(cwd) {
|
|
136
|
+
const kbDir = getKBDir(cwd);
|
|
137
|
+
if (!fs.existsSync(kbDir)) {
|
|
138
|
+
fs.mkdirSync(kbDir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
const ontoDir = path.join(kbDir, "ontology");
|
|
141
|
+
if (!fs.existsSync(ontoDir)) {
|
|
142
|
+
fs.mkdirSync(ontoDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
return kbDir;
|
|
145
|
+
}
|
|
146
|
+
function readSyncState(cwd) {
|
|
147
|
+
const statePath = path.join(getKBDir(cwd), SYNC_STATE_FILE);
|
|
148
|
+
if (fs.existsSync(statePath)) {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// corrupted file
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { botId: "", lastPull: null, lastPush: null, sharedCount: 0, botCount: 0 };
|
|
157
|
+
}
|
|
158
|
+
function writeSyncState(cwd, state) {
|
|
159
|
+
const kbDir = ensureKBDir(cwd);
|
|
160
|
+
fs.writeFileSync(path.join(kbDir, SYNC_STATE_FILE), JSON.stringify(state, null, 2));
|
|
161
|
+
}
|
|
162
|
+
function writeKBFile(cwd, filename, data) {
|
|
163
|
+
const kbDir = ensureKBDir(cwd);
|
|
164
|
+
fs.writeFileSync(path.join(kbDir, filename), JSON.stringify(data, null, 2));
|
|
165
|
+
}
|
|
166
|
+
function readKBFile(cwd, filename) {
|
|
167
|
+
const filePath = path.join(getKBDir(cwd), filename);
|
|
168
|
+
if (!fs.existsSync(filePath))
|
|
169
|
+
return [];
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ============================================================
|
|
178
|
+
// Database Operations
|
|
179
|
+
// ============================================================
|
|
180
|
+
/**
|
|
181
|
+
* Get shared KB entries from semo.knowledge_base
|
|
182
|
+
*/
|
|
183
|
+
async function kbPull(pool, botId, domain, cwd) {
|
|
184
|
+
const client = await pool.connect();
|
|
185
|
+
try {
|
|
186
|
+
// Shared KB
|
|
187
|
+
let sharedQuery = `
|
|
188
|
+
SELECT domain, key, content, metadata, created_by, version,
|
|
189
|
+
created_at::text, updated_at::text
|
|
190
|
+
FROM semo.knowledge_base
|
|
191
|
+
`;
|
|
192
|
+
const sharedParams = [];
|
|
193
|
+
if (domain) {
|
|
194
|
+
sharedQuery += " WHERE domain = $1";
|
|
195
|
+
sharedParams.push(domain);
|
|
196
|
+
}
|
|
197
|
+
sharedQuery += " ORDER BY domain, key";
|
|
198
|
+
const sharedResult = await client.query(sharedQuery, sharedParams);
|
|
199
|
+
const shared = sharedResult.rows;
|
|
200
|
+
// Bot-specific KB
|
|
201
|
+
let botQuery = `
|
|
202
|
+
SELECT bot_id, domain, key, content, metadata, version,
|
|
203
|
+
synced_at::text, created_at::text, updated_at::text
|
|
204
|
+
FROM semo.bot_knowledge
|
|
205
|
+
WHERE bot_id = $1
|
|
206
|
+
`;
|
|
207
|
+
const botParams = [botId];
|
|
208
|
+
if (domain) {
|
|
209
|
+
botQuery += " AND domain = $2";
|
|
210
|
+
botParams.push(domain);
|
|
211
|
+
}
|
|
212
|
+
botQuery += " ORDER BY domain, key";
|
|
213
|
+
const botResult = await client.query(botQuery, botParams);
|
|
214
|
+
const bot = botResult.rows;
|
|
215
|
+
// Write to local files if cwd provided
|
|
216
|
+
if (cwd) {
|
|
217
|
+
writeKBFile(cwd, "team.json", shared);
|
|
218
|
+
writeKBFile(cwd, "bot.json", bot);
|
|
219
|
+
const state = readSyncState(cwd);
|
|
220
|
+
state.botId = botId;
|
|
221
|
+
state.lastPull = new Date().toISOString();
|
|
222
|
+
state.sharedCount = shared.length;
|
|
223
|
+
state.botCount = bot.length;
|
|
224
|
+
writeSyncState(cwd, state);
|
|
225
|
+
}
|
|
226
|
+
return { shared, bot };
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
client.release();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Push local KB entries to database
|
|
234
|
+
*/
|
|
235
|
+
async function kbPush(pool, botId, entries, target = "bot", cwd) {
|
|
236
|
+
const client = await pool.connect();
|
|
237
|
+
let upserted = 0;
|
|
238
|
+
const errors = [];
|
|
239
|
+
try {
|
|
240
|
+
await client.query("BEGIN");
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
try {
|
|
243
|
+
// Generate embedding for content
|
|
244
|
+
const embedding = await generateEmbedding(`${entry.key}: ${entry.content}`);
|
|
245
|
+
const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
|
|
246
|
+
if (target === "shared") {
|
|
247
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
|
|
248
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector)
|
|
249
|
+
ON CONFLICT (domain, key) DO UPDATE SET
|
|
250
|
+
content = EXCLUDED.content,
|
|
251
|
+
metadata = EXCLUDED.metadata,
|
|
252
|
+
embedding = EXCLUDED.embedding`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || botId, embeddingStr]);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
await client.query(`INSERT INTO semo.bot_knowledge (bot_id, domain, key, content, metadata, synced_at, embedding)
|
|
256
|
+
VALUES ($1, $2, $3, $4, $5, NOW(), $6::vector)
|
|
257
|
+
ON CONFLICT (bot_id, domain, key) DO UPDATE SET
|
|
258
|
+
content = EXCLUDED.content,
|
|
259
|
+
metadata = EXCLUDED.metadata,
|
|
260
|
+
synced_at = NOW(),
|
|
261
|
+
embedding = EXCLUDED.embedding`, [botId, entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), embeddingStr]);
|
|
262
|
+
}
|
|
263
|
+
upserted++;
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
errors.push(`${entry.domain}/${entry.key}: ${err}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
await client.query("COMMIT");
|
|
270
|
+
if (cwd) {
|
|
271
|
+
const state = readSyncState(cwd);
|
|
272
|
+
state.botId = botId;
|
|
273
|
+
state.lastPush = new Date().toISOString();
|
|
274
|
+
writeSyncState(cwd, state);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
await client.query("ROLLBACK");
|
|
279
|
+
errors.push(`Transaction failed: ${err}`);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
client.release();
|
|
283
|
+
}
|
|
284
|
+
return { upserted, errors };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get KB status for a bot
|
|
288
|
+
*/
|
|
289
|
+
async function kbStatus(pool, botId) {
|
|
290
|
+
const client = await pool.connect();
|
|
291
|
+
try {
|
|
292
|
+
// Shared KB stats
|
|
293
|
+
const sharedStats = await client.query(`
|
|
294
|
+
SELECT domain, COUNT(*)::int as count
|
|
295
|
+
FROM semo.knowledge_base
|
|
296
|
+
GROUP BY domain ORDER BY domain
|
|
297
|
+
`);
|
|
298
|
+
const sharedTotal = await client.query(`SELECT COUNT(*)::int as total FROM semo.knowledge_base`);
|
|
299
|
+
const sharedLastUpdated = await client.query(`SELECT MAX(updated_at)::text as last FROM semo.knowledge_base`);
|
|
300
|
+
const sharedDomains = {};
|
|
301
|
+
for (const row of sharedStats.rows) {
|
|
302
|
+
sharedDomains[row.domain] = row.count;
|
|
303
|
+
}
|
|
304
|
+
// Bot KB stats
|
|
305
|
+
const botStats = await client.query(`
|
|
306
|
+
SELECT domain, COUNT(*)::int as count
|
|
307
|
+
FROM semo.bot_knowledge WHERE bot_id = $1
|
|
308
|
+
GROUP BY domain ORDER BY domain
|
|
309
|
+
`, [botId]);
|
|
310
|
+
const botTotal = await client.query(`SELECT COUNT(*)::int as total FROM semo.bot_knowledge WHERE bot_id = $1`, [botId]);
|
|
311
|
+
const botLastUpdated = await client.query(`SELECT MAX(updated_at)::text as last FROM semo.bot_knowledge WHERE bot_id = $1`, [botId]);
|
|
312
|
+
const botLastSynced = await client.query(`SELECT MAX(synced_at)::text as last FROM semo.bot_knowledge WHERE bot_id = $1`, [botId]);
|
|
313
|
+
const botDomains = {};
|
|
314
|
+
for (const row of botStats.rows) {
|
|
315
|
+
botDomains[row.domain] = row.count;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
shared: {
|
|
319
|
+
total: sharedTotal.rows[0]?.total || 0,
|
|
320
|
+
domains: sharedDomains,
|
|
321
|
+
lastUpdated: sharedLastUpdated.rows[0]?.last || null,
|
|
322
|
+
},
|
|
323
|
+
bot: {
|
|
324
|
+
total: botTotal.rows[0]?.total || 0,
|
|
325
|
+
domains: botDomains,
|
|
326
|
+
lastUpdated: botLastUpdated.rows[0]?.last || null,
|
|
327
|
+
lastSynced: botLastSynced.rows[0]?.last || null,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
client.release();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* List KB entries with optional filters
|
|
337
|
+
*/
|
|
338
|
+
async function kbList(pool, options) {
|
|
339
|
+
const client = await pool.connect();
|
|
340
|
+
const limit = options.limit || 50;
|
|
341
|
+
const offset = options.offset || 0;
|
|
342
|
+
try {
|
|
343
|
+
// Shared
|
|
344
|
+
let sharedQuery = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
|
|
345
|
+
const sharedParams = [];
|
|
346
|
+
let paramIdx = 1;
|
|
347
|
+
if (options.domain) {
|
|
348
|
+
sharedQuery += ` WHERE domain = $${paramIdx++}`;
|
|
349
|
+
sharedParams.push(options.domain);
|
|
350
|
+
}
|
|
351
|
+
sharedQuery += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
|
|
352
|
+
sharedParams.push(limit, offset);
|
|
353
|
+
const sharedResult = await client.query(sharedQuery, sharedParams);
|
|
354
|
+
// Bot
|
|
355
|
+
let bot = [];
|
|
356
|
+
if (options.botId) {
|
|
357
|
+
let botQuery = "SELECT bot_id, domain, key, content, metadata, version, synced_at::text, updated_at::text FROM semo.bot_knowledge WHERE bot_id = $1";
|
|
358
|
+
const botParams = [options.botId];
|
|
359
|
+
let bParamIdx = 2;
|
|
360
|
+
if (options.domain) {
|
|
361
|
+
botQuery += ` AND domain = $${bParamIdx++}`;
|
|
362
|
+
botParams.push(options.domain);
|
|
363
|
+
}
|
|
364
|
+
botQuery += ` ORDER BY domain, key LIMIT $${bParamIdx++} OFFSET $${bParamIdx++}`;
|
|
365
|
+
botParams.push(limit, offset);
|
|
366
|
+
const botResult = await client.query(botQuery, botParams);
|
|
367
|
+
bot = botResult.rows;
|
|
368
|
+
}
|
|
369
|
+
return { shared: sharedResult.rows, bot };
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
client.release();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Diff local KB files against database
|
|
377
|
+
*/
|
|
378
|
+
async function kbDiff(pool, botId, cwd) {
|
|
379
|
+
const localShared = readKBFile(cwd, "team.json");
|
|
380
|
+
const localBot = readKBFile(cwd, "bot.json");
|
|
381
|
+
const localAll = [...localShared, ...localBot];
|
|
382
|
+
const { shared: remoteShared, bot: remoteBot } = await kbPull(pool, botId);
|
|
383
|
+
const remoteAll = [...remoteShared, ...remoteBot];
|
|
384
|
+
const localMap = new Map(localAll.map(e => [`${e.domain}/${e.key}`, e]));
|
|
385
|
+
const remoteMap = new Map(remoteAll.map(e => [`${e.domain}/${e.key}`, e]));
|
|
386
|
+
const added = [];
|
|
387
|
+
const removed = [];
|
|
388
|
+
const modified = [];
|
|
389
|
+
let unchanged = 0;
|
|
390
|
+
// Remote entries not in local → added
|
|
391
|
+
for (const [k, v] of remoteMap) {
|
|
392
|
+
if (!localMap.has(k)) {
|
|
393
|
+
added.push(v);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Local entries not in remote → removed
|
|
397
|
+
for (const [k, v] of localMap) {
|
|
398
|
+
if (!remoteMap.has(k)) {
|
|
399
|
+
removed.push(v);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Both exist → check content
|
|
403
|
+
for (const [k, local] of localMap) {
|
|
404
|
+
const remote = remoteMap.get(k);
|
|
405
|
+
if (remote) {
|
|
406
|
+
if (local.content !== remote.content || JSON.stringify(local.metadata) !== JSON.stringify(remote.metadata)) {
|
|
407
|
+
modified.push({ local, remote });
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
unchanged++;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { added, removed, modified, unchanged };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Search KB — hybrid: vector similarity (if embedding available) + text fallback
|
|
418
|
+
*/
|
|
419
|
+
async function kbSearch(pool, query, options) {
|
|
420
|
+
const client = await pool.connect();
|
|
421
|
+
const limit = options.limit || 10;
|
|
422
|
+
const mode = options.mode || "hybrid";
|
|
423
|
+
try {
|
|
424
|
+
let results = [];
|
|
425
|
+
// Try semantic search first (if mode allows and embedding API available)
|
|
426
|
+
if (mode !== "text") {
|
|
427
|
+
const queryEmbedding = await generateEmbedding(query);
|
|
428
|
+
if (queryEmbedding) {
|
|
429
|
+
const embeddingStr = `[${queryEmbedding.join(",")}]`;
|
|
430
|
+
// Vector search on shared KB
|
|
431
|
+
let sql = `
|
|
432
|
+
SELECT domain, key, content, metadata, created_by, version, updated_at::text,
|
|
433
|
+
1 - (embedding <=> $1::vector) as score
|
|
434
|
+
FROM semo.knowledge_base
|
|
435
|
+
WHERE embedding IS NOT NULL
|
|
436
|
+
`;
|
|
437
|
+
const params = [embeddingStr];
|
|
438
|
+
let paramIdx = 2;
|
|
439
|
+
if (options.domain) {
|
|
440
|
+
sql += ` AND domain = $${paramIdx++}`;
|
|
441
|
+
params.push(options.domain);
|
|
442
|
+
}
|
|
443
|
+
sql += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIdx++}`;
|
|
444
|
+
params.push(limit);
|
|
445
|
+
const sharedResult = await client.query(sql, params);
|
|
446
|
+
results = sharedResult.rows;
|
|
447
|
+
// Also search bot_knowledge if botId specified
|
|
448
|
+
if (options.botId) {
|
|
449
|
+
let botSql = `
|
|
450
|
+
SELECT bot_id, domain, key, content, metadata, version, updated_at::text,
|
|
451
|
+
1 - (embedding <=> $1::vector) as score
|
|
452
|
+
FROM semo.bot_knowledge
|
|
453
|
+
WHERE bot_id = $2 AND embedding IS NOT NULL
|
|
454
|
+
`;
|
|
455
|
+
const botParams = [embeddingStr, options.botId];
|
|
456
|
+
let bIdx = 3;
|
|
457
|
+
if (options.domain) {
|
|
458
|
+
botSql += ` AND domain = $${bIdx++}`;
|
|
459
|
+
botParams.push(options.domain);
|
|
460
|
+
}
|
|
461
|
+
botSql += ` ORDER BY embedding <=> $1::vector LIMIT $${bIdx++}`;
|
|
462
|
+
botParams.push(limit);
|
|
463
|
+
const botResult = await client.query(botSql, botParams);
|
|
464
|
+
results = [...results, ...botResult.rows]
|
|
465
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
466
|
+
.slice(0, limit);
|
|
467
|
+
}
|
|
468
|
+
// If we got results from semantic search and mode is not hybrid, return
|
|
469
|
+
if (results.length > 0 && mode === "semantic") {
|
|
470
|
+
return results;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Text search (fallback or hybrid supplement)
|
|
475
|
+
if (mode !== "semantic" || results.length === 0) {
|
|
476
|
+
let textSql = `
|
|
477
|
+
SELECT domain, key, content, metadata, created_by, version, updated_at::text,
|
|
478
|
+
0.0 as score
|
|
479
|
+
FROM semo.knowledge_base
|
|
480
|
+
WHERE content ILIKE $1 OR key ILIKE $1
|
|
481
|
+
`;
|
|
482
|
+
const textParams = [`%${query}%`];
|
|
483
|
+
let tIdx = 2;
|
|
484
|
+
if (options.domain) {
|
|
485
|
+
textSql += ` AND domain = $${tIdx++}`;
|
|
486
|
+
textParams.push(options.domain);
|
|
487
|
+
}
|
|
488
|
+
textSql += ` ORDER BY updated_at DESC LIMIT $${tIdx++}`;
|
|
489
|
+
textParams.push(limit);
|
|
490
|
+
const textResult = await client.query(textSql, textParams);
|
|
491
|
+
// Merge: deduplicate by domain/key, prefer semantic results
|
|
492
|
+
const seen = new Set(results.map((r) => `${r.domain}/${r.key}`));
|
|
493
|
+
for (const row of textResult.rows) {
|
|
494
|
+
const k = `${row.domain}/${row.key}`;
|
|
495
|
+
if (!seen.has(k)) {
|
|
496
|
+
results.push(row);
|
|
497
|
+
seen.add(k);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return results.slice(0, limit);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Ultimate fallback: simple ILIKE
|
|
505
|
+
let sql = `
|
|
506
|
+
SELECT domain, key, content, metadata, created_by, version, updated_at::text
|
|
507
|
+
FROM semo.knowledge_base
|
|
508
|
+
WHERE content ILIKE $1 OR key ILIKE $1
|
|
509
|
+
`;
|
|
510
|
+
const params = [`%${query}%`];
|
|
511
|
+
let paramIdx = 2;
|
|
512
|
+
if (options.domain) {
|
|
513
|
+
sql += ` AND domain = $${paramIdx++}`;
|
|
514
|
+
params.push(options.domain);
|
|
515
|
+
}
|
|
516
|
+
sql += ` ORDER BY updated_at DESC LIMIT $${paramIdx++}`;
|
|
517
|
+
params.push(limit);
|
|
518
|
+
const result = await client.query(sql, params);
|
|
519
|
+
return result.rows;
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
client.release();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// ============================================================
|
|
526
|
+
// Ontology Operations
|
|
527
|
+
// ============================================================
|
|
528
|
+
/**
|
|
529
|
+
* List all ontology domains
|
|
530
|
+
*/
|
|
531
|
+
async function ontoList(pool) {
|
|
532
|
+
const client = await pool.connect();
|
|
533
|
+
try {
|
|
534
|
+
const result = await client.query(`
|
|
535
|
+
SELECT domain, schema, description, version, updated_at::text
|
|
536
|
+
FROM semo.ontology ORDER BY domain
|
|
537
|
+
`);
|
|
538
|
+
return result.rows;
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
client.release();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Show ontology detail for a domain
|
|
546
|
+
*/
|
|
547
|
+
async function ontoShow(pool, domain) {
|
|
548
|
+
const client = await pool.connect();
|
|
549
|
+
try {
|
|
550
|
+
const result = await client.query(`SELECT domain, schema, description, version, updated_at::text FROM semo.ontology WHERE domain = $1`, [domain]);
|
|
551
|
+
return result.rows[0] || null;
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
client.release();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Validate KB entries against ontology schema (basic JSON Schema validation)
|
|
559
|
+
*/
|
|
560
|
+
async function ontoValidate(pool, domain, entries) {
|
|
561
|
+
const onto = await ontoShow(pool, domain);
|
|
562
|
+
if (!onto) {
|
|
563
|
+
return { valid: 0, invalid: [{ key: "*", errors: [`Ontology domain '${domain}' not found`] }] };
|
|
564
|
+
}
|
|
565
|
+
// If no entries provided, fetch from DB
|
|
566
|
+
if (!entries) {
|
|
567
|
+
const client = await pool.connect();
|
|
568
|
+
try {
|
|
569
|
+
const result = await client.query(`SELECT domain, key, content, metadata FROM semo.knowledge_base WHERE domain = $1`, [domain]);
|
|
570
|
+
entries = result.rows;
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
client.release();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const schema = onto.schema;
|
|
577
|
+
const required = schema.required || [];
|
|
578
|
+
let valid = 0;
|
|
579
|
+
const invalid = [];
|
|
580
|
+
for (const entry of entries) {
|
|
581
|
+
const errors = [];
|
|
582
|
+
// Check required fields
|
|
583
|
+
for (const field of required) {
|
|
584
|
+
if (!entry[field] && entry[field] !== "") {
|
|
585
|
+
errors.push(`Missing required field: ${field}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Check metadata schema if defined
|
|
589
|
+
if (schema.properties?.metadata?.properties && entry.metadata) {
|
|
590
|
+
const metaSchema = schema.properties.metadata.properties;
|
|
591
|
+
for (const [propKey, propDef] of Object.entries(metaSchema)) {
|
|
592
|
+
const def = propDef;
|
|
593
|
+
const val = entry.metadata[propKey];
|
|
594
|
+
if (val !== undefined) {
|
|
595
|
+
if (def.enum && !def.enum.includes(val)) {
|
|
596
|
+
errors.push(`metadata.${propKey}: '${val}' not in allowed values [${def.enum.join(", ")}]`);
|
|
597
|
+
}
|
|
598
|
+
if (def.type === "string" && typeof val !== "string") {
|
|
599
|
+
errors.push(`metadata.${propKey}: expected string, got ${typeof val}`);
|
|
600
|
+
}
|
|
601
|
+
if (def.type === "array" && !Array.isArray(val)) {
|
|
602
|
+
errors.push(`metadata.${propKey}: expected array, got ${typeof val}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (errors.length > 0) {
|
|
608
|
+
invalid.push({ key: entry.key, errors });
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
valid++;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return { valid, invalid };
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Write ontology schemas to local cache
|
|
618
|
+
*/
|
|
619
|
+
async function ontoPullToLocal(pool, cwd) {
|
|
620
|
+
const domains = await ontoList(pool);
|
|
621
|
+
const kbDir = ensureKBDir(cwd);
|
|
622
|
+
const ontoDir = path.join(kbDir, "ontology");
|
|
623
|
+
for (const d of domains) {
|
|
624
|
+
fs.writeFileSync(path.join(ontoDir, `${d.domain}.json`), JSON.stringify(d, null, 2));
|
|
625
|
+
}
|
|
626
|
+
return domains.length;
|
|
627
|
+
}
|