@ontos-ai/knowhere-claw 0.1.0-beta.0 → 0.1.2
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 +105 -132
- package/dist/__tests__/channel-route.test.d.ts +1 -0
- package/dist/__tests__/ingest-tool.test.d.ts +1 -0
- package/dist/__tests__/read-result-file-tool.test.d.ts +1 -0
- package/dist/__tests__/tracker-progress.test.d.ts +1 -0
- package/dist/channel-delivery.d.ts +21 -0
- package/dist/channel-delivery.js +337 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +37 -23
- package/dist/index.js +13 -11
- package/dist/parser.js +1 -1
- package/dist/session.js +1 -0
- package/dist/store.d.ts +18 -1
- package/dist/store.js +91 -3
- package/dist/tools.d.ts +2 -3
- package/dist/tools.js +473 -105
- package/dist/tracker-progress.d.ts +3 -1
- package/dist/tracker-progress.js +8 -190
- package/dist/types.d.ts +7 -6
- package/openclaw.plugin.json +3 -21
- package/package.json +7 -5
- package/skills/knowhere/SKILL.md +57 -20
- package/dist/hooks.d.ts +0 -8
- package/dist/hooks.js +0 -415
package/dist/tools.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { isRecord } from "./types.js";
|
|
2
|
-
import { assertKnowhereApiKey } from "./config.js";
|
|
2
|
+
import { assertKnowhereApiKey, formatPaymentRequiredMessage, isPaymentRequiredError, persistApiKey } from "./config.js";
|
|
3
|
+
import { normalizeForGrep, normalizeWhitespace, sanitizeStringArray, slugify } from "./text.js";
|
|
3
4
|
import { formatErrorMessage } from "./error-message.js";
|
|
4
5
|
import { KnowhereClient } from "./client.js";
|
|
5
|
-
import {
|
|
6
|
+
import { deliverChannelMessage } from "./channel-delivery.js";
|
|
6
7
|
import { sendTrackerProgress } from "./tracker-progress.js";
|
|
7
|
-
import path from "node:path";
|
|
8
8
|
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/core";
|
|
9
11
|
//#region src/tools.ts
|
|
10
12
|
const PREVIEW_SUMMARY_MAX_CHARS = 120;
|
|
13
|
+
const INGEST_TRACKER_LANGUAGES = new Set(["ch", "en"]);
|
|
11
14
|
function textResult(text) {
|
|
12
15
|
return {
|
|
13
16
|
content: [{
|
|
@@ -17,6 +20,10 @@ function textResult(text) {
|
|
|
17
20
|
details: {}
|
|
18
21
|
};
|
|
19
22
|
}
|
|
23
|
+
function rethrowWithPaymentHint(error) {
|
|
24
|
+
if (isPaymentRequiredError(error)) throw new Error(formatPaymentRequiredMessage());
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
20
27
|
function deriveStoredDocumentDisplayName(document) {
|
|
21
28
|
return document.originalFileName || document.title;
|
|
22
29
|
}
|
|
@@ -89,6 +96,59 @@ function buildIngestProgressLabel(params) {
|
|
|
89
96
|
return params.url;
|
|
90
97
|
}
|
|
91
98
|
}
|
|
99
|
+
function readIngestTrackerLanguage(value) {
|
|
100
|
+
const normalized = readString(value)?.toLowerCase();
|
|
101
|
+
if (!normalized) return "en";
|
|
102
|
+
if (INGEST_TRACKER_LANGUAGES.has(normalized)) return normalized;
|
|
103
|
+
return "en";
|
|
104
|
+
}
|
|
105
|
+
function buildIngestTrackerSuccessText(params) {
|
|
106
|
+
if (params.lang === "ch") return `通知:\`${params.label}\` 已解析完成,可以使用了。任务 ID:\`${params.jobId}\`。`;
|
|
107
|
+
return `Notice: \`${params.label}\` is parsed and ready. Job ID: \`${params.jobId}\`.`;
|
|
108
|
+
}
|
|
109
|
+
function buildIngestTrackerFailureText(params) {
|
|
110
|
+
if (params.lang === "ch") return params.jobId ? `通知:解析 \`${params.label}\` 失败(任务 \`${params.jobId}\`):${params.errorText}` : `通知:解析 \`${params.label}\` 失败:${params.errorText}`;
|
|
111
|
+
return params.jobId ? `Notice: failed to parse \`${params.label}\` (job \`${params.jobId}\`): ${params.errorText}` : `Notice: failed to parse \`${params.label}\`: ${params.errorText}`;
|
|
112
|
+
}
|
|
113
|
+
function queueBackgroundIngestFollowUp(params) {
|
|
114
|
+
const sessionKey = readString(params.sessionKey);
|
|
115
|
+
if (!sessionKey) {
|
|
116
|
+
params.api.logger.warn(`knowhere: background ingest follow-up skipped because sessionKey is missing contextKey=${params.followUp.contextKey}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
if (!params.api.runtime.system.enqueueSystemEvent(params.followUp.sessionEventText, {
|
|
121
|
+
sessionKey,
|
|
122
|
+
contextKey: params.followUp.contextKey
|
|
123
|
+
})) {
|
|
124
|
+
params.api.logger.info(`knowhere: background ingest follow-up not queued sessionKey=${sessionKey} contextKey=${params.followUp.contextKey}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
params.api.runtime.system.requestHeartbeatNow({
|
|
128
|
+
reason: params.followUp.heartbeatReason,
|
|
129
|
+
sessionKey
|
|
130
|
+
});
|
|
131
|
+
params.api.logger.info(`knowhere: background ingest follow-up queued sessionKey=${sessionKey} contextKey=${params.followUp.contextKey}`);
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
params.api.logger.warn(`knowhere: background ingest follow-up queue failed sessionKey=${sessionKey} contextKey=${params.followUp.contextKey}. ${formatErrorMessage(error)}`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function notifyBackgroundIngestOutcome(params) {
|
|
139
|
+
if (await sendTrackerProgress({
|
|
140
|
+
api: params.api,
|
|
141
|
+
context: params.context,
|
|
142
|
+
sessionKey: params.sessionKey,
|
|
143
|
+
channelRoute: params.channelRoute,
|
|
144
|
+
text: params.followUp.trackerText
|
|
145
|
+
})) return;
|
|
146
|
+
queueBackgroundIngestFollowUp({
|
|
147
|
+
api: params.api,
|
|
148
|
+
sessionKey: params.sessionKey,
|
|
149
|
+
followUp: params.followUp
|
|
150
|
+
});
|
|
151
|
+
}
|
|
92
152
|
function normalizeParsingParams(rawParsing) {
|
|
93
153
|
const parsing = isRecord(rawParsing) ? rawParsing : {};
|
|
94
154
|
const result = {};
|
|
@@ -196,6 +256,146 @@ function findResultFile(browseIndex, relativePath) {
|
|
|
196
256
|
function isTextReadableResultFile(fileRecord) {
|
|
197
257
|
return fileRecord.kind !== "image";
|
|
198
258
|
}
|
|
259
|
+
const IMAGE_EXTENSION_MIME_TYPES = {
|
|
260
|
+
".png": "image/png",
|
|
261
|
+
".jpg": "image/jpeg",
|
|
262
|
+
".jpeg": "image/jpeg",
|
|
263
|
+
".gif": "image/gif",
|
|
264
|
+
".webp": "image/webp",
|
|
265
|
+
".svg": "image/svg+xml",
|
|
266
|
+
".bmp": "image/bmp",
|
|
267
|
+
".tiff": "image/tiff",
|
|
268
|
+
".tif": "image/tiff"
|
|
269
|
+
};
|
|
270
|
+
function inferImageMimeType(filePath) {
|
|
271
|
+
return IMAGE_EXTENSION_MIME_TYPES[path.extname(filePath).toLowerCase()] || "image/png";
|
|
272
|
+
}
|
|
273
|
+
async function buildImageToolResult(params) {
|
|
274
|
+
const mimeType = inferImageMimeType(params.absolutePath);
|
|
275
|
+
const stagedImage = await stageImageResultFileForDelivery({
|
|
276
|
+
absolutePath: params.absolutePath,
|
|
277
|
+
documentTitle: params.documentTitle,
|
|
278
|
+
relativePath: params.filePath,
|
|
279
|
+
workspaceDir: params.workspaceDir
|
|
280
|
+
});
|
|
281
|
+
const stagedImagePath = stagedImage.stagedPath;
|
|
282
|
+
const fileName = path.basename(stagedImagePath);
|
|
283
|
+
const caption = `${params.documentTitle} - ${params.filePath}`;
|
|
284
|
+
const directDelivery = await deliverChannelMessage({
|
|
285
|
+
api: params.api,
|
|
286
|
+
operationLabel: "read result image",
|
|
287
|
+
context: params.context,
|
|
288
|
+
sessionKey: params.sessionKey,
|
|
289
|
+
channelRoute: params.channelRoute,
|
|
290
|
+
text: caption,
|
|
291
|
+
mediaUrl: stagedImagePath,
|
|
292
|
+
mediaLocalRoots: [path.dirname(stagedImagePath)]
|
|
293
|
+
});
|
|
294
|
+
if (directDelivery.delivered) {
|
|
295
|
+
const payload = {
|
|
296
|
+
scope: params.scopeLabel,
|
|
297
|
+
docId: params.docId,
|
|
298
|
+
documentTitle: params.documentTitle,
|
|
299
|
+
file: params.file,
|
|
300
|
+
mode: "image_sent",
|
|
301
|
+
data: {
|
|
302
|
+
mimeType,
|
|
303
|
+
sourceRelativePath: params.filePath,
|
|
304
|
+
stagedPath: stagedImagePath,
|
|
305
|
+
fileName,
|
|
306
|
+
caption,
|
|
307
|
+
note: "Image already sent to the current channel by the plugin. Do not call read on stagedPath. Do not call the message tool or attach this file again. If you reply, send only a brief confirmation.",
|
|
308
|
+
delivery: {
|
|
309
|
+
method: "direct_runtime",
|
|
310
|
+
surface: directDelivery.surface,
|
|
311
|
+
target: directDelivery.to,
|
|
312
|
+
accountId: directDelivery.accountId
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
content: [{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: `${JSON.stringify(payload, null, 2)}\n`
|
|
320
|
+
}],
|
|
321
|
+
details: payload
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const sendWithMessageTool = {
|
|
325
|
+
action: "send",
|
|
326
|
+
path: stagedImagePath,
|
|
327
|
+
filePath: stagedImagePath,
|
|
328
|
+
filename: fileName,
|
|
329
|
+
caption
|
|
330
|
+
};
|
|
331
|
+
const replyFallback = stagedImage.workspaceRelativePath ? {
|
|
332
|
+
instructions: "If the message tool is unavailable, send your normal user-visible reply and include this exact line on its own line to attach the image.",
|
|
333
|
+
workspaceRelativePath: stagedImage.workspaceRelativePath,
|
|
334
|
+
replyWithMediaDirective: `MEDIA:${stagedImage.workspaceRelativePath}`
|
|
335
|
+
} : void 0;
|
|
336
|
+
const note = replyFallback ? "Image bytes are not inlined. Do not call read on stagedPath. If the user wants to see this image, use the message tool with sendWithMessageTool. If the message tool is unavailable, send your user-visible reply normally and include replyFallback.replyWithMediaDirective on its own line." : "Image bytes are not inlined. Do not call read on stagedPath. If the user wants to see this image, call the message tool with sendWithMessageTool.";
|
|
337
|
+
const nextActionInstructions = replyFallback ? "Do not call read on stagedPath. Call the message tool with sendWithMessageTool to attach this image. If the message tool is unavailable, use replyFallback.replyWithMediaDirective in your normal reply instead." : "Do not call read on stagedPath. Call the message tool with sendWithMessageTool to attach this image.";
|
|
338
|
+
const payload = {
|
|
339
|
+
scope: params.scopeLabel,
|
|
340
|
+
docId: params.docId,
|
|
341
|
+
documentTitle: params.documentTitle,
|
|
342
|
+
file: params.file,
|
|
343
|
+
mode: "image_attachment",
|
|
344
|
+
data: {
|
|
345
|
+
mimeType,
|
|
346
|
+
sourceRelativePath: params.filePath,
|
|
347
|
+
stagedPath: stagedImagePath,
|
|
348
|
+
fileName,
|
|
349
|
+
caption,
|
|
350
|
+
note,
|
|
351
|
+
nextAction: {
|
|
352
|
+
tool: "message",
|
|
353
|
+
instructions: nextActionInstructions,
|
|
354
|
+
args: sendWithMessageTool
|
|
355
|
+
},
|
|
356
|
+
sendWithMessageTool,
|
|
357
|
+
...replyFallback ? { replyFallback } : {}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
return {
|
|
361
|
+
content: [{
|
|
362
|
+
type: "text",
|
|
363
|
+
text: `${JSON.stringify(payload, null, 2)}\n`
|
|
364
|
+
}],
|
|
365
|
+
details: payload
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function normalizeWorkspaceDir(workspaceDir) {
|
|
369
|
+
const trimmed = readString(workspaceDir);
|
|
370
|
+
return trimmed ? path.resolve(trimmed) : void 0;
|
|
371
|
+
}
|
|
372
|
+
function toWorkspaceRelativeMediaPath(params) {
|
|
373
|
+
const relativePath = path.relative(params.workspaceDir, params.stagedPath);
|
|
374
|
+
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) return;
|
|
375
|
+
const normalizedRelativePath = relativePath.split(path.sep).join("/");
|
|
376
|
+
return normalizedRelativePath.startsWith("./") || normalizedRelativePath.startsWith("../") ? normalizedRelativePath : `./${normalizedRelativePath}`;
|
|
377
|
+
}
|
|
378
|
+
async function stageImageResultFileForDelivery(params) {
|
|
379
|
+
const extension = path.extname(params.relativePath) || path.extname(params.absolutePath) || ".png";
|
|
380
|
+
const imageBaseName = path.basename(params.relativePath, extension) || "image";
|
|
381
|
+
const workspaceDir = normalizeWorkspaceDir(params.workspaceDir);
|
|
382
|
+
let stagedDir;
|
|
383
|
+
if (workspaceDir) {
|
|
384
|
+
const workspaceStageRoot = path.join(workspaceDir, ".openclaw");
|
|
385
|
+
await fs.mkdir(workspaceStageRoot, { recursive: true });
|
|
386
|
+
stagedDir = await fs.mkdtemp(path.join(workspaceStageRoot, "knowhere-read-result-file-"));
|
|
387
|
+
} else stagedDir = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "knowhere-read-result-file-"));
|
|
388
|
+
const stagedFileName = `${slugify(`${params.documentTitle}-${imageBaseName}`, "knowhere-image")}${extension.toLowerCase()}`;
|
|
389
|
+
const stagedPath = path.join(stagedDir, stagedFileName);
|
|
390
|
+
await fs.copyFile(params.absolutePath, stagedPath);
|
|
391
|
+
return {
|
|
392
|
+
stagedPath,
|
|
393
|
+
...workspaceDir ? { workspaceRelativePath: toWorkspaceRelativeMediaPath({
|
|
394
|
+
workspaceDir,
|
|
395
|
+
stagedPath
|
|
396
|
+
}) } : {}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
199
399
|
function stripUtf8Bom(text) {
|
|
200
400
|
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
201
401
|
}
|
|
@@ -292,7 +492,7 @@ function createIngestTool(params) {
|
|
|
292
492
|
return {
|
|
293
493
|
name: "knowhere_ingest_document",
|
|
294
494
|
label: "Knowhere Ingest",
|
|
295
|
-
description: "Parse a local file or remote URL with Knowhere and store the result in the current scope.
|
|
495
|
+
description: "Parse a local file or remote URL with Knowhere and store the result in the current scope. Before calling this for a document that might already be stored in the current scope, use knowhere_list_documents and reuse the existing stored document when Source, File, or Title clearly match unless the user explicitly asks for a fresh parse or overwrite. When the user provides a URL to a document (PDF link, web page, etc.), pass it as the url parameter — Knowhere fetches it directly, no local download needed. Returns immediately with a job ID while parsing continues in the background. Use knowhere_get_job_status to check progress at any time. Use lang to control the language of the direct tracker follow-up (`en` by default, `ch` for Chinese). Provide either filePath or url, not both.",
|
|
296
496
|
parameters: {
|
|
297
497
|
type: "object",
|
|
298
498
|
additionalProperties: false,
|
|
@@ -334,6 +534,10 @@ function createIngestTool(params) {
|
|
|
334
534
|
type: "boolean",
|
|
335
535
|
description: "Replace an existing stored document with the same docId."
|
|
336
536
|
},
|
|
537
|
+
lang: {
|
|
538
|
+
type: "string",
|
|
539
|
+
description: "Language for the direct tracker follow-up message sent after background parsing completes or fails. Supports en and ch; unsupported values fall back to en."
|
|
540
|
+
},
|
|
337
541
|
parsing: {
|
|
338
542
|
type: "object",
|
|
339
543
|
additionalProperties: false,
|
|
@@ -387,31 +591,33 @@ function createIngestTool(params) {
|
|
|
387
591
|
});
|
|
388
592
|
const tags = sanitizeStringArray(paramsRecord.tags);
|
|
389
593
|
const overwrite = readBoolean(paramsRecord.overwrite, false);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
594
|
+
const trackerLanguage = readIngestTrackerLanguage(paramsRecord.lang);
|
|
595
|
+
const sessionKey = params.ctx.sessionKey;
|
|
596
|
+
const sourceType = urlParam ? "url" : "file";
|
|
597
|
+
const channelRoute = await params.store.resolveChannelRoute({ sessionKey });
|
|
598
|
+
params.api.logger.info(`knowhere: knowhere_ingest_document starting background ingest scope=${scope.label} sourceType=${sourceType} label=${JSON.stringify(progressLabel)} overwrite=${overwrite} docId=${docId ?? "auto"} dataId=${dataId ?? "none"} lang=${trackerLanguage} routeState=${channelRoute ? "resolved" : "missing"} routeAccountId=${channelRoute?.accountId ?? "none"}`);
|
|
599
|
+
let resolveJobCreated;
|
|
600
|
+
const jobCreatedPromise = new Promise((resolve) => {
|
|
601
|
+
resolveJobCreated = resolve;
|
|
395
602
|
});
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
});
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
sourceType: urlParam ? "url" : "file",
|
|
603
|
+
let createdJobId;
|
|
604
|
+
const fileName = requestedFileName || (resolvedFilePath ? path.basename(resolvedFilePath) : null);
|
|
605
|
+
const ingestPromise = client.ingestDocument({
|
|
606
|
+
filePath: resolvedFilePath,
|
|
607
|
+
fileName: requestedFileName,
|
|
608
|
+
url: urlParam,
|
|
609
|
+
dataId,
|
|
610
|
+
parsingParams: normalizeParsingParams(paramsRecord.parsing),
|
|
611
|
+
onJobCreated: (job) => {
|
|
612
|
+
createdJobId = job.job_id;
|
|
613
|
+
params.api.logger.info(`knowhere: knowhere_ingest_document job created scope=${scope.label} jobId=${job.job_id} label=${JSON.stringify(progressLabel)}`);
|
|
614
|
+
resolveJobCreated(job);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
ingestPromise.then(async (ingestResult) => {
|
|
618
|
+
params.api.logger.info(`knowhere: knowhere_ingest_document download completed scope=${scope.label} jobId=${ingestResult.job.job_id}; storing extracted result`);
|
|
619
|
+
const storedDocument = await params.store.saveDownloadedDocument(scope, {
|
|
620
|
+
sourceType,
|
|
415
621
|
source: urlParam || resolvedFilePath || "",
|
|
416
622
|
fileName,
|
|
417
623
|
docId,
|
|
@@ -422,30 +628,59 @@ function createIngestTool(params) {
|
|
|
422
628
|
jobResult: ingestResult.jobResult,
|
|
423
629
|
downloadedResult: ingestResult.downloadedResult
|
|
424
630
|
}, { overwrite });
|
|
425
|
-
|
|
631
|
+
params.api.logger.info(`knowhere: knowhere_ingest_document stored document scope=${scope.label} jobId=${ingestResult.job.job_id} docId=${storedDocument.id} label=${JSON.stringify(progressLabel)}`);
|
|
632
|
+
await notifyBackgroundIngestOutcome({
|
|
426
633
|
api: params.api,
|
|
427
|
-
|
|
428
|
-
|
|
634
|
+
context: params.ctx,
|
|
635
|
+
sessionKey,
|
|
636
|
+
channelRoute,
|
|
637
|
+
followUp: {
|
|
638
|
+
trackerText: buildIngestTrackerSuccessText({
|
|
639
|
+
label: progressLabel,
|
|
640
|
+
jobId: ingestResult.job.job_id,
|
|
641
|
+
lang: trackerLanguage
|
|
642
|
+
}),
|
|
643
|
+
sessionEventText: `Knowhere background ingest completed for "${progressLabel}". The parsed document is stored as "${storedDocument.id}" in the current scope and is ready to use. Send one short follow-up to the user now telling them the parsing for "${progressLabel}" is completed and the file is ready.`,
|
|
644
|
+
contextKey: `knowhere:ingest-complete:${ingestResult.job.job_id}`,
|
|
645
|
+
heartbeatReason: "knowhere-ingest-complete"
|
|
646
|
+
}
|
|
429
647
|
});
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
scopeLabel: scope.label,
|
|
435
|
-
includeSource: true,
|
|
436
|
-
includeJobId: true
|
|
437
|
-
}),
|
|
438
|
-
"Next: read manifest.json with knowhere_read_result_file or preview the document with knowhere_preview_document."
|
|
439
|
-
].join("\n"));
|
|
440
|
-
} catch (error) {
|
|
441
|
-
const errorText = formatErrorMessage(error);
|
|
442
|
-
await sendTrackerProgress({
|
|
648
|
+
}).catch(async (error) => {
|
|
649
|
+
const errorText = isPaymentRequiredError(error) ? formatPaymentRequiredMessage() : formatErrorMessage(error);
|
|
650
|
+
params.api.logger.error(`knowhere: knowhere_ingest_document background ingest failed scope=${scope.label} label=${JSON.stringify(progressLabel)} error=${errorText}`);
|
|
651
|
+
await notifyBackgroundIngestOutcome({
|
|
443
652
|
api: params.api,
|
|
444
|
-
|
|
445
|
-
|
|
653
|
+
context: params.ctx,
|
|
654
|
+
sessionKey,
|
|
655
|
+
channelRoute,
|
|
656
|
+
followUp: {
|
|
657
|
+
trackerText: buildIngestTrackerFailureText({
|
|
658
|
+
label: progressLabel,
|
|
659
|
+
jobId: createdJobId,
|
|
660
|
+
errorText,
|
|
661
|
+
lang: trackerLanguage
|
|
662
|
+
}),
|
|
663
|
+
sessionEventText: `Knowhere background ingest failed for "${progressLabel}". Send one short follow-up to the user now telling them the parsing failed. Error: ${errorText}`,
|
|
664
|
+
contextKey: `knowhere:ingest-failed:${createdJobId ?? progressLabel}`,
|
|
665
|
+
heartbeatReason: "knowhere-ingest-failed"
|
|
666
|
+
}
|
|
446
667
|
});
|
|
447
|
-
|
|
668
|
+
});
|
|
669
|
+
const earlyFailure = Symbol("earlyFailure");
|
|
670
|
+
const createdJob = await Promise.race([jobCreatedPromise, ingestPromise.then(() => earlyFailure, (err) => {
|
|
671
|
+
rethrowWithPaymentHint(err);
|
|
672
|
+
})]);
|
|
673
|
+
if (typeof createdJob === "symbol") {
|
|
674
|
+
params.api.logger.warn(`knowhere: knowhere_ingest_document ingest completed before job-created callback scope=${scope.label} label=${JSON.stringify(progressLabel)}`);
|
|
675
|
+
return textResult("Ingest completed synchronously. Use knowhere_list_documents to find the stored document.");
|
|
448
676
|
}
|
|
677
|
+
return textResult([
|
|
678
|
+
"Ingest job created. Parsing in background.",
|
|
679
|
+
`Job ID: ${createdJob.job_id}`,
|
|
680
|
+
`File: ${progressLabel}`,
|
|
681
|
+
`Scope: ${scope.label}`,
|
|
682
|
+
"Use knowhere_get_job_status to check progress at any time."
|
|
683
|
+
].join("\n"));
|
|
449
684
|
}
|
|
450
685
|
};
|
|
451
686
|
}
|
|
@@ -472,8 +707,11 @@ function createJobStatusTool(params) {
|
|
|
472
707
|
config: params.config
|
|
473
708
|
});
|
|
474
709
|
const scope = params.store.resolveScope(params.ctx);
|
|
475
|
-
|
|
710
|
+
params.api.logger.info(`knowhere: knowhere_get_job_status fetching remote status scope=${scope.label} jobId=${jobId}`);
|
|
711
|
+
const job = await client.getJob(jobId).catch(rethrowWithPaymentHint);
|
|
476
712
|
const matchingDocuments = (await params.store.listDocuments(scope)).filter((document) => document.jobId === job.job_id);
|
|
713
|
+
params.api.logger.info(`knowhere: knowhere_get_job_status fetched status scope=${scope.label} jobId=${job.job_id} status=${job.status} storedDocuments=${matchingDocuments.length}`);
|
|
714
|
+
if (job.error?.message) params.api.logger.warn(`knowhere: knowhere_get_job_status job returned error scope=${scope.label} jobId=${job.job_id} error=${job.error.message}`);
|
|
477
715
|
const lines = [
|
|
478
716
|
`Knowhere job ${job.job_id}`,
|
|
479
717
|
`Scope: ${scope.label}`,
|
|
@@ -568,15 +806,21 @@ function createJobListTool(params) {
|
|
|
568
806
|
config: params.config
|
|
569
807
|
});
|
|
570
808
|
const scope = params.store.resolveScope(params.ctx);
|
|
809
|
+
const jobStatus = readString(paramsRecord.jobStatus);
|
|
810
|
+
const jobType = readString(paramsRecord.jobType);
|
|
811
|
+
const recentDays = readRecentDays(paramsRecord.recentDays);
|
|
812
|
+
const startTime = readString(paramsRecord.startTime);
|
|
813
|
+
const endTime = readString(paramsRecord.endTime);
|
|
814
|
+
params.api.logger.info(`knowhere: knowhere_list_jobs listing jobs scope=${scope.label} page=${page} pageSize=${pageSize} jobStatus=${jobStatus ?? "any"} jobType=${jobType ?? "any"} recentDays=${recentDays ?? "none"} startTime=${startTime ?? "none"} endTime=${endTime ?? "none"}`);
|
|
571
815
|
const [jobList, documents] = await Promise.all([client.listJobs({
|
|
572
816
|
page,
|
|
573
817
|
pageSize,
|
|
574
|
-
jobStatus
|
|
575
|
-
jobType
|
|
576
|
-
recentDays
|
|
577
|
-
startTime
|
|
578
|
-
endTime
|
|
579
|
-
}), params.store.listDocuments(scope)]);
|
|
818
|
+
jobStatus,
|
|
819
|
+
jobType,
|
|
820
|
+
recentDays,
|
|
821
|
+
startTime,
|
|
822
|
+
endTime
|
|
823
|
+
}), params.store.listDocuments(scope)]).catch(rethrowWithPaymentHint);
|
|
580
824
|
const documentsByJobId = /* @__PURE__ */ new Map();
|
|
581
825
|
for (const document of documents) {
|
|
582
826
|
if (!document.jobId) continue;
|
|
@@ -584,6 +828,7 @@ function createJobListTool(params) {
|
|
|
584
828
|
entries.push(document);
|
|
585
829
|
documentsByJobId.set(document.jobId, entries);
|
|
586
830
|
}
|
|
831
|
+
params.api.logger.info(`knowhere: knowhere_list_jobs listed jobs scope=${scope.label} returned=${jobList.jobs.length} total=${jobList.total} storedDocuments=${documents.length}`);
|
|
587
832
|
return textResult(formatJobList({
|
|
588
833
|
jobList,
|
|
589
834
|
documentsByJobId
|
|
@@ -634,27 +879,31 @@ function createImportCompletedJobTool(params) {
|
|
|
634
879
|
config: params.config
|
|
635
880
|
});
|
|
636
881
|
const scope = params.store.resolveScope(params.ctx);
|
|
637
|
-
|
|
882
|
+
params.api.logger.info(`knowhere: knowhere_import_completed_job importing completed job scope=${scope.label} jobId=${jobId} overwrite=${readBoolean(paramsRecord.overwrite, false)}`);
|
|
883
|
+
const importResult = await client.getCompletedJobResult(jobId).catch(rethrowWithPaymentHint);
|
|
638
884
|
const overwrite = readBoolean(paramsRecord.overwrite, false);
|
|
639
885
|
const tags = mergeTags(sanitizeStringArray(paramsRecord.tags), ["history-imported", `job:${importResult.jobResult.job_id}`]);
|
|
640
886
|
const fileName = importResult.jobResult.file_name || null;
|
|
641
887
|
const sourceType = importResult.jobResult.source_type === "url" ? "url" : "file";
|
|
888
|
+
params.api.logger.info(`knowhere: knowhere_import_completed_job downloaded job result scope=${scope.label} jobId=${importResult.jobResult.job_id} sourceType=${sourceType}`);
|
|
889
|
+
const document = await params.store.saveDownloadedDocument(scope, {
|
|
890
|
+
sourceType,
|
|
891
|
+
source: buildHistoryJobSource(importResult.jobResult.job_id),
|
|
892
|
+
sourceLabel: buildHistoryJobSourceLabel(importResult.jobResult.job_id, fileName),
|
|
893
|
+
fileName,
|
|
894
|
+
docId: readString(paramsRecord.docId),
|
|
895
|
+
title: readString(paramsRecord.title),
|
|
896
|
+
dataId: importResult.jobResult.data_id || void 0,
|
|
897
|
+
tags,
|
|
898
|
+
job: importResult.job,
|
|
899
|
+
jobResult: importResult.jobResult,
|
|
900
|
+
downloadedResult: importResult.downloadedResult
|
|
901
|
+
}, { overwrite });
|
|
902
|
+
params.api.logger.info(`knowhere: knowhere_import_completed_job stored imported document scope=${scope.label} jobId=${importResult.jobResult.job_id} docId=${document.id}`);
|
|
642
903
|
return textResult([
|
|
643
904
|
"Import complete.",
|
|
644
905
|
...buildStoredDocumentSummaryLines({
|
|
645
|
-
document
|
|
646
|
-
sourceType,
|
|
647
|
-
source: buildHistoryJobSource(importResult.jobResult.job_id),
|
|
648
|
-
sourceLabel: buildHistoryJobSourceLabel(importResult.jobResult.job_id, fileName),
|
|
649
|
-
fileName,
|
|
650
|
-
docId: readString(paramsRecord.docId),
|
|
651
|
-
title: readString(paramsRecord.title),
|
|
652
|
-
dataId: importResult.jobResult.data_id || void 0,
|
|
653
|
-
tags,
|
|
654
|
-
job: importResult.job,
|
|
655
|
-
jobResult: importResult.jobResult,
|
|
656
|
-
downloadedResult: importResult.downloadedResult
|
|
657
|
-
}, { overwrite }),
|
|
906
|
+
document,
|
|
658
907
|
scopeLabel: scope.label,
|
|
659
908
|
includeSource: true,
|
|
660
909
|
includeJobId: true
|
|
@@ -735,11 +984,26 @@ function buildNormalizedFields(chunk) {
|
|
|
735
984
|
fields.set("chunk.path", normalizeForGrep(chunk.path || ""));
|
|
736
985
|
return fields;
|
|
737
986
|
}
|
|
987
|
+
function buildGrepHints(params) {
|
|
988
|
+
const hints = [];
|
|
989
|
+
const maxHints = 3;
|
|
990
|
+
if (params.totalMatches === 0 && params.conditionCount > 0) hints.push("No matches. Try broadening: remove a condition, use a shorter pattern, or check for typos. Call knowhere_preview_document to see the document structure first.");
|
|
991
|
+
if (hints.length < maxHints && params.totalMatches > params.returned) {
|
|
992
|
+
let hint = `Showing ${params.returned} of ${params.totalMatches} matches. Add another condition (e.g., target chunk.path to a specific section) to narrow results.`;
|
|
993
|
+
if (!params.hasPathCondition) hint += " Use knowhere_preview_document to find section paths.";
|
|
994
|
+
hints.push(hint);
|
|
995
|
+
}
|
|
996
|
+
if (hints.length < maxHints && params.truncatedStrings) if (params.returned > 3) hints.push(`Fields truncated at ${params.maxStringChars} chars. Reduce maxResults to 1-3 and increase maxStringChars to 12000-20000 for full content.`);
|
|
997
|
+
else hints.push(`Fields truncated at ${params.maxStringChars} chars. Increase maxStringChars (up to 20000) for full content.`);
|
|
998
|
+
if (hints.length < maxHints && params.totalMatches >= 1 && params.totalMatches <= 5 && !params.includeContext) hints.push("Tip: set includeContext=true to discover sibling chunks in the same section.");
|
|
999
|
+
if (hints.length < maxHints && params.totalChunks > 0 && params.totalMatches > params.totalChunks * .5 && params.conditionCount <= 1) hints.push("Pattern matches over half the document. Add a second condition to narrow.");
|
|
1000
|
+
return hints;
|
|
1001
|
+
}
|
|
738
1002
|
function createGrepTool(params) {
|
|
739
1003
|
return {
|
|
740
1004
|
name: "knowhere_grep",
|
|
741
1005
|
label: "Knowhere Grep",
|
|
742
|
-
description: "Search a stored document's chunks with composable AND conditions. Returns matching chunks with content, summary, keywords, path, and chunkId. Supports substring and regex matching with text normalization (HTML stripping, LaTeX cleanup, unicode normalization). Omit conditions to list all chunks. Omit the target field in a condition to search across all text fields (content, summary, keywords, path) — this is the recommended default. When answering questions from results, cite the chunkId and path. Tip: set maxStringChars up to 20000 when you need full untruncated content from a small number of results (e.g., maxResults=1). The default 4000 may truncate long chunks.",
|
|
1006
|
+
description: "Search a stored document's chunks with composable AND conditions. Returns matching chunks with content, summary, keywords, path, and chunkId. Supports substring and regex matching with text normalization (HTML stripping, LaTeX cleanup, unicode normalization). Omit conditions to list all chunks. Omit the target field in a condition to search across all text fields (content, summary, keywords, path) — this is the recommended default. When answering questions from results, cite the chunkId and path. Tip: set maxStringChars up to 20000 when you need full untruncated content from a small number of results (e.g., maxResults=1). The default 4000 may truncate long chunks. Search strategy: (1) Start with knowhere_preview_document to see document structure. (2) Search broadly with a single short pattern, then narrow by adding conditions. (3) If zero results, broaden or try synonyms. If too many, add a path condition. (4) Once you find the right chunks, re-query with maxResults=1-3 and maxStringChars=12000-20000 to read full content.",
|
|
743
1007
|
parameters: {
|
|
744
1008
|
type: "object",
|
|
745
1009
|
additionalProperties: false,
|
|
@@ -809,13 +1073,17 @@ function createGrepTool(params) {
|
|
|
809
1073
|
if (!docId) throw new Error("docId is required.");
|
|
810
1074
|
const scope = params.store.resolveScope(params.ctx);
|
|
811
1075
|
const payload = await params.store.loadDocumentPayload(scope, docId);
|
|
812
|
-
if (!payload)
|
|
1076
|
+
if (!payload) {
|
|
1077
|
+
params.api.logger.warn(`knowhere: knowhere_grep document not found scope=${scope.label} docId=${docId}`);
|
|
1078
|
+
return textResult(formatStoredDocumentNotFound(docId, scope.label));
|
|
1079
|
+
}
|
|
813
1080
|
const conditions = parseGrepConditions(paramsRecord.conditions);
|
|
814
1081
|
const outerRegex = readBoolean(paramsRecord.regex, false);
|
|
815
1082
|
const outerCaseSensitive = readBoolean(paramsRecord.caseSensitive, false);
|
|
816
1083
|
const includeContext = readBoolean(paramsRecord.includeContext, false);
|
|
817
1084
|
const maxResults = Math.min(50, Math.max(1, Math.trunc(readNumber(paramsRecord.maxResults, 10))));
|
|
818
1085
|
const maxStringChars = Math.min(2e4, Math.max(100, Math.trunc(readNumber(paramsRecord.maxStringChars, 4e3))));
|
|
1086
|
+
params.api.logger.info(`knowhere: knowhere_grep searching document scope=${scope.label} docId=${docId} conditions=${conditions.length} regex=${outerRegex} caseSensitive=${outerCaseSensitive} includeContext=${includeContext} maxResults=${maxResults} maxStringChars=${maxStringChars}`);
|
|
819
1087
|
const pathChunkIndex = includeContext ? new Map(payload.browseIndex.paths.map((p) => [p.path, p.chunkIds])) : void 0;
|
|
820
1088
|
const sortedChunks = sortChunksByBrowseOrder(payload.chunks, payload.browseIndex);
|
|
821
1089
|
const results = [];
|
|
@@ -871,13 +1139,28 @@ function createGrepTool(params) {
|
|
|
871
1139
|
if (entry.siblingChunkIds) projected.siblingChunkIds = entry.siblingChunkIds;
|
|
872
1140
|
return projected;
|
|
873
1141
|
}), maxStringChars);
|
|
874
|
-
|
|
1142
|
+
params.api.logger.info(`knowhere: knowhere_grep completed search scope=${scope.label} docId=${docId} returned=${results.length} totalMatches=${totalMatches} truncated=${truncated.truncated}`);
|
|
1143
|
+
const hasPathCondition = conditions.some((c) => c.target === "chunk.path");
|
|
1144
|
+
const hints = buildGrepHints({
|
|
1145
|
+
totalMatches,
|
|
1146
|
+
returned: results.length,
|
|
1147
|
+
maxResults,
|
|
1148
|
+
maxStringChars,
|
|
1149
|
+
truncatedStrings: truncated.truncated,
|
|
1150
|
+
conditionCount: conditions.length,
|
|
1151
|
+
includeContext,
|
|
1152
|
+
totalChunks: sortedChunks.length,
|
|
1153
|
+
hasPathCondition
|
|
1154
|
+
});
|
|
1155
|
+
const jsonResult = formatJsonToolResult({
|
|
875
1156
|
totalMatches,
|
|
876
1157
|
returned: results.length,
|
|
877
1158
|
results: truncated.value,
|
|
878
1159
|
maxStringChars,
|
|
879
1160
|
truncatedStrings: truncated.truncated
|
|
880
1161
|
});
|
|
1162
|
+
if (hints.length === 0) return jsonResult;
|
|
1163
|
+
return textResult(`${jsonResult.content[0].text}\n---\n${hints.join("\n")}`);
|
|
881
1164
|
}
|
|
882
1165
|
};
|
|
883
1166
|
}
|
|
@@ -885,7 +1168,7 @@ function createReadResultFileTool(params) {
|
|
|
885
1168
|
return {
|
|
886
1169
|
name: "knowhere_read_result_file",
|
|
887
1170
|
label: "Knowhere Read Result File",
|
|
888
|
-
description: "Read a raw result file from the stored document's extracted ZIP package. Common files: manifest.json (parsing metadata), hierarchy.json (document structure), kb.csv (knowledge base export),
|
|
1171
|
+
description: "Read a raw result file from the stored document's extracted ZIP package. Common files: manifest.json (parsing metadata), hierarchy.json (document structure), kb.csv (knowledge base export), table HTML files (e.g., tables/table-1.html), or image assets (e.g., images/img-0.png). Image files are staged into a local attachment path and sent directly to the current channel when routing can be resolved. If direct delivery is unavailable, the tool returns a message-tool handoff and, when the run has a workspace, a workspace-relative MEDIA fallback for a normal assistant reply. When the result mode is image_attachment, do not call generic file-read tools on data.stagedPath; use data.sendWithMessageTool or data.replyFallback as returned. When the result mode is image_sent, the plugin already delivered the image. Use mode='json' for JSON files, mode='csv' for CSV files, or mode='text' (default) for everything else. Increase maxStringChars (up to 20000) for large files.",
|
|
889
1172
|
parameters: {
|
|
890
1173
|
type: "object",
|
|
891
1174
|
additionalProperties: false,
|
|
@@ -924,40 +1207,76 @@ function createReadResultFileTool(params) {
|
|
|
924
1207
|
if (!filePath) throw new Error("filePath is required.");
|
|
925
1208
|
const scope = params.store.resolveScope(params.ctx);
|
|
926
1209
|
const payload = await params.store.loadDocumentPayload(scope, docId);
|
|
927
|
-
if (!payload)
|
|
1210
|
+
if (!payload) {
|
|
1211
|
+
params.api.logger.warn(`knowhere: knowhere_read_result_file document not found scope=${scope.label} docId=${docId}`);
|
|
1212
|
+
return textResult(formatStoredDocumentNotFound(docId, scope.label));
|
|
1213
|
+
}
|
|
928
1214
|
const resultFile = findResultFile(payload.browseIndex, filePath);
|
|
929
|
-
if (!resultFile)
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1215
|
+
if (!resultFile) {
|
|
1216
|
+
params.api.logger.warn(`knowhere: knowhere_read_result_file result file not found scope=${scope.label} docId=${docId} filePath=${filePath}`);
|
|
1217
|
+
return textResult([
|
|
1218
|
+
"Result file not found.",
|
|
1219
|
+
`File path: ${filePath}`,
|
|
1220
|
+
`Document ID: ${docId}`,
|
|
1221
|
+
`Scope: ${scope.label}`
|
|
1222
|
+
].join("\n"));
|
|
1223
|
+
}
|
|
1224
|
+
if (resultFile.kind === "image") {
|
|
1225
|
+
const absolutePath = params.store.getResultFileAbsolutePath(scope, docId, filePath);
|
|
1226
|
+
const channelRoute = await params.store.resolveChannelRoute({ sessionKey: params.ctx.sessionKey });
|
|
1227
|
+
params.api.logger.info(`knowhere: knowhere_read_result_file staging image asset scope=${scope.label} docId=${docId} filePath=${filePath}`);
|
|
1228
|
+
return await buildImageToolResult({
|
|
1229
|
+
api: params.api,
|
|
1230
|
+
absolutePath,
|
|
1231
|
+
channelRoute,
|
|
1232
|
+
context: params.ctx,
|
|
1233
|
+
docId: payload.document.id,
|
|
1234
|
+
documentTitle: payload.document.title,
|
|
1235
|
+
filePath,
|
|
1236
|
+
file: resultFile,
|
|
1237
|
+
sessionKey: params.ctx.sessionKey,
|
|
1238
|
+
scopeLabel: scope.label,
|
|
1239
|
+
workspaceDir: params.ctx.workspaceDir
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
if (!isTextReadableResultFile(resultFile)) {
|
|
1243
|
+
params.api.logger.warn(`knowhere: knowhere_read_result_file unreadable result kind scope=${scope.label} docId=${docId} filePath=${filePath} kind=${resultFile.kind}`);
|
|
1244
|
+
return textResult([
|
|
1245
|
+
"Result file is not readable as text through this tool.",
|
|
1246
|
+
`File path: ${filePath}`,
|
|
1247
|
+
`Kind: ${resultFile.kind}`,
|
|
1248
|
+
`Document ID: ${docId}`,
|
|
1249
|
+
`Scope: ${scope.label}`
|
|
1250
|
+
].join("\n"));
|
|
1251
|
+
}
|
|
942
1252
|
const storedFile = await params.store.readResultFile(scope, docId, filePath);
|
|
943
|
-
if (!storedFile)
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
`
|
|
949
|
-
|
|
1253
|
+
if (!storedFile) {
|
|
1254
|
+
params.api.logger.warn(`knowhere: knowhere_read_result_file payload disappeared scope=${scope.label} docId=${docId} filePath=${filePath}`);
|
|
1255
|
+
return textResult(formatStoredDocumentNotFound(docId, scope.label));
|
|
1256
|
+
}
|
|
1257
|
+
if (storedFile.text === null) {
|
|
1258
|
+
params.api.logger.warn(`knowhere: knowhere_read_result_file text content missing scope=${scope.label} docId=${docId} filePath=${filePath}`);
|
|
1259
|
+
return textResult([
|
|
1260
|
+
"Result file not found.",
|
|
1261
|
+
`File path: ${filePath}`,
|
|
1262
|
+
`Document ID: ${docId}`,
|
|
1263
|
+
`Scope: ${scope.label}`
|
|
1264
|
+
].join("\n"));
|
|
1265
|
+
}
|
|
950
1266
|
const mode = readResultFileReadMode(paramsRecord.mode);
|
|
951
1267
|
const maxStringChars = Math.min(2e4, Math.max(100, Math.trunc(readNumber(paramsRecord.maxStringChars, 4e3))));
|
|
952
1268
|
const normalizedText = stripUtf8Bom(storedFile.text);
|
|
1269
|
+
params.api.logger.info(`knowhere: knowhere_read_result_file reading file scope=${scope.label} docId=${docId} filePath=${filePath} kind=${resultFile.kind} mode=${mode} maxStringChars=${maxStringChars}`);
|
|
953
1270
|
if (mode === "json") {
|
|
954
1271
|
let parsedJson;
|
|
955
1272
|
try {
|
|
956
1273
|
parsedJson = JSON.parse(normalizedText);
|
|
957
1274
|
} catch (error) {
|
|
1275
|
+
params.api.logger.warn(`knowhere: knowhere_read_result_file invalid json scope=${scope.label} docId=${docId} filePath=${filePath} error=${formatErrorMessage(error)}`);
|
|
958
1276
|
throw new Error(`Result file ${filePath} is not valid JSON. ${formatErrorMessage(error)}`, { cause: error });
|
|
959
1277
|
}
|
|
960
1278
|
const truncatedJson = truncateJsonValue(parsedJson, maxStringChars);
|
|
1279
|
+
params.api.logger.info(`knowhere: knowhere_read_result_file parsed json scope=${scope.label} docId=${docId} filePath=${filePath} truncated=${truncatedJson.truncated}`);
|
|
961
1280
|
return formatJsonToolResult({
|
|
962
1281
|
scope: scope.label,
|
|
963
1282
|
docId: payload.document.id,
|
|
@@ -970,6 +1289,7 @@ function createReadResultFileTool(params) {
|
|
|
970
1289
|
});
|
|
971
1290
|
}
|
|
972
1291
|
const data = mode === "csv" ? buildCsvFilePayload(normalizedText, maxStringChars) : buildTextFilePayload(normalizedText, maxStringChars);
|
|
1292
|
+
params.api.logger.info(`knowhere: knowhere_read_result_file prepared text payload scope=${scope.label} docId=${docId} filePath=${filePath} lineCount=${data.lineCount}`);
|
|
973
1293
|
return formatJsonToolResult({
|
|
974
1294
|
scope: scope.label,
|
|
975
1295
|
docId: payload.document.id,
|
|
@@ -1001,7 +1321,10 @@ function createPreviewDocumentTool(params) {
|
|
|
1001
1321
|
if (!docId) throw new Error("docId is required.");
|
|
1002
1322
|
const scope = params.store.resolveScope(params.ctx);
|
|
1003
1323
|
const payload = await params.store.loadDocumentPayload(scope, docId);
|
|
1004
|
-
if (!payload)
|
|
1324
|
+
if (!payload) {
|
|
1325
|
+
params.api.logger.warn(`knowhere: knowhere_preview_document document not found scope=${scope.label} docId=${docId}`);
|
|
1326
|
+
return textResult(formatStoredDocumentNotFound(docId, scope.label));
|
|
1327
|
+
}
|
|
1005
1328
|
const { document } = payload;
|
|
1006
1329
|
const pathSummaryMap = /* @__PURE__ */ new Map();
|
|
1007
1330
|
for (const chunk of payload.chunks) {
|
|
@@ -1021,6 +1344,7 @@ function createPreviewDocumentTool(params) {
|
|
|
1021
1344
|
const pathByName = /* @__PURE__ */ new Map();
|
|
1022
1345
|
for (const p of payload.browseIndex.paths) pathByName.set(p.path, p);
|
|
1023
1346
|
const roots = payload.browseIndex.paths.filter((p) => p.depth === 1);
|
|
1347
|
+
params.api.logger.info(`knowhere: knowhere_preview_document building preview scope=${scope.label} docId=${docId} paths=${payload.browseIndex.paths.length} roots=${roots.length} chunks=${payload.chunks.length}`);
|
|
1024
1348
|
if (roots.length > 0) {
|
|
1025
1349
|
lines.push("");
|
|
1026
1350
|
lines.push("## Table of Contents");
|
|
@@ -1046,6 +1370,7 @@ function createPreviewDocumentTool(params) {
|
|
|
1046
1370
|
} else {
|
|
1047
1371
|
lines.push("");
|
|
1048
1372
|
lines.push("No structural paths available for this document.");
|
|
1373
|
+
params.api.logger.warn(`knowhere: knowhere_preview_document no structural paths scope=${scope.label} docId=${docId}`);
|
|
1049
1374
|
}
|
|
1050
1375
|
return textResult(lines.join("\n"));
|
|
1051
1376
|
}
|
|
@@ -1055,7 +1380,7 @@ function createListTool(params) {
|
|
|
1055
1380
|
return {
|
|
1056
1381
|
name: "knowhere_list_documents",
|
|
1057
1382
|
label: "Knowhere List",
|
|
1058
|
-
description: "List all Knowhere documents stored in the current scope. Returns each document's ID, title, source, chunk count, tags, and last-updated timestamp. Use this first to discover available documents and
|
|
1383
|
+
description: "List all Knowhere documents stored in the current scope. Returns each document's ID, title, source, chunk count, tags, and last-updated timestamp. Use this first to discover available documents, check whether a file or URL is already stored, and find the right docId before calling other tools.",
|
|
1059
1384
|
parameters: {
|
|
1060
1385
|
type: "object",
|
|
1061
1386
|
additionalProperties: false,
|
|
@@ -1063,7 +1388,9 @@ function createListTool(params) {
|
|
|
1063
1388
|
},
|
|
1064
1389
|
execute: async () => {
|
|
1065
1390
|
const scope = params.store.resolveScope(params.ctx);
|
|
1066
|
-
|
|
1391
|
+
const documents = await params.store.listDocuments(scope);
|
|
1392
|
+
params.api.logger.info(`knowhere: knowhere_list_documents listed documents scope=${scope.label} count=${documents.length}`);
|
|
1393
|
+
return textResult(formatDocumentList(documents, scope.label));
|
|
1067
1394
|
}
|
|
1068
1395
|
};
|
|
1069
1396
|
}
|
|
@@ -1085,9 +1412,13 @@ function createRemoveTool(params) {
|
|
|
1085
1412
|
const docId = readString((isRecord(rawParams) ? rawParams : {}).docId);
|
|
1086
1413
|
if (!docId) throw new Error("docId is required.");
|
|
1087
1414
|
const scope = params.store.resolveScope(params.ctx);
|
|
1415
|
+
params.api.logger.info(`knowhere: knowhere_remove_document removing document scope=${scope.label} docId=${docId}`);
|
|
1088
1416
|
const removed = await params.store.removeDocument(scope, docId);
|
|
1089
|
-
if (!removed)
|
|
1090
|
-
|
|
1417
|
+
if (!removed) {
|
|
1418
|
+
params.api.logger.warn(`knowhere: knowhere_remove_document document not found scope=${scope.label} docId=${docId}`);
|
|
1419
|
+
return textResult(formatStoredDocumentNotFound(docId, scope.label));
|
|
1420
|
+
}
|
|
1421
|
+
params.api.logger.info(`knowhere: knowhere_remove_document removed document scope=${scope.label} docId=${removed.id}`);
|
|
1091
1422
|
return textResult([
|
|
1092
1423
|
"Removed stored document.",
|
|
1093
1424
|
`Document ID: ${removed.id}`,
|
|
@@ -1113,9 +1444,38 @@ function createClearScopeTool(params) {
|
|
|
1113
1444
|
execute: async (_toolCallId, rawParams) => {
|
|
1114
1445
|
const paramsRecord = isRecord(rawParams) ? rawParams : {};
|
|
1115
1446
|
const scope = params.store.resolveScope(params.ctx);
|
|
1116
|
-
if (!readBoolean(paramsRecord.confirm, false))
|
|
1117
|
-
|
|
1118
|
-
|
|
1447
|
+
if (!readBoolean(paramsRecord.confirm, false)) {
|
|
1448
|
+
params.api.logger.warn(`knowhere: knowhere_clear_scope skipped without confirm scope=${scope.label}`);
|
|
1449
|
+
return textResult(`Set confirm=true to clear scope ${scope.label}.`);
|
|
1450
|
+
}
|
|
1451
|
+
params.api.logger.info(`knowhere: knowhere_clear_scope clearing scope scope=${scope.label}`);
|
|
1452
|
+
const removedDocuments = await params.store.clearScope(scope);
|
|
1453
|
+
params.api.logger.info(`knowhere: knowhere_clear_scope cleared scope scope=${scope.label} removed=${removedDocuments.length}`);
|
|
1454
|
+
return textResult(formatScopeClearResult(removedDocuments, scope.label));
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function createSetApiKeyTool(params) {
|
|
1459
|
+
return {
|
|
1460
|
+
name: "knowhere_set_api_key",
|
|
1461
|
+
label: "Set Knowhere API Key",
|
|
1462
|
+
description: "Save a Knowhere API key so the plugin can authenticate with the Knowhere service. The key is persisted in the plugin state directory and loaded automatically on next startup.",
|
|
1463
|
+
parameters: {
|
|
1464
|
+
type: "object",
|
|
1465
|
+
additionalProperties: false,
|
|
1466
|
+
properties: { apiKey: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
description: "The Knowhere API key to store."
|
|
1469
|
+
} },
|
|
1470
|
+
required: ["apiKey"]
|
|
1471
|
+
},
|
|
1472
|
+
execute: async (_toolCallId, rawParams) => {
|
|
1473
|
+
const apiKey = readString((isRecord(rawParams) ? rawParams : {}).apiKey);
|
|
1474
|
+
if (!apiKey) throw new Error("apiKey is required.");
|
|
1475
|
+
await persistApiKey(params.config.storageDir, apiKey);
|
|
1476
|
+
params.config.apiKey = apiKey;
|
|
1477
|
+
params.api.logger.info("knowhere: API key saved to plugin state");
|
|
1478
|
+
return textResult("Knowhere API key saved successfully. All Knowhere tools are now ready to use.");
|
|
1119
1479
|
}
|
|
1120
1480
|
};
|
|
1121
1481
|
}
|
|
@@ -1146,30 +1506,38 @@ function createKnowhereToolFactory(params) {
|
|
|
1146
1506
|
ctx
|
|
1147
1507
|
}),
|
|
1148
1508
|
createGrepTool({
|
|
1509
|
+
api: params.api,
|
|
1149
1510
|
store: params.store,
|
|
1150
1511
|
ctx
|
|
1151
1512
|
}),
|
|
1152
1513
|
createReadResultFileTool({
|
|
1514
|
+
api: params.api,
|
|
1153
1515
|
store: params.store,
|
|
1154
1516
|
ctx
|
|
1155
1517
|
}),
|
|
1156
1518
|
createPreviewDocumentTool({
|
|
1519
|
+
api: params.api,
|
|
1157
1520
|
store: params.store,
|
|
1158
1521
|
ctx
|
|
1159
1522
|
}),
|
|
1160
1523
|
createListTool({
|
|
1524
|
+
api: params.api,
|
|
1161
1525
|
store: params.store,
|
|
1162
1526
|
ctx
|
|
1163
1527
|
}),
|
|
1164
1528
|
createRemoveTool({
|
|
1529
|
+
api: params.api,
|
|
1165
1530
|
store: params.store,
|
|
1166
|
-
ctx
|
|
1167
|
-
autoGroundingController: params.autoGroundingController
|
|
1531
|
+
ctx
|
|
1168
1532
|
}),
|
|
1169
1533
|
createClearScopeTool({
|
|
1534
|
+
api: params.api,
|
|
1170
1535
|
store: params.store,
|
|
1171
|
-
ctx
|
|
1172
|
-
|
|
1536
|
+
ctx
|
|
1537
|
+
}),
|
|
1538
|
+
createSetApiKeyTool({
|
|
1539
|
+
api: params.api,
|
|
1540
|
+
config: params.config
|
|
1173
1541
|
})
|
|
1174
1542
|
];
|
|
1175
1543
|
}
|