@louloulinx/metagpt 0.1.3

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 (113) hide show
  1. package/.eslintrc.json +23 -0
  2. package/.prettierrc +7 -0
  3. package/LICENSE +21 -0
  4. package/README-CN.md +754 -0
  5. package/README.md +238 -0
  6. package/bun.lock +1023 -0
  7. package/doc/TutorialAssistant.md +114 -0
  8. package/doc/VercelLLMProvider.md +164 -0
  9. package/eslint.config.js +55 -0
  10. package/examples/data-interpreter-example.ts +173 -0
  11. package/examples/qwen-direct-example.ts +60 -0
  12. package/examples/qwen-example.ts +62 -0
  13. package/examples/tutorial-assistant-example.ts +97 -0
  14. package/jest.config.ts +22 -0
  15. package/output/tutorials/Go/350/257/255/350/250/200/347/274/226/347/250/213/346/225/231/347/250/213_2025-02-25T09-35-15-436Z.md +2208 -0
  16. package/output/tutorials/Rust/346/225/231/347/250/213_2025-02-25T08-27-27-632Z.md +1967 -0
  17. package/output/tutorials//345/246/202/344/275/225/344/275/277/347/224/250TypeScript/345/274/200/345/217/221Node.js/345/272/224/347/224/250_2025-02-25T08-14-39-605Z.md +1721 -0
  18. package/output/tutorials//346/225/260/345/255/227/347/273/217/346/265/216/345/255/246/346/225/231/347/250/213_2025-02-25T10-45-03-605Z.md +902 -0
  19. package/output/tutorials//346/232/250/345/215/227/345/244/247/345/255/246/346/225/260/345/255/227/347/273/217/346/265/216/345/255/246/345/244/215/350/257/225/350/265/204/346/226/231_2025-02-25T11-16-59-133Z.md +719 -0
  20. package/package.json +58 -0
  21. package/plan-cn.md +321 -0
  22. package/plan.md +154 -0
  23. package/src/actions/analyze-task.ts +65 -0
  24. package/src/actions/base-action.ts +103 -0
  25. package/src/actions/di/execute-nb-code.ts +247 -0
  26. package/src/actions/di/write-analysis-code.ts +234 -0
  27. package/src/actions/write-tutorial.ts +232 -0
  28. package/src/config/browser.ts +33 -0
  29. package/src/config/config.ts +345 -0
  30. package/src/config/embedding.ts +26 -0
  31. package/src/config/llm.ts +36 -0
  32. package/src/config/mermaid.ts +37 -0
  33. package/src/config/omniparse.ts +25 -0
  34. package/src/config/redis.ts +34 -0
  35. package/src/config/s3.ts +33 -0
  36. package/src/config/search.ts +30 -0
  37. package/src/config/workspace.ts +20 -0
  38. package/src/index.ts +40 -0
  39. package/src/management/team.ts +168 -0
  40. package/src/memory/longterm.ts +218 -0
  41. package/src/memory/manager.ts +160 -0
  42. package/src/memory/types.ts +100 -0
  43. package/src/memory/working.ts +154 -0
  44. package/src/monitoring/system.ts +413 -0
  45. package/src/monitoring/types.ts +230 -0
  46. package/src/plugin/manager.ts +79 -0
  47. package/src/plugin/types.ts +114 -0
  48. package/src/provider/vercel-llm.ts +314 -0
  49. package/src/rag/base-rag.ts +194 -0
  50. package/src/rag/document-qa.ts +102 -0
  51. package/src/roles/base-role.ts +155 -0
  52. package/src/roles/data-interpreter.ts +360 -0
  53. package/src/roles/engineer.ts +1 -0
  54. package/src/roles/tutorial-assistant.ts +217 -0
  55. package/src/skills/base-skill.ts +144 -0
  56. package/src/skills/code-review.ts +120 -0
  57. package/src/tools/base-tool.ts +155 -0
  58. package/src/tools/file-system.ts +204 -0
  59. package/src/tools/tool-recommend.d.ts +14 -0
  60. package/src/tools/tool-recommend.ts +31 -0
  61. package/src/types/action.ts +38 -0
  62. package/src/types/config.ts +129 -0
  63. package/src/types/document.ts +354 -0
  64. package/src/types/llm.ts +64 -0
  65. package/src/types/memory.ts +36 -0
  66. package/src/types/message.ts +193 -0
  67. package/src/types/rag.ts +86 -0
  68. package/src/types/role.ts +67 -0
  69. package/src/types/skill.ts +71 -0
  70. package/src/types/task.ts +32 -0
  71. package/src/types/team.ts +55 -0
  72. package/src/types/tool.ts +77 -0
  73. package/src/types/workflow.ts +133 -0
  74. package/src/utils/common.ts +73 -0
  75. package/src/utils/yaml.ts +67 -0
  76. package/src/websocket/browser-client.ts +187 -0
  77. package/src/websocket/client.ts +186 -0
  78. package/src/websocket/server.ts +169 -0
  79. package/src/websocket/types.ts +125 -0
  80. package/src/workflow/executor.ts +193 -0
  81. package/src/workflow/executors/action-executor.ts +72 -0
  82. package/src/workflow/executors/condition-executor.ts +118 -0
  83. package/src/workflow/executors/parallel-executor.ts +201 -0
  84. package/src/workflow/executors/role-executor.ts +76 -0
  85. package/src/workflow/executors/sequence-executor.ts +196 -0
  86. package/tests/actions.test.ts +105 -0
  87. package/tests/benchmark/performance.test.ts +147 -0
  88. package/tests/config/config.test.ts +115 -0
  89. package/tests/config.test.ts +106 -0
  90. package/tests/e2e/setup.ts +74 -0
  91. package/tests/e2e/workflow.test.ts +88 -0
  92. package/tests/llm.test.ts +84 -0
  93. package/tests/memory/memory.test.ts +164 -0
  94. package/tests/memory.test.ts +63 -0
  95. package/tests/monitoring/monitoring.test.ts +225 -0
  96. package/tests/plugin/plugin.test.ts +183 -0
  97. package/tests/provider/bailian-llm.test.ts +98 -0
  98. package/tests/rag.test.ts +162 -0
  99. package/tests/roles.test.ts +88 -0
  100. package/tests/skills.test.ts +166 -0
  101. package/tests/team.test.ts +143 -0
  102. package/tests/tools.test.ts +170 -0
  103. package/tests/types/document.test.ts +181 -0
  104. package/tests/types/message.test.ts +122 -0
  105. package/tests/utils/yaml.test.ts +110 -0
  106. package/tests/utils.test.ts +74 -0
  107. package/tests/websocket/browser-client.test.ts +1 -0
  108. package/tests/websocket/websocket.test.ts +42 -0
  109. package/tests/workflow/parallel-executor.test.ts +224 -0
  110. package/tests/workflow/sequence-executor.test.ts +207 -0
  111. package/tests/workflow.test.ts +290 -0
  112. package/tsconfig.json +27 -0
  113. package/typedoc.json +25 -0
