@nocobase/ai 2.1.0-beta.14 → 2.1.0-beta.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.
Files changed (87) hide show
  1. package/lib/ai-employee-manager/index.d.ts +29 -0
  2. package/lib/ai-employee-manager/index.js +167 -0
  3. package/lib/ai-employee-manager/types.d.ts +56 -0
  4. package/lib/ai-employee-manager/types.js +24 -0
  5. package/lib/ai-manager.d.ts +8 -0
  6. package/lib/ai-manager.js +12 -0
  7. package/lib/document-loader/index.d.ts +10 -0
  8. package/lib/document-loader/index.js +90 -0
  9. package/lib/document-loader/loader.worker.d.ts +9 -0
  10. package/lib/document-loader/loader.worker.js +83 -0
  11. package/lib/document-loader/vendor/langchain/document_loaders/fs/text.d.ts +20 -0
  12. package/lib/document-loader/vendor/langchain/document_loaders/fs/text.js +99 -0
  13. package/lib/document-loader/xlsx.d.ts +10 -0
  14. package/lib/document-loader/xlsx.js +100 -0
  15. package/lib/index.d.ts +5 -0
  16. package/lib/index.js +11 -1
  17. package/lib/loader/employee.d.ts +37 -0
  18. package/lib/loader/employee.js +207 -0
  19. package/lib/loader/index.d.ts +3 -0
  20. package/lib/loader/index.js +7 -1
  21. package/lib/loader/mcp.d.ts +35 -0
  22. package/lib/loader/mcp.js +108 -0
  23. package/lib/loader/skills.d.ts +43 -0
  24. package/lib/loader/skills.js +138 -0
  25. package/lib/loader/tools.d.ts +1 -0
  26. package/lib/loader/tools.js +4 -3
  27. package/lib/mcp-manager/index.d.ts +43 -0
  28. package/lib/mcp-manager/index.js +341 -0
  29. package/lib/mcp-manager/types.d.ts +61 -0
  30. package/lib/mcp-manager/types.js +24 -0
  31. package/lib/mcp-tools-manager.d.ts +43 -0
  32. package/lib/mcp-tools-manager.js +77 -0
  33. package/lib/skills-manager/index.d.ts +29 -0
  34. package/lib/skills-manager/index.js +169 -0
  35. package/lib/skills-manager/types.d.ts +33 -0
  36. package/lib/skills-manager/types.js +24 -0
  37. package/lib/tools-manager/index.d.ts +2 -1
  38. package/lib/tools-manager/index.js +17 -7
  39. package/lib/tools-manager/types.d.ts +12 -4
  40. package/package.json +11 -6
  41. package/src/__tests__/ai-employees.test.ts +108 -0
  42. package/src/__tests__/mcp.test.ts +105 -0
  43. package/src/__tests__/resource/ai/ai-employees/index-employee/index.ts +16 -0
  44. package/src/__tests__/resource/ai/ai-employees/index-employee/prompt.md +1 -0
  45. package/src/__tests__/resource/ai/ai-employees/named-file-employee.ts +16 -0
  46. package/src/__tests__/resource/ai/ai-employees/with-skills/index.ts +16 -0
  47. package/src/__tests__/resource/ai/ai-employees/with-skills/skills/analysis/SKILLS.md +6 -0
  48. package/src/__tests__/resource/ai/ai-employees/with-skills-merge/index.ts +17 -0
  49. package/src/__tests__/resource/ai/ai-employees/with-skills-merge/skills/discovered-skill/SKILLS.md +6 -0
  50. package/src/__tests__/resource/ai/ai-employees/with-tools/index.ts +16 -0
  51. package/src/__tests__/resource/ai/ai-employees/with-tools/tools/discoveredTool.ts +23 -0
  52. package/src/__tests__/resource/ai/ai-employees/with-tools-merge/index.ts +16 -0
  53. package/src/__tests__/resource/ai/ai-employees/with-tools-merge/tools/discoveredTool.ts +23 -0
  54. package/src/__tests__/resource/ai/mcp/weather.ts +25 -0
  55. package/src/__tests__/resource/ai/skills/data-modeling/SKILLS.md +24 -0
  56. package/src/__tests__/resource/ai/skills/data-modeling/tools/read.ts +23 -0
  57. package/src/__tests__/resource/ai/skills/data-modeling/tools/search/description.md +1 -0
  58. package/src/__tests__/resource/ai/skills/data-modeling/tools/search/index.ts +23 -0
  59. package/src/__tests__/resource/ai/skills/document/tools/read.ts +1 -1
  60. package/src/__tests__/resource/ai/skills/document/tools/search/index.ts +1 -1
  61. package/src/__tests__/resource/ai/tools/desc/index.ts +1 -1
  62. package/src/__tests__/resource/ai/tools/group/group1.ts +1 -1
  63. package/src/__tests__/resource/ai/tools/group/group2.ts +1 -1
  64. package/src/__tests__/resource/ai/tools/group/group3/index.ts +1 -1
  65. package/src/__tests__/resource/ai/tools/hallow/index.ts +1 -1
  66. package/src/__tests__/resource/ai/tools/print.ts +1 -1
  67. package/src/__tests__/skills.test.ts +55 -0
  68. package/src/ai-employee-manager/index.ts +148 -0
  69. package/src/ai-employee-manager/types.ts +63 -0
  70. package/src/ai-manager.ts +12 -0
  71. package/src/document-loader/index.ts +57 -0
  72. package/src/document-loader/loader.worker.ts +100 -0
  73. package/src/document-loader/vendor/langchain/document_loaders/fs/text.ts +72 -0
  74. package/src/document-loader/xlsx.ts +82 -0
  75. package/src/index.ts +5 -0
  76. package/src/loader/employee.ts +194 -0
  77. package/src/loader/index.ts +3 -0
  78. package/src/loader/mcp.ts +101 -0
  79. package/src/loader/skills.ts +129 -0
  80. package/src/loader/tools.ts +3 -2
  81. package/src/mcp-manager/index.ts +364 -0
  82. package/src/mcp-manager/types.ts +68 -0
  83. package/src/mcp-tools-manager.ts +90 -0
  84. package/src/skills-manager/index.ts +148 -0
  85. package/src/skills-manager/types.ts +38 -0
  86. package/src/tools-manager/index.ts +18 -7
  87. package/src/tools-manager/types.ts +13 -4
