@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,93 @@
|
|
|
1
|
+
import { Injectable } from '../di/decorators';
|
|
2
|
+
import { Inject } from '../di/decorators';
|
|
3
|
+
import type {
|
|
4
|
+
PromptStore,
|
|
5
|
+
PromptTemplate,
|
|
6
|
+
CreatePromptTemplateInput,
|
|
7
|
+
UpdatePromptTemplateInput,
|
|
8
|
+
PromptModuleOptions,
|
|
9
|
+
} from './types';
|
|
10
|
+
import { PROMPT_OPTIONS_TOKEN, renderTemplate } from './types';
|
|
11
|
+
import { HttpException } from '../error/http-exception';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manages prompt templates: CRUD operations, versioning, and variable rendering.
|
|
15
|
+
*/
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class PromptService {
|
|
18
|
+
private readonly store: PromptStore;
|
|
19
|
+
|
|
20
|
+
public constructor(
|
|
21
|
+
@Inject(PROMPT_OPTIONS_TOKEN) options: PromptModuleOptions,
|
|
22
|
+
) {
|
|
23
|
+
this.store = options.store!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get a template by ID
|
|
28
|
+
*/
|
|
29
|
+
public async get(id: string): Promise<PromptTemplate> {
|
|
30
|
+
const template = await this.store.get(id);
|
|
31
|
+
if (!template) throw new HttpException(404, `Prompt template "${id}" not found`);
|
|
32
|
+
return template;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a specific version of a template
|
|
37
|
+
*/
|
|
38
|
+
public async getVersion(id: string, version: number): Promise<PromptTemplate> {
|
|
39
|
+
const template = await this.store.getVersion(id, version);
|
|
40
|
+
if (!template) throw new HttpException(404, `Prompt template "${id}" version ${version} not found`);
|
|
41
|
+
return template;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* List all templates
|
|
46
|
+
*/
|
|
47
|
+
public async list(): Promise<PromptTemplate[]> {
|
|
48
|
+
return this.store.list();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a new template
|
|
53
|
+
*/
|
|
54
|
+
public async create(input: CreatePromptTemplateInput): Promise<PromptTemplate> {
|
|
55
|
+
return this.store.create(input);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update a template (creates a new version)
|
|
60
|
+
*/
|
|
61
|
+
public async update(id: string, input: UpdatePromptTemplateInput): Promise<PromptTemplate> {
|
|
62
|
+
return this.store.update(id, input);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Delete a template
|
|
67
|
+
*/
|
|
68
|
+
public async delete(id: string): Promise<boolean> {
|
|
69
|
+
return this.store.delete(id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Render a template by replacing {{varName}} with provided values
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const rendered = await promptService.render('greet-user', { name: 'Alice', app: 'MyApp' });
|
|
78
|
+
* // "Hello, Alice! Welcome to MyApp."
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
public async render(id: string, vars: Record<string, string>): Promise<string> {
|
|
82
|
+
const template = await this.get(id);
|
|
83
|
+
return renderTemplate(template.content, vars);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Render a specific version of a template
|
|
88
|
+
*/
|
|
89
|
+
public async renderVersion(id: string, version: number, vars: Record<string, string>): Promise<string> {
|
|
90
|
+
const template = await this.getVersion(id, version);
|
|
91
|
+
return renderTemplate(template.content, vars);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PromptStore,
|
|
3
|
+
PromptTemplate,
|
|
4
|
+
CreatePromptTemplateInput,
|
|
5
|
+
UpdatePromptTemplateInput,
|
|
6
|
+
} from '../types';
|
|
7
|
+
import { extractVariables } from '../types';
|
|
8
|
+
import { InMemoryPromptStore } from './memory-store';
|
|
9
|
+
|
|
10
|
+
export interface FilePromptStoreConfig {
|
|
11
|
+
/** Directory containing JSON prompt files (default: './.prompts') */
|
|
12
|
+
promptsDir?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* File-backed prompt store — loads templates from JSON files at startup
|
|
17
|
+
* and persists changes back to disk.
|
|
18
|
+
*
|
|
19
|
+
* Each template is stored as a separate JSON file: `{promptsDir}/{id}.json`
|
|
20
|
+
*
|
|
21
|
+
* File format:
|
|
22
|
+
* ```json
|
|
23
|
+
* {
|
|
24
|
+
* "id": "greet-user",
|
|
25
|
+
* "name": "Greet User",
|
|
26
|
+
* "content": "Hello, {{name}}! Welcome to {{app}}.",
|
|
27
|
+
* "description": "Greeting template"
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class FilePromptStore implements PromptStore {
|
|
32
|
+
private readonly promptsDir: string;
|
|
33
|
+
private readonly memory: InMemoryPromptStore;
|
|
34
|
+
private loaded = false;
|
|
35
|
+
|
|
36
|
+
public constructor(config: FilePromptStoreConfig = {}) {
|
|
37
|
+
this.promptsDir = config.promptsDir ?? './.prompts';
|
|
38
|
+
this.memory = new InMemoryPromptStore();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async get(id: string): Promise<PromptTemplate | null> {
|
|
42
|
+
await this.ensureLoaded();
|
|
43
|
+
return this.memory.get(id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async getVersion(id: string, version: number): Promise<PromptTemplate | null> {
|
|
47
|
+
await this.ensureLoaded();
|
|
48
|
+
return this.memory.getVersion(id, version);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async list(): Promise<PromptTemplate[]> {
|
|
52
|
+
await this.ensureLoaded();
|
|
53
|
+
return this.memory.list();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async create(input: CreatePromptTemplateInput): Promise<PromptTemplate> {
|
|
57
|
+
await this.ensureLoaded();
|
|
58
|
+
const template = await this.memory.create(input);
|
|
59
|
+
await this.writeFile(template);
|
|
60
|
+
return template;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async update(id: string, input: UpdatePromptTemplateInput): Promise<PromptTemplate> {
|
|
64
|
+
await this.ensureLoaded();
|
|
65
|
+
const template = await this.memory.update(id, input);
|
|
66
|
+
await this.writeFile(template);
|
|
67
|
+
return template;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async delete(id: string): Promise<boolean> {
|
|
71
|
+
await this.ensureLoaded();
|
|
72
|
+
const deleted = await this.memory.delete(id);
|
|
73
|
+
if (deleted) {
|
|
74
|
+
try {
|
|
75
|
+
const path = `${this.promptsDir}/${id}.json`;
|
|
76
|
+
await Bun.file(path).exists() && Bun.write(path, ''); // Soft delete (empty file)
|
|
77
|
+
} catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return deleted;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async ensureLoaded(): Promise<void> {
|
|
85
|
+
if (this.loaded) return;
|
|
86
|
+
this.loaded = true;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const glob = new Bun.Glob('*.json');
|
|
90
|
+
const files = Array.from(glob.scanSync(this.promptsDir));
|
|
91
|
+
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
try {
|
|
94
|
+
const content = await Bun.file(`${this.promptsDir}/${file}`).text();
|
|
95
|
+
if (!content.trim()) continue;
|
|
96
|
+
|
|
97
|
+
const data = JSON.parse(content) as {
|
|
98
|
+
id?: string;
|
|
99
|
+
name: string;
|
|
100
|
+
content: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const id = data.id ?? file.replace(/\.json$/, '');
|
|
105
|
+
await this.memory.create({ id, ...data }).catch(() => {
|
|
106
|
+
// Template already exists — skip
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
// Skip malformed files
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Directory doesn't exist — start empty
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async writeFile(template: PromptTemplate): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
const content = JSON.stringify(
|
|
120
|
+
{
|
|
121
|
+
id: template.id,
|
|
122
|
+
name: template.name,
|
|
123
|
+
content: template.content,
|
|
124
|
+
description: template.description,
|
|
125
|
+
variables: extractVariables(template.content),
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2,
|
|
129
|
+
);
|
|
130
|
+
await Bun.write(`${this.promptsDir}/${template.id}.json`, content);
|
|
131
|
+
} catch {
|
|
132
|
+
// Ignore write errors (e.g., read-only filesystem)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -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
|
+
}
|