@@ -0,0 +1,183 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { PluginManagerImpl } from '../../src/plugin/manager';
3
+ import type { Plugin } from '../../src/plugin/types';
4
+
5
+ describe('Plugin System', () => {
6
+ let manager: PluginManagerImpl;
7
+
8
+ beforeEach(() => {
9
+ manager = new PluginManagerImpl();
10
+ });
11
+
12
+ afterEach(async () => {
13
+ await manager.destroy();
14
+ });
15
+
16
+ describe('Plugin Registration', () => {
17
+ test('registers valid plugin', async () => {
18
+ const plugin: Plugin = {
19
+ name: 'test-plugin',
20
+ version: '1.0.0',
21
+ description: 'Test Plugin',
22
+ hooks: {},
23
+ init: async () => {},
24
+ cleanup: async () => {},
25
+ };
26
+
27
+ await manager.register(plugin);
28
+ expect(manager.getPlugin('test-plugin')).toBe(plugin);
29
+ });
30
+
31
+ test('rejects duplicate plugin registration', async () => {
32
+ const plugin: Plugin = {
33
+ name: 'test-plugin',
34
+ version: '1.0.0',
35
+ description: 'Test Plugin',
36
+ hooks: {},
37
+ init: async () => {},
38
+ cleanup: async () => {},
39
+ };
40
+
41
+ await manager.register(plugin);
42
+ await expect(manager.register(plugin)).rejects.toThrow('already registered');
43
+ });
44
+
45
+ test('validates plugin metadata', async () => {
46
+ const invalidPlugin = {
47
+ version: '1.0.0',
48
+ hooks: {},
49
+ init: async () => {},
50
+ cleanup: async () => {},
51
+ };
52
+
53
+ await expect(manager.register(invalidPlugin as Plugin)).rejects.toThrow('name is required');
54
+ });
55
+ });
56
+
57
+ describe('Plugin Dependencies', () => {
58
+ test('handles plugin dependencies', async () => {
59
+ const basePlugin: Plugin = {
60
+ name: 'base-plugin',
61
+ version: '1.0.0',
62
+ hooks: {},
63
+ init: async () => {},
64
+ cleanup: async () => {},
65
+ };
66
+
67
+ const dependentPlugin: Plugin = {
68
+ name: 'dependent-plugin',
69
+ version: '1.0.0',
70
+ hooks: {},
71
+ init: async () => {},
72
+ cleanup: async () => {},
73
+ };
74
+
75
+ await manager.register(basePlugin);
76
+ await manager.register(dependentPlugin);
77
+
78
+ expect(manager.getPlugin('base-plugin')).toBe(basePlugin);
79
+ expect(manager.getPlugin('dependent-plugin')).toBe(dependentPlugin);
80
+ });
81
+
82
+ test('rejects missing dependencies', async () => {
83
+ const dependentPlugin: Plugin = {
84
+ name: 'dependent-plugin',
85
+ version: '1.0.0',
86
+ hooks: {},
87
+ init: async () => {
88
+ throw new Error('Missing dependency');
89
+ },
90
+ cleanup: async () => {},
91
+ };
92
+
93
+ await manager.register(dependentPlugin);
94
+ await expect(manager.init()).rejects.toThrow('Missing dependency');
95
+ });
96
+ });
97
+
98
+ describe('Plugin Lifecycle', () => {
99
+ test('initializes plugins in dependency order', async () => {
100
+ const initOrder: string[] = [];
101
+
102
+ const plugin1: Plugin = {
103
+ name: 'plugin1',
104
+ version: '1.0.0',
105
+ init: async () => {
106
+ initOrder.push('plugin1');
107
+ },
108
+ };
109
+
110
+ const plugin2: Plugin = {
111
+ name: 'plugin2',
112
+ version: '1.0.0',
113
+ init: async () => {
114
+ initOrder.push('plugin2');
115
+ },
116
+ };
117
+
118
+ await manager.register(plugin1);
119
+ await manager.register(plugin2);
120
+ await manager.init();
121
+
122
+ expect(initOrder).toEqual(['plugin1', 'plugin2']);
123
+ });
124
+
125
+ test('executes lifecycle hooks', async () => {
126
+ const hookOrder: string[] = [];
127
+
128
+ const plugin: Plugin = {
129
+ name: 'test-plugin',
130
+ version: '1.0.0',
131
+ hooks: {
132
+ 'before-init': async () => {
133
+ hookOrder.push('before-init');
134
+ },
135
+ 'init': async () => {
136
+ hookOrder.push('init');
137
+ },
138
+ 'after-init': async () => {
139
+ hookOrder.push('after-init');
140
+ },
141
+ },
142
+ };
143
+
144
+ await manager.register(plugin);
145
+ await manager.init();
146
+ await manager.executeHook('before-init');
147
+ await manager.executeHook('init');
148
+ await manager.executeHook('after-init');
149
+
150
+ expect(hookOrder).toEqual(['before-init', 'init', 'after-init']);
151
+ });
152
+ });
153
+
154
+ describe('Plugin Error Handling', () => {
155
+ test('handles hook execution errors', async () => {
156
+ const plugin: Plugin = {
157
+ name: 'test-plugin',
158
+ version: '1.0.0',
159
+ hooks: {
160
+ 'test-hook': async () => {
161
+ throw new Error('Hook error');
162
+ },
163
+ },
164
+ };
165
+
166
+ await manager.register(plugin);
167
+ await expect(manager.executeHook('test-hook')).rejects.toThrow('Hook error');
168
+ });
169
+
170
+ test('handles plugin initialization errors', async () => {
171
+ const plugin: Plugin = {
172
+ name: 'test-plugin',
173
+ version: '1.0.0',
174
+ init: async () => {
175
+ throw new Error('Init error');
176
+ },
177
+ };
178
+
179
+ await manager.register(plugin);
180
+ await expect(manager.init()).rejects.toThrow('Init error');
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it, mock } from 'bun:test';
2
+ import axios from 'axios';
3
+ import { BailianLLMProvider } from '../../src/provider/bailian-llm';
4
+ import type { BailianLLMConfig } from '../../src/provider/bailian-llm';
5
+
6
+ describe('BailianLLMProvider', () => {
7
+ const config: BailianLLMConfig = {
8
+ apiKey: 'test-api-key',
9
+ secretKey: 'test-secret-key',
10
+ baseURL: 'https://test-bailian-api.com',
11
+ model: 'test-model',
12
+ };
13
+
14
+ it('should generate text correctly', async () => {
15
+ // Mock axios.post
16
+ const originalPost = axios.post;
17
+
18
+ try {
19
+ // Replace with mock implementation
20
+ axios.post = mock(() =>
21
+ Promise.resolve({
22
+ data: {
23
+ output: {
24
+ text: '这是百炼大模型的回答',
25
+ },
26
+ },
27
+ })
28
+ );
29
+
30
+ const provider = new BailianLLMProvider(config);
31
+ const result = await provider.generate('测试提示词');
32
+
33
+ expect(result).toBe('这是百炼大模型的回答');
34
+ } finally {
35
+ // Restore original implementation
36
+ axios.post = originalPost;
37
+ }
38
+ });
39
+
40
+ it('should handle errors properly', async () => {
41
+ // Mock axios.post
42
+ const originalPost = axios.post;
43
+
44
+ try {
45
+ // Replace with mock implementation that throws
46
+ axios.post = mock(() =>
47
+ Promise.reject({
48
+ isAxiosError: true,
49
+ response: {
50
+ status: 401,
51
+ data: { error: 'Unauthorized' },
52
+ },
53
+ message: 'Request failed with status code 401',
54
+ })
55
+ );
56
+
57
+ const provider = new BailianLLMProvider(config);
58
+
59
+ try {
60
+ await provider.generate('测试提示词');
61
+ // Should not reach here
62
+ expect(false).toBe(true);
63
+ } catch (error) {
64
+ expect((error as Error).message).toContain('Bailian API Error: 401');
65
+ }
66
+ } finally {
67
+ // Restore original implementation
68
+ axios.post = originalPost;
69
+ }
70
+ });
71
+
72
+ it('should create embeddings correctly', async () => {
73
+ // Mock axios.post
74
+ const originalPost = axios.post;
75
+
76
+ try {
77
+ // Setup mock response with an embedding vector
78
+ const mockEmbedding = Array(1536).fill(0).map(() => Math.random());
79
+
80
+ // Replace with mock implementation
81
+ axios.post = mock(() =>
82
+ Promise.resolve({
83
+ data: {
84
+ embedding: mockEmbedding,
85
+ },
86
+ })
87
+ );
88
+
89
+ const provider = new BailianLLMProvider(config);
90
+ const result = await provider.embed('测试文本');
91
+
92
+ expect(result).toEqual(mockEmbedding);
93
+ } finally {
94
+ // Restore original implementation
95
+ axios.post = originalPost;
96
+ }
97
+ });
98
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test, mock, beforeAll, afterAll } from 'bun:test';
2
+ import { BaseRAG } from '../src/rag/base-rag';
3
+ import { DocumentQA } from '../src/rag/document-qa';
4
+ import type { RAGConfig, Chunk, SearchResult } from '../src/types/rag';
5
+ import type { LLMProvider } from '../src/types/llm';
6
+
7
+ // 创建测试 RAG 类
8
+ class TestRAG extends BaseRAG {
9
+ constructor(config: RAGConfig) {
10
+ super(config);
11
+ }
12
+ }
13
+
14
+ describe.skip('RAG System', () => {
15
+ // 模拟 LLM 提供商
16
+ const mockLLM: LLMProvider = {
17
+ generate: mock(() => Promise.resolve('Generated answer based on context')),
18
+ generateStream: mock(async function* () { yield 'test'; }),
19
+ embed: mock(() => Promise.resolve(new Array(384).fill(0.1))),
20
+ };
21
+
22
+ // 测试配置
23
+ const testConfig: RAGConfig = {
24
+ llm: mockLLM,
25
+ vectorStore: {
26
+ url: 'http://localhost:6333',
27
+ collectionName: 'test_collection',
28
+ dimension: 384,
29
+ distance: 'Cosine',
30
+ },
31
+ chunkSize: 1000,
32
+ chunkOverlap: 200,
33
+ topK: 5,
34
+ minScore: 0.7,
35
+ };
36
+
37
+ describe('BaseRAG', () => {
38
+ let rag: TestRAG;
39
+
40
+ beforeAll(async () => {
41
+ rag = new TestRAG(testConfig);
42
+ // 创建测试集合
43
+ await rag['vectorStore'].createCollection(testConfig.vectorStore.collectionName, {
44
+ vectors: {
45
+ size: testConfig.vectorStore.dimension,
46
+ distance: testConfig.vectorStore.distance.toLowerCase(),
47
+ },
48
+ });
49
+ });
50
+
51
+ afterAll(async () => {
52
+ // 清理测试集合
53
+ await rag['vectorStore'].deleteCollection(testConfig.vectorStore.collectionName);
54
+ });
55
+
56
+ test.skip('should initialize correctly', () => {
57
+ expect(rag.llm).toBe(mockLLM);
58
+ expect(rag.config).toEqual(testConfig);
59
+ });
60
+
61
+ test.skip('should add and retrieve documents', async () => {
62
+ const testDoc = 'This is a test document for RAG system testing.';
63
+ const chunks = await rag.addDocument(testDoc, { source: 'test' });
64
+
65
+ expect(chunks.length).toBeGreaterThan(0);
66
+ expect(chunks[0].content).toBe(testDoc);
67
+ expect(chunks[0].metadata).toEqual({ source: 'test' });
68
+
69
+ const results = await rag.search('test document');
70
+ expect(results.length).toBeGreaterThan(0);
71
+ expect(results[0].chunk.content).toBe(testDoc);
72
+ });
73
+
74
+ test.skip('should generate answers', async () => {
75
+ const answer = await rag.generate('What is this document about?');
76
+ expect(answer).toBe('Generated answer based on context');
77
+ expect(mockLLM.generate).toHaveBeenCalled();
78
+ });
79
+
80
+ test.skip('should delete chunks', async () => {
81
+ const chunks = await rag.addDocument('Document to be deleted');
82
+ await rag.deleteChunks([chunks[0].id]);
83
+
84
+ const results = await rag.search('deleted document');
85
+ expect(results.length).toBe(0);
86
+ });
87
+
88
+ test.skip('should update chunks', async () => {
89
+ const chunks = await rag.addDocument('Original content');
90
+ const updatedChunk: Chunk = {
91
+ ...chunks[0],
92
+ content: 'Updated content',
93
+ };
94
+
95
+ await rag.updateChunk(updatedChunk);
96
+ const results = await rag.search('Updated content');
97
+ expect(results[0].chunk.content).toBe('Updated content');
98
+ });
99
+ });
100
+
101
+ describe('DocumentQA', () => {
102
+ let qa: DocumentQA;
103
+
104
+ beforeAll(async () => {
105
+ qa = new DocumentQA(testConfig);
106
+ await qa['vectorStore'].createCollection(testConfig.vectorStore.collectionName, {
107
+ vectors: {
108
+ size: testConfig.vectorStore.dimension,
109
+ distance: testConfig.vectorStore.distance.toLowerCase(),
110
+ },
111
+ });
112
+ });
113
+
114
+ afterAll(async () => {
115
+ await qa['vectorStore'].deleteCollection(testConfig.vectorStore.collectionName);
116
+ });
117
+
118
+ test.skip('should handle document chunks intelligently', async () => {
119
+ const testDoc = `
120
+ Paragraph 1: This is the first paragraph.
121
+
122
+ Paragraph 2: This is the second paragraph.
123
+
124
+ Paragraph 3: This is the third paragraph.
125
+ `.trim();
126
+
127
+ const chunks = await qa['chunkText'](testDoc);
128
+ expect(chunks.length).toBe(1); // Small enough to be one chunk
129
+ expect(chunks[0]).toContain('Paragraph 1');
130
+ expect(chunks[0]).toContain('Paragraph 2');
131
+ expect(chunks[0]).toContain('Paragraph 3');
132
+ });
133
+
134
+ test.skip('should generate answers with citations', async () => {
135
+ await qa.addDocument('Important information about the topic.');
136
+
137
+ const { answer, citations } = await qa.generateWithCitations('What is the topic?');
138
+ expect(answer).toBe('Generated answer based on context');
139
+ expect(citations.length).toBeGreaterThan(0);
140
+ expect(citations[0].content).toContain('Important information');
141
+ });
142
+
143
+ test.skip('should build appropriate prompts', () => {
144
+ const query = 'Test question';
145
+ const results: SearchResult[] = [{
146
+ chunk: {
147
+ id: '1',
148
+ content: 'Test content',
149
+ embedding: [],
150
+ metadata: {},
151
+ },
152
+ score: 0.9,
153
+ metadata: {},
154
+ }];
155
+
156
+ const prompt = qa['buildPrompt'](query, results);
157
+ expect(prompt).toContain('Reference passages');
158
+ expect(prompt).toContain('Test question');
159
+ expect(prompt).toContain('[1] Test content');
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test, mock } from 'bun:test';
2
+ import { Engineer } from '../src/roles/engineer';
3
+ import type { Message } from '../src/types/message';
4
+ import type { Action, ActionOutput } from '../src/types/action';
5
+ import type { LLMProvider } from '../src/types/llm';
6
+
7
+ describe('Role System', () => {
8
+ // 模拟 LLM 提供商
9
+ const mockLLM: LLMProvider = {
10
+ generate: mock(() => Promise.resolve('Analysis: The task requires...')),
11
+ generateStream: mock(async function* () { yield 'test'; }),
12
+ embed: mock(() => Promise.resolve([0.1, 0.2, 0.3])),
13
+ };
14
+
15
+ // 模拟动作
16
+ const mockAction: Action = {
17
+ name: 'test_action',
18
+ context: {} as any,
19
+ llm: mockLLM,
20
+ run: mock(() => Promise.resolve({
21
+ content: 'Action completed',
22
+ status: 'completed',
23
+ } as ActionOutput)),
24
+ handleException: mock(() => Promise.resolve()),
25
+ };
26
+
27
+ describe('Engineer Role', () => {
28
+ test('should initialize correctly', () => {
29
+ const engineer = new Engineer('test_engineer', mockLLM, [mockAction]);
30
+ expect(engineer.name).toBe('test_engineer');
31
+ expect(engineer.profile).toBe('Software Engineer');
32
+ expect(engineer.state).toBe(-1);
33
+ });
34
+
35
+ test('should observe and analyze tasks', async () => {
36
+ const engineer = new Engineer('test_engineer', mockLLM, [mockAction]);
37
+
38
+ // 模拟一条消息
39
+ const message: Message = {
40
+ id: 'test_message',
41
+ content: 'Implement feature X',
42
+ role: 'user',
43
+ causedBy: 'user_request',
44
+ sentFrom: 'user',
45
+ sendTo: new Set(['test_engineer']),
46
+ };
47
+
48
+ // 添加消息到内存
49
+ engineer.context.memory.add(message);
50
+
51
+ // 执行观察
52
+ const state = await engineer.observe();
53
+ expect(state).toBeGreaterThanOrEqual(-1);
54
+ expect(mockLLM.generate).toHaveBeenCalled();
55
+ });
56
+
57
+ test('should think and select action', async () => {
58
+ const engineer = new Engineer('test_engineer', mockLLM, [mockAction]);
59
+
60
+ // 执行思考
61
+ const hasAction = await engineer.think();
62
+ expect(hasAction).toBe(true);
63
+ expect(engineer.context.todo).toBe(mockAction);
64
+ });
65
+
66
+ test('should execute actions', async () => {
67
+ const engineer = new Engineer('test_engineer', mockLLM, [mockAction]);
68
+
69
+ // 设置动作并执行
70
+ await engineer.think();
71
+ const result = await engineer.act();
72
+
73
+ expect(result.content).toBe('Action completed');
74
+ expect(mockAction.run).toHaveBeenCalled();
75
+ });
76
+
77
+ test('should handle action errors', async () => {
78
+ const engineer = new Engineer('test_engineer', mockLLM, [mockAction]);
79
+
80
+ // 模拟动作执行失败
81
+ mockAction.run = mock(() => Promise.reject(new Error('Action failed')));
82
+
83
+ await engineer.think();
84
+ await expect(engineer.act()).rejects.toThrow('Action failed');
85
+ expect(mockAction.handleException).toHaveBeenCalled();
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,166 @@
1
+ import { describe, expect, test, mock } from 'bun:test';
2
+ import { BaseSkill } from '../src/skills/base-skill';
3
+ import { CodeReviewSkill } from '../src/skills/code-review';
4
+ import type { SkillConfig, SkillResult } from '../src/types/skill';
5
+ import type { Action, ActionOutput } from '../src/types/action';
6
+ import type { LLMProvider } from '../src/types/llm';
7
+
8
+ // 创建测试技能类
9
+ class TestSkill extends BaseSkill {
10
+ async execute(args?: Record<string, any>): Promise<SkillResult> {
11
+ const input = args?.input || 'default';
12
+ return this.createResult(true, `Processed: ${input}`);
13
+ }
14
+ }
15
+
16
+ // 创建测试动作类
17
+ class TestAction implements Action {
18
+ name: string;
19
+ context: any;
20
+ llm: LLMProvider;
21
+
22
+ constructor(name: string, llm: LLMProvider) {
23
+ this.name = name;
24
+ this.llm = llm;
25
+ this.context = { args: {} };
26
+ }
27
+
28
+ async run(): Promise<ActionOutput> {
29
+ return {
30
+ content: 'Test action completed',
31
+ status: 'completed',
32
+ };
33
+ }
34
+
35
+ async handleException(error: Error): Promise<void> {
36
+ console.error(error);
37
+ }
38
+ }
39
+
40
+ describe('Skill System', () => {
41
+ // 模拟 LLM 提供商
42
+ const mockLLM: LLMProvider = {
43
+ generate: mock(() => Promise.resolve('Analysis: The code needs improvement...')),
44
+ generateStream: mock(async function* () { yield 'test'; }),
45
+ embed: mock(() => Promise.resolve([0.1, 0.2, 0.3])),
46
+ };
47
+
48
+ describe('BaseSkill', () => {
49
+ test('should initialize correctly', () => {
50
+ const skill = new TestSkill({
51
+ name: 'test_skill',
52
+ description: 'Test skill',
53
+ llm: mockLLM,
54
+ });
55
+
56
+ expect(skill.name).toBe('test_skill');
57
+ expect(skill.description).toBe('Test skill');
58
+ expect(skill.llm).toBe(mockLLM);
59
+ });
60
+
61
+ test('should handle args correctly', async () => {
62
+ const skill = new TestSkill({
63
+ name: 'test_skill',
64
+ description: 'Test skill',
65
+ llm: mockLLM,
66
+ args: { input: 'test input' },
67
+ });
68
+
69
+ const result = await skill.execute({ input: 'test input' });
70
+ expect(result.success).toBe(true);
71
+ expect(result.message).toBe('Processed: test input');
72
+ });
73
+
74
+ test('should validate skill availability', async () => {
75
+ const skill = new TestSkill({
76
+ name: 'test_skill',
77
+ description: 'Test skill',
78
+ llm: mockLLM,
79
+ });
80
+
81
+ const isValid = await skill.validate();
82
+ expect(isValid).toBe(false); // No actions added
83
+
84
+ skill.actions.push(new TestAction('test_action', mockLLM));
85
+ const isValidWithAction = await skill.validate();
86
+ expect(isValidWithAction).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe('CodeReviewSkill', () => {
91
+ test('should review code correctly', async () => {
92
+ const skill = new CodeReviewSkill({
93
+ name: 'code_review',
94
+ description: 'Code review skill',
95
+ llm: mockLLM,
96
+ });
97
+
98
+ const result = await skill.execute({
99
+ code: `
100
+ function add(a, b) {
101
+ return a + b;
102
+ }`,
103
+ });
104
+
105
+ expect(result.success).toBe(true);
106
+ expect(result.message).toBe('Code review completed successfully');
107
+ expect(mockLLM.generate).toHaveBeenCalled();
108
+ });
109
+
110
+ test('should handle missing code', async () => {
111
+ const skill = new CodeReviewSkill({
112
+ name: 'code_review',
113
+ description: 'Code review skill',
114
+ llm: mockLLM,
115
+ });
116
+
117
+ const result = await skill.execute();
118
+ expect(result.success).toBe(false);
119
+ expect(result.message).toBe('No code provided for review');
120
+ });
121
+
122
+ test('should extract suggestions', async () => {
123
+ const skill = new CodeReviewSkill({
124
+ name: 'code_review',
125
+ description: 'Code review skill',
126
+ llm: mockLLM,
127
+ });
128
+
129
+ mockLLM.generate = mock(() => Promise.resolve(`
130
+ Analysis:
131
+ Some analysis text...
132
+
133
+ Suggested Improvements:
134
+ 1. Add type annotations
135
+ 2. Include error handling
136
+ 3. Improve documentation
137
+ `));
138
+
139
+ const result = await skill.execute({ code: 'test code' });
140
+ expect(result.success).toBe(true);
141
+ expect((result.data as any).suggestions).toEqual([
142
+ 'Add type annotations',
143
+ 'Include error handling',
144
+ 'Improve documentation',
145
+ ]);
146
+ });
147
+
148
+ test.skip('should handle LLM errors', async () => {
149
+ const errorLLM: LLMProvider = {
150
+ ...mockLLM,
151
+ generate: mock(() => Promise.reject(new Error('LLM failed'))),
152
+ };
153
+
154
+ const skill = new CodeReviewSkill({
155
+ name: 'code_review',
156
+ description: 'Code review skill',
157
+ llm: errorLLM,
158
+ });
159
+
160
+ const result = await skill.execute({ code: 'test code' });
161
+ expect(result.success).toBe(false);
162
+ expect(result.message).toContain('Code review failed');
163
+ expect(skill.getArg('lastError')).toBe('LLM failed');
164
+ });
165
+ });
166
+ });