plugin-agent-orchestrator 1.0.14 → 1.0.16
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 +96 -0
- package/dist/externalVersion.js +6 -6
- package/dist/server/plugin.js +11 -1
- package/dist/server/services/CodeValidator.js +1 -0
- package/dist/server/services/SkillManager.js +0 -39
- package/dist/server/skill-hub/plugin.js +3 -3
- package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +2 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +122 -0
- package/dist/server/tools/delegate-task.js +22 -2
- package/dist/server/tools/external-rag-search.d.ts +42 -0
- package/dist/server/tools/external-rag-search.js +140 -0
- package/dist/server/tools/skill-execute.d.ts +1 -1
- package/dist/server/tools/skill-execute.js +1 -2
- package/package.json +1 -1
- package/src/client/index.tsx +1 -1
- package/src/client/plugin.tsx +54 -54
- package/src/client/skill-hub/index.tsx +75 -75
- package/src/server/migrations/20260423000000-add-progress-fields.ts +5 -5
- package/src/server/migrations/20260425000000-add-interaction-schema.ts +5 -5
- package/src/server/migrations/20260427000000-add-tracing-detail-fields.ts +5 -5
- package/src/server/migrations/20260427000000-change-packages-to-text.ts +7 -7
- package/src/server/migrations/20260427000001-change-other-json-to-text.ts +10 -10
- package/src/server/migrations/20260429000000-add-llm-fields.ts +2 -2
- package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +2 -2
- package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +2 -2
- package/src/server/plugin.ts +23 -13
- package/src/server/services/CodeValidator.ts +5 -5
- package/src/server/services/SkillManager.ts +12 -52
- package/src/server/services/WorkerEnvManager.ts +5 -5
- package/src/server/skill-hub/plugin.ts +61 -61
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +162 -16
- package/src/server/tools/delegate-task.ts +25 -1
- package/src/server/tools/external-rag-search.ts +128 -0
- package/src/server/tools/skill-execute.ts +1 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import Application from '@nocobase/server';
|
|
2
|
-
import {
|
|
2
|
+
import { cpSync, existsSync, readFileSync, rmSync } from 'fs';
|
|
3
|
+
import { resolve, sep } from 'path';
|
|
3
4
|
import { SandboxRunner } from '../../services/SandboxRunner';
|
|
4
5
|
import { FileManager } from '../../services/FileManager';
|
|
5
6
|
import { SkillRepositoryService } from '../../services/SkillRepositoryService';
|
|
7
|
+
import { CodeValidator } from '../../services/CodeValidator';
|
|
6
8
|
import { parseJsonText, stringifyJsonText } from '../utils/json-fields';
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -62,13 +64,13 @@ export class SkillExecutionTask {
|
|
|
62
64
|
const abortController = new TaskAbortController();
|
|
63
65
|
const abortChannel = `skill-hub.abort.${execId}`;
|
|
64
66
|
const abortCallback = async () => {
|
|
65
|
-
this.app.logger.info(`[skill-hub] Task ${execId}: received abort signal`);
|
|
67
|
+
(this as any).app.logger.info(`[skill-hub] Task ${execId}: received abort signal`);
|
|
66
68
|
abortController.abort();
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
try {
|
|
70
72
|
// Subscribe to abort channel before starting execution
|
|
71
|
-
await this.app.pubSubManager.subscribe(abortChannel, abortCallback);
|
|
73
|
+
await (this as any).app.pubSubManager.subscribe(abortChannel, abortCallback);
|
|
72
74
|
|
|
73
75
|
// Render code template with input args
|
|
74
76
|
const inputArgs = parseJsonText(this.execution.get('inputArgs'), {});
|
|
@@ -84,7 +86,7 @@ export class SkillExecutionTask {
|
|
|
84
86
|
|
|
85
87
|
if (storageType === 'plugin') {
|
|
86
88
|
const pluginSkillName = (skill.get ? skill.get('pluginSource') : skill.pluginSource) || skillName;
|
|
87
|
-
const orchestratorPlugin = this.app.pm.get('plugin-agent-orchestrator') as any;
|
|
89
|
+
const orchestratorPlugin = (this as any).app.pm.get('plugin-agent-orchestrator') as any;
|
|
88
90
|
const skillHub = orchestratorPlugin?.skillHub;
|
|
89
91
|
let pluginTemplate = typeof skillHub?.resolveSkillTemplate === 'function'
|
|
90
92
|
? skillHub.resolveSkillTemplate(pluginSkillName)
|
|
@@ -92,7 +94,7 @@ export class SkillExecutionTask {
|
|
|
92
94
|
|
|
93
95
|
// Fallback: discover dynamically if not cached (e.g. executed in worker before UI was loaded)
|
|
94
96
|
if (!pluginTemplate && skillHub) {
|
|
95
|
-
const allPlugins = this.app.pm.getPlugins();
|
|
97
|
+
const allPlugins = (this as any).app.pm.getPlugins();
|
|
96
98
|
for (const [, pInstance] of allPlugins) {
|
|
97
99
|
if (typeof (pInstance as any).getSkillTemplates === 'function') {
|
|
98
100
|
const pluginSkills = (pInstance as any).getSkillTemplates();
|
|
@@ -148,7 +150,7 @@ export class SkillExecutionTask {
|
|
|
148
150
|
// Load package whitelist for import validation
|
|
149
151
|
let packageWhitelist: string[] = [];
|
|
150
152
|
try {
|
|
151
|
-
const workerConfig = await this.app.db.getRepository('skillWorkerConfigs').findOne();
|
|
153
|
+
const workerConfig = await (this as any).app.db.getRepository('skillWorkerConfigs').findOne();
|
|
152
154
|
if (workerConfig) {
|
|
153
155
|
const wl = parseJsonText(
|
|
154
156
|
workerConfig.get ? workerConfig.get('packageWhitelist') : workerConfig.packageWhitelist,
|
|
@@ -167,8 +169,8 @@ export class SkillExecutionTask {
|
|
|
167
169
|
|
|
168
170
|
// In multi-node setups, local cache might be missing on this specific worker node. Re-download from S3 if needed.
|
|
169
171
|
if (!require('fs').existsSync(this.skillRepoService.getSkillPath(skillName)) && fileId) {
|
|
170
|
-
const fmPlugin = this.app.pm.get('@nocobase/plugin-file-manager') as any;
|
|
171
|
-
const attachment = await this.app.db.getRepository('attachments').findOne({ filter: { id: fileId } });
|
|
172
|
+
const fmPlugin = (this as any).app.pm.get('@nocobase/plugin-file-manager') as any;
|
|
173
|
+
const attachment = await (this as any).app.db.getRepository('attachments').findOne({ filter: { id: fileId } });
|
|
172
174
|
if (fmPlugin && attachment) {
|
|
173
175
|
try {
|
|
174
176
|
const streamData = await fmPlugin.getFileStream(attachment);
|
|
@@ -183,12 +185,12 @@ export class SkillExecutionTask {
|
|
|
183
185
|
});
|
|
184
186
|
await this.skillRepoService.extractSkillPackage(skillName, tempZipPath);
|
|
185
187
|
require('fs').unlinkSync(tempZipPath);
|
|
186
|
-
this.app.logger.info(
|
|
188
|
+
(this as any).app.logger.info(
|
|
187
189
|
`[skill-hub] Task ${execId}: Auto-restored skill package ${skillName} from S3/Storage`,
|
|
188
190
|
);
|
|
189
191
|
}
|
|
190
192
|
} catch (fetchErr) {
|
|
191
|
-
this.app.logger.warn(
|
|
193
|
+
(this as any).app.logger.warn(
|
|
192
194
|
`[skill-hub] Task ${execId}: Failed to fetch skill package ${skillName} from storage`,
|
|
193
195
|
{ error: fetchErr },
|
|
194
196
|
);
|
|
@@ -212,10 +214,17 @@ export class SkillExecutionTask {
|
|
|
212
214
|
packageWhitelist,
|
|
213
215
|
onProgress: (progress) => {
|
|
214
216
|
// Worker → PubSub → Main Server → runtime.writer → SSE → Client
|
|
215
|
-
this.app.pubSubManager.publish(`skill-hub.progress.${execId}`, progress);
|
|
217
|
+
(this as any).app.pubSubManager.publish(`skill-hub.progress.${execId}`, progress);
|
|
216
218
|
},
|
|
217
219
|
});
|
|
218
220
|
|
|
221
|
+
if (result.success) {
|
|
222
|
+
const installMessage = await this.installGeneratedSkillIfRequested(execId);
|
|
223
|
+
if (installMessage) {
|
|
224
|
+
result.stdout = [result.stdout, installMessage].filter(Boolean).join('\n');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
219
228
|
// Determine final status
|
|
220
229
|
let status: string;
|
|
221
230
|
if (result.canceled) {
|
|
@@ -235,7 +244,7 @@ export class SkillExecutionTask {
|
|
|
235
244
|
});
|
|
236
245
|
|
|
237
246
|
// Notify main server: task completed
|
|
238
|
-
await this.app.pubSubManager.publish(`skill-hub.done.${execId}`, {
|
|
247
|
+
await (this as any).app.pubSubManager.publish(`skill-hub.done.${execId}`, {
|
|
239
248
|
status,
|
|
240
249
|
stdout: result.stdout?.slice(0, 3000),
|
|
241
250
|
stderr: result.stderr?.slice(0, 1000),
|
|
@@ -244,7 +253,7 @@ export class SkillExecutionTask {
|
|
|
244
253
|
});
|
|
245
254
|
|
|
246
255
|
// Log execution metrics
|
|
247
|
-
this.app.logger.info(
|
|
256
|
+
(this as any).app.logger.info(
|
|
248
257
|
`[skill-hub] Execution ${execId} ${status}: ` +
|
|
249
258
|
`skill=${skill.get ? skill.get('name') : skill.name}, ` +
|
|
250
259
|
`language=${language}, ` +
|
|
@@ -260,18 +269,18 @@ export class SkillExecutionTask {
|
|
|
260
269
|
stderr: errorMessage,
|
|
261
270
|
});
|
|
262
271
|
|
|
263
|
-
await this.app.pubSubManager.publish(`skill-hub.done.${execId}`, {
|
|
272
|
+
await (this as any).app.pubSubManager.publish(`skill-hub.done.${execId}`, {
|
|
264
273
|
status: 'failed',
|
|
265
274
|
stderr: errorMessage,
|
|
266
275
|
files: [],
|
|
267
276
|
durationMs: 0,
|
|
268
277
|
});
|
|
269
278
|
|
|
270
|
-
this.app.logger.error(`[skill-hub] Execution ${execId} error: ${errorMessage}`);
|
|
279
|
+
(this as any).app.logger.error(`[skill-hub] Execution ${execId} error: ${errorMessage}`);
|
|
271
280
|
} finally {
|
|
272
281
|
// Always cleanup abort subscription
|
|
273
282
|
try {
|
|
274
|
-
await this.app.pubSubManager.unsubscribe(abortChannel, abortCallback);
|
|
283
|
+
await (this as any).app.pubSubManager.unsubscribe(abortChannel, abortCallback);
|
|
275
284
|
} catch {
|
|
276
285
|
// ignore cleanup errors
|
|
277
286
|
}
|
|
@@ -294,4 +303,141 @@ export class SkillExecutionTask {
|
|
|
294
303
|
code = code.replaceAll('{{skillDir}}', (skillDir || '').replace(/\\/g, '/'));
|
|
295
304
|
return code;
|
|
296
305
|
}
|
|
306
|
+
|
|
307
|
+
private async installGeneratedSkillIfRequested(execId: string): Promise<string | null> {
|
|
308
|
+
const manifestPath = this.fileManager.getOutputFilePath(execId, 'skill-hub-install.json');
|
|
309
|
+
if (!manifestPath) return null;
|
|
310
|
+
|
|
311
|
+
let manifest: any;
|
|
312
|
+
try {
|
|
313
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw new Error(`Generated skill install manifest is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!manifest?.autoInstall) return null;
|
|
319
|
+
|
|
320
|
+
const skill = manifest.skill || {};
|
|
321
|
+
const name = String(skill.name || '').trim();
|
|
322
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
|
|
323
|
+
throw new Error(`Generated skill name "${name}" is invalid. Use lowercase letters, numbers, and hyphens.`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const language = skill.language;
|
|
327
|
+
if (language !== 'python' && language !== 'node') {
|
|
328
|
+
throw new Error(`Generated skill "${name}" has unsupported language "${language}".`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!skill.codeTemplate) {
|
|
332
|
+
throw new Error(`Generated skill "${name}" is missing codeTemplate.`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const validator = new CodeValidator();
|
|
336
|
+
validator.validate(skill.codeTemplate, language);
|
|
337
|
+
await this.validateGeneratedSkillPackages(name, language, skill.packages);
|
|
338
|
+
|
|
339
|
+
const outputDir = this.fileManager.getOutputDir(execId);
|
|
340
|
+
const outputRoot = resolve(outputDir);
|
|
341
|
+
const packageDirName = String(manifest.packageDir || name);
|
|
342
|
+
const packageDir = resolve(outputRoot, packageDirName);
|
|
343
|
+
if ((packageDir !== outputRoot && !packageDir.startsWith(outputRoot + sep)) || !existsSync(packageDir)) {
|
|
344
|
+
throw new Error(`Generated skill package directory "${packageDirName}" was not found in output.`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (manifest.testInput && Object.keys(manifest.testInput).length > 0) {
|
|
348
|
+
const verifyExecId = `${execId}-verify-${name}`;
|
|
349
|
+
const verifyCode = this.renderTemplate(skill.codeTemplate, manifest.testInput, verifyExecId, packageDir);
|
|
350
|
+
const verifyResult = await this.sandboxRunner.execute({
|
|
351
|
+
language,
|
|
352
|
+
code: verifyCode,
|
|
353
|
+
execId: verifyExecId,
|
|
354
|
+
timeoutSeconds: Math.min(Number(skill.timeoutSeconds || 60), 30),
|
|
355
|
+
maxOutputSizeMb: Math.min(Number(skill.maxOutputSizeMb || 50), 10),
|
|
356
|
+
skillDir: packageDir,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (!verifyResult.success) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Generated skill "${name}" failed smoke verification: ${verifyResult.stderr || verifyResult.stdout || 'unknown error'}`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const skillRepoDir = this.skillRepoService.getSkillPath(name);
|
|
367
|
+
if (existsSync(skillRepoDir)) {
|
|
368
|
+
rmSync(skillRepoDir, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
cpSync(packageDir, skillRepoDir, {
|
|
371
|
+
recursive: true,
|
|
372
|
+
force: true,
|
|
373
|
+
filter: (src) => {
|
|
374
|
+
const leaf = src.split(/[\\/]/).pop();
|
|
375
|
+
return !['node_modules', '.git', '__pycache__'].includes(leaf || '') && !src.endsWith('.pyc');
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const values: any = {
|
|
380
|
+
name,
|
|
381
|
+
title: skill.title || name,
|
|
382
|
+
description: skill.description || '',
|
|
383
|
+
instructions: skill.instructions || '',
|
|
384
|
+
language,
|
|
385
|
+
codeTemplate: skill.codeTemplate,
|
|
386
|
+
inputSchema: stringifyJsonText(skill.inputSchema || { type: 'object', properties: {} }),
|
|
387
|
+
packages: stringifyJsonText(skill.packages || [], []),
|
|
388
|
+
timeoutSeconds: skill.timeoutSeconds || 60,
|
|
389
|
+
maxOutputSizeMb: skill.maxOutputSizeMb || 50,
|
|
390
|
+
enabled: skill.enabled !== false,
|
|
391
|
+
toolScope: skill.toolScope || 'CUSTOM',
|
|
392
|
+
autoCall: !!skill.autoCall,
|
|
393
|
+
storageType: 'local',
|
|
394
|
+
storageUrl: `local://generated/${name}`,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
if (skill.interactionSchema) {
|
|
398
|
+
values.interactionSchema = stringifyJsonText(skill.interactionSchema);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const repo = (this as any).app.db.getRepository('skillDefinitions');
|
|
402
|
+
const existing = await repo.findOne({ filter: { name } });
|
|
403
|
+
if (existing) {
|
|
404
|
+
if (manifest.overwrite === false) {
|
|
405
|
+
throw new Error(`Skill "${name}" already exists and overwrite=false.`);
|
|
406
|
+
}
|
|
407
|
+
await repo.update({ filter: { name }, values });
|
|
408
|
+
return `[skill-hub] Updated generated skill "${name}" in Skill Hub.`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await repo.create({ values });
|
|
412
|
+
return `[skill-hub] Installed generated skill "${name}" in Skill Hub.`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async validateGeneratedSkillPackages(name: string, language: 'python' | 'node', packages: any) {
|
|
416
|
+
if (!Array.isArray(packages) || packages.length === 0) return;
|
|
417
|
+
if (!packages.every((pkg) => typeof pkg === 'string' && pkg.trim())) {
|
|
418
|
+
throw new Error(`Generated skill "${name}" has invalid packages. Use an array of package names.`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const workerConfig = await (this as any).app.db.getRepository('skillWorkerConfigs').findOne();
|
|
422
|
+
if (!workerConfig) return;
|
|
423
|
+
|
|
424
|
+
const whitelist = parseJsonText(
|
|
425
|
+
workerConfig.get ? workerConfig.get('packageWhitelist') : workerConfig.packageWhitelist,
|
|
426
|
+
null,
|
|
427
|
+
);
|
|
428
|
+
const allowed = whitelist?.[language];
|
|
429
|
+
if (!Array.isArray(allowed) || allowed.length === 0) return;
|
|
430
|
+
|
|
431
|
+
const allowedSet = new Set(allowed.map((pkg: string) => pkg.toLowerCase()));
|
|
432
|
+
const missing = packages
|
|
433
|
+
.map((pkg: string) => pkg.trim())
|
|
434
|
+
.filter((pkg: string) => !allowedSet.has(pkg.toLowerCase()));
|
|
435
|
+
|
|
436
|
+
if (missing.length > 0) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Generated skill "${name}" requires ${language} package(s) not available in the Skill Hub worker environment: ` +
|
|
439
|
+
`${missing.join(', ')}. Add them to the worker environment and refresh/init Skill Hub before installing this skill.`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
297
443
|
}
|
|
@@ -1046,13 +1046,37 @@ async function invokeDelegateTask(
|
|
|
1046
1046
|
});
|
|
1047
1047
|
|
|
1048
1048
|
// --- Step 4: Construct messages ---
|
|
1049
|
-
|
|
1049
|
+
let systemPrompt =
|
|
1050
1050
|
subAgentEmployee.chatSettings?.systemPrompt ||
|
|
1051
1051
|
subAgentEmployee.bio ||
|
|
1052
1052
|
`You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${
|
|
1053
1053
|
subAgentEmployee.about || ''
|
|
1054
1054
|
}`;
|
|
1055
1055
|
|
|
1056
|
+
// --- Step 4b: Inject shared context from Knowledge Base (soft dependency) ---
|
|
1057
|
+
// If plugin-knowledge-base is installed, inject the session context summary
|
|
1058
|
+
// so the sub-agent is aware of findings from previous agents in this run.
|
|
1059
|
+
try {
|
|
1060
|
+
const kbPlugin = ctx.app.pm.get('plugin-knowledge-base') as any;
|
|
1061
|
+
if (kbPlugin?.sessionContext) {
|
|
1062
|
+
const sessionId =
|
|
1063
|
+
ctx.action?.params?.values?.sessionId ||
|
|
1064
|
+
ctx.action?.params?.sessionId ||
|
|
1065
|
+
ctx.state?.sessionId;
|
|
1066
|
+
|
|
1067
|
+
const contextSummary = await kbPlugin.sessionContext.buildSummary(
|
|
1068
|
+
{ rootRunId, ...(sessionId ? { sessionId } : {}) },
|
|
1069
|
+
6000,
|
|
1070
|
+
);
|
|
1071
|
+
if (contextSummary) {
|
|
1072
|
+
systemPrompt += `\n\n<shared_context>\nThe following context was shared by other agents in this workflow. Use it to avoid redundant work:\n${contextSummary}\n</shared_context>`;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
} catch (e: any) {
|
|
1076
|
+
// Graceful fallback — never block delegation due to context injection failure.
|
|
1077
|
+
ctx.app.log?.debug?.(`[AgentOrchestrator] Shared context injection skipped: ${e.message}`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1056
1080
|
const combinedTask = context ? `Task: ${task}\n\nContext Provided:\n${context}` : `Task: ${task}`;
|
|
1057
1081
|
|
|
1058
1082
|
// --- Step 5: Execute with timeout + abort ---
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const TOOL_NAME = 'external_rag_search';
|
|
4
|
+
const MAX_CONTENT_LENGTH = 4000;
|
|
5
|
+
|
|
6
|
+
function truncate(value: unknown, max = MAX_CONTENT_LENGTH) {
|
|
7
|
+
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
8
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function firstString(...values: unknown[]) {
|
|
12
|
+
for (const value of values) {
|
|
13
|
+
if (typeof value === 'string' && value.trim()) {
|
|
14
|
+
return value.trim();
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeResult(result: any) {
|
|
24
|
+
const metadata = result?.metadata ?? {};
|
|
25
|
+
const sourceId = firstString(
|
|
26
|
+
result?.id,
|
|
27
|
+
metadata.id,
|
|
28
|
+
metadata.sourceId,
|
|
29
|
+
metadata.documentId,
|
|
30
|
+
metadata.docId,
|
|
31
|
+
metadata.fileId,
|
|
32
|
+
metadata.recordId,
|
|
33
|
+
);
|
|
34
|
+
const filename = firstString(metadata.filename, metadata.fileName, metadata.name, metadata.title, metadata.source);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
content: truncate(result?.content),
|
|
38
|
+
score: Number(result?.rerankScore ?? result?.score ?? result?.vectorScore ?? 0),
|
|
39
|
+
knowledgeBaseId: result?.knowledgeBaseId,
|
|
40
|
+
knowledgeBaseName: result?.knowledgeBaseName,
|
|
41
|
+
source: {
|
|
42
|
+
id: sourceId,
|
|
43
|
+
filename,
|
|
44
|
+
url: firstString(metadata.url, metadata.fileUrl, metadata.sourceUrl),
|
|
45
|
+
collection: firstString(metadata.collection, metadata.collectionName),
|
|
46
|
+
recordId: firstString(metadata.recordId, metadata.rowId),
|
|
47
|
+
},
|
|
48
|
+
metadata,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createExternalRagSearchTool(plugin: any) {
|
|
53
|
+
return {
|
|
54
|
+
scope: 'CUSTOM' as const,
|
|
55
|
+
execution: 'backend' as const,
|
|
56
|
+
defaultPermission: 'ALLOW' as const,
|
|
57
|
+
|
|
58
|
+
introduction: {
|
|
59
|
+
title: 'External RAG Search',
|
|
60
|
+
about:
|
|
61
|
+
'Search NocoBase knowledge bases through plugin-knowledge-base, including EXTERNAL_RAG services that own chunking, embedding, and retrieval.',
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
definition: {
|
|
65
|
+
name: TOOL_NAME,
|
|
66
|
+
description: `Search configured knowledge bases for relevant context. Use this before answering questions that require documents, files, or datasource-backed knowledge.
|
|
67
|
+
|
|
68
|
+
The search may be served by an external RAG service. Results include content plus source identifiers such as id, filename, collection, and recordId when the external service provides them.`,
|
|
69
|
+
schema: z.object({
|
|
70
|
+
query: z.string().min(1).describe('Natural-language search query.'),
|
|
71
|
+
knowledgeBaseIds: z
|
|
72
|
+
.array(z.string().min(1))
|
|
73
|
+
.optional()
|
|
74
|
+
.describe(
|
|
75
|
+
'Optional list of NocoBase knowledge base IDs to search. If omitted, all accessible KBs are searched.',
|
|
76
|
+
),
|
|
77
|
+
topK: z.number().int().min(1).max(20).optional().describe('Maximum results to return. Default 5, max 20.'),
|
|
78
|
+
scoreThreshold: z
|
|
79
|
+
.number()
|
|
80
|
+
.min(0)
|
|
81
|
+
.max(1)
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Minimum relevance score. Default is controlled by plugin-knowledge-base.'),
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
invoke: async (
|
|
88
|
+
ctx: any,
|
|
89
|
+
args: { query?: string; knowledgeBaseIds?: string[]; topK?: number; scoreThreshold?: number },
|
|
90
|
+
) => {
|
|
91
|
+
const query = typeof args?.query === 'string' ? args.query.trim() : '';
|
|
92
|
+
if (!query) {
|
|
93
|
+
return { status: 'error' as const, content: 'Missing required field: query.' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const kbPlugin = ctx?.app?.pm?.get?.('plugin-knowledge-base');
|
|
97
|
+
if (!kbPlugin?.searchKnowledgeBases) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'error' as const,
|
|
100
|
+
content: 'plugin-knowledge-base is not installed or does not expose searchKnowledgeBases().',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const results = await kbPlugin.searchKnowledgeBases(ctx, query, {
|
|
106
|
+
knowledgeBaseIds: Array.isArray(args.knowledgeBaseIds) ? args.knowledgeBaseIds.map(String) : undefined,
|
|
107
|
+
topK: args.topK,
|
|
108
|
+
scoreThreshold: args.scoreThreshold,
|
|
109
|
+
rerank: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
status: 'success' as const,
|
|
114
|
+
content: JSON.stringify({
|
|
115
|
+
query,
|
|
116
|
+
results: (results ?? []).map(normalizeResult),
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
plugin.app.log?.error?.('[AgentOrchestrator] external_rag_search failed', error);
|
|
121
|
+
return {
|
|
122
|
+
status: 'error' as const,
|
|
123
|
+
content: `External RAG search failed: ${error?.message || String(error)}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -42,8 +42,7 @@ IMPORTANT: If the skill returns file download URLs, you MUST format them as clic
|
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
|
|
45
|
-
async invoke(args: Record<string, any>,
|
|
46
|
-
const ctx = options?.context;
|
|
45
|
+
async invoke(ctx: any, args: Record<string, any>, _id?: string) {
|
|
47
46
|
plugin.app.logger.info(`[skill-execute] Tool invoked with action: ${args.action}, skillName: ${args.skillName}`);
|
|
48
47
|
|
|
49
48
|
// Action: list available skills
|