@team-semicolon/semo-cli 4.1.5 → 4.2.0
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/README.md +3 -4
- package/dist/commands/audit.d.ts +27 -0
- package/dist/commands/audit.js +338 -0
- package/dist/commands/bots.js +524 -24
- package/dist/commands/context.d.ts +14 -3
- package/dist/commands/context.js +192 -113
- package/dist/commands/db.d.ts +9 -0
- package/dist/commands/db.js +189 -0
- package/dist/commands/get.d.ts +1 -2
- package/dist/commands/get.js +24 -116
- package/dist/commands/sessions.d.ts +2 -1
- package/dist/commands/sessions.js +31 -62
- package/dist/commands/skill-sync.d.ts +28 -0
- package/dist/commands/skill-sync.js +111 -0
- package/dist/commands/skill-sync.test.d.ts +16 -0
- package/dist/commands/skill-sync.test.js +186 -0
- package/dist/database.d.ts +41 -3
- package/dist/database.js +128 -554
- package/dist/env-parser.d.ts +5 -0
- package/dist/env-parser.js +27 -0
- package/dist/global-cache.d.ts +12 -0
- package/dist/global-cache.js +184 -0
- package/dist/index.js +352 -817
- package/dist/kb.d.ts +24 -39
- package/dist/kb.js +121 -175
- package/package.json +1 -1
package/dist/kb.d.ts
CHANGED
|
@@ -27,10 +27,6 @@ export interface KBEntry {
|
|
|
27
27
|
created_at?: string;
|
|
28
28
|
updated_at?: string;
|
|
29
29
|
}
|
|
30
|
-
export interface BotKBEntry extends KBEntry {
|
|
31
|
-
bot_id: string;
|
|
32
|
-
synced_at?: string;
|
|
33
|
-
}
|
|
34
30
|
export interface OntologyDomain {
|
|
35
31
|
domain: string;
|
|
36
32
|
schema: Record<string, unknown>;
|
|
@@ -44,69 +40,53 @@ export interface KBStatusInfo {
|
|
|
44
40
|
domains: Record<string, number>;
|
|
45
41
|
lastUpdated: string | null;
|
|
46
42
|
};
|
|
47
|
-
bot: {
|
|
48
|
-
total: number;
|
|
49
|
-
domains: Record<string, number>;
|
|
50
|
-
lastUpdated: string | null;
|
|
51
|
-
lastSynced: string | null;
|
|
52
|
-
};
|
|
53
43
|
}
|
|
54
|
-
export interface
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
export interface KBDigestEntry {
|
|
45
|
+
domain: string;
|
|
46
|
+
key: string;
|
|
47
|
+
content: string;
|
|
48
|
+
version: number;
|
|
49
|
+
change_type: 'new' | 'updated';
|
|
50
|
+
updated_at: string;
|
|
51
|
+
}
|
|
52
|
+
export interface KBDigestResult {
|
|
53
|
+
changes: KBDigestEntry[];
|
|
54
|
+
since: string;
|
|
55
|
+
generatedAt: string;
|
|
62
56
|
}
|
|
63
57
|
export interface SyncState {
|
|
64
|
-
botId: string;
|
|
65
58
|
lastPull: string | null;
|
|
66
59
|
lastPush: string | null;
|
|
67
60
|
sharedCount: number;
|
|
68
|
-
botCount: number;
|
|
69
61
|
}
|
|
70
62
|
/**
|
|
71
|
-
*
|
|
63
|
+
* Pull KB entries from semo.knowledge_base to local .kb/
|
|
72
64
|
*/
|
|
73
|
-
export declare function kbPull(pool: Pool,
|
|
74
|
-
shared: KBEntry[];
|
|
75
|
-
bot: BotKBEntry[];
|
|
76
|
-
}>;
|
|
65
|
+
export declare function kbPull(pool: Pool, domain?: string, cwd?: string): Promise<KBEntry[]>;
|
|
77
66
|
/**
|
|
78
|
-
* Push local KB entries to database
|
|
67
|
+
* Push local KB entries to database (knowledge_base only)
|
|
79
68
|
*/
|
|
80
|
-
export declare function kbPush(pool: Pool,
|
|
69
|
+
export declare function kbPush(pool: Pool, entries: KBEntry[], createdBy?: string, cwd?: string): Promise<{
|
|
81
70
|
upserted: number;
|
|
82
71
|
errors: string[];
|
|
83
72
|
}>;
|
|
84
73
|
/**
|
|
85
|
-
* Get KB status
|
|
74
|
+
* Get KB status
|
|
86
75
|
*/
|
|
87
|
-
export declare function kbStatus(pool: Pool
|
|
76
|
+
export declare function kbStatus(pool: Pool): Promise<KBStatusInfo>;
|
|
88
77
|
/**
|
|
89
78
|
* List KB entries with optional filters
|
|
90
79
|
*/
|
|
91
80
|
export declare function kbList(pool: Pool, options: {
|
|
92
81
|
domain?: string;
|
|
93
|
-
botId?: string;
|
|
94
82
|
limit?: number;
|
|
95
83
|
offset?: number;
|
|
96
|
-
}): Promise<
|
|
97
|
-
shared: KBEntry[];
|
|
98
|
-
bot: BotKBEntry[];
|
|
99
|
-
}>;
|
|
100
|
-
/**
|
|
101
|
-
* Diff local KB files against database
|
|
102
|
-
*/
|
|
103
|
-
export declare function kbDiff(pool: Pool, botId: string, cwd: string): Promise<KBDiffResult>;
|
|
84
|
+
}): Promise<KBEntry[]>;
|
|
104
85
|
/**
|
|
105
86
|
* Search KB — hybrid: vector similarity (if embedding available) + text fallback
|
|
106
87
|
*/
|
|
107
88
|
export declare function kbSearch(pool: Pool, query: string, options: {
|
|
108
89
|
domain?: string;
|
|
109
|
-
botId?: string;
|
|
110
90
|
limit?: number;
|
|
111
91
|
mode?: "semantic" | "text" | "hybrid";
|
|
112
92
|
}): Promise<KBEntry[]>;
|
|
@@ -128,6 +108,11 @@ export declare function ontoValidate(pool: Pool, domain: string, entries?: KBEnt
|
|
|
128
108
|
errors: string[];
|
|
129
109
|
}>;
|
|
130
110
|
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Generate KB change digest based on a since timestamp.
|
|
113
|
+
* Returns all KB changes since the given ISO timestamp.
|
|
114
|
+
*/
|
|
115
|
+
export declare function kbDigest(pool: Pool, since: string, domain?: string): Promise<KBDigestResult>;
|
|
131
116
|
/**
|
|
132
117
|
* Write ontology schemas to local cache
|
|
133
118
|
*/
|
package/dist/kb.js
CHANGED
|
@@ -48,11 +48,11 @@ exports.kbPull = kbPull;
|
|
|
48
48
|
exports.kbPush = kbPush;
|
|
49
49
|
exports.kbStatus = kbStatus;
|
|
50
50
|
exports.kbList = kbList;
|
|
51
|
-
exports.kbDiff = kbDiff;
|
|
52
51
|
exports.kbSearch = kbSearch;
|
|
53
52
|
exports.ontoList = ontoList;
|
|
54
53
|
exports.ontoShow = ontoShow;
|
|
55
54
|
exports.ontoValidate = ontoValidate;
|
|
55
|
+
exports.kbDigest = kbDigest;
|
|
56
56
|
exports.ontoPullToLocal = ontoPullToLocal;
|
|
57
57
|
const fs = __importStar(require("fs"));
|
|
58
58
|
const path = __importStar(require("path"));
|
|
@@ -153,7 +153,7 @@ function readSyncState(cwd) {
|
|
|
153
153
|
// corrupted file
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
return {
|
|
156
|
+
return { lastPull: null, lastPush: null, sharedCount: 0 };
|
|
157
157
|
}
|
|
158
158
|
function writeSyncState(cwd, state) {
|
|
159
159
|
const kbDir = ensureKBDir(cwd);
|
|
@@ -178,88 +178,75 @@ function readKBFile(cwd, filename) {
|
|
|
178
178
|
// Database Operations
|
|
179
179
|
// ============================================================
|
|
180
180
|
/**
|
|
181
|
-
*
|
|
181
|
+
* Pull KB entries from semo.knowledge_base to local .kb/
|
|
182
182
|
*/
|
|
183
|
-
async function kbPull(pool,
|
|
183
|
+
async function kbPull(pool, domain, cwd) {
|
|
184
184
|
const client = await pool.connect();
|
|
185
185
|
try {
|
|
186
|
-
|
|
187
|
-
let sharedQuery = `
|
|
186
|
+
let query = `
|
|
188
187
|
SELECT domain, key, content, metadata, created_by, version,
|
|
189
188
|
created_at::text, updated_at::text
|
|
190
189
|
FROM semo.knowledge_base
|
|
191
190
|
`;
|
|
192
|
-
const
|
|
191
|
+
const params = [];
|
|
193
192
|
if (domain) {
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
query += " WHERE domain = $1";
|
|
194
|
+
params.push(domain);
|
|
196
195
|
}
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
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
|
|
196
|
+
query += " ORDER BY domain, key";
|
|
197
|
+
const result = await client.query(query, params);
|
|
198
|
+
const entries = result.rows;
|
|
216
199
|
if (cwd) {
|
|
217
|
-
writeKBFile(cwd, "team.json",
|
|
218
|
-
writeKBFile(cwd, "bot.json", bot);
|
|
200
|
+
writeKBFile(cwd, "team.json", entries);
|
|
219
201
|
const state = readSyncState(cwd);
|
|
220
|
-
state.botId = botId;
|
|
221
202
|
state.lastPull = new Date().toISOString();
|
|
222
|
-
state.sharedCount =
|
|
223
|
-
state.botCount = bot.length;
|
|
203
|
+
state.sharedCount = entries.length;
|
|
224
204
|
writeSyncState(cwd, state);
|
|
225
205
|
}
|
|
226
|
-
return
|
|
206
|
+
return entries;
|
|
227
207
|
}
|
|
228
208
|
finally {
|
|
229
209
|
client.release();
|
|
230
210
|
}
|
|
231
211
|
}
|
|
232
212
|
/**
|
|
233
|
-
* Push local KB entries to database
|
|
213
|
+
* Push local KB entries to database (knowledge_base only)
|
|
234
214
|
*/
|
|
235
|
-
async function kbPush(pool,
|
|
215
|
+
async function kbPush(pool, entries, createdBy, cwd) {
|
|
236
216
|
const client = await pool.connect();
|
|
237
217
|
let upserted = 0;
|
|
238
218
|
const errors = [];
|
|
239
219
|
try {
|
|
240
|
-
|
|
220
|
+
// Domain validation: check all domains against ontology before transaction
|
|
221
|
+
const ontologyResult = await client.query("SELECT domain FROM semo.ontology");
|
|
222
|
+
const knownDomains = new Set(ontologyResult.rows.map((r) => r.domain));
|
|
223
|
+
const invalidEntries = [];
|
|
224
|
+
const validEntries = [];
|
|
241
225
|
for (const entry of entries) {
|
|
226
|
+
if (knownDomains.has(entry.domain)) {
|
|
227
|
+
validEntries.push(entry);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
invalidEntries.push(`${entry.domain}/${entry.key}: 미등록 도메인 '${entry.domain}'`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (invalidEntries.length > 0) {
|
|
234
|
+
errors.push(...invalidEntries);
|
|
235
|
+
}
|
|
236
|
+
await client.query("BEGIN");
|
|
237
|
+
const texts = validEntries.map(e => `${e.key}: ${e.content}`);
|
|
238
|
+
const embeddings = await generateEmbeddings(texts);
|
|
239
|
+
for (let i = 0; i < validEntries.length; i++) {
|
|
240
|
+
const entry = validEntries[i];
|
|
242
241
|
try {
|
|
243
|
-
|
|
244
|
-
const embedding = await generateEmbedding(`${entry.key}: ${entry.content}`);
|
|
242
|
+
const embedding = embeddings[i];
|
|
245
243
|
const embeddingStr = embedding ? `[${embedding.join(",")}]` : null;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
}
|
|
244
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
|
|
245
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector)
|
|
246
|
+
ON CONFLICT (domain, key) DO UPDATE SET
|
|
247
|
+
content = EXCLUDED.content,
|
|
248
|
+
metadata = EXCLUDED.metadata,
|
|
249
|
+
embedding = EXCLUDED.embedding`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || createdBy || "unknown", embeddingStr]);
|
|
263
250
|
upserted++;
|
|
264
251
|
}
|
|
265
252
|
catch (err) {
|
|
@@ -269,7 +256,6 @@ async function kbPush(pool, botId, entries, target = "bot", cwd) {
|
|
|
269
256
|
await client.query("COMMIT");
|
|
270
257
|
if (cwd) {
|
|
271
258
|
const state = readSyncState(cwd);
|
|
272
|
-
state.botId = botId;
|
|
273
259
|
state.lastPush = new Date().toISOString();
|
|
274
260
|
writeSyncState(cwd, state);
|
|
275
261
|
}
|
|
@@ -284,12 +270,11 @@ async function kbPush(pool, botId, entries, target = "bot", cwd) {
|
|
|
284
270
|
return { upserted, errors };
|
|
285
271
|
}
|
|
286
272
|
/**
|
|
287
|
-
* Get KB status
|
|
273
|
+
* Get KB status
|
|
288
274
|
*/
|
|
289
|
-
async function kbStatus(pool
|
|
275
|
+
async function kbStatus(pool) {
|
|
290
276
|
const client = await pool.connect();
|
|
291
277
|
try {
|
|
292
|
-
// Shared KB stats
|
|
293
278
|
const sharedStats = await client.query(`
|
|
294
279
|
SELECT domain, COUNT(*)::int as count
|
|
295
280
|
FROM semo.knowledge_base
|
|
@@ -301,31 +286,12 @@ async function kbStatus(pool, botId) {
|
|
|
301
286
|
for (const row of sharedStats.rows) {
|
|
302
287
|
sharedDomains[row.domain] = row.count;
|
|
303
288
|
}
|
|
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
289
|
return {
|
|
318
290
|
shared: {
|
|
319
291
|
total: sharedTotal.rows[0]?.total || 0,
|
|
320
292
|
domains: sharedDomains,
|
|
321
293
|
lastUpdated: sharedLastUpdated.rows[0]?.last || null,
|
|
322
294
|
},
|
|
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
295
|
};
|
|
330
296
|
}
|
|
331
297
|
finally {
|
|
@@ -340,79 +306,22 @@ async function kbList(pool, options) {
|
|
|
340
306
|
const limit = options.limit || 50;
|
|
341
307
|
const offset = options.offset || 0;
|
|
342
308
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const sharedParams = [];
|
|
309
|
+
let query = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
|
|
310
|
+
const params = [];
|
|
346
311
|
let paramIdx = 1;
|
|
347
312
|
if (options.domain) {
|
|
348
|
-
|
|
349
|
-
|
|
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;
|
|
313
|
+
query += ` WHERE domain = $${paramIdx++}`;
|
|
314
|
+
params.push(options.domain);
|
|
368
315
|
}
|
|
369
|
-
|
|
316
|
+
query += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
|
|
317
|
+
params.push(limit, offset);
|
|
318
|
+
const result = await client.query(query, params);
|
|
319
|
+
return result.rows;
|
|
370
320
|
}
|
|
371
321
|
finally {
|
|
372
322
|
client.release();
|
|
373
323
|
}
|
|
374
324
|
}
|
|
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
325
|
/**
|
|
417
326
|
* Search KB — hybrid: vector similarity (if embedding available) + text fallback
|
|
418
327
|
*/
|
|
@@ -444,27 +353,6 @@ async function kbSearch(pool, query, options) {
|
|
|
444
353
|
params.push(limit);
|
|
445
354
|
const sharedResult = await client.query(sql, params);
|
|
446
355
|
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
356
|
// If we got results from semantic search and mode is not hybrid, return
|
|
469
357
|
if (results.length > 0 && mode === "semantic") {
|
|
470
358
|
return results;
|
|
@@ -472,31 +360,58 @@ async function kbSearch(pool, query, options) {
|
|
|
472
360
|
}
|
|
473
361
|
}
|
|
474
362
|
// Text search (fallback or hybrid supplement)
|
|
363
|
+
// Split query into tokens and match ANY token via ILIKE (Korean-friendly)
|
|
475
364
|
if (mode !== "semantic" || results.length === 0) {
|
|
365
|
+
const tokens = query.split(/\s+/).filter(t => t.length >= 2);
|
|
366
|
+
const textParams = [];
|
|
367
|
+
let tIdx = 1;
|
|
368
|
+
// Build per-token ILIKE conditions + count matching tokens for scoring
|
|
369
|
+
const tokenConditions = tokens.map(token => {
|
|
370
|
+
textParams.push(`%${token}%`);
|
|
371
|
+
return `(CASE WHEN content ILIKE $${tIdx} OR key ILIKE $${tIdx++} THEN 1 ELSE 0 END)`;
|
|
372
|
+
});
|
|
373
|
+
// Score = 0.7 base + 0.15 * (matched_tokens / total_tokens), capped at 0.95
|
|
374
|
+
const matchCountExpr = tokenConditions.length > 0
|
|
375
|
+
? tokenConditions.join(" + ")
|
|
376
|
+
: "0";
|
|
377
|
+
const scoreExpr = `LEAST(0.95, 0.7 + 0.15 * (${matchCountExpr})::float / ${Math.max(tokens.length, 1)})`;
|
|
378
|
+
// WHERE: any token matches
|
|
379
|
+
const whereTokens = tokens.map((_, i) => `(content ILIKE $${i + 1} OR key ILIKE $${i + 1})`);
|
|
380
|
+
const whereClause = whereTokens.length > 0
|
|
381
|
+
? whereTokens.join(" OR ")
|
|
382
|
+
: "FALSE";
|
|
476
383
|
let textSql = `
|
|
477
384
|
SELECT domain, key, content, metadata, created_by, version, updated_at::text,
|
|
478
|
-
|
|
385
|
+
${scoreExpr} as score
|
|
479
386
|
FROM semo.knowledge_base
|
|
480
|
-
WHERE
|
|
387
|
+
WHERE ${whereClause}
|
|
481
388
|
`;
|
|
482
|
-
const textParams = [`%${query}%`];
|
|
483
|
-
let tIdx = 2;
|
|
484
389
|
if (options.domain) {
|
|
485
390
|
textSql += ` AND domain = $${tIdx++}`;
|
|
486
391
|
textParams.push(options.domain);
|
|
487
392
|
}
|
|
488
|
-
textSql += ` ORDER BY updated_at DESC LIMIT $${tIdx++}`;
|
|
393
|
+
textSql += ` ORDER BY score DESC, updated_at DESC LIMIT $${tIdx++}`;
|
|
489
394
|
textParams.push(limit);
|
|
490
395
|
const textResult = await client.query(textSql, textParams);
|
|
491
|
-
// Merge:
|
|
492
|
-
|
|
396
|
+
// Merge: text matches get priority score (0.85) for exact keyword hits
|
|
397
|
+
// Deduplicate by domain/key; if already in semantic results, boost its score
|
|
398
|
+
const resultMap = new Map();
|
|
399
|
+
for (const r of results) {
|
|
400
|
+
resultMap.set(`${r.domain}/${r.key}`, r);
|
|
401
|
+
}
|
|
493
402
|
for (const row of textResult.rows) {
|
|
494
403
|
const k = `${row.domain}/${row.key}`;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
404
|
+
const existing = resultMap.get(k);
|
|
405
|
+
if (existing) {
|
|
406
|
+
// Boost: semantic match + text match = highest relevance
|
|
407
|
+
existing.score = Math.max(Number(existing.score), 0.85);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
resultMap.set(k, row);
|
|
498
411
|
}
|
|
499
412
|
}
|
|
413
|
+
// Sort by score descending
|
|
414
|
+
results = Array.from(resultMap.values()).sort((a, b) => Number(b.score) - Number(a.score));
|
|
500
415
|
}
|
|
501
416
|
return results.slice(0, limit);
|
|
502
417
|
}
|
|
@@ -613,6 +528,37 @@ async function ontoValidate(pool, domain, entries) {
|
|
|
613
528
|
}
|
|
614
529
|
return { valid, invalid };
|
|
615
530
|
}
|
|
531
|
+
// ============================================================
|
|
532
|
+
// KB Digest
|
|
533
|
+
// ============================================================
|
|
534
|
+
/**
|
|
535
|
+
* Generate KB change digest based on a since timestamp.
|
|
536
|
+
* Returns all KB changes since the given ISO timestamp.
|
|
537
|
+
*/
|
|
538
|
+
async function kbDigest(pool, since, domain) {
|
|
539
|
+
const client = await pool.connect();
|
|
540
|
+
const generatedAt = new Date().toISOString();
|
|
541
|
+
try {
|
|
542
|
+
let sql = `
|
|
543
|
+
SELECT domain, key, content, version, updated_at::text,
|
|
544
|
+
CASE WHEN created_at > $1 THEN 'new' ELSE 'updated' END as change_type
|
|
545
|
+
FROM semo.knowledge_base
|
|
546
|
+
WHERE updated_at > $1 OR created_at > $1
|
|
547
|
+
`;
|
|
548
|
+
const params = [since];
|
|
549
|
+
let paramIdx = 2;
|
|
550
|
+
if (domain) {
|
|
551
|
+
sql += ` AND domain = $${paramIdx++}`;
|
|
552
|
+
params.push(domain);
|
|
553
|
+
}
|
|
554
|
+
sql += ` ORDER BY updated_at DESC LIMIT 100`;
|
|
555
|
+
const result = await client.query(sql, params);
|
|
556
|
+
return { changes: result.rows, since, generatedAt };
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
client.release();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
616
562
|
/**
|
|
617
563
|
* Write ontology schemas to local cache
|
|
618
564
|
*/
|