@dangao/bun-server 1.12.1 → 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/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 +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/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,125 @@
|
|
|
1
|
+
import type { ConversationStore, Conversation } from '../types';
|
|
2
|
+
import type { AiMessage } from '../../ai/types';
|
|
3
|
+
|
|
4
|
+
export interface DatabaseConversationStoreConfig {
|
|
5
|
+
/** DatabaseService instance from DatabaseModule */
|
|
6
|
+
database: DatabaseServiceLike;
|
|
7
|
+
/** Table name for conversations (default: 'conversations') */
|
|
8
|
+
tableName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimal interface matching DatabaseService from DatabaseModule
|
|
13
|
+
*/
|
|
14
|
+
export interface DatabaseServiceLike {
|
|
15
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
16
|
+
execute(sql: string, params?: unknown[]): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Database-backed conversation store — suitable for durable storage with full history.
|
|
21
|
+
*
|
|
22
|
+
* Creates tables automatically on first use. Requires DatabaseModule to be configured.
|
|
23
|
+
*
|
|
24
|
+
* Schema:
|
|
25
|
+
* ```sql
|
|
26
|
+
* CREATE TABLE conversations (
|
|
27
|
+
* id TEXT PRIMARY KEY,
|
|
28
|
+
* messages TEXT NOT NULL, -- JSON array
|
|
29
|
+
* metadata TEXT NOT NULL, -- JSON object
|
|
30
|
+
* created_at TEXT NOT NULL,
|
|
31
|
+
* updated_at TEXT NOT NULL
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class DatabaseConversationStore implements ConversationStore {
|
|
36
|
+
private readonly db: DatabaseServiceLike;
|
|
37
|
+
private readonly tableName: string;
|
|
38
|
+
private initialized = false;
|
|
39
|
+
|
|
40
|
+
public constructor(config: DatabaseConversationStoreConfig) {
|
|
41
|
+
this.db = config.database;
|
|
42
|
+
this.tableName = config.tableName ?? 'conversations';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async create(metadata: Record<string, unknown> = {}): Promise<Conversation> {
|
|
46
|
+
await this.ensureTable();
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
await this.db.execute(
|
|
50
|
+
`INSERT INTO ${this.tableName} (id, messages, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`,
|
|
51
|
+
[id, '[]', JSON.stringify(metadata), now, now],
|
|
52
|
+
);
|
|
53
|
+
return { id, messages: [], metadata, createdAt: new Date(now), updatedAt: new Date(now) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async get(id: string): Promise<Conversation | null> {
|
|
57
|
+
await this.ensureTable();
|
|
58
|
+
const rows = await this.db.query<Record<string, string>>(
|
|
59
|
+
`SELECT * FROM ${this.tableName} WHERE id = ?`,
|
|
60
|
+
[id],
|
|
61
|
+
);
|
|
62
|
+
if (!rows.length) return null;
|
|
63
|
+
return this.rowToConversation(rows[0]!);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async appendMessage(id: string, message: AiMessage): Promise<void> {
|
|
67
|
+
await this.ensureTable();
|
|
68
|
+
const conv = await this.get(id);
|
|
69
|
+
if (!conv) throw new Error(`Conversation "${id}" not found`);
|
|
70
|
+
const messages = [...conv.messages, message];
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
await this.db.execute(
|
|
73
|
+
`UPDATE ${this.tableName} SET messages = ?, updated_at = ? WHERE id = ?`,
|
|
74
|
+
[JSON.stringify(messages), now, id],
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public async trim(id: string, maxMessages: number): Promise<void> {
|
|
79
|
+
await this.ensureTable();
|
|
80
|
+
const conv = await this.get(id);
|
|
81
|
+
if (!conv || conv.messages.length <= maxMessages) return;
|
|
82
|
+
const trimmed = conv.messages.slice(-maxMessages);
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
await this.db.execute(
|
|
85
|
+
`UPDATE ${this.tableName} SET messages = ?, updated_at = ? WHERE id = ?`,
|
|
86
|
+
[JSON.stringify(trimmed), now, id],
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async delete(id: string): Promise<boolean> {
|
|
91
|
+
await this.ensureTable();
|
|
92
|
+
await this.db.execute(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async list(): Promise<string[]> {
|
|
97
|
+
await this.ensureTable();
|
|
98
|
+
const rows = await this.db.query<{ id: string }>(`SELECT id FROM ${this.tableName}`);
|
|
99
|
+
return rows.map((r) => r.id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async ensureTable(): Promise<void> {
|
|
103
|
+
if (this.initialized) return;
|
|
104
|
+
await this.db.execute(`
|
|
105
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
108
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
109
|
+
created_at TEXT NOT NULL,
|
|
110
|
+
updated_at TEXT NOT NULL
|
|
111
|
+
)
|
|
112
|
+
`);
|
|
113
|
+
this.initialized = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private rowToConversation(row: Record<string, string>): Conversation {
|
|
117
|
+
return {
|
|
118
|
+
id: row['id']!,
|
|
119
|
+
messages: JSON.parse(row['messages']!) as AiMessage[],
|
|
120
|
+
metadata: JSON.parse(row['metadata']!) as Record<string, unknown>,
|
|
121
|
+
createdAt: new Date(row['created_at']!),
|
|
122
|
+
updatedAt: new Date(row['updated_at']!),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ConversationStore, Conversation } from '../types';
|
|
2
|
+
import type { AiMessage } from '../../ai/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* In-memory conversation store — suitable for development and single-instance deployments.
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryConversationStore implements ConversationStore {
|
|
8
|
+
private readonly conversations = new Map<string, Conversation>();
|
|
9
|
+
|
|
10
|
+
public async create(metadata: Record<string, unknown> = {}): Promise<Conversation> {
|
|
11
|
+
const id = crypto.randomUUID();
|
|
12
|
+
const conversation: Conversation = {
|
|
13
|
+
id,
|
|
14
|
+
messages: [],
|
|
15
|
+
metadata,
|
|
16
|
+
createdAt: new Date(),
|
|
17
|
+
updatedAt: new Date(),
|
|
18
|
+
};
|
|
19
|
+
this.conversations.set(id, conversation);
|
|
20
|
+
return { ...conversation, messages: [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async get(id: string): Promise<Conversation | null> {
|
|
24
|
+
const conv = this.conversations.get(id);
|
|
25
|
+
if (!conv) return null;
|
|
26
|
+
return { ...conv, messages: [...conv.messages] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async appendMessage(id: string, message: AiMessage): Promise<void> {
|
|
30
|
+
const conv = this.conversations.get(id);
|
|
31
|
+
if (!conv) throw new Error(`Conversation "${id}" not found`);
|
|
32
|
+
conv.messages.push(message);
|
|
33
|
+
conv.updatedAt = new Date();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async trim(id: string, maxMessages: number): Promise<void> {
|
|
37
|
+
const conv = this.conversations.get(id);
|
|
38
|
+
if (!conv) return;
|
|
39
|
+
if (conv.messages.length > maxMessages) {
|
|
40
|
+
conv.messages = conv.messages.slice(-maxMessages);
|
|
41
|
+
conv.updatedAt = new Date();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async delete(id: string): Promise<boolean> {
|
|
46
|
+
return this.conversations.delete(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async list(): Promise<string[]> {
|
|
50
|
+
return Array.from(this.conversations.keys());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Number of stored conversations (for testing) */
|
|
54
|
+
public get size(): number {
|
|
55
|
+
return this.conversations.size;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ConversationStore, Conversation } from '../types';
|
|
2
|
+
import type { AiMessage } from '../../ai/types';
|
|
3
|
+
|
|
4
|
+
export interface RedisConversationStoreConfig {
|
|
5
|
+
/** Redis client that supports get/set/del/keys commands */
|
|
6
|
+
client: RedisClient;
|
|
7
|
+
/** Key prefix (default: 'conv:') */
|
|
8
|
+
keyPrefix?: string;
|
|
9
|
+
/** TTL in seconds for conversation keys (default: 86400 = 24h) */
|
|
10
|
+
ttl?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimal Redis client interface — compatible with ioredis and node-redis
|
|
15
|
+
*/
|
|
16
|
+
export interface RedisClient {
|
|
17
|
+
get(key: string): Promise<string | null>;
|
|
18
|
+
set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
|
|
19
|
+
del(key: string): Promise<unknown>;
|
|
20
|
+
keys(pattern: string): Promise<string[]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Redis-backed conversation store — suitable for multi-instance production deployments.
|
|
25
|
+
*
|
|
26
|
+
* Requires an external Redis client to be injected. Compatible with ioredis / node-redis.
|
|
27
|
+
*/
|
|
28
|
+
export class RedisConversationStore implements ConversationStore {
|
|
29
|
+
private readonly client: RedisClient;
|
|
30
|
+
private readonly keyPrefix: string;
|
|
31
|
+
private readonly ttl: number;
|
|
32
|
+
|
|
33
|
+
public constructor(config: RedisConversationStoreConfig) {
|
|
34
|
+
this.client = config.client;
|
|
35
|
+
this.keyPrefix = config.keyPrefix ?? 'conv:';
|
|
36
|
+
this.ttl = config.ttl ?? 86400;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async create(metadata: Record<string, unknown> = {}): Promise<Conversation> {
|
|
40
|
+
const id = crypto.randomUUID();
|
|
41
|
+
const conversation: Conversation = {
|
|
42
|
+
id,
|
|
43
|
+
messages: [],
|
|
44
|
+
metadata,
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
updatedAt: new Date(),
|
|
47
|
+
};
|
|
48
|
+
await this.save(conversation);
|
|
49
|
+
return conversation;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async get(id: string): Promise<Conversation | null> {
|
|
53
|
+
const raw = await this.client.get(this.key(id));
|
|
54
|
+
if (!raw) return null;
|
|
55
|
+
const parsed = JSON.parse(raw) as Conversation;
|
|
56
|
+
parsed.createdAt = new Date(parsed.createdAt);
|
|
57
|
+
parsed.updatedAt = new Date(parsed.updatedAt);
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async appendMessage(id: string, message: AiMessage): Promise<void> {
|
|
62
|
+
const conv = await this.get(id);
|
|
63
|
+
if (!conv) throw new Error(`Conversation "${id}" not found`);
|
|
64
|
+
conv.messages.push(message);
|
|
65
|
+
conv.updatedAt = new Date();
|
|
66
|
+
await this.save(conv);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async trim(id: string, maxMessages: number): Promise<void> {
|
|
70
|
+
const conv = await this.get(id);
|
|
71
|
+
if (!conv) return;
|
|
72
|
+
if (conv.messages.length > maxMessages) {
|
|
73
|
+
conv.messages = conv.messages.slice(-maxMessages);
|
|
74
|
+
conv.updatedAt = new Date();
|
|
75
|
+
await this.save(conv);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async delete(id: string): Promise<boolean> {
|
|
80
|
+
const result = await this.client.del(this.key(id));
|
|
81
|
+
return Number(result) > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async list(): Promise<string[]> {
|
|
85
|
+
const keys = await this.client.keys(`${this.keyPrefix}*`);
|
|
86
|
+
return keys.map((k) => k.slice(this.keyPrefix.length));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private key(id: string): string {
|
|
90
|
+
return `${this.keyPrefix}${id}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async save(conversation: Conversation): Promise<void> {
|
|
94
|
+
await this.client.set(
|
|
95
|
+
this.key(conversation.id),
|
|
96
|
+
JSON.stringify(conversation),
|
|
97
|
+
'EX',
|
|
98
|
+
this.ttl,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AiMessage } from '../ai/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A conversation session
|
|
5
|
+
*/
|
|
6
|
+
export interface Conversation {
|
|
7
|
+
id: string;
|
|
8
|
+
messages: AiMessage[];
|
|
9
|
+
metadata: Record<string, unknown>;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Abstract conversation storage interface
|
|
16
|
+
*/
|
|
17
|
+
export interface ConversationStore {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new conversation
|
|
20
|
+
*/
|
|
21
|
+
create(metadata?: Record<string, unknown>): Promise<Conversation>;
|
|
22
|
+
/**
|
|
23
|
+
* Retrieve an existing conversation
|
|
24
|
+
*/
|
|
25
|
+
get(id: string): Promise<Conversation | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Append a message to a conversation
|
|
28
|
+
*/
|
|
29
|
+
appendMessage(id: string, message: AiMessage): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Trim messages to maxMessages (keeping the most recent)
|
|
32
|
+
*/
|
|
33
|
+
trim(id: string, maxMessages: number): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Delete a conversation
|
|
36
|
+
*/
|
|
37
|
+
delete(id: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* List all conversation IDs
|
|
40
|
+
*/
|
|
41
|
+
list(): Promise<string[]>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Summarizer callback — produce a summary string from conversation history
|
|
46
|
+
*/
|
|
47
|
+
export type ConversationSummarizer = (messages: AiMessage[]) => Promise<string>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* ConversationModule configuration
|
|
51
|
+
*/
|
|
52
|
+
export interface ConversationModuleOptions {
|
|
53
|
+
store?: ConversationStore;
|
|
54
|
+
/** Maximum messages before auto-trim (default: 100) */
|
|
55
|
+
maxMessages?: number;
|
|
56
|
+
/** Enable auto-trim when maxMessages is reached (default: true) */
|
|
57
|
+
autoTrim?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* When message count exceeds summaryThreshold, call summarizer and
|
|
60
|
+
* replace old messages with a summary message.
|
|
61
|
+
*/
|
|
62
|
+
summaryThreshold?: number;
|
|
63
|
+
/** Summarizer callback — inject AiService here to avoid circular deps */
|
|
64
|
+
summarizer?: ConversationSummarizer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const CONVERSATION_SERVICE_TOKEN = Symbol('@dangao/bun-server:conversation:service');
|
|
68
|
+
export const CONVERSATION_OPTIONS_TOKEN = Symbol('@dangao/bun-server:conversation:options');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import type { ModuleProvider } from '../di/module';
|
|
3
|
+
import { EmbeddingService } from './service';
|
|
4
|
+
import {
|
|
5
|
+
EMBEDDING_SERVICE_TOKEN,
|
|
6
|
+
EMBEDDING_OPTIONS_TOKEN,
|
|
7
|
+
type EmbeddingModuleOptions,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
@Module({ providers: [] })
|
|
11
|
+
export class EmbeddingModule {
|
|
12
|
+
/**
|
|
13
|
+
* Configure the embedding module.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* EmbeddingModule.forRoot({
|
|
18
|
+
* provider: {
|
|
19
|
+
* name: 'openai',
|
|
20
|
+
* provider: OpenAIEmbeddingProvider,
|
|
21
|
+
* config: { apiKey: process.env.OPENAI_API_KEY! },
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
public static forRoot(options: EmbeddingModuleOptions): typeof EmbeddingModule {
|
|
27
|
+
const service = new EmbeddingService(options);
|
|
28
|
+
|
|
29
|
+
const providers: ModuleProvider[] = [
|
|
30
|
+
{ provide: EMBEDDING_OPTIONS_TOKEN, useValue: options },
|
|
31
|
+
{ provide: EMBEDDING_SERVICE_TOKEN, useValue: service },
|
|
32
|
+
EmbeddingService,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const existing = Reflect.getMetadata(MODULE_METADATA_KEY, EmbeddingModule) || {};
|
|
36
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, {
|
|
37
|
+
...existing,
|
|
38
|
+
providers: [...(existing.providers || []), ...providers],
|
|
39
|
+
exports: [
|
|
40
|
+
...(existing.exports || []),
|
|
41
|
+
EMBEDDING_SERVICE_TOKEN,
|
|
42
|
+
EmbeddingService,
|
|
43
|
+
],
|
|
44
|
+
}, EmbeddingModule);
|
|
45
|
+
|
|
46
|
+
return EmbeddingModule;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static reset(): void {
|
|
50
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, EmbeddingModule);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EmbeddingProvider } from '../types';
|
|
2
|
+
import { AiProviderError } from '../../ai/errors';
|
|
3
|
+
|
|
4
|
+
export interface OllamaEmbeddingProviderConfig {
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** Default: nomic-embed-text */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** Dimensions depend on model. Default 768 for nomic-embed-text */
|
|
9
|
+
dimensions?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class OllamaEmbeddingProvider implements EmbeddingProvider {
|
|
13
|
+
public readonly name = 'ollama';
|
|
14
|
+
public readonly dimensions: number;
|
|
15
|
+
private readonly baseUrl: string;
|
|
16
|
+
private readonly model: string;
|
|
17
|
+
|
|
18
|
+
public constructor(config: OllamaEmbeddingProviderConfig = {}) {
|
|
19
|
+
this.baseUrl = (config.baseUrl ?? 'http://localhost:11434').replace(/\/$/, '');
|
|
20
|
+
this.model = config.model ?? 'nomic-embed-text';
|
|
21
|
+
this.dimensions = config.dimensions ?? 768;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async embed(text: string): Promise<number[]> {
|
|
25
|
+
const res = await fetch(`${this.baseUrl}/api/embed`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ model: this.model, input: text }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!res.ok) throw new AiProviderError(await res.text(), 'ollama-embedding', res.status);
|
|
32
|
+
const data = await res.json() as { embeddings: number[][] };
|
|
33
|
+
return data.embeddings[0]!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async embedBatch(texts: string[]): Promise<number[][]> {
|
|
37
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EmbeddingProvider } from '../types';
|
|
2
|
+
import { AiProviderError, AiRateLimitError } from '../../ai/errors';
|
|
3
|
+
|
|
4
|
+
export interface OpenAIEmbeddingProviderConfig {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Default: text-embedding-3-small */
|
|
7
|
+
model?: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
12
|
+
public readonly name = 'openai';
|
|
13
|
+
public readonly dimensions: number;
|
|
14
|
+
private readonly apiKey: string;
|
|
15
|
+
private readonly model: string;
|
|
16
|
+
private readonly baseUrl: string;
|
|
17
|
+
|
|
18
|
+
public constructor(config: OpenAIEmbeddingProviderConfig) {
|
|
19
|
+
this.apiKey = config.apiKey;
|
|
20
|
+
this.model = config.model ?? 'text-embedding-3-small';
|
|
21
|
+
this.baseUrl = (config.baseUrl ?? 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
22
|
+
// text-embedding-3-small: 1536, text-embedding-3-large: 3072
|
|
23
|
+
this.dimensions = this.model.includes('large') ? 3072 : 1536;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async embed(text: string): Promise<number[]> {
|
|
27
|
+
const results = await this.embedBatch([text]);
|
|
28
|
+
return results[0]!;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async embedBatch(texts: string[]): Promise<number[][]> {
|
|
32
|
+
const res = await fetch(`${this.baseUrl}/embeddings`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({ input: texts, model: this.model }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (res.status === 429) throw new AiRateLimitError('openai-embedding');
|
|
42
|
+
if (!res.ok) throw new AiProviderError(await res.text(), 'openai-embedding', res.status);
|
|
43
|
+
|
|
44
|
+
const data = await res.json() as { data: Array<{ embedding: number[] }> };
|
|
45
|
+
return data.data.map((d) => d.embedding);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Injectable } from '../di/decorators';
|
|
2
|
+
import { Inject } from '../di/decorators';
|
|
3
|
+
import type { EmbeddingProvider, EmbeddingModuleOptions } from './types';
|
|
4
|
+
import { EMBEDDING_OPTIONS_TOKEN } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Embedding service — generates vector embeddings for text using the configured provider.
|
|
8
|
+
*/
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class EmbeddingService {
|
|
11
|
+
private readonly provider: EmbeddingProvider;
|
|
12
|
+
private readonly batchSize: number;
|
|
13
|
+
|
|
14
|
+
public constructor(
|
|
15
|
+
@Inject(EMBEDDING_OPTIONS_TOKEN) options: EmbeddingModuleOptions,
|
|
16
|
+
) {
|
|
17
|
+
this.provider = new options.provider.provider(options.provider.config);
|
|
18
|
+
this.batchSize = options.batchSize ?? 100;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate an embedding vector for a single text
|
|
23
|
+
*/
|
|
24
|
+
public async embed(text: string): Promise<number[]> {
|
|
25
|
+
return this.provider.embed(text);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate embedding vectors for multiple texts.
|
|
30
|
+
* Automatically batches requests to stay within provider limits.
|
|
31
|
+
*/
|
|
32
|
+
public async embedBatch(texts: string[]): Promise<number[][]> {
|
|
33
|
+
const results: number[][] = [];
|
|
34
|
+
for (let i = 0; i < texts.length; i += this.batchSize) {
|
|
35
|
+
const batch = texts.slice(i, i + this.batchSize);
|
|
36
|
+
const embeddings = await this.provider.embedBatch(batch);
|
|
37
|
+
results.push(...embeddings);
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Number of dimensions in embeddings produced by this provider
|
|
44
|
+
*/
|
|
45
|
+
public get dimensions(): number {
|
|
46
|
+
return this.provider.dimensions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Provider name
|
|
51
|
+
*/
|
|
52
|
+
public get providerName(): string {
|
|
53
|
+
return this.provider.name;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract embedding provider interface
|
|
3
|
+
*/
|
|
4
|
+
export interface EmbeddingProvider {
|
|
5
|
+
/** Generate embedding vector for a single text */
|
|
6
|
+
embed(text: string): Promise<number[]>;
|
|
7
|
+
/** Generate embedding vectors for multiple texts (batch) */
|
|
8
|
+
embedBatch(texts: string[]): Promise<number[][]>;
|
|
9
|
+
/** Embedding dimensions */
|
|
10
|
+
readonly dimensions: number;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EmbeddingProviderConfig<T = unknown> {
|
|
15
|
+
name: string;
|
|
16
|
+
provider: new (config: T) => EmbeddingProvider;
|
|
17
|
+
config: T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EmbeddingModuleOptions {
|
|
21
|
+
provider: EmbeddingProviderConfig;
|
|
22
|
+
/** Max texts per batch request (default: 100) */
|
|
23
|
+
batchSize?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const EMBEDDING_SERVICE_TOKEN = Symbol('@dangao/bun-server:embedding:service');
|
|
27
|
+
export const EMBEDDING_OPTIONS_TOKEN = Symbol('@dangao/bun-server:embedding:options');
|
package/src/index.ts
CHANGED
|
@@ -562,3 +562,13 @@ export {
|
|
|
562
562
|
type RegisteredListener,
|
|
563
563
|
} from './events';
|
|
564
564
|
|
|
565
|
+
// ── AI Modules (v2.0.0) ──────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
export * from './ai';
|
|
568
|
+
export * from './conversation';
|
|
569
|
+
export * from './prompt';
|
|
570
|
+
export * from './embedding';
|
|
571
|
+
export * from './vector-store';
|
|
572
|
+
export * from './rag';
|
|
573
|
+
export * from './mcp';
|
|
574
|
+
export * from './ai-guard';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { McpToolDefinition, McpResourceDefinition } from './types';
|
|
2
|
+
import { MCP_TOOL_METADATA_KEY, MCP_RESOURCE_METADATA_KEY } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mark a method as an MCP tool.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* \@McpTool({
|
|
10
|
+
* name: 'get_weather',
|
|
11
|
+
* description: 'Get current weather for a city',
|
|
12
|
+
* inputSchema: {
|
|
13
|
+
* type: 'object',
|
|
14
|
+
* properties: { city: { type: 'string' } },
|
|
15
|
+
* required: ['city'],
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
* public async getWeather({ city }: { city: string }) {
|
|
19
|
+
* return { temperature: 22, city };
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function McpTool(definition: McpToolDefinition): MethodDecorator {
|
|
24
|
+
return (target, propertyKey) => {
|
|
25
|
+
Reflect.defineMetadata(MCP_TOOL_METADATA_KEY, definition, target, propertyKey);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mark a method as an MCP resource provider.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* \@McpResource({
|
|
35
|
+
* uri: 'user://{id}',
|
|
36
|
+
* name: 'User Profile',
|
|
37
|
+
* mimeType: 'application/json',
|
|
38
|
+
* })
|
|
39
|
+
* public async getUserResource(\@McpParam('id') id: string) {
|
|
40
|
+
* return this.userService.find(id);
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function McpResource(definition: McpResourceDefinition): MethodDecorator {
|
|
45
|
+
return (target, propertyKey) => {
|
|
46
|
+
Reflect.defineMetadata(MCP_RESOURCE_METADATA_KEY, definition, target, propertyKey);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract a named parameter from MCP tool input arguments.
|
|
52
|
+
*/
|
|
53
|
+
export function McpParam(name: string): ParameterDecorator {
|
|
54
|
+
return (target, propertyKey, parameterIndex) => {
|
|
55
|
+
const existing: Array<{ index: number; name: string }> =
|
|
56
|
+
Reflect.getMetadata('mcp:params', target, propertyKey!) ?? [];
|
|
57
|
+
existing.push({ index: parameterIndex, name });
|
|
58
|
+
Reflect.defineMetadata('mcp:params', existing, target, propertyKey!);
|
|
59
|
+
};
|
|
60
|
+
}
|