@team-semicolon/semo-cli 4.1.5 → 4.3.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/memory.d.ts +8 -0
- package/dist/commands/memory.js +297 -0
- 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 +42 -3
- package/dist/database.js +129 -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 +197 -0
- package/dist/index.js +515 -821
- package/dist/kb.d.ts +40 -39
- package/dist/kb.js +185 -176
- package/package.json +1 -1
package/dist/kb.d.ts
CHANGED
|
@@ -27,86 +27,78 @@ 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>;
|
|
37
33
|
description: string | null;
|
|
38
34
|
version: number;
|
|
35
|
+
service?: string | null;
|
|
36
|
+
entity_type?: string | null;
|
|
37
|
+
parent?: string | null;
|
|
38
|
+
tags?: string[];
|
|
39
39
|
updated_at?: string;
|
|
40
40
|
}
|
|
41
|
+
export interface OntologyType {
|
|
42
|
+
type_key: string;
|
|
43
|
+
schema: Record<string, unknown>;
|
|
44
|
+
description: string | null;
|
|
45
|
+
version: number;
|
|
46
|
+
}
|
|
41
47
|
export interface KBStatusInfo {
|
|
42
48
|
shared: {
|
|
43
49
|
total: number;
|
|
44
50
|
domains: Record<string, number>;
|
|
45
51
|
lastUpdated: string | null;
|
|
46
52
|
};
|
|
47
|
-
bot: {
|
|
48
|
-
total: number;
|
|
49
|
-
domains: Record<string, number>;
|
|
50
|
-
lastUpdated: string | null;
|
|
51
|
-
lastSynced: string | null;
|
|
52
|
-
};
|
|
53
53
|
}
|
|
54
|
-
export interface
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
export interface KBDigestEntry {
|
|
55
|
+
domain: string;
|
|
56
|
+
key: string;
|
|
57
|
+
content: string;
|
|
58
|
+
version: number;
|
|
59
|
+
change_type: 'new' | 'updated';
|
|
60
|
+
updated_at: string;
|
|
61
|
+
}
|
|
62
|
+
export interface KBDigestResult {
|
|
63
|
+
changes: KBDigestEntry[];
|
|
64
|
+
since: string;
|
|
65
|
+
generatedAt: string;
|
|
62
66
|
}
|
|
63
67
|
export interface SyncState {
|
|
64
|
-
botId: string;
|
|
65
68
|
lastPull: string | null;
|
|
66
69
|
lastPush: string | null;
|
|
67
70
|
sharedCount: number;
|
|
68
|
-
botCount: number;
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
|
-
*
|
|
73
|
+
* Pull KB entries from semo.knowledge_base to local .kb/
|
|
72
74
|
*/
|
|
73
|
-
export declare function kbPull(pool: Pool,
|
|
74
|
-
shared: KBEntry[];
|
|
75
|
-
bot: BotKBEntry[];
|
|
76
|
-
}>;
|
|
75
|
+
export declare function kbPull(pool: Pool, domain?: string, cwd?: string): Promise<KBEntry[]>;
|
|
77
76
|
/**
|
|
78
|
-
* Push local KB entries to database
|
|
77
|
+
* Push local KB entries to database (knowledge_base only)
|
|
79
78
|
*/
|
|
80
|
-
export declare function kbPush(pool: Pool,
|
|
79
|
+
export declare function kbPush(pool: Pool, entries: KBEntry[], createdBy?: string, cwd?: string): Promise<{
|
|
81
80
|
upserted: number;
|
|
82
81
|
errors: string[];
|
|
83
82
|
}>;
|
|
84
83
|
/**
|
|
85
|
-
* Get KB status
|
|
84
|
+
* Get KB status
|
|
86
85
|
*/
|
|
87
|
-
export declare function kbStatus(pool: Pool
|
|
86
|
+
export declare function kbStatus(pool: Pool): Promise<KBStatusInfo>;
|
|
88
87
|
/**
|
|
89
88
|
* List KB entries with optional filters
|
|
90
89
|
*/
|
|
91
90
|
export declare function kbList(pool: Pool, options: {
|
|
92
91
|
domain?: string;
|
|
93
|
-
|
|
92
|
+
service?: string;
|
|
94
93
|
limit?: number;
|
|
95
94
|
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>;
|
|
95
|
+
}): Promise<KBEntry[]>;
|
|
104
96
|
/**
|
|
105
97
|
* Search KB — hybrid: vector similarity (if embedding available) + text fallback
|
|
106
98
|
*/
|
|
107
99
|
export declare function kbSearch(pool: Pool, query: string, options: {
|
|
108
100
|
domain?: string;
|
|
109
|
-
|
|
101
|
+
service?: string;
|
|
110
102
|
limit?: number;
|
|
111
103
|
mode?: "semantic" | "text" | "hybrid";
|
|
112
104
|
}): Promise<KBEntry[]>;
|
|
@@ -118,6 +110,10 @@ export declare function ontoList(pool: Pool): Promise<OntologyDomain[]>;
|
|
|
118
110
|
* Show ontology detail for a domain
|
|
119
111
|
*/
|
|
120
112
|
export declare function ontoShow(pool: Pool, domain: string): Promise<OntologyDomain | null>;
|
|
113
|
+
/**
|
|
114
|
+
* List all ontology types (structural templates)
|
|
115
|
+
*/
|
|
116
|
+
export declare function ontoListTypes(pool: Pool): Promise<OntologyType[]>;
|
|
121
117
|
/**
|
|
122
118
|
* Validate KB entries against ontology schema (basic JSON Schema validation)
|
|
123
119
|
*/
|
|
@@ -128,6 +124,11 @@ export declare function ontoValidate(pool: Pool, domain: string, entries?: KBEnt
|
|
|
128
124
|
errors: string[];
|
|
129
125
|
}>;
|
|
130
126
|
}>;
|
|
127
|
+
/**
|
|
128
|
+
* Generate KB change digest based on a since timestamp.
|
|
129
|
+
* Returns all KB changes since the given ISO timestamp.
|
|
130
|
+
*/
|
|
131
|
+
export declare function kbDigest(pool: Pool, since: string, domain?: string): Promise<KBDigestResult>;
|
|
131
132
|
/**
|
|
132
133
|
* Write ontology schemas to local cache
|
|
133
134
|
*/
|
package/dist/kb.js
CHANGED
|
@@ -48,11 +48,12 @@ 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;
|
|
54
|
+
exports.ontoListTypes = ontoListTypes;
|
|
55
55
|
exports.ontoValidate = ontoValidate;
|
|
56
|
+
exports.kbDigest = kbDigest;
|
|
56
57
|
exports.ontoPullToLocal = ontoPullToLocal;
|
|
57
58
|
const fs = __importStar(require("fs"));
|
|
58
59
|
const path = __importStar(require("path"));
|
|
@@ -153,7 +154,7 @@ function readSyncState(cwd) {
|
|
|
153
154
|
// corrupted file
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
|
-
return {
|
|
157
|
+
return { lastPull: null, lastPush: null, sharedCount: 0 };
|
|
157
158
|
}
|
|
158
159
|
function writeSyncState(cwd, state) {
|
|
159
160
|
const kbDir = ensureKBDir(cwd);
|
|
@@ -178,88 +179,75 @@ function readKBFile(cwd, filename) {
|
|
|
178
179
|
// Database Operations
|
|
179
180
|
// ============================================================
|
|
180
181
|
/**
|
|
181
|
-
*
|
|
182
|
+
* Pull KB entries from semo.knowledge_base to local .kb/
|
|
182
183
|
*/
|
|
183
|
-
async function kbPull(pool,
|
|
184
|
+
async function kbPull(pool, domain, cwd) {
|
|
184
185
|
const client = await pool.connect();
|
|
185
186
|
try {
|
|
186
|
-
|
|
187
|
-
let sharedQuery = `
|
|
187
|
+
let query = `
|
|
188
188
|
SELECT domain, key, content, metadata, created_by, version,
|
|
189
189
|
created_at::text, updated_at::text
|
|
190
190
|
FROM semo.knowledge_base
|
|
191
191
|
`;
|
|
192
|
-
const
|
|
192
|
+
const params = [];
|
|
193
193
|
if (domain) {
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
query += " WHERE domain = $1";
|
|
195
|
+
params.push(domain);
|
|
196
196
|
}
|
|
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
|
|
197
|
+
query += " ORDER BY domain, key";
|
|
198
|
+
const result = await client.query(query, params);
|
|
199
|
+
const entries = result.rows;
|
|
216
200
|
if (cwd) {
|
|
217
|
-
writeKBFile(cwd, "team.json",
|
|
218
|
-
writeKBFile(cwd, "bot.json", bot);
|
|
201
|
+
writeKBFile(cwd, "team.json", entries);
|
|
219
202
|
const state = readSyncState(cwd);
|
|
220
|
-
state.botId = botId;
|
|
221
203
|
state.lastPull = new Date().toISOString();
|
|
222
|
-
state.sharedCount =
|
|
223
|
-
state.botCount = bot.length;
|
|
204
|
+
state.sharedCount = entries.length;
|
|
224
205
|
writeSyncState(cwd, state);
|
|
225
206
|
}
|
|
226
|
-
return
|
|
207
|
+
return entries;
|
|
227
208
|
}
|
|
228
209
|
finally {
|
|
229
210
|
client.release();
|
|
230
211
|
}
|
|
231
212
|
}
|
|
232
213
|
/**
|
|
233
|
-
* Push local KB entries to database
|
|
214
|
+
* Push local KB entries to database (knowledge_base only)
|
|
234
215
|
*/
|
|
235
|
-
async function kbPush(pool,
|
|
216
|
+
async function kbPush(pool, entries, createdBy, cwd) {
|
|
236
217
|
const client = await pool.connect();
|
|
237
218
|
let upserted = 0;
|
|
238
219
|
const errors = [];
|
|
239
220
|
try {
|
|
240
|
-
|
|
221
|
+
// Domain validation: check all domains against ontology before transaction
|
|
222
|
+
const ontologyResult = await client.query("SELECT domain FROM semo.ontology");
|
|
223
|
+
const knownDomains = new Set(ontologyResult.rows.map((r) => r.domain));
|
|
224
|
+
const invalidEntries = [];
|
|
225
|
+
const validEntries = [];
|
|
241
226
|
for (const entry of entries) {
|
|
227
|
+
if (knownDomains.has(entry.domain)) {
|
|
228
|
+
validEntries.push(entry);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
invalidEntries.push(`${entry.domain}/${entry.key}: 미등록 도메인 '${entry.domain}'`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (invalidEntries.length > 0) {
|
|
235
|
+
errors.push(...invalidEntries);
|
|
236
|
+
}
|
|
237
|
+
await client.query("BEGIN");
|
|
238
|
+
const texts = validEntries.map(e => `${e.key}: ${e.content}`);
|
|
239
|
+
const embeddings = await generateEmbeddings(texts);
|
|
240
|
+
for (let i = 0; i < validEntries.length; i++) {
|
|
241
|
+
const entry = validEntries[i];
|
|
242
242
|
try {
|
|
243
|
-
|
|
244
|
-
const embedding = await generateEmbedding(`${entry.key}: ${entry.content}`);
|
|
243
|
+
const embedding = embeddings[i];
|
|
245
244
|
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
|
-
}
|
|
245
|
+
await client.query(`INSERT INTO semo.knowledge_base (domain, key, content, metadata, created_by, embedding)
|
|
246
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector)
|
|
247
|
+
ON CONFLICT (domain, key) DO UPDATE SET
|
|
248
|
+
content = EXCLUDED.content,
|
|
249
|
+
metadata = EXCLUDED.metadata,
|
|
250
|
+
embedding = EXCLUDED.embedding`, [entry.domain, entry.key, entry.content, JSON.stringify(entry.metadata || {}), entry.created_by || createdBy || "unknown", embeddingStr]);
|
|
263
251
|
upserted++;
|
|
264
252
|
}
|
|
265
253
|
catch (err) {
|
|
@@ -269,7 +257,6 @@ async function kbPush(pool, botId, entries, target = "bot", cwd) {
|
|
|
269
257
|
await client.query("COMMIT");
|
|
270
258
|
if (cwd) {
|
|
271
259
|
const state = readSyncState(cwd);
|
|
272
|
-
state.botId = botId;
|
|
273
260
|
state.lastPush = new Date().toISOString();
|
|
274
261
|
writeSyncState(cwd, state);
|
|
275
262
|
}
|
|
@@ -284,12 +271,11 @@ async function kbPush(pool, botId, entries, target = "bot", cwd) {
|
|
|
284
271
|
return { upserted, errors };
|
|
285
272
|
}
|
|
286
273
|
/**
|
|
287
|
-
* Get KB status
|
|
274
|
+
* Get KB status
|
|
288
275
|
*/
|
|
289
|
-
async function kbStatus(pool
|
|
276
|
+
async function kbStatus(pool) {
|
|
290
277
|
const client = await pool.connect();
|
|
291
278
|
try {
|
|
292
|
-
// Shared KB stats
|
|
293
279
|
const sharedStats = await client.query(`
|
|
294
280
|
SELECT domain, COUNT(*)::int as count
|
|
295
281
|
FROM semo.knowledge_base
|
|
@@ -301,31 +287,12 @@ async function kbStatus(pool, botId) {
|
|
|
301
287
|
for (const row of sharedStats.rows) {
|
|
302
288
|
sharedDomains[row.domain] = row.count;
|
|
303
289
|
}
|
|
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
290
|
return {
|
|
318
291
|
shared: {
|
|
319
292
|
total: sharedTotal.rows[0]?.total || 0,
|
|
320
293
|
domains: sharedDomains,
|
|
321
294
|
lastUpdated: sharedLastUpdated.rows[0]?.last || null,
|
|
322
295
|
},
|
|
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
296
|
};
|
|
330
297
|
}
|
|
331
298
|
finally {
|
|
@@ -340,79 +307,30 @@ async function kbList(pool, options) {
|
|
|
340
307
|
const limit = options.limit || 50;
|
|
341
308
|
const offset = options.offset || 0;
|
|
342
309
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const sharedParams = [];
|
|
310
|
+
let query = "SELECT domain, key, content, metadata, created_by, version, updated_at::text FROM semo.knowledge_base";
|
|
311
|
+
const params = [];
|
|
346
312
|
let paramIdx = 1;
|
|
347
313
|
if (options.domain) {
|
|
348
|
-
|
|
349
|
-
|
|
314
|
+
query += ` WHERE domain = $${paramIdx++}`;
|
|
315
|
+
params.push(options.domain);
|
|
350
316
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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);
|
|
317
|
+
else if (options.service) {
|
|
318
|
+
// Resolve service to domain list: service name itself + dot-notation domains
|
|
319
|
+
const serviceDomains = await resolveServiceDomainsLocal(client, options.service);
|
|
320
|
+
if (serviceDomains.length > 0) {
|
|
321
|
+
query += ` WHERE domain = ANY($${paramIdx++})`;
|
|
322
|
+
params.push(serviceDomains);
|
|
363
323
|
}
|
|
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
324
|
}
|
|
369
|
-
|
|
325
|
+
query += ` ORDER BY domain, key LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;
|
|
326
|
+
params.push(limit, offset);
|
|
327
|
+
const result = await client.query(query, params);
|
|
328
|
+
return result.rows;
|
|
370
329
|
}
|
|
371
330
|
finally {
|
|
372
331
|
client.release();
|
|
373
332
|
}
|
|
374
333
|
}
|
|
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
334
|
/**
|
|
417
335
|
* Search KB — hybrid: vector similarity (if embedding available) + text fallback
|
|
418
336
|
*/
|
|
@@ -420,6 +338,11 @@ async function kbSearch(pool, query, options) {
|
|
|
420
338
|
const client = await pool.connect();
|
|
421
339
|
const limit = options.limit || 10;
|
|
422
340
|
const mode = options.mode || "hybrid";
|
|
341
|
+
// Resolve service → domain list for filtering
|
|
342
|
+
let serviceDomains = null;
|
|
343
|
+
if (options.service && !options.domain) {
|
|
344
|
+
serviceDomains = await resolveServiceDomainsLocal(client, options.service);
|
|
345
|
+
}
|
|
423
346
|
try {
|
|
424
347
|
let results = [];
|
|
425
348
|
// Try semantic search first (if mode allows and embedding API available)
|
|
@@ -440,31 +363,14 @@ async function kbSearch(pool, query, options) {
|
|
|
440
363
|
sql += ` AND domain = $${paramIdx++}`;
|
|
441
364
|
params.push(options.domain);
|
|
442
365
|
}
|
|
366
|
+
else if (serviceDomains && serviceDomains.length > 0) {
|
|
367
|
+
sql += ` AND domain = ANY($${paramIdx++})`;
|
|
368
|
+
params.push(serviceDomains);
|
|
369
|
+
}
|
|
443
370
|
sql += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIdx++}`;
|
|
444
371
|
params.push(limit);
|
|
445
372
|
const sharedResult = await client.query(sql, params);
|
|
446
373
|
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
374
|
// If we got results from semantic search and mode is not hybrid, return
|
|
469
375
|
if (results.length > 0 && mode === "semantic") {
|
|
470
376
|
return results;
|
|
@@ -472,31 +378,62 @@ async function kbSearch(pool, query, options) {
|
|
|
472
378
|
}
|
|
473
379
|
}
|
|
474
380
|
// Text search (fallback or hybrid supplement)
|
|
381
|
+
// Split query into tokens and match ANY token via ILIKE (Korean-friendly)
|
|
475
382
|
if (mode !== "semantic" || results.length === 0) {
|
|
383
|
+
const tokens = query.split(/\s+/).filter(t => t.length >= 2);
|
|
384
|
+
const textParams = [];
|
|
385
|
+
let tIdx = 1;
|
|
386
|
+
// Build per-token ILIKE conditions + count matching tokens for scoring
|
|
387
|
+
const tokenConditions = tokens.map(token => {
|
|
388
|
+
textParams.push(`%${token}%`);
|
|
389
|
+
return `(CASE WHEN content ILIKE $${tIdx} OR key ILIKE $${tIdx++} THEN 1 ELSE 0 END)`;
|
|
390
|
+
});
|
|
391
|
+
// Score = 0.7 base + 0.15 * (matched_tokens / total_tokens), capped at 0.95
|
|
392
|
+
const matchCountExpr = tokenConditions.length > 0
|
|
393
|
+
? tokenConditions.join(" + ")
|
|
394
|
+
: "0";
|
|
395
|
+
const scoreExpr = `LEAST(0.95, 0.7 + 0.15 * (${matchCountExpr})::float / ${Math.max(tokens.length, 1)})`;
|
|
396
|
+
// WHERE: any token matches
|
|
397
|
+
const whereTokens = tokens.map((_, i) => `(content ILIKE $${i + 1} OR key ILIKE $${i + 1})`);
|
|
398
|
+
const whereClause = whereTokens.length > 0
|
|
399
|
+
? whereTokens.join(" OR ")
|
|
400
|
+
: "FALSE";
|
|
476
401
|
let textSql = `
|
|
477
402
|
SELECT domain, key, content, metadata, created_by, version, updated_at::text,
|
|
478
|
-
|
|
403
|
+
${scoreExpr} as score
|
|
479
404
|
FROM semo.knowledge_base
|
|
480
|
-
WHERE
|
|
405
|
+
WHERE ${whereClause}
|
|
481
406
|
`;
|
|
482
|
-
const textParams = [`%${query}%`];
|
|
483
|
-
let tIdx = 2;
|
|
484
407
|
if (options.domain) {
|
|
485
408
|
textSql += ` AND domain = $${tIdx++}`;
|
|
486
409
|
textParams.push(options.domain);
|
|
487
410
|
}
|
|
488
|
-
|
|
411
|
+
else if (serviceDomains && serviceDomains.length > 0) {
|
|
412
|
+
textSql += ` AND domain = ANY($${tIdx++})`;
|
|
413
|
+
textParams.push(serviceDomains);
|
|
414
|
+
}
|
|
415
|
+
textSql += ` ORDER BY score DESC, updated_at DESC LIMIT $${tIdx++}`;
|
|
489
416
|
textParams.push(limit);
|
|
490
417
|
const textResult = await client.query(textSql, textParams);
|
|
491
|
-
// Merge:
|
|
492
|
-
|
|
418
|
+
// Merge: text matches get priority score (0.85) for exact keyword hits
|
|
419
|
+
// Deduplicate by domain/key; if already in semantic results, boost its score
|
|
420
|
+
const resultMap = new Map();
|
|
421
|
+
for (const r of results) {
|
|
422
|
+
resultMap.set(`${r.domain}/${r.key}`, r);
|
|
423
|
+
}
|
|
493
424
|
for (const row of textResult.rows) {
|
|
494
425
|
const k = `${row.domain}/${row.key}`;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
426
|
+
const existing = resultMap.get(k);
|
|
427
|
+
if (existing) {
|
|
428
|
+
// Boost: semantic match + text match = highest relevance
|
|
429
|
+
existing.score = Math.max(Number(existing.score), 0.85);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
resultMap.set(k, row);
|
|
498
433
|
}
|
|
499
434
|
}
|
|
435
|
+
// Sort by score descending
|
|
436
|
+
results = Array.from(resultMap.values()).sort((a, b) => Number(b.score) - Number(a.score));
|
|
500
437
|
}
|
|
501
438
|
return results.slice(0, limit);
|
|
502
439
|
}
|
|
@@ -532,8 +469,10 @@ async function ontoList(pool) {
|
|
|
532
469
|
const client = await pool.connect();
|
|
533
470
|
try {
|
|
534
471
|
const result = await client.query(`
|
|
535
|
-
SELECT domain, schema, description, version,
|
|
536
|
-
|
|
472
|
+
SELECT domain, schema, description, version,
|
|
473
|
+
service, entity_type, parent, tags,
|
|
474
|
+
updated_at::text
|
|
475
|
+
FROM semo.ontology ORDER BY service NULLS FIRST, domain
|
|
537
476
|
`);
|
|
538
477
|
return result.rows;
|
|
539
478
|
}
|
|
@@ -547,13 +486,52 @@ async function ontoList(pool) {
|
|
|
547
486
|
async function ontoShow(pool, domain) {
|
|
548
487
|
const client = await pool.connect();
|
|
549
488
|
try {
|
|
550
|
-
const result = await client.query(`SELECT domain, schema, description, version,
|
|
489
|
+
const result = await client.query(`SELECT domain, schema, description, version,
|
|
490
|
+
service, entity_type, parent, tags,
|
|
491
|
+
updated_at::text
|
|
492
|
+
FROM semo.ontology WHERE domain = $1`, [domain]);
|
|
551
493
|
return result.rows[0] || null;
|
|
552
494
|
}
|
|
553
495
|
finally {
|
|
554
496
|
client.release();
|
|
555
497
|
}
|
|
556
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* List all ontology types (structural templates)
|
|
501
|
+
*/
|
|
502
|
+
async function ontoListTypes(pool) {
|
|
503
|
+
const client = await pool.connect();
|
|
504
|
+
try {
|
|
505
|
+
const result = await client.query(`
|
|
506
|
+
SELECT type_key, schema, description, version
|
|
507
|
+
FROM semo.ontology_types ORDER BY type_key
|
|
508
|
+
`);
|
|
509
|
+
return result.rows;
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return []; // Table may not exist yet (pre-016 migration)
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
client.release();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Resolve a service name to its associated domain list.
|
|
520
|
+
* Uses ontology.service column + dot-notation domain detection.
|
|
521
|
+
*/
|
|
522
|
+
async function resolveServiceDomainsLocal(client, service) {
|
|
523
|
+
try {
|
|
524
|
+
const result = await client.query(`SELECT domain FROM semo.ontology WHERE service = $1
|
|
525
|
+
UNION
|
|
526
|
+
SELECT domain FROM semo.ontology WHERE domain LIKE $2
|
|
527
|
+
UNION
|
|
528
|
+
SELECT domain FROM semo.ontology WHERE domain = $1`, [service, `${service}.%`]);
|
|
529
|
+
return result.rows.map((r) => r.domain);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
557
535
|
/**
|
|
558
536
|
* Validate KB entries against ontology schema (basic JSON Schema validation)
|
|
559
537
|
*/
|
|
@@ -613,6 +591,37 @@ async function ontoValidate(pool, domain, entries) {
|
|
|
613
591
|
}
|
|
614
592
|
return { valid, invalid };
|
|
615
593
|
}
|
|
594
|
+
// ============================================================
|
|
595
|
+
// KB Digest
|
|
596
|
+
// ============================================================
|
|
597
|
+
/**
|
|
598
|
+
* Generate KB change digest based on a since timestamp.
|
|
599
|
+
* Returns all KB changes since the given ISO timestamp.
|
|
600
|
+
*/
|
|
601
|
+
async function kbDigest(pool, since, domain) {
|
|
602
|
+
const client = await pool.connect();
|
|
603
|
+
const generatedAt = new Date().toISOString();
|
|
604
|
+
try {
|
|
605
|
+
let sql = `
|
|
606
|
+
SELECT domain, key, content, version, updated_at::text,
|
|
607
|
+
CASE WHEN created_at > $1 THEN 'new' ELSE 'updated' END as change_type
|
|
608
|
+
FROM semo.knowledge_base
|
|
609
|
+
WHERE updated_at > $1 OR created_at > $1
|
|
610
|
+
`;
|
|
611
|
+
const params = [since];
|
|
612
|
+
let paramIdx = 2;
|
|
613
|
+
if (domain) {
|
|
614
|
+
sql += ` AND domain = $${paramIdx++}`;
|
|
615
|
+
params.push(domain);
|
|
616
|
+
}
|
|
617
|
+
sql += ` ORDER BY updated_at DESC LIMIT 100`;
|
|
618
|
+
const result = await client.query(sql, params);
|
|
619
|
+
return { changes: result.rows, since, generatedAt };
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
client.release();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
616
625
|
/**
|
|
617
626
|
* Write ontology schemas to local cache
|
|
618
627
|
*/
|