@dangao/bun-server 1.12.0 → 2.0.0
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 +32 -0
- package/dist/ai/ai-module.d.ts +24 -0
- package/dist/ai/ai-module.d.ts.map +1 -0
- package/dist/ai/decorators.d.ts +25 -0
- package/dist/ai/decorators.d.ts.map +1 -0
- package/dist/ai/errors.d.ts +39 -0
- package/dist/ai/errors.d.ts.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/providers/anthropic-provider.d.ts +23 -0
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/providers/google-provider.d.ts +20 -0
- package/dist/ai/providers/google-provider.d.ts.map +1 -0
- package/dist/ai/providers/ollama-provider.d.ts +17 -0
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
- package/dist/ai/providers/openai-provider.d.ts +28 -0
- package/dist/ai/providers/openai-provider.d.ts.map +1 -0
- package/dist/ai/service.d.ts +40 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/tools/tool-executor.d.ts +15 -0
- package/dist/ai/tools/tool-executor.d.ts.map +1 -0
- package/dist/ai/tools/tool-registry.d.ts +39 -0
- package/dist/ai/tools/tool-registry.d.ts.map +1 -0
- package/dist/ai/types.d.ts +134 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai-guard/ai-guard-module.d.ts +18 -0
- package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
- package/dist/ai-guard/decorators.d.ts +16 -0
- package/dist/ai-guard/decorators.d.ts.map +1 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
- package/dist/ai-guard/index.d.ts +8 -0
- package/dist/ai-guard/index.d.ts.map +1 -0
- package/dist/ai-guard/service.d.ts +21 -0
- package/dist/ai-guard/service.d.ts.map +1 -0
- package/dist/ai-guard/types.d.ts +59 -0
- package/dist/ai-guard/types.d.ts.map +1 -0
- package/dist/conversation/conversation-module.d.ts +25 -0
- package/dist/conversation/conversation-module.d.ts.map +1 -0
- package/dist/conversation/decorators.d.ts +28 -0
- package/dist/conversation/decorators.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +8 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/service.d.ts +43 -0
- package/dist/conversation/service.d.ts.map +1 -0
- package/dist/conversation/stores/database-store.d.ts +46 -0
- package/dist/conversation/stores/database-store.d.ts.map +1 -0
- package/dist/conversation/stores/memory-store.d.ts +17 -0
- package/dist/conversation/stores/memory-store.d.ts.map +1 -0
- package/dist/conversation/stores/redis-store.d.ts +39 -0
- package/dist/conversation/stores/redis-store.d.ts.map +1 -0
- package/dist/conversation/types.d.ts +64 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/core/cluster.d.ts +42 -3
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/embedding/embedding-module.d.ts +20 -0
- package/dist/embedding/embedding-module.d.ts.map +1 -0
- package/dist/embedding/index.d.ts +6 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/service.d.ts +27 -0
- package/dist/embedding/service.d.ts.map +1 -0
- package/dist/embedding/types.d.ts +25 -0
- package/dist/embedding/types.d.ts.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2870 -88
- package/dist/mcp/decorators.d.ts +42 -0
- package/dist/mcp/decorators.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/mcp-module.d.ts +22 -0
- package/dist/mcp/mcp-module.d.ts.map +1 -0
- package/dist/mcp/registry.d.ts +23 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +29 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/types.d.ts +60 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/prompt/index.d.ts +6 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/prompt-module.d.ts +23 -0
- package/dist/prompt/prompt-module.d.ts.map +1 -0
- package/dist/prompt/service.d.ts +47 -0
- package/dist/prompt/service.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts +36 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -0
- package/dist/prompt/stores/memory-store.d.ts +17 -0
- package/dist/prompt/stores/memory-store.d.ts.map +1 -0
- package/dist/prompt/types.d.ts +68 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
- package/dist/rag/chunkers/text-chunker.d.ts +11 -0
- package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
- package/dist/rag/decorators.d.ts +24 -0
- package/dist/rag/decorators.d.ts.map +1 -0
- package/dist/rag/index.d.ts +7 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/rag-module.d.ts +23 -0
- package/dist/rag/rag-module.d.ts.map +1 -0
- package/dist/rag/service.d.ts +36 -0
- package/dist/rag/service.d.ts.map +1 -0
- package/dist/rag/types.d.ts +56 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/vector-store/index.d.ts +6 -0
- package/dist/vector-store/index.d.ts.map +1 -0
- package/dist/vector-store/stores/memory-store.d.ts +17 -0
- package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
- package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
- package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
- package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
- package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
- package/dist/vector-store/types.d.ts +60 -0
- package/dist/vector-store/types.d.ts.map +1 -0
- package/dist/vector-store/vector-store-module.d.ts +20 -0
- package/dist/vector-store/vector-store-module.d.ts.map +1 -0
- package/docs/ai.md +500 -0
- package/docs/best-practices.md +83 -8
- package/docs/database.md +23 -0
- package/docs/guide.md +90 -27
- package/docs/migration.md +81 -7
- package/docs/security.md +23 -0
- package/docs/zh/ai.md +441 -0
- package/docs/zh/best-practices.md +43 -0
- package/docs/zh/database.md +23 -0
- package/docs/zh/guide.md +40 -1
- package/docs/zh/migration.md +39 -0
- package/docs/zh/security.md +23 -0
- package/package.json +2 -2
- package/src/ai/ai-module.ts +62 -0
- package/src/ai/decorators.ts +30 -0
- package/src/ai/errors.ts +71 -0
- package/src/ai/index.ts +11 -0
- package/src/ai/providers/anthropic-provider.ts +190 -0
- package/src/ai/providers/google-provider.ts +179 -0
- package/src/ai/providers/ollama-provider.ts +126 -0
- package/src/ai/providers/openai-provider.ts +242 -0
- package/src/ai/service.ts +155 -0
- package/src/ai/tools/tool-executor.ts +38 -0
- package/src/ai/tools/tool-registry.ts +91 -0
- package/src/ai/types.ts +145 -0
- package/src/ai-guard/ai-guard-module.ts +50 -0
- package/src/ai-guard/decorators.ts +21 -0
- package/src/ai-guard/detectors/content-moderator.ts +80 -0
- package/src/ai-guard/detectors/injection-detector.ts +48 -0
- package/src/ai-guard/detectors/pii-detector.ts +64 -0
- package/src/ai-guard/index.ts +7 -0
- package/src/ai-guard/service.ts +100 -0
- package/src/ai-guard/types.ts +61 -0
- package/src/conversation/conversation-module.ts +63 -0
- package/src/conversation/decorators.ts +47 -0
- package/src/conversation/index.ts +7 -0
- package/src/conversation/service.ts +133 -0
- package/src/conversation/stores/database-store.ts +125 -0
- package/src/conversation/stores/memory-store.ts +57 -0
- package/src/conversation/stores/redis-store.ts +101 -0
- package/src/conversation/types.ts +68 -0
- package/src/core/cluster.ts +239 -46
- package/src/core/index.ts +1 -1
- package/src/core/server.ts +91 -78
- package/src/embedding/embedding-module.ts +52 -0
- package/src/embedding/index.ts +5 -0
- package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
- package/src/embedding/providers/openai-embedding-provider.ts +47 -0
- package/src/embedding/service.ts +55 -0
- package/src/embedding/types.ts +27 -0
- package/src/index.ts +11 -1
- package/src/mcp/decorators.ts +60 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/mcp-module.ts +58 -0
- package/src/mcp/registry.ts +72 -0
- package/src/mcp/server.ts +164 -0
- package/src/mcp/types.ts +63 -0
- package/src/prompt/index.ts +5 -0
- package/src/prompt/prompt-module.ts +61 -0
- package/src/prompt/service.ts +93 -0
- package/src/prompt/stores/file-store.ts +135 -0
- package/src/prompt/stores/memory-store.ts +82 -0
- package/src/prompt/types.ts +84 -0
- package/src/rag/chunkers/markdown-chunker.ts +40 -0
- package/src/rag/chunkers/text-chunker.ts +30 -0
- package/src/rag/decorators.ts +26 -0
- package/src/rag/index.ts +6 -0
- package/src/rag/rag-module.ts +78 -0
- package/src/rag/service.ts +134 -0
- package/src/rag/types.ts +47 -0
- package/src/vector-store/index.ts +5 -0
- package/src/vector-store/stores/memory-store.ts +69 -0
- package/src/vector-store/stores/pinecone-store.ts +123 -0
- package/src/vector-store/stores/qdrant-store.ts +147 -0
- package/src/vector-store/types.ts +77 -0
- package/src/vector-store/vector-store-module.ts +50 -0
- package/tests/ai/ai-module.test.ts +46 -0
- package/tests/ai/ai-service.test.ts +91 -0
- package/tests/ai/tool-registry.test.ts +57 -0
- package/tests/ai-guard/ai-guard-module.test.ts +23 -0
- package/tests/ai-guard/content-moderator.test.ts +65 -0
- package/tests/ai-guard/pii-detector.test.ts +41 -0
- package/tests/conversation/conversation-module.test.ts +26 -0
- package/tests/conversation/conversation-service.test.ts +64 -0
- package/tests/conversation/memory-store.test.ts +68 -0
- package/tests/core/cluster.test.ts +45 -1
- package/tests/embedding/embedding-service.test.ts +55 -0
- package/tests/mcp/mcp-server.test.ts +85 -0
- package/tests/prompt/prompt-module.test.ts +30 -0
- package/tests/prompt/prompt-service.test.ts +74 -0
- package/tests/rag/chunkers.test.ts +58 -0
- package/tests/rag/rag-service.test.ts +66 -0
- package/tests/vector-store/memory-vector-store.test.ts +84 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
- package/tests/perf/optimization.test.ts +0 -182
- package/tests/perf/regression.test.ts +0 -120
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { McpServer } from '../../src/mcp/server';
|
|
3
|
+
import { McpRegistry } from '../../src/mcp/registry';
|
|
4
|
+
import { McpTool } from '../../src/mcp/decorators';
|
|
5
|
+
import type { McpServerInfo } from '../../src/mcp/types';
|
|
6
|
+
|
|
7
|
+
const serverInfo: McpServerInfo = { name: 'test-server', version: '1.0.0' };
|
|
8
|
+
|
|
9
|
+
function createServer(): { server: McpServer; registry: McpRegistry } {
|
|
10
|
+
const registry = new McpRegistry();
|
|
11
|
+
const server = new McpServer(registry, serverInfo);
|
|
12
|
+
return { server, registry };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('McpServer', () => {
|
|
16
|
+
test('should handle initialize request', async () => {
|
|
17
|
+
const { server } = createServer();
|
|
18
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
19
|
+
|
|
20
|
+
expect(response.error).toBeUndefined();
|
|
21
|
+
expect((response.result as Record<string, unknown>)['protocolVersion']).toBeDefined();
|
|
22
|
+
expect((response.result as Record<string, unknown>)['serverInfo']).toMatchObject(serverInfo);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should list tools', async () => {
|
|
26
|
+
const { server, registry } = createServer();
|
|
27
|
+
|
|
28
|
+
class MyTools {
|
|
29
|
+
@McpTool({ name: 'echo', description: 'Echo input', inputSchema: { type: 'object' } })
|
|
30
|
+
async echo({ text }: { text: string }) { return text; }
|
|
31
|
+
}
|
|
32
|
+
registry.scan(new MyTools());
|
|
33
|
+
|
|
34
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
|
|
35
|
+
const tools = (response.result as { tools: unknown[] }).tools;
|
|
36
|
+
expect(tools).toHaveLength(1);
|
|
37
|
+
expect((tools[0] as Record<string, unknown>)['name']).toBe('echo');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should call a tool', async () => {
|
|
41
|
+
const { server, registry } = createServer();
|
|
42
|
+
|
|
43
|
+
class EchoTool {
|
|
44
|
+
@McpTool({ name: 'echo', description: 'Echo', inputSchema: {} })
|
|
45
|
+
async echo(args: { text: string }) { return `Echo: ${args.text}`; }
|
|
46
|
+
}
|
|
47
|
+
registry.scan(new EchoTool());
|
|
48
|
+
|
|
49
|
+
const response = await server.handle({
|
|
50
|
+
jsonrpc: '2.0',
|
|
51
|
+
id: 3,
|
|
52
|
+
method: 'tools/call',
|
|
53
|
+
params: { name: 'echo', arguments: { text: 'Hello' } },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(response.error).toBeUndefined();
|
|
57
|
+
const content = (response.result as { content: Array<{ text: string }> }).content;
|
|
58
|
+
expect(content[0]!.text).toContain('Hello');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should return error for unknown method', async () => {
|
|
62
|
+
const { server } = createServer();
|
|
63
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 4, method: 'unknown/method' });
|
|
64
|
+
expect(response.error).toBeDefined();
|
|
65
|
+
expect(response.error!.code).toBe(-32601);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should return error for unknown tool call', async () => {
|
|
69
|
+
const { server } = createServer();
|
|
70
|
+
const response = await server.handle({
|
|
71
|
+
jsonrpc: '2.0',
|
|
72
|
+
id: 5,
|
|
73
|
+
method: 'tools/call',
|
|
74
|
+
params: { name: 'non-existent', arguments: {} },
|
|
75
|
+
});
|
|
76
|
+
expect(response.error).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should handle ping', async () => {
|
|
80
|
+
const { server } = createServer();
|
|
81
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 6, method: 'ping' });
|
|
82
|
+
expect(response.error).toBeUndefined();
|
|
83
|
+
expect(response.result).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { PromptModule } from '../../src/prompt/prompt-module';
|
|
3
|
+
import { PROMPT_SERVICE_TOKEN } from '../../src/prompt/types';
|
|
4
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
5
|
+
|
|
6
|
+
describe('PromptModule', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
PromptModule.reset();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should register providers on forRoot()', () => {
|
|
12
|
+
PromptModule.forRoot();
|
|
13
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, PromptModule);
|
|
14
|
+
expect(metadata).toBeDefined();
|
|
15
|
+
expect(metadata.exports).toContain(PROMPT_SERVICE_TOKEN);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should use InMemoryPromptStore by default', () => {
|
|
19
|
+
PromptModule.forRoot();
|
|
20
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, PromptModule);
|
|
21
|
+
expect(metadata.providers.length).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('reset() should clear module metadata', () => {
|
|
25
|
+
PromptModule.forRoot();
|
|
26
|
+
PromptModule.reset();
|
|
27
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, PromptModule);
|
|
28
|
+
expect(metadata).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { PromptService } from '../../src/prompt/service';
|
|
3
|
+
import { InMemoryPromptStore } from '../../src/prompt/stores/memory-store';
|
|
4
|
+
|
|
5
|
+
function createService(): PromptService {
|
|
6
|
+
return new PromptService({ store: new InMemoryPromptStore() } as never);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('PromptService', () => {
|
|
10
|
+
test('should create and retrieve a template', async () => {
|
|
11
|
+
const service = createService();
|
|
12
|
+
const created = await service.create({ name: 'greeting', content: 'Hello, {{name}}!' });
|
|
13
|
+
|
|
14
|
+
expect(created.id).toBeDefined();
|
|
15
|
+
expect(created.version).toBe(1);
|
|
16
|
+
expect(created.variables).toContain('name');
|
|
17
|
+
|
|
18
|
+
const fetched = await service.get(created.id);
|
|
19
|
+
expect(fetched.content).toBe('Hello, {{name}}!');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should render template with variables', async () => {
|
|
23
|
+
const service = createService();
|
|
24
|
+
const template = await service.create({ name: 't', content: 'Hi {{name}}, welcome to {{app}}!' });
|
|
25
|
+
const rendered = await service.render(template.id, { name: 'Alice', app: 'MyApp' });
|
|
26
|
+
expect(rendered).toBe('Hi Alice, welcome to MyApp!');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should leave unresolved variables as-is', async () => {
|
|
30
|
+
const service = createService();
|
|
31
|
+
const template = await service.create({ name: 't', content: 'Hello {{name}}, from {{sender}}!' });
|
|
32
|
+
const rendered = await service.render(template.id, { name: 'Bob' });
|
|
33
|
+
expect(rendered).toBe('Hello Bob, from {{sender}}!');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should increment version on update', async () => {
|
|
37
|
+
const service = createService();
|
|
38
|
+
const template = await service.create({ name: 't', content: 'v1 {{x}}' });
|
|
39
|
+
const updated = await service.update(template.id, { content: 'v2 {{x}} {{y}}' });
|
|
40
|
+
expect(updated.version).toBe(2);
|
|
41
|
+
expect(updated.variables).toContain('y');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should retrieve specific version', async () => {
|
|
45
|
+
const service = createService();
|
|
46
|
+
const template = await service.create({ name: 't', content: 'v1' });
|
|
47
|
+
await service.update(template.id, { content: 'v2' });
|
|
48
|
+
|
|
49
|
+
const v1 = await service.getVersion(template.id, 1);
|
|
50
|
+
expect(v1.content).toBe('v1');
|
|
51
|
+
const v2 = await service.getVersion(template.id, 2);
|
|
52
|
+
expect(v2.content).toBe('v2');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should list all templates', async () => {
|
|
56
|
+
const service = createService();
|
|
57
|
+
await service.create({ name: 'a', content: 'A' });
|
|
58
|
+
await service.create({ name: 'b', content: 'B' });
|
|
59
|
+
const list = await service.list();
|
|
60
|
+
expect(list).toHaveLength(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should delete template', async () => {
|
|
64
|
+
const service = createService();
|
|
65
|
+
const template = await service.create({ name: 'd', content: 'delete me' });
|
|
66
|
+
await service.delete(template.id);
|
|
67
|
+
expect(service.get(template.id)).rejects.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should throw 404 for missing template', async () => {
|
|
71
|
+
const service = createService();
|
|
72
|
+
expect(service.get('non-existent')).rejects.toThrow('not found');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { TextChunker } from '../../src/rag/chunkers/text-chunker';
|
|
3
|
+
import { MarkdownChunker } from '../../src/rag/chunkers/markdown-chunker';
|
|
4
|
+
|
|
5
|
+
describe('TextChunker', () => {
|
|
6
|
+
test('should split text into chunks of specified size', () => {
|
|
7
|
+
const chunker = new TextChunker(10, 0);
|
|
8
|
+
const chunks = chunker.chunk('Hello World this is a test string');
|
|
9
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
10
|
+
for (const chunk of chunks) {
|
|
11
|
+
expect(chunk.content.length).toBeLessThanOrEqual(10);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should include overlap between chunks', () => {
|
|
16
|
+
const chunker = new TextChunker(20, 5);
|
|
17
|
+
const text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
18
|
+
const chunks = chunker.chunk(text);
|
|
19
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should return single chunk for short text', () => {
|
|
23
|
+
const chunker = new TextChunker(512, 50);
|
|
24
|
+
const chunks = chunker.chunk('Short text');
|
|
25
|
+
expect(chunks).toHaveLength(1);
|
|
26
|
+
expect(chunks[0]!.content).toBe('Short text');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should not return empty chunks', () => {
|
|
30
|
+
const chunker = new TextChunker(10, 0);
|
|
31
|
+
const chunks = chunker.chunk(' \n\n ');
|
|
32
|
+
expect(chunks).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('MarkdownChunker', () => {
|
|
37
|
+
test('should split by headings', () => {
|
|
38
|
+
const chunker = new MarkdownChunker(1000);
|
|
39
|
+
const markdown = `# Title\n\nIntro text.\n\n## Section 1\n\nContent 1.\n\n## Section 2\n\nContent 2.`;
|
|
40
|
+
const chunks = chunker.chunk(markdown);
|
|
41
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should handle text without headings', () => {
|
|
45
|
+
const chunker = new MarkdownChunker(1000);
|
|
46
|
+
const text = 'Just plain text without any headings.';
|
|
47
|
+
const chunks = chunker.chunk(text);
|
|
48
|
+
expect(chunks).toHaveLength(1);
|
|
49
|
+
expect(chunks[0]!.content).toBe(text);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should split large sections into paragraphs', () => {
|
|
53
|
+
const chunker = new MarkdownChunker(50);
|
|
54
|
+
const markdown = `## Big Section\n\nParagraph one with some content.\n\nParagraph two with more content that makes it too long.`;
|
|
55
|
+
const chunks = chunker.chunk(markdown);
|
|
56
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { RagService } from '../../src/rag/service';
|
|
3
|
+
import { MemoryVectorStore } from '../../src/vector-store/stores/memory-store';
|
|
4
|
+
import { EmbeddingService } from '../../src/embedding/service';
|
|
5
|
+
|
|
6
|
+
class MockEmbeddingProvider {
|
|
7
|
+
readonly name = 'mock';
|
|
8
|
+
readonly dimensions = 4;
|
|
9
|
+
|
|
10
|
+
async embed(text: string): Promise<number[]> {
|
|
11
|
+
const codes = text.split('').slice(0, 4).map((c) => c.charCodeAt(0) / 255);
|
|
12
|
+
while (codes.length < 4) codes.push(0);
|
|
13
|
+
return codes;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
17
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createRagService(): RagService {
|
|
22
|
+
const options = { collection: 'test', chunkSize: 100, chunkOverlap: 10, topK: 3, minScore: 0 };
|
|
23
|
+
const embeddingService = new EmbeddingService({
|
|
24
|
+
provider: { name: 'mock', provider: MockEmbeddingProvider as never, config: {} },
|
|
25
|
+
} as never);
|
|
26
|
+
const vectorStore = new MemoryVectorStore();
|
|
27
|
+
|
|
28
|
+
return new RagService(options as never, embeddingService, vectorStore);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('RagService', () => {
|
|
32
|
+
test('should ingest text and return chunk count', async () => {
|
|
33
|
+
const service = createRagService();
|
|
34
|
+
const count = await service.ingest({ type: 'text', content: 'Hello world, this is a test document for RAG.' });
|
|
35
|
+
expect(count).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should retrieve context after ingestion', async () => {
|
|
39
|
+
const service = createRagService();
|
|
40
|
+
await service.ingest({ type: 'text', content: 'The sky is blue and the grass is green.' });
|
|
41
|
+
const context = await service.retrieve('sky');
|
|
42
|
+
expect(context.chunks.length).toBeGreaterThanOrEqual(0);
|
|
43
|
+
expect(typeof context.formatted).toBe('string');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should return empty context for empty store', async () => {
|
|
47
|
+
const service = createRagService();
|
|
48
|
+
const context = await service.retrieve('anything');
|
|
49
|
+
expect(context.chunks).toHaveLength(0);
|
|
50
|
+
expect(context.formatted).toBe('');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should build context prompt string', async () => {
|
|
54
|
+
const service = createRagService();
|
|
55
|
+
await service.ingest({ type: 'text', content: 'Water is H2O.' });
|
|
56
|
+
const prompt = await service.buildContextPrompt('water');
|
|
57
|
+
expect(typeof prompt).toBe('string');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should ingest multiple documents', async () => {
|
|
61
|
+
const service = createRagService();
|
|
62
|
+
const count1 = await service.ingest({ type: 'text', content: 'Document one content.' });
|
|
63
|
+
const count2 = await service.ingest({ type: 'text', content: 'Document two content.' });
|
|
64
|
+
expect(count1 + count2).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { MemoryVectorStore } from '../../src/vector-store/stores/memory-store';
|
|
3
|
+
import { cosineSimilarity } from '../../src/vector-store/types';
|
|
4
|
+
|
|
5
|
+
describe('MemoryVectorStore', () => {
|
|
6
|
+
test('should upsert and retrieve a document', async () => {
|
|
7
|
+
const store = new MemoryVectorStore();
|
|
8
|
+
await store.upsert({ id: 'doc1', vector: [1, 0, 0], content: 'Test', collection: 'col' });
|
|
9
|
+
const doc = await store.get('doc1', 'col');
|
|
10
|
+
expect(doc).not.toBeNull();
|
|
11
|
+
expect(doc!.content).toBe('Test');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should return null for missing document', async () => {
|
|
15
|
+
const store = new MemoryVectorStore();
|
|
16
|
+
expect(await store.get('missing')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should search by cosine similarity', async () => {
|
|
20
|
+
const store = new MemoryVectorStore();
|
|
21
|
+
await store.upsert({ id: 'a', vector: [1, 0, 0], content: 'Horizontal', collection: 'test' });
|
|
22
|
+
await store.upsert({ id: 'b', vector: [0, 1, 0], content: 'Vertical', collection: 'test' });
|
|
23
|
+
await store.upsert({ id: 'c', vector: [0.9, 0.1, 0], content: 'Mostly horizontal', collection: 'test' });
|
|
24
|
+
|
|
25
|
+
const results = await store.search([1, 0, 0], { topK: 2, collection: 'test' });
|
|
26
|
+
expect(results).toHaveLength(2);
|
|
27
|
+
// Most similar to [1,0,0] should be doc 'a' first
|
|
28
|
+
expect(results[0]!.document.id).toBe('a');
|
|
29
|
+
expect(results[0]!.score).toBeCloseTo(1.0, 2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should filter by collection', async () => {
|
|
33
|
+
const store = new MemoryVectorStore();
|
|
34
|
+
await store.upsert({ id: '1', vector: [1, 0], content: 'A', collection: 'col1' });
|
|
35
|
+
await store.upsert({ id: '2', vector: [1, 0], content: 'B', collection: 'col2' });
|
|
36
|
+
|
|
37
|
+
const results = await store.search([1, 0], { collection: 'col1' });
|
|
38
|
+
expect(results).toHaveLength(1);
|
|
39
|
+
expect(results[0]!.document.content).toBe('A');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should delete a document', async () => {
|
|
43
|
+
const store = new MemoryVectorStore();
|
|
44
|
+
await store.upsert({ id: 'del', vector: [1, 0], content: 'Delete me' });
|
|
45
|
+
const deleted = await store.delete('del');
|
|
46
|
+
expect(deleted).toBe(true);
|
|
47
|
+
expect(await store.get('del')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should delete entire collection', async () => {
|
|
51
|
+
const store = new MemoryVectorStore();
|
|
52
|
+
await store.upsert({ id: '1', vector: [1, 0], content: 'A', collection: 'purge' });
|
|
53
|
+
await store.upsert({ id: '2', vector: [0, 1], content: 'B', collection: 'purge' });
|
|
54
|
+
await store.deleteCollection('purge');
|
|
55
|
+
expect(await store.count('purge')).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should count documents', async () => {
|
|
59
|
+
const store = new MemoryVectorStore();
|
|
60
|
+
await store.upsert({ id: '1', vector: [1], content: 'A', collection: 'x' });
|
|
61
|
+
await store.upsert({ id: '2', vector: [2], content: 'B', collection: 'x' });
|
|
62
|
+
await store.upsert({ id: '3', vector: [3], content: 'C', collection: 'y' });
|
|
63
|
+
expect(await store.count()).toBe(3);
|
|
64
|
+
expect(await store.count('x')).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('cosineSimilarity', () => {
|
|
69
|
+
test('identical vectors have similarity 1.0', () => {
|
|
70
|
+
expect(cosineSimilarity([1, 0, 0], [1, 0, 0])).toBeCloseTo(1.0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('orthogonal vectors have similarity 0.0', () => {
|
|
74
|
+
expect(cosineSimilarity([1, 0], [0, 1])).toBeCloseTo(0.0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('opposite vectors have similarity -1.0', () => {
|
|
78
|
+
expect(cosineSimilarity([1, 0], [-1, 0])).toBeCloseTo(-1.0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('zero vectors return 0', () => {
|
|
82
|
+
expect(cosineSimilarity([0, 0], [1, 1])).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|