@@ -0,0 +1,63 @@
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
+ export type AIEmployeeLocalizedProfile = {
11
+ avatar?: string;
12
+ nickname?: string;
13
+ position?: string;
14
+ bio?: string;
15
+ greeting?: string;
16
+ about?: string;
17
+ };
18
+
19
+ export type AIEmployeeKnowledgeBase = {
20
+ topK: number;
21
+ score: string;
22
+ knowledgeBaseIds: string[];
23
+ };
24
+
25
+ export type AIEmployeeToolSetting = {
26
+ name: string;
27
+ autoCall?: boolean;
28
+ };
29
+
30
+ export type AIEmployeeOptions = {
31
+ username: string;
32
+ description?: string;
33
+ skills?: string[];
34
+ tools?: AIEmployeeToolSetting[];
35
+ avatar?: string;
36
+ nickname?: string;
37
+ position?: string;
38
+ bio?: string;
39
+ greeting?: string;
40
+ systemPrompt?: string | null;
41
+ sort?: number;
42
+ };
43
+
44
+ export type AIEmployeeEntry = Omit<AIEmployeeOptions, 'skills' | 'tools' | 'systemPrompt'> & {
45
+ about?: string;
46
+ defaultPrompt?: string;
47
+ skillSettings: {
48
+ skills: string[];
49
+ tools: AIEmployeeToolSetting[];
50
+ };
51
+ };
52
+
53
+ export type AIEmployeeFilter = {
54
+ builtIn?: boolean;
55
+ username?: string;
56
+ };
57
+
58
+ export interface AIEmployeeManager {
59
+ init(): Promise<void>;
60
+ getEmployee(username: string): Promise<AIEmployeeEntry>;
61
+ listEmployees(filter?: AIEmployeeFilter): Promise<AIEmployeeEntry[]>;
62
+ registerEmployee(options: AIEmployeeOptions): Promise<void>;
63
+ }
package/src/ai-manager.ts CHANGED
@@ -9,13 +9,25 @@
9
9
 
