@dangao/bun-server 1.12.1 → 2.0.1
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/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 +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2638 -1
- 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 +3 -3
- 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/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 +10 -0
- 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/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,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PromptStore,
|
|
3
|
+
PromptTemplate,
|
|
4
|
+
CreatePromptTemplateInput,
|
|
5
|
+
UpdatePromptTemplateInput,
|
|
6
|
+
} from '../types';
|
|
7
|
+
import { extractVariables } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory prompt store. Supports versioning via a separate history map.
|
|
11
|
+
*/
|
|
12
|
+
export class InMemoryPromptStore implements PromptStore {
|
|
13
|
+
private readonly templates = new Map<string, PromptTemplate>();
|
|
14
|
+
/** id → version → template snapshot */
|
|
15
|
+
private readonly history = new Map<string, Map<number, PromptTemplate>>();
|
|
16
|
+
|
|
17
|
+
public async get(id: string): Promise<PromptTemplate | null> {
|
|
18
|
+
return this.templates.get(id) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async getVersion(id: string, version: number): Promise<PromptTemplate | null> {
|
|
22
|
+
return this.history.get(id)?.get(version) ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async list(): Promise<PromptTemplate[]> {
|
|
26
|
+
return Array.from(this.templates.values());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async create(input: CreatePromptTemplateInput): Promise<PromptTemplate> {
|
|
30
|
+
const id = input.id ?? crypto.randomUUID();
|
|
31
|
+
if (this.templates.has(id)) {
|
|
32
|
+
throw new Error(`Prompt template "${id}" already exists`);
|
|
33
|
+
}
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const template: PromptTemplate = {
|
|
36
|
+
id,
|
|
37
|
+
name: input.name,
|
|
38
|
+
content: input.content,
|
|
39
|
+
version: 1,
|
|
40
|
+
variables: extractVariables(input.content),
|
|
41
|
+
description: input.description,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
updatedAt: now,
|
|
44
|
+
};
|
|
45
|
+
this.templates.set(id, template);
|
|
46
|
+
this.saveVersion(template);
|
|
47
|
+
return { ...template };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async update(id: string, input: UpdatePromptTemplateInput): Promise<PromptTemplate> {
|
|
51
|
+
const existing = this.templates.get(id);
|
|
52
|
+
if (!existing) throw new Error(`Prompt template "${id}" not found`);
|
|
53
|
+
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const content = input.content ?? existing.content;
|
|
56
|
+
const updated: PromptTemplate = {
|
|
57
|
+
...existing,
|
|
58
|
+
name: input.name ?? existing.name,
|
|
59
|
+
content,
|
|
60
|
+
description: input.description ?? existing.description,
|
|
61
|
+
version: existing.version + 1,
|
|
62
|
+
variables: extractVariables(content),
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
};
|
|
65
|
+
this.templates.set(id, updated);
|
|
66
|
+
this.saveVersion(updated);
|
|
67
|
+
return { ...updated };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async delete(id: string): Promise<boolean> {
|
|
71
|
+
const existed = this.templates.delete(id);
|
|
72
|
+
this.history.delete(id);
|
|
73
|
+
return existed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private saveVersion(template: PromptTemplate): void {
|
|
77
|
+
if (!this.history.has(template.id)) {
|
|
78
|
+
this.history.set(template.id, new Map());
|
|
79
|
+
}
|
|
80
|
+
this.history.get(template.id)!.set(template.version, { ...template });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A versioned prompt template with variable interpolation
|
|
3
|
+
*/
|
|
4
|
+
export interface PromptTemplate {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** Template content with {{variable}} placeholders */
|
|
8
|
+
content: string;
|
|
9
|
+
/** Current version number (starts at 1, increments on each update) */
|
|
10
|
+
version: number;
|
|
11
|
+
/** Declared variable names (auto-extracted from content) */
|
|
12
|
+
variables: string[];
|
|
13
|
+
/** Optional description */
|
|
14
|
+
description?: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Input for creating a prompt template
|
|
21
|
+
*/
|
|
22
|
+
export interface CreatePromptTemplateInput {
|
|
23
|
+
id?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
content: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Input for updating a prompt template (creates new version)
|
|
31
|
+
*/
|
|
32
|
+
export interface UpdatePromptTemplateInput {
|
|
33
|
+
name?: string;
|
|
34
|
+
content?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Abstract prompt store interface
|
|
40
|
+
*/
|
|
41
|
+
export interface PromptStore {
|
|
42
|
+
get(id: string): Promise<PromptTemplate | null>;
|
|
43
|
+
getVersion(id: string, version: number): Promise<PromptTemplate | null>;
|
|
44
|
+
list(): Promise<PromptTemplate[]>;
|
|
45
|
+
create(input: CreatePromptTemplateInput): Promise<PromptTemplate>;
|
|
46
|
+
update(id: string, input: UpdatePromptTemplateInput): Promise<PromptTemplate>;
|
|
47
|
+
delete(id: string): Promise<boolean>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PromptModule configuration
|
|
52
|
+
*/
|
|
53
|
+
export interface PromptModuleOptions {
|
|
54
|
+
store?: PromptStore;
|
|
55
|
+
/**
|
|
56
|
+
* Preload templates on module init
|
|
57
|
+
* (only used by FilePromptStore, which loads from promptsDir)
|
|
58
|
+
*/
|
|
59
|
+
promptsDir?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const PROMPT_SERVICE_TOKEN = Symbol('@dangao/bun-server:prompt:service');
|
|
63
|
+
export const PROMPT_OPTIONS_TOKEN = Symbol('@dangao/bun-server:prompt:options');
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract variable names from template content
|
|
67
|
+
* Matches {{varName}} patterns
|
|
68
|
+
*/
|
|
69
|
+
export function extractVariables(content: string): string[] {
|
|
70
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
71
|
+
const vars = new Set<string>();
|
|
72
|
+
let match: RegExpExecArray | null;
|
|
73
|
+
while ((match = regex.exec(content)) !== null) {
|
|
74
|
+
vars.add(match[1]!);
|
|
75
|
+
}
|
|
76
|
+
return Array.from(vars);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Render a template by replacing {{varName}} with provided values
|
|
81
|
+
*/
|
|
82
|
+
export function renderTemplate(content: string, vars: Record<string, string>): string {
|
|
83
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key: string) => vars[key] ?? `{{${key}}}`);
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DocumentChunker, DocumentChunk } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Splits Markdown text into semantic chunks by heading boundaries.
|
|
5
|
+
* Falls back to text chunking for sections that are too large.
|
|
6
|
+
*/
|
|
7
|
+
export class MarkdownChunker implements DocumentChunker {
|
|
8
|
+
private readonly maxChunkSize: number;
|
|
9
|
+
|
|
10
|
+
public constructor(maxChunkSize = 1024) {
|
|
11
|
+
this.maxChunkSize = maxChunkSize;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public chunk(text: string): DocumentChunk[] {
|
|
15
|
+
const chunks: DocumentChunk[] = [];
|
|
16
|
+
// Split on headings (## or ###)
|
|
17
|
+
const sections = text.split(/(?=^#{1,3} )/m).filter((s) => s.trim());
|
|
18
|
+
|
|
19
|
+
for (const section of sections) {
|
|
20
|
+
if (section.length <= this.maxChunkSize) {
|
|
21
|
+
chunks.push({ content: section.trim() });
|
|
22
|
+
} else {
|
|
23
|
+
// Split large sections into paragraphs
|
|
24
|
+
const paragraphs = section.split(/\n\n+/).filter((p) => p.trim());
|
|
25
|
+
let current = '';
|
|
26
|
+
for (const para of paragraphs) {
|
|
27
|
+
if ((current + '\n\n' + para).length > this.maxChunkSize && current) {
|
|
28
|
+
chunks.push({ content: current.trim() });
|
|
29
|
+
current = para;
|
|
30
|
+
} else {
|
|
31
|
+
current = current ? current + '\n\n' + para : para;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (current.trim()) chunks.push({ content: current.trim() });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return chunks.length > 0 ? chunks : [{ content: text.trim() }];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DocumentChunker, DocumentChunk } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Splits plain text into overlapping chunks of fixed character size.
|
|
5
|
+
*/
|
|
6
|
+
export class TextChunker implements DocumentChunker {
|
|
7
|
+
private readonly chunkSize: number;
|
|
8
|
+
private readonly chunkOverlap: number;
|
|
9
|
+
|
|
10
|
+
public constructor(chunkSize = 512, chunkOverlap = 50) {
|
|
11
|
+
this.chunkSize = chunkSize;
|
|
12
|
+
this.chunkOverlap = chunkOverlap;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public chunk(text: string): DocumentChunk[] {
|
|
16
|
+
const chunks: DocumentChunk[] = [];
|
|
17
|
+
const step = this.chunkSize - this.chunkOverlap;
|
|
18
|
+
|
|
19
|
+
for (let start = 0; start < text.length; start += step) {
|
|
20
|
+
const end = Math.min(start + this.chunkSize, text.length);
|
|
21
|
+
const content = text.slice(start, end).trim();
|
|
22
|
+
if (content.length > 0) {
|
|
23
|
+
chunks.push({ content });
|
|
24
|
+
}
|
|
25
|
+
if (end >= text.length) break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return chunks;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata key for @Rag() decorator
|
|
3
|
+
*/
|
|
4
|
+
export const RAG_METADATA_KEY = '@dangao/bun-server:rag:collection';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mark a controller method to auto-retrieve RAG context before execution.
|
|
8
|
+
* The retrieved context is available via `@Inject(RAG_SERVICE_TOKEN)` in the service layer.
|
|
9
|
+
*
|
|
10
|
+
* @param collection - VectorStore collection to search (uses default if omitted)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* \@POST('/ask')
|
|
15
|
+
* \@Rag({ collection: 'product-docs' })
|
|
16
|
+
* public async ask(\@Body() body: { question: string }) {
|
|
17
|
+
* // RAG context is automatically fetched before this method
|
|
18
|
+
* // Access via RagService.retrieve() in your service
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function Rag(options: { collection?: string } = {}): MethodDecorator {
|
|
23
|
+
return (target, propertyKey) => {
|
|
24
|
+
Reflect.defineMetadata(RAG_METADATA_KEY, options, target, propertyKey);
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/rag/index.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import type { ModuleProvider } from '../di/module';
|
|
3
|
+
import type { Container } from '../di/container';
|
|
4
|
+
import { RagService } from './service';
|
|
5
|
+
import { RAG_SERVICE_TOKEN, RAG_OPTIONS_TOKEN, type RagModuleOptions } from './types';
|
|
6
|
+
import { EMBEDDING_SERVICE_TOKEN } from '../embedding/types';
|
|
7
|
+
import { VECTOR_STORE_TOKEN } from '../vector-store/types';
|
|
8
|
+
import { EmbeddingModule } from '../embedding/embedding-module';
|
|
9
|
+
import { VectorStoreModule } from '../vector-store/vector-store-module';
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
imports: [EmbeddingModule, VectorStoreModule],
|
|
13
|
+
providers: [],
|
|
14
|
+
})
|
|
15
|
+
export class RagModule {
|
|
16
|
+
/**
|
|
17
|
+
* Configure the RAG module.
|
|
18
|
+
*
|
|
19
|
+
* **Requires** `EmbeddingModule` and `VectorStoreModule` to be imported first.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* EmbeddingModule.forRoot({ provider: { ... } });
|
|
24
|
+
* VectorStoreModule.forRoot({});
|
|
25
|
+
* RagModule.forRoot({ collection: 'docs', chunkSize: 512, topK: 5 });
|
|
26
|
+
*
|
|
27
|
+
* \@Module({
|
|
28
|
+
* imports: [EmbeddingModule, VectorStoreModule, RagModule],
|
|
29
|
+
* })
|
|
30
|
+
* class AppModule {}
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
public static forRoot(options: RagModuleOptions = {}): typeof RagModule {
|
|
34
|
+
const resolvedOptions: RagModuleOptions = {
|
|
35
|
+
collection: 'rag',
|
|
36
|
+
chunkSize: 512,
|
|
37
|
+
chunkOverlap: 50,
|
|
38
|
+
topK: 5,
|
|
39
|
+
minScore: 0.5,
|
|
40
|
+
...options,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const providers: ModuleProvider[] = [
|
|
44
|
+
{ provide: RAG_OPTIONS_TOKEN, useValue: resolvedOptions },
|
|
45
|
+
{
|
|
46
|
+
provide: RAG_SERVICE_TOKEN,
|
|
47
|
+
useFactory: (container: Container) => {
|
|
48
|
+
const embeddingService = container.resolve(EMBEDDING_SERVICE_TOKEN);
|
|
49
|
+
const vectorStore = container.resolve(VECTOR_STORE_TOKEN);
|
|
50
|
+
return new RagService(resolvedOptions, embeddingService as never, vectorStore as never);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
RagService,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const existing = Reflect.getMetadata(MODULE_METADATA_KEY, RagModule) || {};
|
|
57
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, {
|
|
58
|
+
...existing,
|
|
59
|
+
imports: [
|
|
60
|
+
...(existing.imports || []),
|
|
61
|
+
EmbeddingModule,
|
|
62
|
+
VectorStoreModule,
|
|
63
|
+
],
|
|
64
|
+
providers: [...(existing.providers || []), ...providers],
|
|
65
|
+
exports: [
|
|
66
|
+
...(existing.exports || []),
|
|
67
|
+
RAG_SERVICE_TOKEN,
|
|
68
|
+
RagService,
|
|
69
|
+
],
|
|
70
|
+
}, RagModule);
|
|
71
|
+
|
|
72
|
+
return RagModule;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public static reset(): void {
|
|
76
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, RagModule);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Injectable } from '../di/decorators';
|
|
2
|
+
import { Inject } from '../di/decorators';
|
|
3
|
+
import type { EmbeddingService } from '../embedding/service';
|
|
4
|
+
import { EMBEDDING_SERVICE_TOKEN } from '../embedding/types';
|
|
5
|
+
import type { VectorStore } from '../vector-store/types';
|
|
6
|
+
import { VECTOR_STORE_TOKEN } from '../vector-store/types';
|
|
7
|
+
import type { IngestSource, RagContext, RagModuleOptions, DocumentChunker } from './types';
|
|
8
|
+
import { RAG_OPTIONS_TOKEN } from './types';
|
|
9
|
+
import { TextChunker } from './chunkers/text-chunker';
|
|
10
|
+
import { MarkdownChunker } from './chunkers/markdown-chunker';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RAG service — document ingestion pipeline and context retrieval.
|
|
14
|
+
*
|
|
15
|
+
* Ingestion: source → load text → chunk → embed → store in VectorStore
|
|
16
|
+
* Retrieval: query → embed → search VectorStore → format context
|
|
17
|
+
*/
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class RagService {
|
|
20
|
+
private readonly options: RagModuleOptions;
|
|
21
|
+
private readonly embeddingService: EmbeddingService;
|
|
22
|
+
private readonly vectorStore: VectorStore;
|
|
23
|
+
private readonly textChunker: DocumentChunker;
|
|
24
|
+
private readonly markdownChunker: DocumentChunker;
|
|
25
|
+
|
|
26
|
+
public constructor(
|
|
27
|
+
@Inject(RAG_OPTIONS_TOKEN) options: RagModuleOptions,
|
|
28
|
+
@Inject(EMBEDDING_SERVICE_TOKEN) embeddingService: EmbeddingService,
|
|
29
|
+
@Inject(VECTOR_STORE_TOKEN) vectorStore: VectorStore,
|
|
30
|
+
) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.embeddingService = embeddingService;
|
|
33
|
+
this.vectorStore = vectorStore;
|
|
34
|
+
this.textChunker = new TextChunker(options.chunkSize, options.chunkOverlap);
|
|
35
|
+
this.markdownChunker = new MarkdownChunker(options.chunkSize);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ingest a document source into the vector store
|
|
40
|
+
* @param source - text, file path, or URL
|
|
41
|
+
* @param collection - override default collection
|
|
42
|
+
* @returns Number of chunks stored
|
|
43
|
+
*/
|
|
44
|
+
public async ingest(source: IngestSource, collection?: string): Promise<number> {
|
|
45
|
+
const targetCollection = collection ?? this.options.collection ?? 'rag';
|
|
46
|
+
const text = await this.loadText(source);
|
|
47
|
+
|
|
48
|
+
if (!text.trim()) return 0;
|
|
49
|
+
|
|
50
|
+
// Choose chunker based on content type
|
|
51
|
+
const isMarkdown = source.type === 'file' && (source.path.endsWith('.md') || source.path.endsWith('.mdx'));
|
|
52
|
+
const chunker = isMarkdown ? this.markdownChunker : this.textChunker;
|
|
53
|
+
const chunks = chunker.chunk(text);
|
|
54
|
+
|
|
55
|
+
// Batch embed all chunks
|
|
56
|
+
const vectors = await this.embeddingService.embedBatch(chunks.map((c) => c.content));
|
|
57
|
+
|
|
58
|
+
// Store in vector store
|
|
59
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
60
|
+
const chunk = chunks[i]!;
|
|
61
|
+
const vector = vectors[i]!;
|
|
62
|
+
await this.vectorStore.upsert({
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
vector,
|
|
65
|
+
content: chunk.content,
|
|
66
|
+
collection: targetCollection,
|
|
67
|
+
metadata: {
|
|
68
|
+
...(chunk.metadata ?? {}),
|
|
69
|
+
...(source.metadata ?? {}),
|
|
70
|
+
sourceType: source.type,
|
|
71
|
+
...(source.type === 'file' ? { sourcePath: source.path } : {}),
|
|
72
|
+
...(source.type === 'url' ? { sourceUrl: source.url } : {}),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return chunks.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieve relevant context for a query
|
|
82
|
+
* @param query - User query text
|
|
83
|
+
* @param collection - override default collection
|
|
84
|
+
*/
|
|
85
|
+
public async retrieve(query: string, collection?: string): Promise<RagContext> {
|
|
86
|
+
const targetCollection = collection ?? this.options.collection ?? 'rag';
|
|
87
|
+
const queryVector = await this.embeddingService.embed(query);
|
|
88
|
+
|
|
89
|
+
const results = await this.vectorStore.search(queryVector, {
|
|
90
|
+
topK: this.options.topK ?? 5,
|
|
91
|
+
minScore: this.options.minScore ?? 0.5,
|
|
92
|
+
collection: targetCollection,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const chunks = results.map((r) => ({
|
|
96
|
+
content: r.document.content,
|
|
97
|
+
score: r.score,
|
|
98
|
+
metadata: r.document.metadata,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const formatted = chunks.length > 0
|
|
102
|
+
? chunks.map((c, i) => `[${i + 1}] ${c.content}`).join('\n\n')
|
|
103
|
+
: '';
|
|
104
|
+
|
|
105
|
+
return { chunks, formatted };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a system prompt with retrieved context
|
|
110
|
+
*/
|
|
111
|
+
public async buildContextPrompt(query: string, collection?: string): Promise<string> {
|
|
112
|
+
const context = await this.retrieve(query, collection);
|
|
113
|
+
if (!context.formatted) return '';
|
|
114
|
+
return `Use the following context to answer the question:\n\n${context.formatted}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async loadText(source: IngestSource): Promise<string> {
|
|
118
|
+
switch (source.type) {
|
|
119
|
+
case 'text':
|
|
120
|
+
return source.content;
|
|
121
|
+
|
|
122
|
+
case 'file': {
|
|
123
|
+
const file = Bun.file(source.path);
|
|
124
|
+
return file.text();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'url': {
|
|
128
|
+
const res = await fetch(source.url);
|
|
129
|
+
if (!res.ok) throw new Error(`Failed to fetch ${source.url}: ${res.status}`);
|
|
130
|
+
return res.text();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/rag/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chunk of text produced by a document chunker
|
|
3
|
+
*/
|
|
4
|
+
export interface DocumentChunk {
|
|
5
|
+
content: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Source for document ingestion
|
|
11
|
+
*/
|
|
12
|
+
export type IngestSource =
|
|
13
|
+
| { type: 'text'; content: string; metadata?: Record<string, unknown> }
|
|
14
|
+
| { type: 'file'; path: string; metadata?: Record<string, unknown> }
|
|
15
|
+
| { type: 'url'; url: string; metadata?: Record<string, unknown> };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Abstract document chunker
|
|
19
|
+
*/
|
|
20
|
+
export interface DocumentChunker {
|
|
21
|
+
chunk(text: string): DocumentChunk[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieved context for RAG
|
|
26
|
+
*/
|
|
27
|
+
export interface RagContext {
|
|
28
|
+
chunks: Array<{ content: string; score: number; metadata?: Record<string, unknown> }>;
|
|
29
|
+
/** Formatted context string for inclusion in prompts */
|
|
30
|
+
formatted: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RagModuleOptions {
|
|
34
|
+
/** Collection name in VectorStore (default: 'rag') */
|
|
35
|
+
collection?: string;
|
|
36
|
+
/** Number of chunks per ingested document (default: 512) */
|
|
37
|
+
chunkSize?: number;
|
|
38
|
+
/** Overlap between consecutive chunks (default: 50) */
|
|
39
|
+
chunkOverlap?: number;
|
|
40
|
+
/** Number of top results to retrieve (default: 5) */
|
|
41
|
+
topK?: number;
|
|
42
|
+
/** Minimum similarity score to include (default: 0.5) */
|
|
43
|
+
minScore?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const RAG_SERVICE_TOKEN = Symbol('@dangao/bun-server:rag:service');
|
|
47
|
+
export const RAG_OPTIONS_TOKEN = Symbol('@dangao/bun-server:rag:options');
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { VectorStore, VectorDocument, VectorSearchResult, VectorSearchOptions } from '../types';
|
|
2
|
+
import { cosineSimilarity } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* High-performance in-memory vector store using cosine similarity.
|
|
6
|
+
* Suitable for development and small to medium datasets (up to ~100K documents).
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryVectorStore implements VectorStore {
|
|
9
|
+
private readonly documents = new Map<string, VectorDocument>();
|
|
10
|
+
|
|
11
|
+
public async upsert(document: VectorDocument): Promise<void> {
|
|
12
|
+
this.documents.set(this.key(document.id, document.collection), document);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async upsertBatch(documents: VectorDocument[]): Promise<void> {
|
|
16
|
+
for (const doc of documents) {
|
|
17
|
+
this.documents.set(this.key(doc.id, doc.collection), doc);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async get(id: string, collection?: string): Promise<VectorDocument | null> {
|
|
22
|
+
return this.documents.get(this.key(id, collection)) ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async search(query: number[], options: VectorSearchOptions = {}): Promise<VectorSearchResult[]> {
|
|
26
|
+
const { topK = 5, minScore = 0, collection, filter } = options;
|
|
27
|
+
|
|
28
|
+
const results: VectorSearchResult[] = [];
|
|
29
|
+
|
|
30
|
+
for (const doc of this.documents.values()) {
|
|
31
|
+
if (collection && doc.collection !== collection) continue;
|
|
32
|
+
if (filter && !filter(doc)) continue;
|
|
33
|
+
|
|
34
|
+
const score = cosineSimilarity(query, doc.vector);
|
|
35
|
+
if (score >= minScore) {
|
|
36
|
+
results.push({ document: doc, score });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results
|
|
41
|
+
.sort((a, b) => b.score - a.score)
|
|
42
|
+
.slice(0, topK);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async delete(id: string, collection?: string): Promise<boolean> {
|
|
46
|
+
return this.documents.delete(this.key(id, collection));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async deleteCollection(collection: string): Promise<void> {
|
|
50
|
+
for (const [key, doc] of this.documents.entries()) {
|
|
51
|
+
if (doc.collection === collection) {
|
|
52
|
+
this.documents.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async count(collection?: string): Promise<number> {
|
|
58
|
+
if (!collection) return this.documents.size;
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const doc of this.documents.values()) {
|
|
61
|
+
if (doc.collection === collection) count++;
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private key(id: string, collection?: string): string {
|
|
67
|
+
return collection ? `${collection}:${id}` : id;
|
|
68
|
+
}
|
|
69
|
+
}
|