@opensaas/stack-rag 0.1.6
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +565 -0
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/dist/config/index.d.ts +63 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +94 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/plugin.d.ts +38 -0
- package/dist/config/plugin.d.ts.map +1 -0
- package/dist/config/plugin.js +215 -0
- package/dist/config/plugin.js.map +1 -0
- package/dist/config/plugin.test.d.ts +2 -0
- package/dist/config/plugin.test.d.ts.map +1 -0
- package/dist/config/plugin.test.js +554 -0
- package/dist/config/plugin.test.js.map +1 -0
- package/dist/config/types.d.ts +249 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +5 -0
- package/dist/config/types.js.map +1 -0
- package/dist/fields/embedding.d.ts +85 -0
- package/dist/fields/embedding.d.ts.map +1 -0
- package/dist/fields/embedding.js +81 -0
- package/dist/fields/embedding.js.map +1 -0
- package/dist/fields/embedding.test.d.ts +2 -0
- package/dist/fields/embedding.test.d.ts.map +1 -0
- package/dist/fields/embedding.test.js +323 -0
- package/dist/fields/embedding.test.js.map +1 -0
- package/dist/fields/index.d.ts +6 -0
- package/dist/fields/index.d.ts.map +1 -0
- package/dist/fields/index.js +5 -0
- package/dist/fields/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +18 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +68 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.d.ts +49 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +151 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +41 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +126 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/providers.test.d.ts +2 -0
- package/dist/providers/providers.test.d.ts.map +1 -0
- package/dist/providers/providers.test.js +224 -0
- package/dist/providers/providers.test.js.map +1 -0
- package/dist/providers/types.d.ts +88 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/runtime/batch.d.ts +183 -0
- package/dist/runtime/batch.d.ts.map +1 -0
- package/dist/runtime/batch.js +240 -0
- package/dist/runtime/batch.js.map +1 -0
- package/dist/runtime/batch.test.d.ts +2 -0
- package/dist/runtime/batch.test.d.ts.map +1 -0
- package/dist/runtime/batch.test.js +251 -0
- package/dist/runtime/batch.test.js.map +1 -0
- package/dist/runtime/chunking.d.ts +42 -0
- package/dist/runtime/chunking.d.ts.map +1 -0
- package/dist/runtime/chunking.js +264 -0
- package/dist/runtime/chunking.js.map +1 -0
- package/dist/runtime/chunking.test.d.ts +2 -0
- package/dist/runtime/chunking.test.d.ts.map +1 -0
- package/dist/runtime/chunking.test.js +212 -0
- package/dist/runtime/chunking.test.js.map +1 -0
- package/dist/runtime/embeddings.d.ts +147 -0
- package/dist/runtime/embeddings.d.ts.map +1 -0
- package/dist/runtime/embeddings.js +201 -0
- package/dist/runtime/embeddings.js.map +1 -0
- package/dist/runtime/embeddings.test.d.ts +2 -0
- package/dist/runtime/embeddings.test.d.ts.map +1 -0
- package/dist/runtime/embeddings.test.js +366 -0
- package/dist/runtime/embeddings.test.js.map +1 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +18 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/search.d.ts +135 -0
- package/dist/runtime/search.d.ts.map +1 -0
- package/dist/runtime/search.js +101 -0
- package/dist/runtime/search.js.map +1 -0
- package/dist/storage/index.d.ts +41 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +73 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/json.d.ts +34 -0
- package/dist/storage/json.d.ts.map +1 -0
- package/dist/storage/json.js +82 -0
- package/dist/storage/json.js.map +1 -0
- package/dist/storage/pgvector.d.ts +53 -0
- package/dist/storage/pgvector.d.ts.map +1 -0
- package/dist/storage/pgvector.js +168 -0
- package/dist/storage/pgvector.js.map +1 -0
- package/dist/storage/sqlite-vss.d.ts +49 -0
- package/dist/storage/sqlite-vss.d.ts.map +1 -0
- package/dist/storage/sqlite-vss.js +148 -0
- package/dist/storage/sqlite-vss.js.map +1 -0
- package/dist/storage/storage.test.d.ts +2 -0
- package/dist/storage/storage.test.d.ts.map +1 -0
- package/dist/storage/storage.test.js +440 -0
- package/dist/storage/storage.test.js.map +1 -0
- package/dist/storage/types.d.ts +79 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +49 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +82 -0
- package/src/config/index.ts +116 -0
- package/src/config/plugin.test.ts +664 -0
- package/src/config/plugin.ts +257 -0
- package/src/config/types.ts +283 -0
- package/src/fields/embedding.test.ts +408 -0
- package/src/fields/embedding.ts +150 -0
- package/src/fields/index.ts +6 -0
- package/src/index.ts +33 -0
- package/src/mcp/index.ts +21 -0
- package/src/providers/index.ts +81 -0
- package/src/providers/ollama.ts +186 -0
- package/src/providers/openai.ts +161 -0
- package/src/providers/providers.test.ts +275 -0
- package/src/providers/types.ts +100 -0
- package/src/runtime/batch.test.ts +332 -0
- package/src/runtime/batch.ts +424 -0
- package/src/runtime/chunking.test.ts +258 -0
- package/src/runtime/chunking.ts +334 -0
- package/src/runtime/embeddings.test.ts +441 -0
- package/src/runtime/embeddings.ts +380 -0
- package/src/runtime/index.ts +51 -0
- package/src/runtime/search.ts +243 -0
- package/src/storage/index.ts +86 -0
- package/src/storage/json.ts +106 -0
- package/src/storage/pgvector.ts +206 -0
- package/src/storage/sqlite-vss.ts +193 -0
- package/src/storage/storage.test.ts +521 -0
- package/src/storage/types.ts +126 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +18 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { ragPlugin } from './plugin.js'
|
|
3
|
+
import type { RAGConfig } from './types.js'
|
|
4
|
+
import type { Plugin, PluginContext } from '@opensaas/stack-core'
|
|
5
|
+
|
|
6
|
+
describe('RAG Plugin', () => {
|
|
7
|
+
describe('plugin creation', () => {
|
|
8
|
+
it('should create plugin with minimal config', () => {
|
|
9
|
+
const config: RAGConfig = {
|
|
10
|
+
provider: {
|
|
11
|
+
type: 'openai',
|
|
12
|
+
apiKey: 'test-key',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const plugin = ragPlugin(config)
|
|
17
|
+
|
|
18
|
+
expect(plugin.name).toBe('rag')
|
|
19
|
+
expect(plugin.version).toBe('0.1.0')
|
|
20
|
+
expect(plugin.init).toBeDefined()
|
|
21
|
+
expect(typeof plugin.init).toBe('function')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should create plugin with full config', () => {
|
|
25
|
+
const config: RAGConfig = {
|
|
26
|
+
provider: {
|
|
27
|
+
type: 'openai',
|
|
28
|
+
apiKey: 'test-key',
|
|
29
|
+
},
|
|
30
|
+
storage: {
|
|
31
|
+
type: 'json',
|
|
32
|
+
},
|
|
33
|
+
enableMcpTools: true,
|
|
34
|
+
batchSize: 20,
|
|
35
|
+
rateLimit: 200,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const plugin = ragPlugin(config)
|
|
39
|
+
|
|
40
|
+
expect(plugin).toBeDefined()
|
|
41
|
+
expect(plugin.name).toBe('rag')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should create plugin with multiple providers', () => {
|
|
45
|
+
const config: RAGConfig = {
|
|
46
|
+
providers: {
|
|
47
|
+
openai: {
|
|
48
|
+
type: 'openai',
|
|
49
|
+
apiKey: 'test-key',
|
|
50
|
+
},
|
|
51
|
+
ollama: {
|
|
52
|
+
type: 'ollama',
|
|
53
|
+
baseURL: 'http://localhost:11434',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
storage: {
|
|
57
|
+
type: 'json',
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const plugin = ragPlugin(config)
|
|
62
|
+
|
|
63
|
+
expect(plugin).toBeDefined()
|
|
64
|
+
expect(plugin.name).toBe('rag')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('plugin initialization', () => {
|
|
69
|
+
it('should find and process embedding fields with autoGenerate', async () => {
|
|
70
|
+
const config: RAGConfig = {
|
|
71
|
+
provider: {
|
|
72
|
+
type: 'openai',
|
|
73
|
+
apiKey: 'test-key',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const plugin = ragPlugin(config)
|
|
78
|
+
|
|
79
|
+
const mockContext = {
|
|
80
|
+
config: {
|
|
81
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
82
|
+
lists: {
|
|
83
|
+
Article: {
|
|
84
|
+
fields: {
|
|
85
|
+
content: { type: 'text' },
|
|
86
|
+
contentEmbedding: {
|
|
87
|
+
type: 'embedding',
|
|
88
|
+
sourceField: 'content',
|
|
89
|
+
autoGenerate: true,
|
|
90
|
+
provider: 'openai',
|
|
91
|
+
dimensions: 1536,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
extendList: vi.fn(),
|
|
98
|
+
setPluginData: vi.fn(),
|
|
99
|
+
registerMcpTool: vi.fn(),
|
|
100
|
+
addList: vi.fn(),
|
|
101
|
+
} as PluginContext
|
|
102
|
+
|
|
103
|
+
await plugin.init!(mockContext as PluginContext)
|
|
104
|
+
|
|
105
|
+
expect(mockContext.extendList).toHaveBeenCalledWith(
|
|
106
|
+
'Article',
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
hooks: expect.any(Object),
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should throw error if autoGenerate is enabled without sourceField', async () => {
|
|
114
|
+
const config: RAGConfig = {
|
|
115
|
+
provider: {
|
|
116
|
+
type: 'openai',
|
|
117
|
+
apiKey: 'test-key',
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const plugin = ragPlugin(config)
|
|
122
|
+
|
|
123
|
+
const mockContext = {
|
|
124
|
+
config: {
|
|
125
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
126
|
+
lists: {
|
|
127
|
+
Article: {
|
|
128
|
+
fields: {
|
|
129
|
+
embedding: {
|
|
130
|
+
type: 'embedding',
|
|
131
|
+
autoGenerate: true,
|
|
132
|
+
// Missing sourceField
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
extendList: vi.fn(),
|
|
139
|
+
setPluginData: vi.fn(),
|
|
140
|
+
registerMcpTool: vi.fn(),
|
|
141
|
+
addList: vi.fn(),
|
|
142
|
+
} as PluginContext
|
|
143
|
+
|
|
144
|
+
await expect(plugin.init!(mockContext as PluginContext)).rejects.toThrow(
|
|
145
|
+
/has autoGenerate enabled but no sourceField specified/,
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should skip fields without autoGenerate', async () => {
|
|
150
|
+
const config: RAGConfig = {
|
|
151
|
+
provider: {
|
|
152
|
+
type: 'openai',
|
|
153
|
+
apiKey: 'test-key',
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const plugin = ragPlugin(config)
|
|
158
|
+
|
|
159
|
+
const mockContext = {
|
|
160
|
+
config: {
|
|
161
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
162
|
+
lists: {
|
|
163
|
+
Article: {
|
|
164
|
+
fields: {
|
|
165
|
+
embedding: {
|
|
166
|
+
type: 'embedding',
|
|
167
|
+
// autoGenerate is false
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
extendList: vi.fn(),
|
|
174
|
+
setPluginData: vi.fn(),
|
|
175
|
+
registerMcpTool: vi.fn(),
|
|
176
|
+
addList: vi.fn(),
|
|
177
|
+
} as PluginContext
|
|
178
|
+
|
|
179
|
+
await plugin.init!(mockContext as PluginContext)
|
|
180
|
+
|
|
181
|
+
// Should not extend list for fields without autoGenerate
|
|
182
|
+
expect(mockContext.extendList).not.toHaveBeenCalled()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should store normalized config in plugin data', async () => {
|
|
186
|
+
const config: RAGConfig = {
|
|
187
|
+
provider: {
|
|
188
|
+
type: 'openai',
|
|
189
|
+
apiKey: 'test-key',
|
|
190
|
+
},
|
|
191
|
+
storage: {
|
|
192
|
+
type: 'json',
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const plugin = ragPlugin(config)
|
|
197
|
+
|
|
198
|
+
const mockContext = {
|
|
199
|
+
config: {
|
|
200
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
201
|
+
lists: {},
|
|
202
|
+
},
|
|
203
|
+
setPluginData: vi.fn(),
|
|
204
|
+
registerMcpTool: vi.fn(),
|
|
205
|
+
extendList: vi.fn(),
|
|
206
|
+
addList: vi.fn(),
|
|
207
|
+
} as PluginContext
|
|
208
|
+
|
|
209
|
+
await plugin.init!(mockContext as PluginContext)
|
|
210
|
+
|
|
211
|
+
expect(mockContext.setPluginData).toHaveBeenCalledWith(
|
|
212
|
+
'rag',
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
provider: expect.any(Object),
|
|
215
|
+
storage: expect.any(Object),
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('automatic embedding generation', () => {
|
|
222
|
+
it('should inject afterOperation hook for autoGenerate fields', async () => {
|
|
223
|
+
const config: RAGConfig = {
|
|
224
|
+
provider: {
|
|
225
|
+
type: 'openai',
|
|
226
|
+
apiKey: 'test-key',
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const plugin = ragPlugin(config)
|
|
231
|
+
|
|
232
|
+
const mockContext = {
|
|
233
|
+
config: {
|
|
234
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
235
|
+
lists: {
|
|
236
|
+
Article: {
|
|
237
|
+
fields: {
|
|
238
|
+
content: { type: 'text' },
|
|
239
|
+
contentEmbedding: {
|
|
240
|
+
type: 'embedding',
|
|
241
|
+
sourceField: 'content',
|
|
242
|
+
autoGenerate: true,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
extendList: vi.fn(),
|
|
249
|
+
setPluginData: vi.fn(),
|
|
250
|
+
registerMcpTool: vi.fn(),
|
|
251
|
+
addList: vi.fn(),
|
|
252
|
+
} as PluginContext
|
|
253
|
+
|
|
254
|
+
await plugin.init!(mockContext as PluginContext)
|
|
255
|
+
|
|
256
|
+
expect(mockContext.extendList).toHaveBeenCalledWith(
|
|
257
|
+
'Article',
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
hooks: expect.objectContaining({
|
|
260
|
+
resolveInput: expect.any(Function),
|
|
261
|
+
}),
|
|
262
|
+
}),
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should handle multiple embedding fields in same list', async () => {
|
|
267
|
+
const config: RAGConfig = {
|
|
268
|
+
providers: {
|
|
269
|
+
openai: { type: 'openai', apiKey: 'key1' },
|
|
270
|
+
ollama: { type: 'ollama' },
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const plugin = ragPlugin(config)
|
|
275
|
+
|
|
276
|
+
const mockContext = {
|
|
277
|
+
config: {
|
|
278
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
279
|
+
lists: {
|
|
280
|
+
Article: {
|
|
281
|
+
fields: {
|
|
282
|
+
title: { type: 'text' },
|
|
283
|
+
content: { type: 'text' },
|
|
284
|
+
titleEmbedding: {
|
|
285
|
+
type: 'embedding',
|
|
286
|
+
sourceField: 'title',
|
|
287
|
+
autoGenerate: true,
|
|
288
|
+
provider: 'ollama',
|
|
289
|
+
},
|
|
290
|
+
contentEmbedding: {
|
|
291
|
+
type: 'embedding',
|
|
292
|
+
sourceField: 'content',
|
|
293
|
+
autoGenerate: true,
|
|
294
|
+
provider: 'openai',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
extendList: vi.fn(),
|
|
301
|
+
setPluginData: vi.fn(),
|
|
302
|
+
registerMcpTool: vi.fn(),
|
|
303
|
+
addList: vi.fn(),
|
|
304
|
+
} as PluginContext
|
|
305
|
+
|
|
306
|
+
await plugin.init!(mockContext as PluginContext)
|
|
307
|
+
|
|
308
|
+
// Should be called once per list (hooks are merged)
|
|
309
|
+
expect(mockContext.extendList).toHaveBeenCalledTimes(2)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('MCP integration', () => {
|
|
314
|
+
it('should register MCP tools when enabled', async () => {
|
|
315
|
+
const config: RAGConfig = {
|
|
316
|
+
provider: {
|
|
317
|
+
type: 'openai',
|
|
318
|
+
apiKey: 'test-key',
|
|
319
|
+
},
|
|
320
|
+
enableMcpTools: true,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const plugin = ragPlugin(config)
|
|
324
|
+
|
|
325
|
+
const mockContext = {
|
|
326
|
+
config: {
|
|
327
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
328
|
+
lists: {
|
|
329
|
+
Article: {
|
|
330
|
+
fields: {
|
|
331
|
+
contentEmbedding: {
|
|
332
|
+
type: 'embedding',
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
extendList: vi.fn(),
|
|
339
|
+
setPluginData: vi.fn(),
|
|
340
|
+
registerMcpTool: vi.fn(),
|
|
341
|
+
addList: vi.fn(),
|
|
342
|
+
} as PluginContext
|
|
343
|
+
|
|
344
|
+
await plugin.init!(mockContext as PluginContext)
|
|
345
|
+
|
|
346
|
+
expect(mockContext.registerMcpTool).toHaveBeenCalledWith(
|
|
347
|
+
expect.objectContaining({
|
|
348
|
+
name: 'semantic_search_article',
|
|
349
|
+
description: expect.stringContaining('Search Article'),
|
|
350
|
+
inputSchema: expect.any(Object),
|
|
351
|
+
handler: expect.any(Function),
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('should not register MCP tools when disabled', async () => {
|
|
357
|
+
const config: RAGConfig = {
|
|
358
|
+
provider: {
|
|
359
|
+
type: 'openai',
|
|
360
|
+
apiKey: 'test-key',
|
|
361
|
+
},
|
|
362
|
+
enableMcpTools: false,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const plugin = ragPlugin(config)
|
|
366
|
+
|
|
367
|
+
const mockContext = {
|
|
368
|
+
config: {
|
|
369
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
370
|
+
lists: {
|
|
371
|
+
Article: {
|
|
372
|
+
fields: {
|
|
373
|
+
contentEmbedding: {
|
|
374
|
+
type: 'embedding',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
extendList: vi.fn(),
|
|
381
|
+
setPluginData: vi.fn(),
|
|
382
|
+
registerMcpTool: vi.fn(),
|
|
383
|
+
addList: vi.fn(),
|
|
384
|
+
} as PluginContext
|
|
385
|
+
|
|
386
|
+
await plugin.init!(mockContext as PluginContext)
|
|
387
|
+
|
|
388
|
+
expect(mockContext.registerMcpTool).not.toHaveBeenCalled()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should skip lists without embedding fields for MCP', async () => {
|
|
392
|
+
const config: RAGConfig = {
|
|
393
|
+
provider: {
|
|
394
|
+
type: 'openai',
|
|
395
|
+
apiKey: 'test-key',
|
|
396
|
+
},
|
|
397
|
+
enableMcpTools: true,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const plugin = ragPlugin(config)
|
|
401
|
+
|
|
402
|
+
const mockContext = {
|
|
403
|
+
config: {
|
|
404
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
405
|
+
lists: {
|
|
406
|
+
User: {
|
|
407
|
+
fields: {
|
|
408
|
+
name: { type: 'text' },
|
|
409
|
+
email: { type: 'text' },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
extendList: vi.fn(),
|
|
415
|
+
setPluginData: vi.fn(),
|
|
416
|
+
registerMcpTool: vi.fn(),
|
|
417
|
+
addList: vi.fn(),
|
|
418
|
+
} as PluginContext
|
|
419
|
+
|
|
420
|
+
await plugin.init!(mockContext as PluginContext)
|
|
421
|
+
|
|
422
|
+
expect(mockContext.registerMcpTool).not.toHaveBeenCalled()
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('should generate correct MCP tool names', async () => {
|
|
426
|
+
const config: RAGConfig = {
|
|
427
|
+
provider: {
|
|
428
|
+
type: 'openai',
|
|
429
|
+
apiKey: 'test-key',
|
|
430
|
+
},
|
|
431
|
+
enableMcpTools: true,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const plugin = ragPlugin(config)
|
|
435
|
+
|
|
436
|
+
const mockContext = {
|
|
437
|
+
config: {
|
|
438
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
439
|
+
lists: {
|
|
440
|
+
BlogPost: {
|
|
441
|
+
fields: {
|
|
442
|
+
embedding: { type: 'embedding' },
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
UserProfile: {
|
|
446
|
+
fields: {
|
|
447
|
+
embedding: { type: 'embedding' },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
extendList: vi.fn(),
|
|
453
|
+
setPluginData: vi.fn(),
|
|
454
|
+
registerMcpTool: vi.fn(),
|
|
455
|
+
addList: vi.fn(),
|
|
456
|
+
} as PluginContext
|
|
457
|
+
|
|
458
|
+
await plugin.init!(mockContext as PluginContext)
|
|
459
|
+
|
|
460
|
+
expect(mockContext.registerMcpTool).toHaveBeenCalledWith(
|
|
461
|
+
expect.objectContaining({
|
|
462
|
+
name: 'semantic_search_blogpost',
|
|
463
|
+
}),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
expect(mockContext.registerMcpTool).toHaveBeenCalledWith(
|
|
467
|
+
expect.objectContaining({
|
|
468
|
+
name: 'semantic_search_userprofile',
|
|
469
|
+
}),
|
|
470
|
+
)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('should create tool with correct input schema', async () => {
|
|
474
|
+
const config: RAGConfig = {
|
|
475
|
+
provider: {
|
|
476
|
+
type: 'openai',
|
|
477
|
+
apiKey: 'test-key',
|
|
478
|
+
},
|
|
479
|
+
enableMcpTools: true,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const plugin = ragPlugin(config)
|
|
483
|
+
|
|
484
|
+
const mockContext = {
|
|
485
|
+
config: {
|
|
486
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
487
|
+
lists: {
|
|
488
|
+
Article: {
|
|
489
|
+
fields: {
|
|
490
|
+
embedding1: { type: 'embedding' },
|
|
491
|
+
embedding2: { type: 'embedding' },
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
extendList: vi.fn(),
|
|
497
|
+
setPluginData: vi.fn(),
|
|
498
|
+
registerMcpTool: vi.fn(),
|
|
499
|
+
addList: vi.fn(),
|
|
500
|
+
} as PluginContext
|
|
501
|
+
|
|
502
|
+
await plugin.init!(mockContext as PluginContext)
|
|
503
|
+
|
|
504
|
+
const toolCall = (
|
|
505
|
+
mockContext.registerMcpTool as unknown as {
|
|
506
|
+
mock: { calls: [[{ inputSchema: unknown }]] }
|
|
507
|
+
}
|
|
508
|
+
).mock.calls[0][0]
|
|
509
|
+
|
|
510
|
+
expect(toolCall.inputSchema).toEqual(
|
|
511
|
+
expect.objectContaining({
|
|
512
|
+
type: 'object',
|
|
513
|
+
properties: expect.objectContaining({
|
|
514
|
+
query: expect.any(Object),
|
|
515
|
+
limit: expect.any(Object),
|
|
516
|
+
minScore: expect.any(Object),
|
|
517
|
+
field: expect.objectContaining({
|
|
518
|
+
enum: ['embedding1', 'embedding2'],
|
|
519
|
+
}),
|
|
520
|
+
}),
|
|
521
|
+
required: ['query'],
|
|
522
|
+
}),
|
|
523
|
+
)
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
describe('provider selection', () => {
|
|
528
|
+
it('should use default provider when field does not specify one', async () => {
|
|
529
|
+
const config: RAGConfig = {
|
|
530
|
+
provider: {
|
|
531
|
+
type: 'openai',
|
|
532
|
+
apiKey: 'test-key',
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const plugin = ragPlugin(config)
|
|
537
|
+
|
|
538
|
+
const mockContext = {
|
|
539
|
+
config: {
|
|
540
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
541
|
+
lists: {
|
|
542
|
+
Article: {
|
|
543
|
+
fields: {
|
|
544
|
+
embedding: {
|
|
545
|
+
type: 'embedding',
|
|
546
|
+
sourceField: 'content',
|
|
547
|
+
autoGenerate: true,
|
|
548
|
+
// No provider specified, should use default
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
extendList: vi.fn(),
|
|
555
|
+
setPluginData: vi.fn(),
|
|
556
|
+
registerMcpTool: vi.fn(),
|
|
557
|
+
addList: vi.fn(),
|
|
558
|
+
} as PluginContext
|
|
559
|
+
|
|
560
|
+
await plugin.init!(mockContext as PluginContext)
|
|
561
|
+
|
|
562
|
+
expect(mockContext.extendList).toHaveBeenCalled()
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('should use named provider when field specifies one', async () => {
|
|
566
|
+
const config: RAGConfig = {
|
|
567
|
+
providers: {
|
|
568
|
+
openai: { type: 'openai', apiKey: 'key1' },
|
|
569
|
+
ollama: { type: 'ollama' },
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const plugin = ragPlugin(config)
|
|
574
|
+
|
|
575
|
+
const mockContext = {
|
|
576
|
+
config: {
|
|
577
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
578
|
+
lists: {
|
|
579
|
+
Article: {
|
|
580
|
+
fields: {
|
|
581
|
+
embedding: {
|
|
582
|
+
type: 'embedding',
|
|
583
|
+
sourceField: 'content',
|
|
584
|
+
autoGenerate: true,
|
|
585
|
+
provider: 'ollama', // Use named provider
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
extendList: vi.fn(),
|
|
592
|
+
setPluginData: vi.fn(),
|
|
593
|
+
registerMcpTool: vi.fn(),
|
|
594
|
+
addList: vi.fn(),
|
|
595
|
+
} as PluginContext
|
|
596
|
+
|
|
597
|
+
await plugin.init!(mockContext as PluginContext)
|
|
598
|
+
|
|
599
|
+
expect(mockContext.extendList).toHaveBeenCalled()
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
describe('configuration normalization', () => {
|
|
604
|
+
it('should apply default values', async () => {
|
|
605
|
+
const config: RAGConfig = {
|
|
606
|
+
provider: {
|
|
607
|
+
type: 'openai',
|
|
608
|
+
apiKey: 'test-key',
|
|
609
|
+
},
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const plugin = ragPlugin(config)
|
|
613
|
+
|
|
614
|
+
const mockContext = {
|
|
615
|
+
config: {
|
|
616
|
+
db: { provider: 'sqlite', url: 'file:./test.db' },
|
|
617
|
+
lists: {},
|
|
618
|
+
},
|
|
619
|
+
setPluginData: vi.fn(),
|
|
620
|
+
registerMcpTool: vi.fn(),
|
|
621
|
+
extendList: vi.fn(),
|
|
622
|
+
addList: vi.fn(),
|
|
623
|
+
} as PluginContext
|
|
624
|
+
|
|
625
|
+
await plugin.init!(mockContext as PluginContext)
|
|
626
|
+
|
|
627
|
+
const normalizedConfig = (
|
|
628
|
+
mockContext.setPluginData as unknown as { mock: { calls: [[string, unknown]] } }
|
|
629
|
+
).mock.calls[0][1]
|
|
630
|
+
|
|
631
|
+
expect(normalizedConfig).toMatchObject({
|
|
632
|
+
enableMcpTools: expect.any(Boolean),
|
|
633
|
+
batchSize: expect.any(Number),
|
|
634
|
+
rateLimit: expect.any(Number),
|
|
635
|
+
storage: expect.any(Object),
|
|
636
|
+
chunking: expect.objectContaining({
|
|
637
|
+
strategy: expect.any(String),
|
|
638
|
+
maxTokens: expect.any(Number),
|
|
639
|
+
overlap: expect.any(Number),
|
|
640
|
+
}),
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
describe('plugin interface conformance', () => {
|
|
646
|
+
it('should conform to Plugin interface', () => {
|
|
647
|
+
const config: RAGConfig = {
|
|
648
|
+
provider: {
|
|
649
|
+
type: 'openai',
|
|
650
|
+
apiKey: 'test-key',
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const plugin: Plugin = ragPlugin(config)
|
|
655
|
+
|
|
656
|
+
expect(plugin.name).toBeDefined()
|
|
657
|
+
expect(plugin.version).toBeDefined()
|
|
658
|
+
expect(plugin.init).toBeDefined()
|
|
659
|
+
expect(typeof plugin.name).toBe('string')
|
|
660
|
+
expect(typeof plugin.version).toBe('string')
|
|
661
|
+
expect(typeof plugin.init).toBe('function')
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
})
|