10
10
  import { DocumentManager } from './document-manager';
11
11
  import { DefaultToolsManager, ToolsManager } from './tools-manager';
12
+ import { DefaultSkillsManager, SkillsManager } from './skills-manager';
13
+ import { AIEmployeeManager, DefaultAIEmployeeManager } from './ai-employee-manager';
14
+ import { DefaultMCPManager, MCPManager } from './mcp-manager';
15
+ import { McpToolsManager } from './mcp-tools-manager';
12
16
 
13
17
  export class AIManager {
14
18
  documentManager: DocumentManager;
15
19
  toolsManager: ToolsManager;
20
+ skillsManager: SkillsManager;
21
+ employeeManager: AIEmployeeManager;
22
+ mcpManager: MCPManager;
23
+ mcpToolsManager: McpToolsManager;
16
24
 
17
25
  constructor(protected readonly app: any) {
18
26
  this.documentManager = new DocumentManager();
19
27
  this.toolsManager = new DefaultToolsManager();
28
+ this.skillsManager = new DefaultSkillsManager(app);
29
+ this.employeeManager = new DefaultAIEmployeeManager(app);
30
+ this.mcpManager = new DefaultMCPManager(app);
31
+ this.mcpToolsManager = new McpToolsManager();
20
32
  }
21
33
  }
