@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.
Files changed (214) hide show
  1. package/README.md +32 -0
  2. package/dist/ai/ai-module.d.ts +24 -0
  3. package/dist/ai/ai-module.d.ts.map +1 -0
  4. package/dist/ai/decorators.d.ts +25 -0
  5. package/dist/ai/decorators.d.ts.map +1 -0
  6. package/dist/ai/errors.d.ts +39 -0
  7. package/dist/ai/errors.d.ts.map +1 -0
  8. package/dist/ai/index.d.ts +12 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/providers/anthropic-provider.d.ts +23 -0
  11. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
  12. package/dist/ai/providers/google-provider.d.ts +20 -0
  13. package/dist/ai/providers/google-provider.d.ts.map +1 -0
  14. package/dist/ai/providers/ollama-provider.d.ts +17 -0
  15. package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
  16. package/dist/ai/providers/openai-provider.d.ts +28 -0
  17. package/dist/ai/providers/openai-provider.d.ts.map +1 -0
  18. package/dist/ai/service.d.ts +40 -0
  19. package/dist/ai/service.d.ts.map +1 -0
  20. package/dist/ai/tools/tool-executor.d.ts +15 -0
  21. package/dist/ai/tools/tool-executor.d.ts.map +1 -0
  22. package/dist/ai/tools/tool-registry.d.ts +39 -0
  23. package/dist/ai/tools/tool-registry.d.ts.map +1 -0
  24. package/dist/ai/types.d.ts +134 -0
  25. package/dist/ai/types.d.ts.map +1 -0
  26. package/dist/ai-guard/ai-guard-module.d.ts +18 -0
  27. package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
  28. package/dist/ai-guard/decorators.d.ts +16 -0
  29. package/dist/ai-guard/decorators.d.ts.map +1 -0
  30. package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
  31. package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
  32. package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
  33. package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
  34. package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
  35. package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
  36. package/dist/ai-guard/index.d.ts +8 -0
  37. package/dist/ai-guard/index.d.ts.map +1 -0
  38. package/dist/ai-guard/service.d.ts +21 -0
  39. package/dist/ai-guard/service.d.ts.map +1 -0
  40. package/dist/ai-guard/types.d.ts +59 -0
  41. package/dist/ai-guard/types.d.ts.map +1 -0
  42. package/dist/conversation/conversation-module.d.ts +25 -0
  43. package/dist/conversation/conversation-module.d.ts.map +1 -0
  44. package/dist/conversation/decorators.d.ts +28 -0
  45. package/dist/conversation/decorators.d.ts.map +1 -0
  46. package/dist/conversation/index.d.ts +8 -0
  47. package/dist/conversation/index.d.ts.map +1 -0
  48. package/dist/conversation/service.d.ts +43 -0
  49. package/dist/conversation/service.d.ts.map +1 -0
  50. package/dist/conversation/stores/database-store.d.ts +46 -0
  51. package/dist/conversation/stores/database-store.d.ts.map +1 -0
  52. package/dist/conversation/stores/memory-store.d.ts +17 -0
  53. package/dist/conversation/stores/memory-store.d.ts.map +1 -0
  54. package/dist/conversation/stores/redis-store.d.ts +39 -0
  55. package/dist/conversation/stores/redis-store.d.ts.map +1 -0
  56. package/dist/conversation/types.d.ts +64 -0
  57. package/dist/conversation/types.d.ts.map +1 -0
  58. package/dist/embedding/embedding-module.d.ts +20 -0
  59. package/dist/embedding/embedding-module.d.ts.map +1 -0
  60. package/dist/embedding/index.d.ts +6 -0
  61. package/dist/embedding/index.d.ts.map +1 -0
  62. package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
  63. package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
  64. package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
  65. package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
  66. package/dist/embedding/service.d.ts +27 -0
  67. package/dist/embedding/service.d.ts.map +1 -0
  68. package/dist/embedding/types.d.ts +25 -0
  69. package/dist/embedding/types.d.ts.map +1 -0
  70. package/dist/index.d.ts +8 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2638 -1
  73. package/dist/mcp/decorators.d.ts +42 -0
  74. package/dist/mcp/decorators.d.ts.map +1 -0
  75. package/dist/mcp/index.d.ts +6 -0
  76. package/dist/mcp/index.d.ts.map +1 -0
  77. package/dist/mcp/mcp-module.d.ts +22 -0
  78. package/dist/mcp/mcp-module.d.ts.map +1 -0
  79. package/dist/mcp/registry.d.ts +23 -0
  80. package/dist/mcp/registry.d.ts.map +1 -0
  81. package/dist/mcp/server.d.ts +29 -0
  82. package/dist/mcp/server.d.ts.map +1 -0
  83. package/dist/mcp/types.d.ts +60 -0
  84. package/dist/mcp/types.d.ts.map +1 -0
  85. package/dist/prompt/index.d.ts +6 -0
  86. package/dist/prompt/index.d.ts.map +1 -0
  87. package/dist/prompt/prompt-module.d.ts +23 -0
  88. package/dist/prompt/prompt-module.d.ts.map +1 -0
  89. package/dist/prompt/service.d.ts +47 -0
  90. package/dist/prompt/service.d.ts.map +1 -0
  91. package/dist/prompt/stores/file-store.d.ts +36 -0
  92. package/dist/prompt/stores/file-store.d.ts.map +1 -0
  93. package/dist/prompt/stores/memory-store.d.ts +17 -0
  94. package/dist/prompt/stores/memory-store.d.ts.map +1 -0
  95. package/dist/prompt/types.d.ts +68 -0
  96. package/dist/prompt/types.d.ts.map +1 -0
  97. package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
  98. package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
  99. package/dist/rag/chunkers/text-chunker.d.ts +11 -0
  100. package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
  101. package/dist/rag/decorators.d.ts +24 -0
  102. package/dist/rag/decorators.d.ts.map +1 -0
  103. package/dist/rag/index.d.ts +7 -0
  104. package/dist/rag/index.d.ts.map +1 -0
  105. package/dist/rag/rag-module.d.ts +23 -0
  106. package/dist/rag/rag-module.d.ts.map +1 -0
  107. package/dist/rag/service.d.ts +36 -0
  108. package/dist/rag/service.d.ts.map +1 -0
  109. package/dist/rag/types.d.ts +56 -0
  110. package/dist/rag/types.d.ts.map +1 -0
  111. package/dist/vector-store/index.d.ts +6 -0
  112. package/dist/vector-store/index.d.ts.map +1 -0
  113. package/dist/vector-store/stores/memory-store.d.ts +17 -0
  114. package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
  115. package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
  116. package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
  117. package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
  118. package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
  119. package/dist/vector-store/types.d.ts +60 -0
  120. package/dist/vector-store/types.d.ts.map +1 -0
  121. package/dist/vector-store/vector-store-module.d.ts +20 -0
  122. package/dist/vector-store/vector-store-module.d.ts.map +1 -0
  123. package/docs/ai.md +500 -0
  124. package/docs/best-practices.md +83 -8
  125. package/docs/database.md +23 -0
  126. package/docs/guide.md +90 -27
  127. package/docs/migration.md +81 -7
  128. package/docs/security.md +23 -0
  129. package/docs/zh/ai.md +441 -0
  130. package/docs/zh/best-practices.md +43 -0
  131. package/docs/zh/database.md +23 -0
  132. package/docs/zh/guide.md +40 -1
  133. package/docs/zh/migration.md +39 -0
  134. package/docs/zh/security.md +23 -0
  135. package/package.json +3 -3
  136. package/src/ai/ai-module.ts +62 -0
  137. package/src/ai/decorators.ts +30 -0
  138. package/src/ai/errors.ts +71 -0
  139. package/src/ai/index.ts +11 -0
  140. package/src/ai/providers/anthropic-provider.ts +190 -0
  141. package/src/ai/providers/google-provider.ts +179 -0
  142. package/src/ai/providers/ollama-provider.ts +126 -0
  143. package/src/ai/providers/openai-provider.ts +242 -0
  144. package/src/ai/service.ts +155 -0
  145. package/src/ai/tools/tool-executor.ts +38 -0
  146. package/src/ai/tools/tool-registry.ts +91 -0
  147. package/src/ai/types.ts +145 -0
  148. package/src/ai-guard/ai-guard-module.ts +50 -0
  149. package/src/ai-guard/decorators.ts +21 -0
  150. package/src/ai-guard/detectors/content-moderator.ts +80 -0
  151. package/src/ai-guard/detectors/injection-detector.ts +48 -0
  152. package/src/ai-guard/detectors/pii-detector.ts +64 -0
  153. package/src/ai-guard/index.ts +7 -0
  154. package/src/ai-guard/service.ts +100 -0
  155. package/src/ai-guard/types.ts +61 -0
  156. package/src/conversation/conversation-module.ts +63 -0
  157. package/src/conversation/decorators.ts +47 -0
  158. package/src/conversation/index.ts +7 -0
  159. package/src/conversation/service.ts +133 -0
  160. package/src/conversation/stores/database-store.ts +125 -0
  161. package/src/conversation/stores/memory-store.ts +57 -0
  162. package/src/conversation/stores/redis-store.ts +101 -0
  163. package/src/conversation/types.ts +68 -0
  164. package/src/embedding/embedding-module.ts +52 -0
  165. package/src/embedding/index.ts +5 -0
  166. package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
  167. package/src/embedding/providers/openai-embedding-provider.ts +47 -0
  168. package/src/embedding/service.ts +55 -0
  169. package/src/embedding/types.ts +27 -0
  170. package/src/index.ts +10 -0
  171. package/src/mcp/decorators.ts +60 -0
  172. package/src/mcp/index.ts +5 -0
  173. package/src/mcp/mcp-module.ts +58 -0
  174. package/src/mcp/registry.ts +72 -0
  175. package/src/mcp/server.ts +164 -0
  176. package/src/mcp/types.ts +63 -0
  177. package/src/prompt/index.ts +5 -0
  178. package/src/prompt/prompt-module.ts +61 -0
  179. package/src/prompt/service.ts +93 -0
  180. package/src/prompt/stores/file-store.ts +135 -0
  181. package/src/prompt/stores/memory-store.ts +82 -0
  182. package/src/prompt/types.ts +84 -0
  183. package/src/rag/chunkers/markdown-chunker.ts +40 -0
  184. package/src/rag/chunkers/text-chunker.ts +30 -0
  185. package/src/rag/decorators.ts +26 -0
  186. package/src/rag/index.ts +6 -0
  187. package/src/rag/rag-module.ts +78 -0
  188. package/src/rag/service.ts +134 -0
  189. package/src/rag/types.ts +47 -0
  190. package/src/vector-store/index.ts +5 -0
  191. package/src/vector-store/stores/memory-store.ts +69 -0
  192. package/src/vector-store/stores/pinecone-store.ts +123 -0
  193. package/src/vector-store/stores/qdrant-store.ts +147 -0
  194. package/src/vector-store/types.ts +77 -0
  195. package/src/vector-store/vector-store-module.ts +50 -0
  196. package/tests/ai/ai-module.test.ts +46 -0
  197. package/tests/ai/ai-service.test.ts +91 -0
  198. package/tests/ai/tool-registry.test.ts +57 -0
  199. package/tests/ai-guard/ai-guard-module.test.ts +23 -0
  200. package/tests/ai-guard/content-moderator.test.ts +65 -0
  201. package/tests/ai-guard/pii-detector.test.ts +41 -0
  202. package/tests/conversation/conversation-module.test.ts +26 -0
  203. package/tests/conversation/conversation-service.test.ts +64 -0
  204. package/tests/conversation/memory-store.test.ts +68 -0
  205. package/tests/embedding/embedding-service.test.ts +55 -0
  206. package/tests/mcp/mcp-server.test.ts +85 -0
  207. package/tests/prompt/prompt-module.test.ts +30 -0
  208. package/tests/prompt/prompt-service.test.ts +74 -0
  209. package/tests/rag/chunkers.test.ts +58 -0
  210. package/tests/rag/rag-service.test.ts +66 -0
  211. package/tests/vector-store/memory-vector-store.test.ts +84 -0
  212. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
  213. package/tests/perf/optimization.test.ts +0 -182
  214. 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
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './service';
3
+ export * from './decorators';
4
+ export * from './rag-module';
5
+ export * from './chunkers/text-chunker';
6
+ export * from './chunkers/markdown-chunker';
@@ -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
+ }
@@ -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,5 @@
1
+ export * from './types';
2
+ export * from './vector-store-module';
3
+ export * from './stores/memory-store';
4
+ export * from './stores/pinecone-store';
5
+ export * from './stores/qdrant-store';
@@ -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
+ }