plugin-agent-orchestrator 1.0.15 → 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/server/collections/agent-execution-spans.d.ts +1 -1
- package/dist/server/collections/orchestrator-config.d.ts +1 -1
- package/dist/server/collections/orchestrator-logs.d.ts +1 -1
- package/dist/server/collections/skill-definitions.d.ts +1 -0
- package/dist/server/collections/skill-executions.d.ts +1 -0
- package/dist/server/collections/skill-worker-configs.d.ts +1 -0
- package/dist/server/plugin.js +11 -1
- package/dist/server/services/SkillManager.js +0 -39
- package/dist/server/skill-hub/plugin.js +3 -3
- 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/server/plugin.ts +104 -94
- package/src/server/services/SkillManager.ts +13 -53
- package/src/server/skill-hub/plugin.ts +3 -3
- 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
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Hierarchical Multi-Agent orchestration for NocoBase AI Employees. Enables Leader
|
|
|
7
7
|
- **Hierarchical Delegation**: Allows AI Leader agents to break down complex tasks and assign them to specialized sub-agents.
|
|
8
8
|
- **Seamless Integration**: Plugs directly into the existing AI Employee framework.
|
|
9
9
|
- **Execution Tracking**: Monitor sub-agent task execution and responses within the main chat interface.
|
|
10
|
+
- **External RAG Search**: Exposes `external_rag_search` so leaders and sub-agents can retrieve context from NocoBase knowledge bases, including external RAG services.
|
|
10
11
|
|
|
11
12
|
## Usage
|
|
12
13
|
1. Enable the plugin in the NocoBase Plugin Manager.
|
|
@@ -14,3 +15,98 @@ Hierarchical Multi-Agent orchestration for NocoBase AI Employees. Enables Leader
|
|
|
14
15
|
3. Configure a primary "Leader" agent.
|
|
15
16
|
4. Add available "Sub-Agents" as tools or skills to the Leader agent.
|
|
16
17
|
5. Interact with the Leader agent; it will automatically delegate tasks when necessary.
|
|
18
|
+
|
|
19
|
+
## External RAG / Embedding service
|
|
20
|
+
|
|
21
|
+
Recommended approach: keep source ownership in NocoBase and move chunking, embedding, vector storage, and retrieval to an external lightweight RAG service. This plugin should not own vector infrastructure directly. It delegates search to `plugin-knowledge-base`, which already supports knowledge bases of type `EXTERNAL_RAG`.
|
|
22
|
+
|
|
23
|
+
### Architecture
|
|
24
|
+
|
|
25
|
+
1. NocoBase stores the source metadata:
|
|
26
|
+
- uploaded file attachment id, filename, storage id, URL, owner/role access, or
|
|
27
|
+
- datasource/collection/record id for database-backed knowledge.
|
|
28
|
+
2. The external RAG service owns:
|
|
29
|
+
- document fetching or receiving source payloads,
|
|
30
|
+
- parsing/chunking,
|
|
31
|
+
- embedding,
|
|
32
|
+
- vector index,
|
|
33
|
+
- retrieval.
|
|
34
|
+
3. NocoBase creates a knowledge base with `type = EXTERNAL_RAG` and `options` such as:
|
|
35
|
+
- `ragProvider: "external-http"`
|
|
36
|
+
- `ragApiUrl: "https://rag.example.com/search"`
|
|
37
|
+
- `ragApiKey`
|
|
38
|
+
- `ragNamespace`
|
|
39
|
+
- `ragTopK`
|
|
40
|
+
- `ragScoreThreshold`
|
|
41
|
+
4. Agents use the `external_rag_search` tool. The tool calls `plugin-knowledge-base.searchKnowledgeBases()`, so access control and mixed local/external search remain centralized.
|
|
42
|
+
|
|
43
|
+
### External search contract
|
|
44
|
+
|
|
45
|
+
The built-in `external-http` strategy expects:
|
|
46
|
+
|
|
47
|
+
```http
|
|
48
|
+
POST /search
|
|
49
|
+
Authorization: Bearer <ragApiKey>
|
|
50
|
+
Content-Type: application/json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"query": "search text",
|
|
56
|
+
"topK": 5,
|
|
57
|
+
"scoreThreshold": 0.3,
|
|
58
|
+
"namespace": "optional-kb-namespace",
|
|
59
|
+
"filter": {}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Response:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"results": [
|
|
68
|
+
{
|
|
69
|
+
"id": "chunk-or-source-id",
|
|
70
|
+
"content": "matched text",
|
|
71
|
+
"score": 0.82,
|
|
72
|
+
"metadata": {
|
|
73
|
+
"fileId": "123",
|
|
74
|
+
"filename": "contract.pdf",
|
|
75
|
+
"collection": "orders",
|
|
76
|
+
"recordId": "456",
|
|
77
|
+
"sourceUrl": "/api/attachments/123:download"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The important rule is that every result should return enough metadata for NocoBase to resolve the original source: `fileId`/`filename` for files, or `collection`/`recordId` for datasource records.
|
|
85
|
+
|
|
86
|
+
### Lightweight open-source service target
|
|
87
|
+
|
|
88
|
+
For a small but useful deployment, use a standalone service with:
|
|
89
|
+
|
|
90
|
+
- Qdrant or LanceDB for vector search.
|
|
91
|
+
- FastAPI or Express for `/ingest`, `/delete`, and `/search`.
|
|
92
|
+
- A small embedding model such as `BAAI/bge-small-en-v1.5`, `intfloat/multilingual-e5-small`, or another model matching the deployment language.
|
|
93
|
+
- A namespace per NocoBase knowledge base.
|
|
94
|
+
|
|
95
|
+
Minimum API surface:
|
|
96
|
+
|
|
97
|
+
- `POST /ingest`: receive `{ namespace, source, content | fileUrl | storageRef, metadata }`.
|
|
98
|
+
- `POST /delete`: receive `{ namespace, sourceId }`.
|
|
99
|
+
- `POST /search`: implement the contract above.
|
|
100
|
+
|
|
101
|
+
For NocoBase datasource knowledge, send each record as a source document with metadata `{ collection, recordId, fields, updatedAt }`; the RAG service chunks and embeds the selected text fields, then returns `collection` and `recordId` in search results.
|
|
102
|
+
|
|
103
|
+
### E5/OpenAI-compatible embedding
|
|
104
|
+
|
|
105
|
+
If the external RAG service uses an E5 family model behind an OpenAI-compatible `/v1/embeddings` API, configure the NocoBase knowledge base with `ragProvider: "e5-http"`. `plugin-knowledge-base` will read the selected embedding LLM service and forward its `baseURL`, `apiKey`, and model to the RAG service.
|
|
106
|
+
|
|
107
|
+
- embed user queries as `query: <question>`;
|
|
108
|
+
- embed document chunks as `passage: <chunk>`;
|
|
109
|
+
- use the same model for ingest and search;
|
|
110
|
+
- recreate the vector collection when changing models or vector dimensions.
|
|
111
|
+
|
|
112
|
+
The RAG service still owns chunking, embedding calls, vector storage, and metadata mapping. NocoBase owns the LLM service configuration and search authorization path.
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* keeps its own sandbox execution records; this collection stores the flow
|
|
6
6
|
* relationship and links to those records when applicable.
|
|
7
7
|
*/
|
|
8
|
-
declare const _default:
|
|
8
|
+
declare const _default: import("@nocobase/database").CollectionOptions;
|
|
9
9
|
export default _default;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("@nocobase/database").CollectionOptions;
|
|
2
2
|
export default _default;
|
package/dist/server/plugin.js
CHANGED
|
@@ -43,6 +43,7 @@ module.exports = __toCommonJS(plugin_exports);
|
|
|
43
43
|
var import_server = require("@nocobase/server");
|
|
44
44
|
var import_path = __toESM(require("path"));
|
|
45
45
|
var import_delegate_task = require("./tools/delegate-task");
|
|
46
|
+
var import_external_rag_search = require("./tools/external-rag-search");
|
|
46
47
|
var import_tracing = require("./resources/tracing");
|
|
47
48
|
var import_plugin = __toESM(require("./skill-hub/plugin"));
|
|
48
49
|
class PluginAgentOrchestratorServer extends import_server.Plugin {
|
|
@@ -62,9 +63,18 @@ class PluginAgentOrchestratorServer extends import_server.Plugin {
|
|
|
62
63
|
await this.skillHub.load();
|
|
63
64
|
this.app.acl.registerSnippet({
|
|
64
65
|
name: `pm.${this.name}`,
|
|
65
|
-
actions: [
|
|
66
|
+
actions: [
|
|
67
|
+
"orchestratorConfig:*",
|
|
68
|
+
"orchestratorTracing:*",
|
|
69
|
+
"agentExecutionSpans:*",
|
|
70
|
+
"skillDefinitions:*",
|
|
71
|
+
"skillExecutions:*",
|
|
72
|
+
"skillHub:*",
|
|
73
|
+
"skillWorkerConfigs:*"
|
|
74
|
+
]
|
|
66
75
|
});
|
|
67
76
|
const toolsManager = this.app.aiManager.toolsManager;
|
|
77
|
+
toolsManager.registerTools((0, import_external_rag_search.createExternalRagSearchTool)(this));
|
|
68
78
|
toolsManager.registerDynamicTools((0, import_delegate_task.createDelegateToolsProvider)(this));
|
|
69
79
|
(0, import_tracing.registerTracingResource)(this);
|
|
70
80
|
this.app.cronJobManager.addJob({
|
|
@@ -347,45 +347,6 @@ if table_data_raw and table_data_raw != '{{' + 'tableData}}':
|
|
|
347
347
|
doc.build(story)
|
|
348
348
|
print('Generated: report.pdf')
|
|
349
349
|
`;
|
|
350
|
-
const SEED_PPTX = `import os, json
|
|
351
|
-
from pptx import Presentation
|
|
352
|
-
from pptx.util import Inches, Pt
|
|
353
|
-
from pptx.enum.text import PP_ALIGN
|
|
354
|
-
|
|
355
|
-
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
356
|
-
subtitle_raw = '''{{subtitle}}'''
|
|
357
|
-
subtitle = json.loads(subtitle_raw) if subtitle_raw.startswith('"') else subtitle_raw if subtitle_raw != '{{' + 'subtitle}}' else ''
|
|
358
|
-
slides_data = json.loads('''{{slides}}''')
|
|
359
|
-
|
|
360
|
-
prs = Presentation()
|
|
361
|
-
prs.slide_width = Inches(13.333)
|
|
362
|
-
prs.slide_height = Inches(7.5)
|
|
363
|
-
|
|
364
|
-
# Title slide
|
|
365
|
-
slide = prs.slides.add_slide(prs.slide_layouts[0])
|
|
366
|
-
slide.shapes.title.text = title
|
|
367
|
-
if subtitle and slide.placeholders[1]:
|
|
368
|
-
slide.placeholders[1].text = subtitle
|
|
369
|
-
|
|
370
|
-
# Content slides
|
|
371
|
-
for s in slides_data:
|
|
372
|
-
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
|
373
|
-
slide.shapes.title.text = s.get('title', '')
|
|
374
|
-
body = slide.placeholders[1].text_frame
|
|
375
|
-
body.clear()
|
|
376
|
-
for i, bullet in enumerate(s.get('bullets', [])):
|
|
377
|
-
if i == 0:
|
|
378
|
-
body.paragraphs[0].text = bullet
|
|
379
|
-
else:
|
|
380
|
-
p = body.add_paragraph()
|
|
381
|
-
p.text = bullet
|
|
382
|
-
body.paragraphs[-1].font.size = Pt(18)
|
|
383
|
-
|
|
384
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
385
|
-
filepath = os.path.join(output_dir, 'presentation.pptx')
|
|
386
|
-
prs.save(filepath)
|
|
387
|
-
print('Generated: presentation.pptx')
|
|
388
|
-
`;
|
|
389
350
|
const SEED_CHART = `import os, json
|
|
390
351
|
import matplotlib
|
|
391
352
|
matplotlib.use('Agg')
|
|
@@ -498,7 +498,7 @@ IMPORTANT: This skill requires human confirmation. Pass best-effort args; the us
|
|
|
498
498
|
filter: { id: skill.get("id"), enabled: true }
|
|
499
499
|
});
|
|
500
500
|
if (!latestSkill) {
|
|
501
|
-
return { error: `Skill "${skill.get("name")}" is no longer available` };
|
|
501
|
+
return { status: "error", content: `Skill "${skill.get("name")}" is no longer available` };
|
|
502
502
|
}
|
|
503
503
|
const result = await this.executeSkill(latestSkill, args, toolCtx);
|
|
504
504
|
return {
|
|
@@ -530,7 +530,7 @@ IMPORTANT: This skill requires human confirmation. Pass best-effort args; the us
|
|
|
530
530
|
const cutoff = new Date(Date.now() - MAX_AGE_MS);
|
|
531
531
|
const repo = this.db.getRepository("skillExecutions");
|
|
532
532
|
const outdated = await repo.find({
|
|
533
|
-
|
|
533
|
+
filter: { createdAt: { $lt: cutoff } }
|
|
534
534
|
});
|
|
535
535
|
if (outdated.length > 0) {
|
|
536
536
|
for (const record of outdated) {
|
|
@@ -579,7 +579,7 @@ IMPORTANT: This skill requires human confirmation. Pass best-effort args; the us
|
|
|
579
579
|
const hours = config ? config.get("retentionHours") : 24;
|
|
580
580
|
if (hours > 0) {
|
|
581
581
|
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1e3);
|
|
582
|
-
const results = await repo.find({
|
|
582
|
+
const results = await repo.find({ filter: { createdAt: { $lt: cutoff } }, fields: ["id"] });
|
|
583
583
|
for (const rec of results) {
|
|
584
584
|
await rec.destroy();
|
|
585
585
|
}
|
|
@@ -554,7 +554,7 @@ function invalidateDelegateToolsCache() {
|
|
|
554
554
|
registeredDelegateNamesByPlugin = /* @__PURE__ */ new WeakMap();
|
|
555
555
|
}
|
|
556
556
|
async function invokeDelegateTask(ctx, plugin, options) {
|
|
557
|
-
var _a, _b, _c, _d, _e;
|
|
557
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
558
558
|
const {
|
|
559
559
|
leaderUsername,
|
|
560
560
|
subAgentUsername,
|
|
@@ -771,7 +771,27 @@ async function invokeDelegateTask(ctx, plugin, options) {
|
|
|
771
771
|
llm: chatModel,
|
|
772
772
|
tools: langchainTools
|
|
773
773
|
});
|
|
774
|
-
|
|
774
|
+
let systemPrompt = ((_e = subAgentEmployee.chatSettings) == null ? void 0 : _e.systemPrompt) || subAgentEmployee.bio || `You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${subAgentEmployee.about || ""}`;
|
|
775
|
+
try {
|
|
776
|
+
const kbPlugin = ctx.app.pm.get("plugin-knowledge-base");
|
|
777
|
+
if (kbPlugin == null ? void 0 : kbPlugin.sessionContext) {
|
|
778
|
+
const sessionId = ((_h = (_g = (_f = ctx.action) == null ? void 0 : _f.params) == null ? void 0 : _g.values) == null ? void 0 : _h.sessionId) || ((_j = (_i = ctx.action) == null ? void 0 : _i.params) == null ? void 0 : _j.sessionId) || ((_k = ctx.state) == null ? void 0 : _k.sessionId);
|
|
779
|
+
const contextSummary = await kbPlugin.sessionContext.buildSummary(
|
|
780
|
+
{ rootRunId, ...sessionId ? { sessionId } : {} },
|
|
781
|
+
6e3
|
|
782
|
+
);
|
|
783
|
+
if (contextSummary) {
|
|
784
|
+
systemPrompt += `
|
|
785
|
+
|
|
786
|
+
<shared_context>
|
|
787
|
+
The following context was shared by other agents in this workflow. Use it to avoid redundant work:
|
|
788
|
+
${contextSummary}
|
|
789
|
+
</shared_context>`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
} catch (e) {
|
|
793
|
+
(_m = (_l = ctx.app.log) == null ? void 0 : _l.debug) == null ? void 0 : _m.call(_l, `[AgentOrchestrator] Shared context injection skipped: ${e.message}`);
|
|
794
|
+
}
|
|
775
795
|
const combinedTask = context ? `Task: ${task}
|
|
776
796
|
|
|
777
797
|
Context Provided:
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare function createExternalRagSearchTool(plugin: any): {
|
|
3
|
+
scope: "CUSTOM";
|
|
4
|
+
execution: "backend";
|
|
5
|
+
defaultPermission: "ALLOW";
|
|
6
|
+
introduction: {
|
|
7
|
+
title: string;
|
|
8
|
+
about: string;
|
|
9
|
+
};
|
|
10
|
+
definition: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
schema: z.ZodObject<{
|
|
14
|
+
query: z.ZodString;
|
|
15
|
+
knowledgeBaseIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
16
|
+
topK: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
scoreThreshold: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
query?: string;
|
|
20
|
+
knowledgeBaseIds?: string[];
|
|
21
|
+
topK?: number;
|
|
22
|
+
scoreThreshold?: number;
|
|
23
|
+
}, {
|
|
24
|
+
query?: string;
|
|
25
|
+
knowledgeBaseIds?: string[];
|
|
26
|
+
topK?: number;
|
|
27
|
+
scoreThreshold?: number;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
invoke: (ctx: any, args: {
|
|
31
|
+
query?: string;
|
|
32
|
+
knowledgeBaseIds?: string[];
|
|
33
|
+
topK?: number;
|
|
34
|
+
scoreThreshold?: number;
|
|
35
|
+
}) => Promise<{
|
|
36
|
+
status: "error";
|
|
37
|
+
content: string;
|
|
38
|
+
} | {
|
|
39
|
+
status: "success";
|
|
40
|
+
content: string;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var external_rag_search_exports = {};
|
|
28
|
+
__export(external_rag_search_exports, {
|
|
29
|
+
createExternalRagSearchTool: () => createExternalRagSearchTool
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(external_rag_search_exports);
|
|
32
|
+
var import_zod = require("zod");
|
|
33
|
+
const TOOL_NAME = "external_rag_search";
|
|
34
|
+
const MAX_CONTENT_LENGTH = 4e3;
|
|
35
|
+
function truncate(value, max = MAX_CONTENT_LENGTH) {
|
|
36
|
+
const text = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
37
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
38
|
+
}
|
|
39
|
+
function firstString(...values) {
|
|
40
|
+
for (const value of values) {
|
|
41
|
+
if (typeof value === "string" && value.trim()) {
|
|
42
|
+
return value.trim();
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
function normalizeResult(result) {
|
|
51
|
+
const metadata = (result == null ? void 0 : result.metadata) ?? {};
|
|
52
|
+
const sourceId = firstString(
|
|
53
|
+
result == null ? void 0 : result.id,
|
|
54
|
+
metadata.id,
|
|
55
|
+
metadata.sourceId,
|
|
56
|
+
metadata.documentId,
|
|
57
|
+
metadata.docId,
|
|
58
|
+
metadata.fileId,
|
|
59
|
+
metadata.recordId
|
|
60
|
+
);
|
|
61
|
+
const filename = firstString(metadata.filename, metadata.fileName, metadata.name, metadata.title, metadata.source);
|
|
62
|
+
return {
|
|
63
|
+
content: truncate(result == null ? void 0 : result.content),
|
|
64
|
+
score: Number((result == null ? void 0 : result.rerankScore) ?? (result == null ? void 0 : result.score) ?? (result == null ? void 0 : result.vectorScore) ?? 0),
|
|
65
|
+
knowledgeBaseId: result == null ? void 0 : result.knowledgeBaseId,
|
|
66
|
+
knowledgeBaseName: result == null ? void 0 : result.knowledgeBaseName,
|
|
67
|
+
source: {
|
|
68
|
+
id: sourceId,
|
|
69
|
+
filename,
|
|
70
|
+
url: firstString(metadata.url, metadata.fileUrl, metadata.sourceUrl),
|
|
71
|
+
collection: firstString(metadata.collection, metadata.collectionName),
|
|
72
|
+
recordId: firstString(metadata.recordId, metadata.rowId)
|
|
73
|
+
},
|
|
74
|
+
metadata
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function createExternalRagSearchTool(plugin) {
|
|
78
|
+
return {
|
|
79
|
+
scope: "CUSTOM",
|
|
80
|
+
execution: "backend",
|
|
81
|
+
defaultPermission: "ALLOW",
|
|
82
|
+
introduction: {
|
|
83
|
+
title: "External RAG Search",
|
|
84
|
+
about: "Search NocoBase knowledge bases through plugin-knowledge-base, including EXTERNAL_RAG services that own chunking, embedding, and retrieval."
|
|
85
|
+
},
|
|
86
|
+
definition: {
|
|
87
|
+
name: TOOL_NAME,
|
|
88
|
+
description: `Search configured knowledge bases for relevant context. Use this before answering questions that require documents, files, or datasource-backed knowledge.
|
|
89
|
+
|
|
90
|
+
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.`,
|
|
91
|
+
schema: import_zod.z.object({
|
|
92
|
+
query: import_zod.z.string().min(1).describe("Natural-language search query."),
|
|
93
|
+
knowledgeBaseIds: import_zod.z.array(import_zod.z.string().min(1)).optional().describe(
|
|
94
|
+
"Optional list of NocoBase knowledge base IDs to search. If omitted, all accessible KBs are searched."
|
|
95
|
+
),
|
|
96
|
+
topK: import_zod.z.number().int().min(1).max(20).optional().describe("Maximum results to return. Default 5, max 20."),
|
|
97
|
+
scoreThreshold: import_zod.z.number().min(0).max(1).optional().describe("Minimum relevance score. Default is controlled by plugin-knowledge-base.")
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
invoke: async (ctx, args) => {
|
|
101
|
+
var _a, _b, _c, _d, _e;
|
|
102
|
+
const query = typeof (args == null ? void 0 : args.query) === "string" ? args.query.trim() : "";
|
|
103
|
+
if (!query) {
|
|
104
|
+
return { status: "error", content: "Missing required field: query." };
|
|
105
|
+
}
|
|
106
|
+
const kbPlugin = (_c = (_b = (_a = ctx == null ? void 0 : ctx.app) == null ? void 0 : _a.pm) == null ? void 0 : _b.get) == null ? void 0 : _c.call(_b, "plugin-knowledge-base");
|
|
107
|
+
if (!(kbPlugin == null ? void 0 : kbPlugin.searchKnowledgeBases)) {
|
|
108
|
+
return {
|
|
109
|
+
status: "error",
|
|
110
|
+
content: "plugin-knowledge-base is not installed or does not expose searchKnowledgeBases()."
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const results = await kbPlugin.searchKnowledgeBases(ctx, query, {
|
|
115
|
+
knowledgeBaseIds: Array.isArray(args.knowledgeBaseIds) ? args.knowledgeBaseIds.map(String) : void 0,
|
|
116
|
+
topK: args.topK,
|
|
117
|
+
scoreThreshold: args.scoreThreshold,
|
|
118
|
+
rerank: true
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
status: "success",
|
|
122
|
+
content: JSON.stringify({
|
|
123
|
+
query,
|
|
124
|
+
results: (results ?? []).map(normalizeResult)
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
(_e = (_d = plugin.app.log) == null ? void 0 : _d.error) == null ? void 0 : _e.call(_d, "[AgentOrchestrator] external_rag_search failed", error);
|
|
129
|
+
return {
|
|
130
|
+
status: "error",
|
|
131
|
+
content: `External RAG search failed: ${(error == null ? void 0 : error.message) || String(error)}`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
138
|
+
0 && (module.exports = {
|
|
139
|
+
createExternalRagSearchTool
|
|
140
|
+
});
|
|
@@ -29,7 +29,7 @@ export declare function createSkillExecuteTool(plugin: any): {
|
|
|
29
29
|
required: string[];
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
|
-
invoke(args: Record<string, any>,
|
|
32
|
+
invoke(ctx: any, args: Record<string, any>, _id?: string): Promise<{
|
|
33
33
|
status: string;
|
|
34
34
|
content: string;
|
|
35
35
|
}>;
|
|
@@ -69,9 +69,8 @@ IMPORTANT: If the skill returns file download URLs, you MUST format them as clic
|
|
|
69
69
|
required: ["action"]
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
|
-
async invoke(args,
|
|
72
|
+
async invoke(ctx, args, _id) {
|
|
73
73
|
var _a;
|
|
74
|
-
const ctx = options == null ? void 0 : options.context;
|
|
75
74
|
plugin.app.logger.info(`[skill-execute] Tool invoked with action: ${args.action}, skillName: ${args.skillName}`);
|
|
76
75
|
if (args.action === "list") {
|
|
77
76
|
const skills = await plugin.db.getRepository("skillDefinitions").find({
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"displayName.zh-CN": "代理协调器",
|
|
5
5
|
"displayName.vi-VN": "Điều phối Agent",
|
|
6
6
|
"description": "Hierarchical Multi-Agent orchestration for NocoBase AI Employees. Enables Leader agents to delegate tasks to Sub-Agent employees without modifying core plugin-ai.",
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.16",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"main": "dist/server/index.js",
|
|
10
10
|
"keywords": [
|
package/src/server/plugin.ts
CHANGED
|
@@ -1,94 +1,104 @@
|
|
|
1
|
-
import { Plugin } from '@nocobase/server';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { createDelegateToolsProvider } from './tools/delegate-task';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
(this
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createDelegateToolsProvider } from './tools/delegate-task';
|
|
4
|
+
import { createExternalRagSearchTool } from './tools/external-rag-search';
|
|
5
|
+
import { registerTracingResource } from './resources/tracing';
|
|
6
|
+
import SkillHubSubFeature from './skill-hub/plugin';
|
|
7
|
+
|
|
8
|
+
export class PluginAgentOrchestratorServer extends Plugin {
|
|
9
|
+
skillHub: SkillHubSubFeature;
|
|
10
|
+
|
|
11
|
+
async afterAdd() {
|
|
12
|
+
this.skillHub = new SkillHubSubFeature(this);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async beforeLoad() {
|
|
16
|
+
// Import collection definitions
|
|
17
|
+
(this as any).db.import({ directory: path.resolve(__dirname, 'collections') });
|
|
18
|
+
|
|
19
|
+
(this as any).db.addMigrations({
|
|
20
|
+
namespace: (this as any).name,
|
|
21
|
+
directory: path.resolve(__dirname, 'migrations'),
|
|
22
|
+
context: { plugin: this },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async load() {
|
|
27
|
+
await this.skillHub.load();
|
|
28
|
+
|
|
29
|
+
// --- ACL ---
|
|
30
|
+
(this as any).app.acl.registerSnippet({
|
|
31
|
+
name: `pm.${(this as any).name}`,
|
|
32
|
+
actions: [
|
|
33
|
+
'orchestratorConfig:*',
|
|
34
|
+
'orchestratorTracing:*',
|
|
35
|
+
'agentExecutionSpans:*',
|
|
36
|
+
'skillDefinitions:*',
|
|
37
|
+
'skillExecutions:*',
|
|
38
|
+
'skillHub:*',
|
|
39
|
+
'skillWorkerConfigs:*',
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// --- Register Dynamic Tools ---
|
|
44
|
+
// Each configured sub-agent becomes a callable tool for its leader.
|
|
45
|
+
// Uses createReactAgent (LangGraph public API) instead of private AIEmployee class.
|
|
46
|
+
// Tools are registered via app.aiManager.toolsManager (public API from @nocobase/ai core).
|
|
47
|
+
const toolsManager = (this as any).app.aiManager.toolsManager;
|
|
48
|
+
toolsManager.registerTools(createExternalRagSearchTool(this));
|
|
49
|
+
toolsManager.registerDynamicTools(createDelegateToolsProvider(this));
|
|
50
|
+
|
|
51
|
+
// --- Register Tracing Resource (Phase 5) ---
|
|
52
|
+
// Custom read-only resource for the Swarm Tracing admin page.
|
|
53
|
+
registerTracingResource(this);
|
|
54
|
+
|
|
55
|
+
// --- Log Retention ---
|
|
56
|
+
// Daily prune of orchestratorLogs / agentExecutionSpans to keep tables bounded.
|
|
57
|
+
// Override window via env: ORCHESTRATOR_LOG_RETENTION_DAYS (default 30).
|
|
58
|
+
(this as any).app.cronJobManager.addJob({
|
|
59
|
+
cronTime: '0 30 2 * * *',
|
|
60
|
+
onTick: async () => {
|
|
61
|
+
try {
|
|
62
|
+
const days = Number(process.env.ORCHESTRATOR_LOG_RETENTION_DAYS || 30);
|
|
63
|
+
if (!Number.isFinite(days) || days <= 0) return;
|
|
64
|
+
const cutoff = new Date(Date.now() - days * 86400000);
|
|
65
|
+
const repo = (this as any).db.getRepository('orchestratorLogs');
|
|
66
|
+
const spansRepo = (this as any).db.getRepository('agentExecutionSpans');
|
|
67
|
+
const deletedLogs = repo
|
|
68
|
+
? await repo.destroy({
|
|
69
|
+
filter: { createdAt: { $lt: cutoff.toISOString() } },
|
|
70
|
+
})
|
|
71
|
+
: 0;
|
|
72
|
+
const deletedSpans = spansRepo
|
|
73
|
+
? await spansRepo.destroy({
|
|
74
|
+
filter: { createdAt: { $lt: cutoff.toISOString() } },
|
|
75
|
+
})
|
|
76
|
+
: 0;
|
|
77
|
+
(this as any).app.log.info(
|
|
78
|
+
`[AgentOrchestrator] Pruned ${deletedLogs} orchestratorLogs and ${deletedSpans} agentExecutionSpans rows older than ${days} day(s).`,
|
|
79
|
+
);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
(this as any).app.log.error('[AgentOrchestrator] Log retention job failed', e);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// NOTE: The createReactAgent approach does NOT create aiConversation records,
|
|
87
|
+
// so there is no need for a middleware to hide "headless" conversations.
|
|
88
|
+
// If future versions need conversation logging, add it here.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async install() {
|
|
92
|
+
await this.skillHub.install();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async afterEnable() {}
|
|
96
|
+
async afterDisable() {}
|
|
97
|
+
async remove() {}
|
|
98
|
+
|
|
99
|
+
async beforeStop() {
|
|
100
|
+
await this.skillHub.beforeStop();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default PluginAgentOrchestratorServer;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Database } from '@nocobase/database';
|
|
2
|
-
import { stringifyJsonText } from '../skill-hub/utils/json-fields';
|
|
1
|
+
import { Database } from '@nocobase/database';
|
|
2
|
+
import { stringifyJsonText } from '../skill-hub/utils/json-fields';
|
|
3
3
|
|
|
4
4
|
export class SkillManager {
|
|
5
5
|
constructor(private db: Database) {}
|
|
6
6
|
|
|
7
7
|
async seedDefaults() {
|
|
8
|
-
const repo =
|
|
8
|
+
const repo = this.db.getRepository('skillDefinitions');
|
|
9
9
|
|
|
10
10
|
const seeds = [
|
|
11
11
|
{
|
|
@@ -203,16 +203,16 @@ export class SkillManager {
|
|
|
203
203
|
|
|
204
204
|
for (const seed of seeds) {
|
|
205
205
|
try {
|
|
206
|
-
const count = await repo.count({ filter: { name: seed.name } });
|
|
207
|
-
if (count === 0) {
|
|
208
|
-
await repo.create({
|
|
209
|
-
values: {
|
|
210
|
-
...seed,
|
|
211
|
-
inputSchema: stringifyJsonText(seed.inputSchema),
|
|
212
|
-
packages: stringifyJsonText(seed.packages, []),
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
}
|
|
206
|
+
const count = await repo.count({ filter: { name: seed.name } });
|
|
207
|
+
if (count === 0) {
|
|
208
|
+
await repo.create({
|
|
209
|
+
values: {
|
|
210
|
+
...seed,
|
|
211
|
+
inputSchema: stringifyJsonText(seed.inputSchema),
|
|
212
|
+
packages: stringifyJsonText(seed.packages, []),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
216
|
} catch (err) {
|
|
217
217
|
console.error(`[import-skill] Failed to insert ${seed.name}:`, err);
|
|
218
218
|
// Skip if already exists (unique constraint on name)
|
|
@@ -330,46 +330,6 @@ doc.build(story)
|
|
|
330
330
|
print('Generated: report.pdf')
|
|
331
331
|
`;
|
|
332
332
|
|
|
333
|
-
const SEED_PPTX = `import os, json
|
|
334
|
-
from pptx import Presentation
|
|
335
|
-
from pptx.util import Inches, Pt
|
|
336
|
-
from pptx.enum.text import PP_ALIGN
|
|
337
|
-
|
|
338
|
-
title = json.loads('''{{title}}''') if '{{title}}'.startswith('"') else '{{title}}'
|
|
339
|
-
subtitle_raw = '''{{subtitle}}'''
|
|
340
|
-
subtitle = json.loads(subtitle_raw) if subtitle_raw.startswith('"') else subtitle_raw if subtitle_raw != '{{' + 'subtitle}}' else ''
|
|
341
|
-
slides_data = json.loads('''{{slides}}''')
|
|
342
|
-
|
|
343
|
-
prs = Presentation()
|
|
344
|
-
prs.slide_width = Inches(13.333)
|
|
345
|
-
prs.slide_height = Inches(7.5)
|
|
346
|
-
|
|
347
|
-
# Title slide
|
|
348
|
-
slide = prs.slides.add_slide(prs.slide_layouts[0])
|
|
349
|
-
slide.shapes.title.text = title
|
|
350
|
-
if subtitle and slide.placeholders[1]:
|
|
351
|
-
slide.placeholders[1].text = subtitle
|
|
352
|
-
|
|
353
|
-
# Content slides
|
|
354
|
-
for s in slides_data:
|
|
355
|
-
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
|
356
|
-
slide.shapes.title.text = s.get('title', '')
|
|
357
|
-
body = slide.placeholders[1].text_frame
|
|
358
|
-
body.clear()
|
|
359
|
-
for i, bullet in enumerate(s.get('bullets', [])):
|
|
360
|
-
if i == 0:
|
|
361
|
-
body.paragraphs[0].text = bullet
|
|
362
|
-
else:
|
|
363
|
-
p = body.add_paragraph()
|
|
364
|
-
p.text = bullet
|
|
365
|
-
body.paragraphs[-1].font.size = Pt(18)
|
|
366
|
-
|
|
367
|
-
output_dir = os.environ.get('OUTPUT_DIR', '/output')
|
|
368
|
-
filepath = os.path.join(output_dir, 'presentation.pptx')
|
|
369
|
-
prs.save(filepath)
|
|
370
|
-
print('Generated: presentation.pptx')
|
|
371
|
-
`;
|
|
372
|
-
|
|
373
333
|
const SEED_CHART = `import os, json
|
|
374
334
|
import matplotlib
|
|
375
335
|
matplotlib.use('Agg')
|
|
@@ -543,7 +543,7 @@ export class SkillHubSubFeature {
|
|
|
543
543
|
filter: { id: skill.get('id'), enabled: true },
|
|
544
544
|
});
|
|
545
545
|
if (!latestSkill) {
|
|
546
|
-
return { error: `Skill "${skill.get('name')}" is no longer available` };
|
|
546
|
+
return { status: 'error', content: `Skill "${skill.get('name')}" is no longer available` };
|
|
547
547
|
}
|
|
548
548
|
const result = await this.executeSkill(latestSkill, args, toolCtx);
|
|
549
549
|
return {
|
|
@@ -582,7 +582,7 @@ export class SkillHubSubFeature {
|
|
|
582
582
|
const repo = (this as any).db.getRepository('skillExecutions');
|
|
583
583
|
|
|
584
584
|
const outdated = await repo.find({
|
|
585
|
-
|
|
585
|
+
filter: { createdAt: { $lt: cutoff } }
|
|
586
586
|
});
|
|
587
587
|
|
|
588
588
|
if (outdated.length > 0) {
|
|
@@ -638,7 +638,7 @@ export class SkillHubSubFeature {
|
|
|
638
638
|
const hours = config ? config.get('retentionHours') : 24;
|
|
639
639
|
if (hours > 0) {
|
|
640
640
|
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
641
|
-
const results = await repo.find({
|
|
641
|
+
const results = await repo.find({ filter: { createdAt: { $lt: cutoff } }, fields: ['id'] });
|
|
642
642
|
for (const rec of results) {
|
|
643
643
|
await rec.destroy();
|
|
644
644
|
}
|
|
@@ -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
|