@@ -0,0 +1,57 @@
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
+ import { Document } from '@langchain/core/documents';
11
+ import { Worker } from 'node:worker_threads';
12
+ import path from 'node:path';
13
+
14
+ export const loadByWorker = async (extname: string, blob: Blob): Promise<Document[]> => {
15
+ const buffer = Buffer.from(await blob.arrayBuffer());
16
+ const isTsRuntime = __filename.endsWith('.ts');
17
+ const workerPath = path.join(__dirname, `loader.worker.${isTsRuntime ? 'ts' : 'js'}`);
18
+ const worker = new Worker(workerPath, {
19
+ execArgv: isTsRuntime ? ['--require', 'tsx/cjs'] : undefined,
20
+ });
21
+ return new Promise<Document[]>((resolve, reject) => {
22
+ let settled = false;
23
+ const close = (error?: Error, result?: Document[]) => {
24
+ if (settled) {
25
+ return;
26
+ }
27
+ settled = true;
28
+ if (error) {
29
+ reject(error);
30
+ return;
31
+ }
32
+ resolve(result || []);
33
+ };
34
+
35
+ worker.once('message', (payload: { documents?: Document[]; error?: string }) => {
36
+ if (payload?.error) {
37
+ close(new Error(payload.error));
38
+ return;
39
+ }
40
+ close(undefined, payload?.documents || []);
41
+ });
42
+ worker.once('error', (error) => close(error));
43
+ worker.once('exit', (code) => {
44
+ if (!settled && code !== 0) {
45
+ close(new Error(`Document loader worker exited with code ${code}`));
46
+ }
47
+ });
48
+
49
+ worker.postMessage({
50
+ extname,
51
+ mimeType: blob.type,
52
+ buffer: Uint8Array.from(buffer),
53
+ });
54
+ }).finally(() => {
55
+ worker.terminate().catch(() => undefined);
56
+ });
57
+ };
@@ -0,0 +1,100 @@
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
+ import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
11
+ import { PPTXLoader } from '@langchain/community/document_loaders/fs/pptx';
12
+ import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
13
+ import type { Document } from '@langchain/core/documents';
14
+ import { parentPort } from 'node:worker_threads';
15
+ import { TextLoader } from './vendor/langchain/document_loaders/fs/text';
16
+ import { CSVLoader } from '@langchain/community/document_loaders/fs/csv';
17
+ import { loadXlsx } from './xlsx';
18
+
19
+ type ParsePayload = {
20
+ extname: string;
21
+ mimeType?: string;
22
+ buffer: Uint8Array;
23
+ };
24
+
25
+ type WorkerResponse = {
26
+ documents?: Array<Pick<Document, 'pageContent' | 'metadata' | 'id'>>;
27
+ error?: string;
28
+ };
29
+
30
+ const loadPdf = async (blob: Blob): Promise<Document[]> => {
31
+ const loader = new PDFLoader(blob);
32
+ return loader.load();
33
+ };
34
+
35
+ const loadDoc = async (blob: Blob, type: 'docx' | 'doc'): Promise<Document[]> => {
36
+ const loader = new DocxLoader(blob, { type });
37
+ return loader.load();
38
+ };
39
+
40
+ const loadPpt = async (blob: Blob): Promise<Document[]> => {
41
+ const loader = new PPTXLoader(blob);
42
+ return loader.load();
43
+ };
44
+
45
+ const loadTxt = async (blob: Blob): Promise<Document[]> => {
46
+ const loader = new TextLoader(blob);
47
+ return loader.load();
48
+ };
49
+
50
+ const loadCsv = async (blob: Blob): Promise<Document[]> => {
51
+ const loader = new CSVLoader(blob);
52
+ return loader.load();
53
+ };
54
+
55
+ const loadByExtname = async (payload: ParsePayload): Promise<Document[]> => {
56
+ // @ts-ignore
57
+ const blob = new Blob([Buffer.from(payload.buffer)], { type: payload.mimeType ?? 'application/octet-stream' });
58
+
59
+ switch (payload.extname) {
60
+ case '.pdf':
61
+ return loadPdf(blob);
62
+ case '.ppt':
63
+ case '.pptx':
64
+ return loadPpt(blob);
65
+ case '.doc':
66
+ return loadDoc(blob, 'doc');
67
+ case '.docx':
68
+ return loadDoc(blob, 'docx');
69
+ case '.csv':
70
+ return loadCsv(blob);
71
+ case '.xls':
72
+ case '.xlsx':
73
+ return loadXlsx(blob);
74
+ case '.json':
75
+ case '.md':
76
+ case '.txt':
77
+ return loadTxt(blob);
78
+ default:
79
+ return [];
80
+ }
81
+ };
82
+
83
+ parentPort?.on('message', async (payload: ParsePayload) => {
84
+ try {
85
+ const documents = await loadByExtname(payload);
86
+ const response: WorkerResponse = {
87
+ documents: documents.map((doc) => ({
88
+ pageContent: doc.pageContent,
89
+ metadata: doc.metadata,
90
+ id: doc.id,
91
+ })),
92
+ };
93
+ parentPort?.postMessage(response);
94
+ } catch (error) {
95
+ const response: WorkerResponse = {
96
+ error: String(error?.stack || error),
97
+ };
98
+ parentPort?.postMessage(response);
99
+ }
100
+ });
@@ -0,0 +1,72 @@
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
+ import { Document } from '@langchain/core/documents';
11
+ import { getEnv } from '@langchain/core/utils/env';
12
+ import { BaseDocumentLoader } from '@langchain/core/document_loaders/base';
13
+
14
+ export class TextLoader extends BaseDocumentLoader {
15
+ private filePathOrBlob: any;
16
+
17
+ constructor(filePathOrBlob) {
18
+ super();
19
+ this.filePathOrBlob = filePathOrBlob;
20
+ }
21
+
22
+ async parse(raw) {
23
+ return [raw];
24
+ }
25
+
26
+ async load() {
27
+ let text;
28
+ let metadata;
29
+
30
+ if (typeof this.filePathOrBlob === 'string') {
31
+ const { readFile } = await TextLoader.imports();
32
+ text = await readFile(this.filePathOrBlob, 'utf8');
33
+ metadata = { source: this.filePathOrBlob };
34
+ } else {
35
+ text = await this.filePathOrBlob.text();
36
+ metadata = { source: 'blob', blobType: this.filePathOrBlob.type };
37
+ }
38
+
39
+ const parsed = await this.parse(text);
40
+ parsed.forEach((pageContent, i) => {
41
+ if (typeof pageContent !== 'string') {
42
+ throw new Error(`Expected string, at position ${i} got ${typeof pageContent}`);
43
+ }
44
+ });
45
+
46
+ return parsed.map(
47
+ (pageContent, i) =>
48
+ new Document({
49
+ pageContent,
50
+ metadata:
51
+ parsed.length === 1
52
+ ? metadata
53
+ : {
54
+ ...metadata,
55
+ line: i + 1,
56
+ },
57
+ }),
58
+ );
59
+ }
60
+
61
+ static async imports() {
62
+ try {
63
+ const { readFile } = await import('node:fs/promises');
64
+ return { readFile };
65
+ } catch (e) {
66
+ console.error(e);
67
+ throw new Error(
68
+ `Failed to load fs/promises. TextLoader available only on environment 'node'. It appears you are running environment '${getEnv()}'. See https://<link to docs> for alternatives.`,
69
+ );
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,82 @@
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
+ import { Document } from '@langchain/core/documents';
11
+ import * as XLSX from 'xlsx';
12
+
13
+ const normalizeCellValue = (value: unknown): string => {
14
+ if (value === undefined || value === null) {
15
+ return '';
16
+ }
17
+
18
+ return String(value);
19
+ };
20
+
21
+ const trimTrailingEmptyCells = (row: unknown[]): unknown[] => {
22
+ let end = row.length;
23
+
24
+ while (end > 0 && normalizeCellValue(row[end - 1]).trim() === '') {
25
+ end -= 1;
26
+ }
27
+
28
+ return row.slice(0, end);
29
+ };
30
+
31
+ const sheetToLines = (sheet: XLSX.WorkSheet): string[] => {
32
+ const rows = XLSX.utils.sheet_to_json(sheet, {
33
+ header: 1,
34
+ raw: false,
35
+ defval: '',
36
+ blankrows: false,
37
+ }) as unknown[][];
38
+
39
+ return rows
40
+ .map((row) => trimTrailingEmptyCells(Array.isArray(row) ? row : []))
41
+ .filter((row) => row.length > 0)
42
+ .map((row) => row.map((cell) => normalizeCellValue(cell)).join('\t'))
43
+ .filter((line) => line.trim().length > 0);
44
+ };
45
+
46
+ export const loadXlsx = async (blob: Blob): Promise<Document[]> => {
47
+ const buffer = await blob.arrayBuffer();
48
+ const workbook = XLSX.read(buffer, {
49
+ type: 'array',
50
+ cellText: true,
51
+ });
52
+
53
+ const documents: Document[] = [];
54
+
55
+ workbook.SheetNames.forEach((sheetName, index) => {
56
+ const sheet = workbook.Sheets[sheetName];
57
+
58
+ if (!sheet) {
59
+ return;
60
+ }
61
+
62
+ const lines = sheetToLines(sheet);
63
+
64
+ if (!lines.length) {
65
+ return;
66
+ }
67
+
68
+ documents.push(
69
+ new Document({
70
+ pageContent: [`Sheet: ${sheetName}`, ...lines].join('\n'),
71
+ metadata: {
72
+ source: 'blob',
73
+ blobType: blob.type,
74
+ sheetName,
75
+ sheetIndex: index,
76
+ },
77
+ }),
78
+ );
79
+ });
80
+
81
+ return documents;
82
+ };
package/src/index.ts CHANGED
@@ -8,6 +8,11 @@
8
8
  */
9
9
 
10
10
  export * from './ai-manager';
11
+ export * from './ai-employee-manager';
11
12
  export * from './document-manager';
13
+ export * from './mcp-tools-manager';
12
14
  export * from './tools-manager';
15
+ export * from './skills-manager';
16
+ export * from './mcp-manager';
13
17
  export * from './loader';
18
+ export * from './document-loader';
@@ -0,0 +1,194 @@
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
+ import { importModule } from '@nocobase/utils';
11
+ import { DirectoryScanner, DirectoryScannerOptions, FileDescriptor } from './scanner';
12
+ import { existsSync } from 'fs';
13
+ import { readFile } from 'fs/promises';
14
+ import { AIManager } from '../ai-manager';
15
+ import { LoadAndRegister } from './types';
16
+ import { Logger } from '@nocobase/logger';
17
+ import path from 'path';
18
+ import { AIEmployeeOptions, AIEmployeeToolSetting } from '../ai-employee-manager';
19
+
20
+ export type AIEmployeeLoaderOptions = { pluginName: string; scan: DirectoryScannerOptions; log?: Logger };
21
+
22
+ export class AIEmployeeLoader extends LoadAndRegister<AIEmployeeLoaderOptions> {
23
+ protected readonly scanner: DirectoryScanner;
24
+
25
+ protected files: FileDescriptor[] = [];
26
+ protected employeeDescriptors: AIEmployeeDescriptor[] = [];
27
+ protected log: Logger;
28
+
29
+ constructor(
30
+ protected readonly ai: AIManager,
31
+ protected readonly options: AIEmployeeLoaderOptions,
32
+ ) {
33
+ super(ai, options);
34
+ this.log = options.log;
35
+ this.scanner = new DirectoryScanner(this.options.scan);
36
+ }
37
+
38
+ protected async scan(): Promise<void> {
39
+ this.files = await this.scanner.scan();
40
+ }
41
+
42
+ protected async import(): Promise<void> {
43
+ if (!this.files.length) {
44
+ return;
45
+ }
46
+
47
+ const grouped = new Map<string, FileDescriptor[]>();
48
+ for (const fd of this.files) {
49
+ const employeeRoot = getEmployeeRoot(fd);
50
+ const group = grouped.get(employeeRoot) ?? [];
51
+ group.push(fd);
52
+ grouped.set(employeeRoot, group);
53
+ }
54
+
55
+ const descriptors = await Promise.all(
56
+ Array.from(grouped.entries()).map(async ([employeeRoot, fds]) => {
57
+ const file = fds.find((fd) => fd.extname === '.ts' || fd.extname === '.js');
58
+ if (!file || !existsSync(file.path)) {
59
+ return null;
60
+ }
61
+ const promptFile = fds.find((fd) => fd.basename === 'prompt.md');
62
+
63
+ const name = path.basename(employeeRoot);
64
+ try {
65
+ const imported = await importModule(file.path);
66
+ const mod = imported?.default ?? imported;
67
+ const employeeOptions = typeof mod === 'function' ? mod() : mod;
68
+ if (!employeeOptions || !employeeOptions.username) {
69
+ this.log?.warn(`ai employee [${name}] register ignored: invalid definition at ${file.path}`);
70
+ return null;
71
+ }
72
+ const { skills = [], tools = [] } = employeeOptions;
73
+
74
+ if (promptFile && existsSync(promptFile.path)) {
75
+ try {
76
+ employeeOptions.systemPrompt = await readFile(promptFile.path, 'utf-8');
77
+ } catch (e) {
78
+ this.log?.error(
79
+ `ai employee [${name}] load fail: error occur when reading prompt.md at ${promptFile.path}`,
80
+ e,
81
+ );
82
+ return null;
83
+ }
84
+ }
85
+
86
+ return {
87
+ name,
88
+ employeeRoot,
89
+ file,
90
+ options: {
91
+ ...employeeOptions,
92
+ skills: [...skills, ...(await discoverSkills(employeeRoot))],
93
+ tools: [...tools, ...(await discoverTools(employeeRoot))],
94
+ } as AIEmployeeOptions,
95
+ } satisfies AIEmployeeDescriptor;
96
+ } catch (e) {
97
+ this.log?.error(`ai employee [${name}] load fail: error occur when import ${file.path}`, e);
98
+ return null;
99
+ }
100
+ }),
101
+ );
102
+
103
+ this.employeeDescriptors = descriptors.filter((item): item is AIEmployeeDescriptor => Boolean(item));
104
+ }
105
+
106
+ protected async register(): Promise<void> {
107
+ if (!this.employeeDescriptors.length) {
108
+ return;
109
+ }
110
+ const { employeeManager } = this.ai;
111
+ for (const descriptor of this.employeeDescriptors) {
112
+ await employeeManager.registerEmployee(descriptor.options);
113
+ }
114
+ }
115
+ }
116
+
117
+ export type AIEmployeeDescriptor = {
118
+ name: string;
119
+ employeeRoot: string;
120
+ file: FileDescriptor;
121
+ options: AIEmployeeOptions;
122
+ };
123
+
124
+ function getEmployeeRoot(fd: FileDescriptor) {
125
+ if (fd.basename === 'index.ts' || fd.basename === 'index.js' || fd.basename === 'prompt.md') {
126
+ return path.dirname(fd.path);
127
+ }
128
+ return fd.path;
129
+ }
130
+
131
+ async function discoverSkills(employeeRoot: string): Promise<string[]> {
132
+ const skillsDir = path.join(employeeRoot, 'skills');
133
+ if (!existsSync(skillsDir)) {
134
+ return [];
135
+ }
136
+ const scanner = new DirectoryScanner({
137
+ basePath: employeeRoot,
138
+ pattern: ['skills/**/SKILLS.md'],
139
+ });
140
+ const skillFiles = await scanner.scan();
141
+ return uniq(skillFiles.map((it) => it.directory).filter(Boolean));
142
+ }
143
+
144
+ async function discoverTools(employeeRoot: string): Promise<AIEmployeeToolSetting[]> {
145
+ const toolsDir = path.join(employeeRoot, 'tools');
146
+ if (!existsSync(toolsDir)) {
147
+ return [];
148
+ }
149
+ const scanner = new DirectoryScanner({
150
+ basePath: employeeRoot,
151
+ pattern: ['tools/**/*.ts', 'tools/**/*.js', '!tools/**/*.d.ts'],
152
+ });
153
+ const toolFiles = await scanner.scan();
154
+ return uniqBy(
155
+ toolFiles
156
+ .map((it) =>
157
+ normalizeAIEmployeeToolSetting(
158
+ it.basename === 'index.ts' || it.basename === 'index.js' ? it.directory : it.name,
159
+ ),
160
+ )
161
+ .filter((tool): tool is AIEmployeeToolSetting => Boolean(tool?.name)),
162
+ 'name',
163
+ );
164
+ }
165
+
166
+ function uniq<T>(values: T[]) {
167
+ return [...new Set(values)];
168
+ }
169
+
170
+ function uniqBy<T extends Record<string, unknown>>(values: T[], key: keyof T) {
171
+ const valueMap = new Map<unknown, T>();
172
+ for (const value of values) {
173
+ valueMap.set(value[key], value);
174
+ }
175
+ return [...valueMap.values()];
176
+ }
177
+
178
+ export function normalizeAIEmployeeToolSetting(
179
+ tool?: string | AIEmployeeToolSetting,
180
+ ): AIEmployeeToolSetting | undefined {
181
+ if (!tool) {
182
+ return;
183
+ }
184
+ if (typeof tool === 'string') {
185
+ return { name: tool };
186
+ }
187
+ if (!tool.name) {
188
+ return;
189
+ }
190
+ return {
191
+ name: tool.name,
192
+ ...(tool.autoCall != null ? { autoCall: tool.autoCall } : {}),
193
+ };
194
+ }
@@ -10,3 +10,6 @@
10
10
  export * from './types';
11
11
  export * from './scanner';
12
12
  export * from './tools';
13
+ export * from './skills';
14
+ export * from './mcp';
15
+ export * from './employee';