strapi-content-embeddings 0.1.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.
Files changed (61) hide show
  1. package/README.md +276 -0
  2. package/dist/_chunks/App-7LMg3lrX.mjs +1004 -0
  3. package/dist/_chunks/App-wC2qv6kC.js +1008 -0
  4. package/dist/_chunks/en-B4KWt_jN.js +4 -0
  5. package/dist/_chunks/en-Byx4XI2L.mjs +4 -0
  6. package/dist/_chunks/index-Cz9cEuvw.mjs +411 -0
  7. package/dist/_chunks/index-o-tbBpJG.js +413 -0
  8. package/dist/admin/index.js +4 -0
  9. package/dist/admin/index.mjs +5 -0
  10. package/dist/admin/src/components/Initializer.d.ts +5 -0
  11. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  12. package/dist/admin/src/components/custom/BackLink.d.ts +5 -0
  13. package/dist/admin/src/components/custom/ChatModal.d.ts +1 -0
  14. package/dist/admin/src/components/custom/EmbeddingsModal.d.ts +1 -0
  15. package/dist/admin/src/components/custom/EmbeddingsTable.d.ts +12 -0
  16. package/dist/admin/src/components/custom/EmbeddingsWidget.d.ts +1 -0
  17. package/dist/admin/src/components/custom/EmptyState.d.ts +1 -0
  18. package/dist/admin/src/components/custom/Illo.d.ts +1 -0
  19. package/dist/admin/src/components/custom/Markdown.d.ts +5 -0
  20. package/dist/admin/src/components/custom/MarkdownEditor.d.ts +9 -0
  21. package/dist/admin/src/components/custom/RobotIcon.d.ts +6 -0
  22. package/dist/admin/src/components/forms/CreateEmbeddingForm.d.ts +15 -0
  23. package/dist/admin/src/index.d.ts +12 -0
  24. package/dist/admin/src/pages/App.d.ts +2 -0
  25. package/dist/admin/src/pages/CreateEmbeddings.d.ts +1 -0
  26. package/dist/admin/src/pages/EmbeddingDetails.d.ts +1 -0
  27. package/dist/admin/src/pages/HomePage.d.ts +1 -0
  28. package/dist/admin/src/pluginId.d.ts +1 -0
  29. package/dist/admin/src/utils/api.d.ts +33 -0
  30. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  31. package/dist/server/index.js +1359 -0
  32. package/dist/server/index.mjs +1360 -0
  33. package/dist/server/src/bootstrap.d.ts +5 -0
  34. package/dist/server/src/config/index.d.ts +26 -0
  35. package/dist/server/src/content-types/embedding/index.d.ts +54 -0
  36. package/dist/server/src/content-types/index.d.ts +56 -0
  37. package/dist/server/src/controllers/controller.d.ts +12 -0
  38. package/dist/server/src/controllers/index.d.ts +18 -0
  39. package/dist/server/src/controllers/mcp.d.ts +18 -0
  40. package/dist/server/src/destroy.d.ts +5 -0
  41. package/dist/server/src/index.d.ts +154 -0
  42. package/dist/server/src/mcp/index.d.ts +6 -0
  43. package/dist/server/src/mcp/schemas/index.d.ts +65 -0
  44. package/dist/server/src/mcp/server.d.ts +8 -0
  45. package/dist/server/src/mcp/tools/create-embedding.d.ts +38 -0
  46. package/dist/server/src/mcp/tools/get-embedding.d.ts +34 -0
  47. package/dist/server/src/mcp/tools/index.d.ts +114 -0
  48. package/dist/server/src/mcp/tools/list-embeddings.d.ts +40 -0
  49. package/dist/server/src/mcp/tools/rag-query.d.ts +34 -0
  50. package/dist/server/src/mcp/tools/semantic-search.d.ts +34 -0
  51. package/dist/server/src/middlewares/index.d.ts +2 -0
  52. package/dist/server/src/plugin-manager.d.ts +45 -0
  53. package/dist/server/src/policies/index.d.ts +2 -0
  54. package/dist/server/src/register.d.ts +5 -0
  55. package/dist/server/src/routes/admin.d.ts +14 -0
  56. package/dist/server/src/routes/content-api.d.ts +15 -0
  57. package/dist/server/src/routes/index.d.ts +36 -0
  58. package/dist/server/src/services/embeddings.d.ts +45 -0
  59. package/dist/server/src/services/index.d.ts +26 -0
  60. package/dist/style.css +95 -0
  61. package/package.json +104 -0
@@ -0,0 +1,1359 @@
1
+ "use strict";
2
+ const openai = require("@langchain/openai");
3
+ const pgvector = require("@langchain/community/vectorstores/pgvector");
4
+ const documents = require("@langchain/core/documents");
5
+ const output_parsers = require("@langchain/core/output_parsers");
6
+ const prompts = require("@langchain/core/prompts");
7
+ const runnables = require("@langchain/core/runnables");
8
+ const pg = require("pg");
9
+ const index_js = require("@modelcontextprotocol/sdk/server/index.js");
10
+ const types_js = require("@modelcontextprotocol/sdk/types.js");
11
+ const zod = require("zod");
12
+ const node_crypto = require("node:crypto");
13
+ const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
14
+ const EMBEDDING_MODELS = {
15
+ "text-embedding-3-small": { dimensions: 1536 },
16
+ "text-embedding-3-large": { dimensions: 3072 },
17
+ "text-embedding-ada-002": { dimensions: 1536 }
18
+ };
19
+ const config = {
20
+ default: {
21
+ openAIApiKey: "",
22
+ neonConnectionString: "",
23
+ embeddingModel: "text-embedding-3-small"
24
+ },
25
+ validator(config2) {
26
+ if (!config2.openAIApiKey) {
27
+ console.warn(
28
+ "strapi-content-embeddings: openAIApiKey is not configured. Plugin features will be disabled."
29
+ );
30
+ }
31
+ if (!config2.neonConnectionString) {
32
+ console.warn(
33
+ "strapi-content-embeddings: neonConnectionString is not configured. Plugin features will be disabled."
34
+ );
35
+ }
36
+ if (config2.embeddingModel && !EMBEDDING_MODELS[config2.embeddingModel]) {
37
+ console.warn(
38
+ `strapi-content-embeddings: Invalid embeddingModel "${config2.embeddingModel}". Valid options: ${Object.keys(EMBEDDING_MODELS).join(", ")}. Defaulting to "text-embedding-3-small".`
39
+ );
40
+ }
41
+ }
42
+ };
43
+ class PluginManager {
44
+ constructor() {
45
+ this.embeddings = null;
46
+ this.chat = null;
47
+ this.pool = null;
48
+ this.embeddingModel = "text-embedding-3-small";
49
+ this.dimensions = 1536;
50
+ this.vectorStoreConfig = null;
51
+ }
52
+ async initializePool(connectionString) {
53
+ console.log("Initializing Neon DB Pool");
54
+ if (this.pool) return this.pool;
55
+ try {
56
+ const poolConfig = {
57
+ connectionString,
58
+ ssl: { rejectUnauthorized: false },
59
+ max: 10
60
+ };
61
+ this.pool = new pg.Pool(poolConfig);
62
+ const client = await this.pool.connect();
63
+ await client.query("SELECT 1");
64
+ client.release();
65
+ await this.initializeVectorTable();
66
+ console.log("Neon DB Pool initialized successfully");
67
+ return this.pool;
68
+ } catch (error) {
69
+ console.error(`Failed to initialize Neon DB Pool: ${error}`);
70
+ throw new Error(`Failed to initialize Neon DB Pool: ${error}`);
71
+ }
72
+ }
73
+ async initializeVectorTable() {
74
+ if (!this.pool) throw new Error("Pool not initialized");
75
+ const client = await this.pool.connect();
76
+ try {
77
+ await client.query("CREATE EXTENSION IF NOT EXISTS vector");
78
+ await client.query(`
79
+ CREATE TABLE IF NOT EXISTS embeddings_documents (
80
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
81
+ content TEXT,
82
+ metadata JSONB,
83
+ embedding vector(${this.dimensions})
84
+ )
85
+ `);
86
+ await client.query(`
87
+ DROP INDEX IF EXISTS embeddings_documents_embedding_idx
88
+ `);
89
+ await client.query(`
90
+ CREATE INDEX IF NOT EXISTS embeddings_documents_embedding_hnsw_idx
91
+ ON embeddings_documents
92
+ USING hnsw (embedding vector_cosine_ops)
93
+ `);
94
+ await client.query(`
95
+ CREATE INDEX IF NOT EXISTS embeddings_documents_metadata_idx
96
+ ON embeddings_documents
97
+ USING gin (metadata)
98
+ `);
99
+ console.log(`Vector table initialized (dimensions: ${this.dimensions})`);
100
+ } catch (error) {
101
+ console.log("Note: Index creation may require more data");
102
+ } finally {
103
+ client.release();
104
+ }
105
+ }
106
+ async initializeEmbeddings(openAIApiKey) {
107
+ console.log(`Initializing OpenAI Embeddings (model: ${this.embeddingModel})`);
108
+ if (this.embeddings) return this.embeddings;
109
+ try {
110
+ this.embeddings = new openai.OpenAIEmbeddings({
111
+ openAIApiKey,
112
+ modelName: this.embeddingModel,
113
+ dimensions: this.dimensions
114
+ });
115
+ return this.embeddings;
116
+ } catch (error) {
117
+ console.error(`Failed to initialize Embeddings: ${error}`);
118
+ throw new Error(`Failed to initialize Embeddings: ${error}`);
119
+ }
120
+ }
121
+ async initializeChat(openAIApiKey) {
122
+ console.log("Initializing Chat Model");
123
+ if (this.chat) return this.chat;
124
+ try {
125
+ this.chat = new openai.ChatOpenAI({
126
+ modelName: "gpt-4o-mini",
127
+ temperature: 0.7,
128
+ openAIApiKey
129
+ });
130
+ return this.chat;
131
+ } catch (error) {
132
+ console.error(`Failed to initialize Chat: ${error}`);
133
+ throw new Error(`Failed to initialize Chat: ${error}`);
134
+ }
135
+ }
136
+ async initialize(config2) {
137
+ const model = config2.embeddingModel || "text-embedding-3-small";
138
+ if (EMBEDDING_MODELS[model]) {
139
+ this.embeddingModel = model;
140
+ this.dimensions = EMBEDDING_MODELS[model].dimensions;
141
+ } else {
142
+ console.warn(`Invalid embedding model "${model}", using default`);
143
+ this.embeddingModel = "text-embedding-3-small";
144
+ this.dimensions = EMBEDDING_MODELS["text-embedding-3-small"].dimensions;
145
+ }
146
+ console.log(`Using embedding model: ${this.embeddingModel} (${this.dimensions} dimensions)`);
147
+ await this.initializePool(config2.neonConnectionString);
148
+ await this.initializeEmbeddings(config2.openAIApiKey);
149
+ await this.initializeChat(config2.openAIApiKey);
150
+ if (this.pool) {
151
+ this.vectorStoreConfig = {
152
+ pool: this.pool,
153
+ tableName: "embeddings_documents",
154
+ columns: {
155
+ idColumnName: "id",
156
+ vectorColumnName: "embedding",
157
+ contentColumnName: "content",
158
+ metadataColumnName: "metadata"
159
+ },
160
+ distanceStrategy: "cosine"
161
+ };
162
+ }
163
+ console.log("Plugin Manager Initialization Complete");
164
+ }
165
+ async createEmbedding(docData) {
166
+ if (!this.embeddings || !this.vectorStoreConfig) {
167
+ throw new Error("Plugin manager not initialized");
168
+ }
169
+ try {
170
+ const embeddingVector = await this.embeddings.embedQuery(docData.content);
171
+ const doc = new documents.Document({
172
+ pageContent: docData.content,
173
+ metadata: {
174
+ id: docData.id,
175
+ title: docData.title,
176
+ collectionType: docData.collectionType || "standalone",
177
+ fieldName: docData.fieldName || "content"
178
+ }
179
+ });
180
+ await pgvector.PGVectorStore.fromDocuments(
181
+ [doc],
182
+ this.embeddings,
183
+ this.vectorStoreConfig
184
+ );
185
+ const result = await this.pool.query(
186
+ `SELECT id FROM embeddings_documents
187
+ WHERE metadata->>'id' = $1
188
+ ORDER BY id DESC LIMIT 1`,
189
+ [docData.id]
190
+ );
191
+ return {
192
+ embeddingId: result.rows[0]?.id || "",
193
+ embedding: embeddingVector
194
+ };
195
+ } catch (error) {
196
+ console.error(`Failed to create embedding: ${error}`);
197
+ throw new Error(`Failed to create embedding: ${error}`);
198
+ }
199
+ }
200
+ async deleteEmbedding(strapiId) {
201
+ if (!this.pool) {
202
+ throw new Error("Plugin manager not initialized");
203
+ }
204
+ try {
205
+ await this.pool.query(
206
+ `DELETE FROM embeddings_documents WHERE metadata->>'id' = $1`,
207
+ [strapiId]
208
+ );
209
+ } catch (error) {
210
+ console.error(`Failed to delete embedding: ${error}`);
211
+ throw new Error(`Failed to delete embedding: ${error}`);
212
+ }
213
+ }
214
+ async queryEmbedding(query) {
215
+ if (!this.embeddings || !this.chat || !this.vectorStoreConfig) {
216
+ throw new Error("Plugin manager not initialized");
217
+ }
218
+ try {
219
+ const vectorStore = await pgvector.PGVectorStore.initialize(
220
+ this.embeddings,
221
+ this.vectorStoreConfig
222
+ );
223
+ const retriever = vectorStore.asRetriever({ k: 4 });
224
+ const sourceDocuments = await retriever.invoke(query);
225
+ const formatDocs = (docs) => {
226
+ return docs.map((doc) => {
227
+ const title = doc.metadata?.title ? `Title: ${doc.metadata.title}
228
+ ` : "";
229
+ return `${title}${doc.pageContent}`;
230
+ }).join("\n\n");
231
+ };
232
+ const ragPrompt = prompts.ChatPromptTemplate.fromMessages([
233
+ [
234
+ "system",
235
+ `You are a helpful assistant that answers questions based on the provided context.
236
+ If you cannot find the answer in the context, say so. Be concise and accurate.
237
+
238
+ Context:
239
+ {context}`
240
+ ],
241
+ ["human", "{question}"]
242
+ ]);
243
+ const ragChain = runnables.RunnableSequence.from([
244
+ {
245
+ context: async () => formatDocs(sourceDocuments),
246
+ question: new runnables.RunnablePassthrough()
247
+ },
248
+ ragPrompt,
249
+ this.chat,
250
+ new output_parsers.StringOutputParser()
251
+ ]);
252
+ const text = await ragChain.invoke(query);
253
+ return {
254
+ text,
255
+ sourceDocuments
256
+ };
257
+ } catch (error) {
258
+ console.error(`Failed to query embeddings: ${error}`);
259
+ throw new Error(`Failed to query embeddings: ${error}`);
260
+ }
261
+ }
262
+ async similaritySearch(query, k = 4) {
263
+ if (!this.embeddings || !this.vectorStoreConfig) {
264
+ throw new Error("Plugin manager not initialized");
265
+ }
266
+ try {
267
+ const vectorStore = await pgvector.PGVectorStore.initialize(
268
+ this.embeddings,
269
+ this.vectorStoreConfig
270
+ );
271
+ return await vectorStore.similaritySearch(query, k);
272
+ } catch (error) {
273
+ console.error(`Failed to perform similarity search: ${error}`);
274
+ throw new Error(`Failed to perform similarity search: ${error}`);
275
+ }
276
+ }
277
+ isInitialized() {
278
+ return !!(this.embeddings && this.chat && this.pool);
279
+ }
280
+ async destroy() {
281
+ if (this.pool) {
282
+ await this.pool.end();
283
+ this.pool = null;
284
+ }
285
+ this.embeddings = null;
286
+ this.chat = null;
287
+ this.vectorStoreConfig = null;
288
+ }
289
+ }
290
+ const pluginManager = new PluginManager();
291
+ const SemanticSearchSchema = zod.z.object({
292
+ query: zod.z.string().min(1, "Query is required"),
293
+ limit: zod.z.number().min(1).max(20).optional().default(5)
294
+ });
295
+ const RagQuerySchema = zod.z.object({
296
+ query: zod.z.string().min(1, "Query is required"),
297
+ includeSourceDocuments: zod.z.boolean().optional().default(true)
298
+ });
299
+ const ListEmbeddingsSchema = zod.z.object({
300
+ page: zod.z.number().min(1).optional().default(1),
301
+ pageSize: zod.z.number().min(1).max(50).optional().default(25),
302
+ search: zod.z.string().optional()
303
+ });
304
+ const GetEmbeddingSchema = zod.z.object({
305
+ documentId: zod.z.string().min(1, "Document ID is required"),
306
+ includeContent: zod.z.boolean().optional().default(true)
307
+ });
308
+ const CreateEmbeddingSchema = zod.z.object({
309
+ title: zod.z.string().min(1, "Title is required"),
310
+ content: zod.z.string().min(1, "Content is required"),
311
+ metadata: zod.z.record(zod.z.any()).optional()
312
+ });
313
+ const ToolSchemas = {
314
+ semantic_search: SemanticSearchSchema,
315
+ rag_query: RagQuerySchema,
316
+ list_embeddings: ListEmbeddingsSchema,
317
+ get_embedding: GetEmbeddingSchema,
318
+ create_embedding: CreateEmbeddingSchema
319
+ };
320
+ function validateToolInput(toolName, input) {
321
+ const schema2 = ToolSchemas[toolName];
322
+ if (!schema2) {
323
+ throw new Error(`No schema defined for tool: ${toolName}`);
324
+ }
325
+ const result = schema2.safeParse(input);
326
+ if (!result.success) {
327
+ const errors = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
328
+ throw new Error(`Validation failed for ${toolName}: ${errors}`);
329
+ }
330
+ return result.data;
331
+ }
332
+ const semanticSearchTool = {
333
+ name: "semantic_search",
334
+ description: 'TRIGGER: Use when user types "/rag" or asks to search embeddings/content. Search for semantically similar content using vector embeddings. Returns the most relevant documents matching your query based on meaning, not just keywords.',
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ query: {
339
+ type: "string",
340
+ description: "The search query text to find similar content"
341
+ },
342
+ limit: {
343
+ type: "number",
344
+ description: "Maximum number of results to return (default: 5, max: 20)",
345
+ default: 5
346
+ }
347
+ },
348
+ required: ["query"]
349
+ }
350
+ };
351
+ async function handleSemanticSearch(strapi, args) {
352
+ const { query, limit = 5 } = args;
353
+ const maxLimit = Math.min(limit, 20);
354
+ try {
355
+ const pluginManager2 = strapi.contentEmbeddingsManager;
356
+ if (!pluginManager2) {
357
+ throw new Error("Content embeddings plugin not initialized");
358
+ }
359
+ const results = await pluginManager2.similaritySearch(query, maxLimit);
360
+ const formattedResults = results.map((doc, index2) => ({
361
+ rank: index2 + 1,
362
+ content: doc.pageContent,
363
+ metadata: doc.metadata,
364
+ score: doc.score || null
365
+ }));
366
+ return {
367
+ content: [
368
+ {
369
+ type: "text",
370
+ text: JSON.stringify(
371
+ {
372
+ query,
373
+ resultCount: formattedResults.length,
374
+ results: formattedResults
375
+ },
376
+ null,
377
+ 2
378
+ )
379
+ }
380
+ ]
381
+ };
382
+ } catch (error) {
383
+ throw new Error(
384
+ `Semantic search failed: ${error instanceof Error ? error.message : String(error)}`
385
+ );
386
+ }
387
+ }
388
+ const ragQueryTool = {
389
+ name: "rag_query",
390
+ description: 'TRIGGER: Use when user types "/rag" followed by a question. Ask a question and get an AI-generated answer based on your embedded content. Uses RAG (Retrieval-Augmented Generation) to find relevant documents and generate a contextual response. This is the PRIMARY tool for /rag queries.',
391
+ inputSchema: {
392
+ type: "object",
393
+ properties: {
394
+ query: {
395
+ type: "string",
396
+ description: "The question or query to answer using embedded content"
397
+ },
398
+ includeSourceDocuments: {
399
+ type: "boolean",
400
+ description: "Include the source documents used to generate the answer (default: true)",
401
+ default: true
402
+ }
403
+ },
404
+ required: ["query"]
405
+ }
406
+ };
407
+ async function handleRagQuery(strapi, args) {
408
+ const { query, includeSourceDocuments = true } = args;
409
+ try {
410
+ const embeddingsService = strapi.plugin("strapi-content-embeddings").service("embeddings");
411
+ const result = await embeddingsService.queryEmbeddings(query);
412
+ const response = {
413
+ query,
414
+ answer: result.text
415
+ };
416
+ if (includeSourceDocuments && result.sourceDocuments) {
417
+ response.sourceDocuments = result.sourceDocuments.map((doc, index2) => ({
418
+ rank: index2 + 1,
419
+ content: doc.pageContent?.substring(0, 500) + (doc.pageContent?.length > 500 ? "..." : ""),
420
+ metadata: doc.metadata
421
+ }));
422
+ response.sourceCount = result.sourceDocuments.length;
423
+ }
424
+ return {
425
+ content: [
426
+ {
427
+ type: "text",
428
+ text: JSON.stringify(response, null, 2)
429
+ }
430
+ ]
431
+ };
432
+ } catch (error) {
433
+ throw new Error(
434
+ `RAG query failed: ${error instanceof Error ? error.message : String(error)}`
435
+ );
436
+ }
437
+ }
438
+ const listEmbeddingsTool = {
439
+ name: "list_embeddings",
440
+ description: "List all embeddings stored in the database. Returns metadata without the full content to avoid context overflow.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ page: {
445
+ type: "number",
446
+ description: "Page number (starts at 1)",
447
+ default: 1
448
+ },
449
+ pageSize: {
450
+ type: "number",
451
+ description: "Number of items per page (max: 50)",
452
+ default: 25
453
+ },
454
+ search: {
455
+ type: "string",
456
+ description: "Search filter for title"
457
+ }
458
+ },
459
+ required: []
460
+ }
461
+ };
462
+ async function handleListEmbeddings(strapi, args) {
463
+ const { page = 1, pageSize = 25, search } = args;
464
+ const limit = Math.min(pageSize, 50);
465
+ try {
466
+ const embeddingsService = strapi.plugin("strapi-content-embeddings").service("embeddings");
467
+ const filters = {};
468
+ if (search) {
469
+ filters.title = { $containsi: search };
470
+ }
471
+ const result = await embeddingsService.getEmbeddings({
472
+ page,
473
+ pageSize: limit,
474
+ filters
475
+ });
476
+ const embeddings2 = (result.results || []).map((emb) => ({
477
+ id: emb.id,
478
+ documentId: emb.documentId,
479
+ title: emb.title,
480
+ collectionType: emb.collectionType,
481
+ fieldName: emb.fieldName,
482
+ metadata: emb.metadata,
483
+ contentPreview: emb.content?.substring(0, 200) + (emb.content?.length > 200 ? "..." : ""),
484
+ createdAt: emb.createdAt,
485
+ updatedAt: emb.updatedAt
486
+ }));
487
+ return {
488
+ content: [
489
+ {
490
+ type: "text",
491
+ text: JSON.stringify(
492
+ {
493
+ embeddings: embeddings2,
494
+ pagination: result.pagination || {
495
+ page,
496
+ pageSize: limit,
497
+ total: embeddings2.length
498
+ }
499
+ },
500
+ null,
501
+ 2
502
+ )
503
+ }
504
+ ]
505
+ };
506
+ } catch (error) {
507
+ throw new Error(
508
+ `Failed to list embeddings: ${error instanceof Error ? error.message : String(error)}`
509
+ );
510
+ }
511
+ }
512
+ const getEmbeddingTool = {
513
+ name: "get_embedding",
514
+ description: "Get a specific embedding by its document ID. Returns the full content and metadata.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ documentId: {
519
+ type: "string",
520
+ description: "The document ID of the embedding to retrieve"
521
+ },
522
+ includeContent: {
523
+ type: "boolean",
524
+ description: "Include the full content text (default: true)",
525
+ default: true
526
+ }
527
+ },
528
+ required: ["documentId"]
529
+ }
530
+ };
531
+ async function handleGetEmbedding(strapi, args) {
532
+ const { documentId, includeContent = true } = args;
533
+ try {
534
+ const embeddingsService = strapi.plugin("strapi-content-embeddings").service("embeddings");
535
+ const embedding2 = await embeddingsService.getEmbedding(documentId);
536
+ if (!embedding2) {
537
+ return {
538
+ content: [
539
+ {
540
+ type: "text",
541
+ text: JSON.stringify({
542
+ error: true,
543
+ message: `Embedding not found with documentId: ${documentId}`
544
+ })
545
+ }
546
+ ]
547
+ };
548
+ }
549
+ const result = {
550
+ id: embedding2.id,
551
+ documentId: embedding2.documentId,
552
+ title: embedding2.title,
553
+ collectionType: embedding2.collectionType,
554
+ fieldName: embedding2.fieldName,
555
+ metadata: embedding2.metadata,
556
+ embeddingId: embedding2.embeddingId,
557
+ createdAt: embedding2.createdAt,
558
+ updatedAt: embedding2.updatedAt
559
+ };
560
+ if (includeContent) {
561
+ result.content = embedding2.content;
562
+ }
563
+ return {
564
+ content: [
565
+ {
566
+ type: "text",
567
+ text: JSON.stringify(result, null, 2)
568
+ }
569
+ ]
570
+ };
571
+ } catch (error) {
572
+ throw new Error(
573
+ `Failed to get embedding: ${error instanceof Error ? error.message : String(error)}`
574
+ );
575
+ }
576
+ }
577
+ const createEmbeddingTool = {
578
+ name: "create_embedding",
579
+ description: "Create a new embedding from text content. The content will be vectorized and stored for semantic search.",
580
+ inputSchema: {
581
+ type: "object",
582
+ properties: {
583
+ title: {
584
+ type: "string",
585
+ description: "A descriptive title for the embedding"
586
+ },
587
+ content: {
588
+ type: "string",
589
+ description: "The text content to embed (will be vectorized)"
590
+ },
591
+ metadata: {
592
+ type: "object",
593
+ description: "Optional metadata to associate with the embedding (tags, source, etc.)"
594
+ }
595
+ },
596
+ required: ["title", "content"]
597
+ }
598
+ };
599
+ async function handleCreateEmbedding(strapi, args) {
600
+ const { title, content, metadata } = args;
601
+ try {
602
+ const embeddingsService = strapi.plugin("strapi-content-embeddings").service("embeddings");
603
+ const embedding2 = await embeddingsService.createEmbedding({
604
+ title,
605
+ content,
606
+ metadata: metadata || {},
607
+ collectionType: "standalone",
608
+ fieldName: "content"
609
+ });
610
+ return {
611
+ content: [
612
+ {
613
+ type: "text",
614
+ text: JSON.stringify(
615
+ {
616
+ success: true,
617
+ message: "Embedding created successfully",
618
+ embedding: {
619
+ id: embedding2.id,
620
+ documentId: embedding2.documentId,
621
+ title: embedding2.title,
622
+ embeddingId: embedding2.embeddingId,
623
+ contentLength: content.length,
624
+ metadata: embedding2.metadata,
625
+ createdAt: embedding2.createdAt
626
+ }
627
+ },
628
+ null,
629
+ 2
630
+ )
631
+ }
632
+ ]
633
+ };
634
+ } catch (error) {
635
+ throw new Error(
636
+ `Failed to create embedding: ${error instanceof Error ? error.message : String(error)}`
637
+ );
638
+ }
639
+ }
640
+ const tools = [
641
+ semanticSearchTool,
642
+ ragQueryTool,
643
+ listEmbeddingsTool,
644
+ getEmbeddingTool,
645
+ createEmbeddingTool
646
+ ];
647
+ const toolHandlers = {
648
+ semantic_search: handleSemanticSearch,
649
+ rag_query: handleRagQuery,
650
+ list_embeddings: handleListEmbeddings,
651
+ get_embedding: handleGetEmbedding,
652
+ create_embedding: handleCreateEmbedding
653
+ };
654
+ async function handleToolCall(strapi, request) {
655
+ const { name, arguments: args } = request.params;
656
+ const handler = toolHandlers[name];
657
+ if (!handler) {
658
+ return {
659
+ content: [
660
+ {
661
+ type: "text",
662
+ text: JSON.stringify({
663
+ error: true,
664
+ message: `Unknown tool: ${name}`,
665
+ availableTools: Object.keys(toolHandlers)
666
+ })
667
+ }
668
+ ]
669
+ };
670
+ }
671
+ try {
672
+ const validatedArgs = validateToolInput(name, args || {});
673
+ const result = await handler(strapi, validatedArgs);
674
+ return result;
675
+ } catch (error) {
676
+ const errorMessage = error instanceof Error ? error.message : String(error);
677
+ strapi.log.error(`[strapi-content-embeddings] Tool ${name} error:`, { error: errorMessage });
678
+ return {
679
+ content: [
680
+ {
681
+ type: "text",
682
+ text: JSON.stringify({
683
+ error: true,
684
+ tool: name,
685
+ message: errorMessage
686
+ }, null, 2)
687
+ }
688
+ ]
689
+ };
690
+ }
691
+ }
692
+ function createMcpServer(strapi) {
693
+ const server = new index_js.Server(
694
+ {
695
+ name: "strapi-content-embeddings-mcp",
696
+ version: "1.0.0"
697
+ },
698
+ {
699
+ capabilities: {
700
+ tools: {}
701
+ }
702
+ }
703
+ );
704
+ server.setRequestHandler(types_js.ListToolsRequestSchema, async () => {
705
+ return { tools };
706
+ });
707
+ server.setRequestHandler(types_js.CallToolRequestSchema, async (request) => {
708
+ return handleToolCall(strapi, request);
709
+ });
710
+ return server;
711
+ }
712
+ const PLUGIN_ID$4 = "strapi-content-embeddings";
713
+ const OAUTH_PLUGIN_ID = "strapi-oauth-mcp-manager";
714
+ function createFallbackAuthMiddleware(strapi) {
715
+ const mcpPath = `/api/${PLUGIN_ID$4}/mcp`;
716
+ return async (ctx, next) => {
717
+ if (!ctx.path.startsWith(mcpPath)) {
718
+ return next();
719
+ }
720
+ const authHeader = ctx.request.headers.authorization;
721
+ if (!authHeader?.startsWith("Bearer ")) {
722
+ ctx.status = 401;
723
+ ctx.body = {
724
+ error: "Unauthorized",
725
+ message: "Bearer token required. Provide a Strapi API token."
726
+ };
727
+ return;
728
+ }
729
+ const token = authHeader.slice(7);
730
+ ctx.state.strapiToken = token;
731
+ ctx.state.authMethod = "api-token";
732
+ return next();
733
+ };
734
+ }
735
+ const bootstrap = async ({ strapi }) => {
736
+ const actions = [
737
+ {
738
+ section: "plugins",
739
+ displayName: "Read",
740
+ uid: "read",
741
+ pluginName: PLUGIN_ID$4
742
+ },
743
+ {
744
+ section: "plugins",
745
+ displayName: "Update",
746
+ uid: "update",
747
+ pluginName: PLUGIN_ID$4
748
+ },
749
+ {
750
+ section: "plugins",
751
+ displayName: "Create",
752
+ uid: "create",
753
+ pluginName: PLUGIN_ID$4
754
+ },
755
+ {
756
+ section: "plugins",
757
+ displayName: "Delete",
758
+ uid: "delete",
759
+ pluginName: PLUGIN_ID$4
760
+ },
761
+ {
762
+ section: "plugins",
763
+ displayName: "Chat",
764
+ uid: "chat",
765
+ pluginName: PLUGIN_ID$4
766
+ }
767
+ ];
768
+ await strapi.admin.services.permission.actionProvider.registerMany(actions);
769
+ const pluginConfig = strapi.config.get(`plugin::${PLUGIN_ID$4}`);
770
+ if (pluginConfig?.openAIApiKey && pluginConfig?.neonConnectionString) {
771
+ try {
772
+ await pluginManager.initialize({
773
+ openAIApiKey: pluginConfig.openAIApiKey,
774
+ neonConnectionString: pluginConfig.neonConnectionString,
775
+ embeddingModel: pluginConfig.embeddingModel
776
+ });
777
+ strapi.contentEmbeddingsManager = pluginManager;
778
+ strapi.log.info(`[${PLUGIN_ID$4}] Plugin initialized successfully`);
779
+ } catch (error) {
780
+ strapi.log.error(`[${PLUGIN_ID$4}] Failed to initialize:`, error);
781
+ }
782
+ } else {
783
+ strapi.log.warn(
784
+ `[${PLUGIN_ID$4}] Missing configuration. Set openAIApiKey and neonConnectionString in plugin config.`
785
+ );
786
+ }
787
+ const plugin = strapi.plugin(PLUGIN_ID$4);
788
+ plugin.createMcpServer = () => createMcpServer(strapi);
789
+ plugin.sessions = /* @__PURE__ */ new Map();
790
+ const oauthPlugin = strapi.plugin(OAUTH_PLUGIN_ID);
791
+ if (oauthPlugin) {
792
+ strapi.log.info(`[${PLUGIN_ID$4}] OAuth manager detected - OAuth + API token auth enabled`);
793
+ } else {
794
+ const fallbackMiddleware = createFallbackAuthMiddleware();
795
+ strapi.server.use(fallbackMiddleware);
796
+ strapi.log.info(`[${PLUGIN_ID$4}] Using API token authentication (OAuth manager not installed)`);
797
+ }
798
+ strapi.log.info(`[${PLUGIN_ID$4}] MCP endpoint available at: /api/${PLUGIN_ID$4}/mcp`);
799
+ };
800
+ const destroy = async ({ strapi }) => {
801
+ await pluginManager.destroy();
802
+ console.log("Content Embeddings plugin destroyed");
803
+ };
804
+ const PLUGIN_ID$3 = "strapi-content-embeddings";
805
+ const register = ({ strapi }) => {
806
+ Object.values(strapi.contentTypes).forEach((contentType) => {
807
+ if (contentType.uid.startsWith("admin::") || contentType.uid.startsWith("strapi::") || contentType.uid === `plugin::${PLUGIN_ID$3}.embedding`) {
808
+ return;
809
+ }
810
+ contentType.attributes.embedding = {
811
+ type: "relation",
812
+ relation: "morphOne",
813
+ target: `plugin::${PLUGIN_ID$3}.embedding`,
814
+ morphBy: "related",
815
+ private: false,
816
+ configurable: false
817
+ };
818
+ });
819
+ };
820
+ const kind = "collectionType";
821
+ const collectionName = "strapi_content_embeddings";
822
+ const info = {
823
+ singularName: "embedding",
824
+ pluralName: "embeddings",
825
+ displayName: "Embedding"
826
+ };
827
+ const options = {
828
+ draftAndPublish: false
829
+ };
830
+ const pluginOptions = {
831
+ "content-manager": {
832
+ visible: true
833
+ },
834
+ "content-type-builder": {
835
+ visible: false
836
+ }
837
+ };
838
+ const attributes = {
839
+ title: {
840
+ type: "string",
841
+ required: true
842
+ },
843
+ content: {
844
+ type: "text"
845
+ },
846
+ embedding: {
847
+ type: "json"
848
+ },
849
+ embeddingId: {
850
+ type: "string"
851
+ },
852
+ collectionType: {
853
+ type: "string",
854
+ "default": "standalone"
855
+ },
856
+ fieldName: {
857
+ type: "string",
858
+ "default": "content"
859
+ },
860
+ metadata: {
861
+ type: "json"
862
+ },
863
+ related: {
864
+ type: "relation",
865
+ relation: "morphToOne",
866
+ configurable: false
867
+ }
868
+ };
869
+ const schema = {
870
+ kind,
871
+ collectionName,
872
+ info,
873
+ options,
874
+ pluginOptions,
875
+ attributes
876
+ };
877
+ const embedding = {
878
+ schema
879
+ };
880
+ const contentTypes = {
881
+ embedding
882
+ };
883
+ const PLUGIN_ID$2 = "strapi-content-embeddings";
884
+ const controller = ({ strapi }) => ({
885
+ async createEmbedding(ctx) {
886
+ try {
887
+ const result = await strapi.plugin(PLUGIN_ID$2).service("embeddings").createEmbedding(ctx.request.body);
888
+ ctx.body = result;
889
+ } catch (error) {
890
+ ctx.throw(500, error.message || "Failed to create embedding");
891
+ }
892
+ },
893
+ async deleteEmbedding(ctx) {
894
+ try {
895
+ const { id } = ctx.params;
896
+ const result = await strapi.plugin(PLUGIN_ID$2).service("embeddings").deleteEmbedding(id);
897
+ ctx.body = result;
898
+ } catch (error) {
899
+ ctx.throw(500, error.message || "Failed to delete embedding");
900
+ }
901
+ },
902
+ async updateEmbedding(ctx) {
903
+ try {
904
+ const { id } = ctx.params;
905
+ const result = await strapi.plugin(PLUGIN_ID$2).service("embeddings").updateEmbedding(id, ctx.request.body);
906
+ ctx.body = result;
907
+ } catch (error) {
908
+ ctx.throw(500, error.message || "Failed to update embedding");
909
+ }
910
+ },
911
+ async getEmbeddings(ctx) {
912
+ try {
913
+ const { page, pageSize, filters } = ctx.query;
914
+ const result = await strapi.plugin(PLUGIN_ID$2).service("embeddings").getEmbeddings({
915
+ page: page ? parseInt(page, 10) : 1,
916
+ pageSize: pageSize ? parseInt(pageSize, 10) : 10,
917
+ filters
918
+ });
919
+ ctx.body = result;
920
+ } catch (error) {
921
+ ctx.throw(500, error.message || "Failed to get embeddings");
922
+ }
923
+ },
924
+ async getEmbedding(ctx) {
925
+ try {
926
+ const { id } = ctx.params;
927
+ const result = await strapi.plugin(PLUGIN_ID$2).service("embeddings").getEmbedding(id);
928
+ if (!result) {
929
+ ctx.throw(404, "Embedding not found");
930
+ }
931
+ ctx.body = result;
932
+ } catch (error) {
933
+ if (error.status === 404) {
934
+ ctx.throw(404, error.message);
935
+ }
936
+ ctx.throw(500, error.message || "Failed to get embedding");
937
+ }
938
+ },
939
+ async queryEmbeddings(ctx) {
940
+ try {
941
+ const { query } = ctx.query;
942
+ const result = await strapi.plugin(PLUGIN_ID$2).service("embeddings").queryEmbeddings(query);
943
+ ctx.body = result;
944
+ } catch (error) {
945
+ ctx.throw(500, error.message || "Failed to query embeddings");
946
+ }
947
+ }
948
+ });
949
+ const PLUGIN_ID$1 = "strapi-content-embeddings";
950
+ const SESSION_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
951
+ function isSessionExpired(session) {
952
+ return Date.now() - session.createdAt > SESSION_TIMEOUT_MS;
953
+ }
954
+ function cleanupExpiredSessions(plugin, strapi) {
955
+ let cleaned = 0;
956
+ for (const [sessionId, session] of plugin.sessions.entries()) {
957
+ if (isSessionExpired(session)) {
958
+ try {
959
+ session.server.close();
960
+ } catch {
961
+ }
962
+ plugin.sessions.delete(sessionId);
963
+ cleaned++;
964
+ }
965
+ }
966
+ if (cleaned > 0) {
967
+ strapi.log.debug(`[${PLUGIN_ID$1}] Cleaned up ${cleaned} expired MCP sessions`);
968
+ }
969
+ }
970
+ const mcpController = ({ strapi }) => ({
971
+ /**
972
+ * Handle MCP requests (POST, GET, DELETE)
973
+ */
974
+ async handle(ctx) {
975
+ const plugin = strapi.plugin(PLUGIN_ID$1);
976
+ if (!plugin.createMcpServer) {
977
+ ctx.status = 503;
978
+ ctx.body = {
979
+ error: "MCP not initialized",
980
+ message: "The MCP server is not available. Check plugin configuration."
981
+ };
982
+ return;
983
+ }
984
+ if (Math.random() < 0.01) {
985
+ cleanupExpiredSessions(plugin, strapi);
986
+ }
987
+ try {
988
+ const requestedSessionId = ctx.request.headers["mcp-session-id"];
989
+ let session = requestedSessionId ? plugin.sessions.get(requestedSessionId) : null;
990
+ if (session && isSessionExpired(session)) {
991
+ strapi.log.debug(`[${PLUGIN_ID$1}] Session expired, removing: ${requestedSessionId}`);
992
+ try {
993
+ session.server.close();
994
+ } catch {
995
+ }
996
+ plugin.sessions.delete(requestedSessionId);
997
+ session = null;
998
+ }
999
+ if (requestedSessionId && !session) {
1000
+ ctx.status = 400;
1001
+ ctx.body = {
1002
+ jsonrpc: "2.0",
1003
+ error: {
1004
+ code: -32e3,
1005
+ message: "Session expired or invalid. Please reinitialize the connection."
1006
+ },
1007
+ id: null
1008
+ };
1009
+ return;
1010
+ }
1011
+ if (!session) {
1012
+ const sessionId = node_crypto.randomUUID();
1013
+ const server = plugin.createMcpServer();
1014
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
1015
+ sessionIdGenerator: () => sessionId
1016
+ });
1017
+ await server.connect(transport);
1018
+ session = {
1019
+ server,
1020
+ transport,
1021
+ createdAt: Date.now(),
1022
+ strapiToken: ctx.state.strapiToken
1023
+ };
1024
+ plugin.sessions.set(sessionId, session);
1025
+ strapi.log.debug(
1026
+ `[${PLUGIN_ID$1}] New MCP session created: ${sessionId} (auth: ${ctx.state.authMethod || "unknown"})`
1027
+ );
1028
+ }
1029
+ try {
1030
+ await session.transport.handleRequest(ctx.req, ctx.res, ctx.request.body);
1031
+ } catch (transportError) {
1032
+ strapi.log.warn(`[${PLUGIN_ID$1}] Transport error, cleaning up session: ${requestedSessionId}`, {
1033
+ error: transportError instanceof Error ? transportError.message : String(transportError)
1034
+ });
1035
+ try {
1036
+ session.server.close();
1037
+ } catch {
1038
+ }
1039
+ plugin.sessions.delete(requestedSessionId);
1040
+ if (!ctx.res.headersSent) {
1041
+ ctx.status = 400;
1042
+ ctx.body = {
1043
+ jsonrpc: "2.0",
1044
+ error: {
1045
+ code: -32e3,
1046
+ message: "Session transport error. Please reinitialize the connection."
1047
+ },
1048
+ id: null
1049
+ };
1050
+ }
1051
+ return;
1052
+ }
1053
+ ctx.respond = false;
1054
+ } catch (error) {
1055
+ strapi.log.error(`[${PLUGIN_ID$1}] Error handling MCP request`, {
1056
+ error: error instanceof Error ? error.message : String(error),
1057
+ method: ctx.method,
1058
+ path: ctx.path
1059
+ });
1060
+ if (!ctx.res.headersSent) {
1061
+ ctx.status = 500;
1062
+ ctx.body = {
1063
+ error: "MCP request failed",
1064
+ message: error instanceof Error ? error.message : "Unknown error"
1065
+ };
1066
+ }
1067
+ }
1068
+ }
1069
+ });
1070
+ const controllers = {
1071
+ controller,
1072
+ mcp: mcpController
1073
+ };
1074
+ const middlewares = {};
1075
+ const policies = {};
1076
+ const contentApi = [
1077
+ {
1078
+ method: "GET",
1079
+ path: "/embeddings-query",
1080
+ handler: "controller.queryEmbeddings"
1081
+ },
1082
+ // MCP routes - auth handled by middleware
1083
+ {
1084
+ method: "POST",
1085
+ path: "/mcp",
1086
+ handler: "mcp.handle",
1087
+ config: {
1088
+ auth: false,
1089
+ policies: []
1090
+ }
1091
+ },
1092
+ {
1093
+ method: "GET",
1094
+ path: "/mcp",
1095
+ handler: "mcp.handle",
1096
+ config: {
1097
+ auth: false,
1098
+ policies: []
1099
+ }
1100
+ },
1101
+ {
1102
+ method: "DELETE",
1103
+ path: "/mcp",
1104
+ handler: "mcp.handle",
1105
+ config: {
1106
+ auth: false,
1107
+ policies: []
1108
+ }
1109
+ }
1110
+ ];
1111
+ const admin = [
1112
+ {
1113
+ method: "POST",
1114
+ path: "/embeddings/create-embedding",
1115
+ handler: "controller.createEmbedding",
1116
+ config: {
1117
+ policies: [
1118
+ {
1119
+ name: "admin::hasPermissions",
1120
+ config: { actions: ["plugin::strapi-content-embeddings.create"] }
1121
+ }
1122
+ ]
1123
+ }
1124
+ },
1125
+ {
1126
+ method: "DELETE",
1127
+ path: "/embeddings/delete-embedding/:id",
1128
+ handler: "controller.deleteEmbedding",
1129
+ config: {
1130
+ policies: [
1131
+ {
1132
+ name: "admin::hasPermissions",
1133
+ config: { actions: ["plugin::strapi-content-embeddings.delete"] }
1134
+ }
1135
+ ]
1136
+ }
1137
+ },
1138
+ {
1139
+ method: "PUT",
1140
+ path: "/embeddings/update-embedding/:id",
1141
+ handler: "controller.updateEmbedding",
1142
+ config: {
1143
+ policies: [
1144
+ {
1145
+ name: "admin::hasPermissions",
1146
+ config: { actions: ["plugin::strapi-content-embeddings.update"] }
1147
+ }
1148
+ ]
1149
+ }
1150
+ },
1151
+ {
1152
+ method: "GET",
1153
+ path: "/embeddings/embeddings-query",
1154
+ handler: "controller.queryEmbeddings",
1155
+ config: {
1156
+ policies: [
1157
+ {
1158
+ name: "admin::hasPermissions",
1159
+ config: { actions: ["plugin::strapi-content-embeddings.chat"] }
1160
+ }
1161
+ ]
1162
+ }
1163
+ },
1164
+ {
1165
+ method: "GET",
1166
+ path: "/embeddings/find/:id",
1167
+ handler: "controller.getEmbedding",
1168
+ config: {
1169
+ policies: [
1170
+ {
1171
+ name: "admin::hasPermissions",
1172
+ config: { actions: ["plugin::strapi-content-embeddings.read"] }
1173
+ }
1174
+ ]
1175
+ }
1176
+ },
1177
+ {
1178
+ method: "GET",
1179
+ path: "/embeddings/find",
1180
+ handler: "controller.getEmbeddings",
1181
+ config: {
1182
+ policies: [
1183
+ {
1184
+ name: "admin::hasPermissions",
1185
+ config: { actions: ["plugin::strapi-content-embeddings.read"] }
1186
+ }
1187
+ ]
1188
+ }
1189
+ }
1190
+ ];
1191
+ const routes = {
1192
+ "content-api": {
1193
+ type: "content-api",
1194
+ routes: [...contentApi]
1195
+ },
1196
+ admin: {
1197
+ type: "admin",
1198
+ routes: [...admin]
1199
+ }
1200
+ };
1201
+ const PLUGIN_ID = "strapi-content-embeddings";
1202
+ const CONTENT_TYPE_UID = `plugin::${PLUGIN_ID}.embedding`;
1203
+ const embeddings = ({ strapi }) => ({
1204
+ async createEmbedding(data) {
1205
+ const { title, content, collectionType, fieldName, metadata, related } = data.data;
1206
+ const entityData = {
1207
+ title,
1208
+ content,
1209
+ collectionType: collectionType || "standalone",
1210
+ fieldName: fieldName || "content",
1211
+ metadata: metadata || null
1212
+ };
1213
+ if (related && related.__type && related.id) {
1214
+ entityData.related = related;
1215
+ }
1216
+ const entity = await strapi.documents(CONTENT_TYPE_UID).create({
1217
+ data: entityData
1218
+ });
1219
+ if (!pluginManager.isInitialized()) {
1220
+ console.warn("Plugin manager not initialized, skipping vector embedding");
1221
+ return entity;
1222
+ }
1223
+ try {
1224
+ const result = await pluginManager.createEmbedding({
1225
+ id: entity.documentId,
1226
+ title,
1227
+ content,
1228
+ collectionType: collectionType || "standalone",
1229
+ fieldName: fieldName || "content"
1230
+ });
1231
+ const updatedEntity = await strapi.documents(CONTENT_TYPE_UID).update({
1232
+ documentId: entity.documentId,
1233
+ data: {
1234
+ embeddingId: result.embeddingId,
1235
+ embedding: result.embedding
1236
+ }
1237
+ });
1238
+ return updatedEntity;
1239
+ } catch (error) {
1240
+ console.error("Failed to create embedding in vector store:", error);
1241
+ return entity;
1242
+ }
1243
+ },
1244
+ async deleteEmbedding(id) {
1245
+ const currentEntry = await strapi.documents(CONTENT_TYPE_UID).findOne({
1246
+ documentId: String(id)
1247
+ });
1248
+ if (!currentEntry) {
1249
+ throw new Error(`Embedding with id ${id} not found`);
1250
+ }
1251
+ if (pluginManager.isInitialized()) {
1252
+ try {
1253
+ await pluginManager.deleteEmbedding(String(id));
1254
+ } catch (error) {
1255
+ console.error("Failed to delete from vector store:", error);
1256
+ }
1257
+ }
1258
+ const deletedEntry = await strapi.documents(CONTENT_TYPE_UID).delete({
1259
+ documentId: String(id)
1260
+ });
1261
+ return deletedEntry;
1262
+ },
1263
+ async updateEmbedding(id, data) {
1264
+ const { title, content, metadata } = data.data;
1265
+ const currentEntry = await strapi.documents(CONTENT_TYPE_UID).findOne({
1266
+ documentId: id
1267
+ });
1268
+ if (!currentEntry) {
1269
+ throw new Error(`Embedding with id ${id} not found`);
1270
+ }
1271
+ const updateData = {};
1272
+ if (title !== void 0) updateData.title = title;
1273
+ if (content !== void 0) updateData.content = content;
1274
+ if (metadata !== void 0) updateData.metadata = metadata;
1275
+ const contentChanged = content !== void 0 && content !== currentEntry.content;
1276
+ let updatedEntity = await strapi.documents(CONTENT_TYPE_UID).update({
1277
+ documentId: id,
1278
+ data: updateData
1279
+ });
1280
+ if (contentChanged && pluginManager.isInitialized()) {
1281
+ try {
1282
+ await pluginManager.deleteEmbedding(id);
1283
+ const result = await pluginManager.createEmbedding({
1284
+ id,
1285
+ title: title || currentEntry.title,
1286
+ content,
1287
+ collectionType: currentEntry.collectionType || "standalone",
1288
+ fieldName: currentEntry.fieldName || "content"
1289
+ });
1290
+ updatedEntity = await strapi.documents(CONTENT_TYPE_UID).update({
1291
+ documentId: id,
1292
+ data: {
1293
+ embeddingId: result.embeddingId,
1294
+ embedding: result.embedding
1295
+ }
1296
+ });
1297
+ } catch (error) {
1298
+ console.error("Failed to update embedding in vector store:", error);
1299
+ }
1300
+ }
1301
+ return updatedEntity;
1302
+ },
1303
+ async queryEmbeddings(query) {
1304
+ if (!query || query.trim() === "") {
1305
+ return { error: "Please provide a query" };
1306
+ }
1307
+ if (!pluginManager.isInitialized()) {
1308
+ return { error: "Plugin not initialized. Check your configuration." };
1309
+ }
1310
+ try {
1311
+ const response = await pluginManager.queryEmbedding(query);
1312
+ return response;
1313
+ } catch (error) {
1314
+ console.error("Query failed:", error);
1315
+ return { error: "Failed to query embeddings" };
1316
+ }
1317
+ },
1318
+ async getEmbedding(id) {
1319
+ return await strapi.documents(CONTENT_TYPE_UID).findOne({
1320
+ documentId: String(id)
1321
+ });
1322
+ },
1323
+ async getEmbeddings(params) {
1324
+ const page = params?.page || 1;
1325
+ const pageSize = params?.pageSize || 10;
1326
+ const start = (page - 1) * pageSize;
1327
+ const [data, totalCount] = await Promise.all([
1328
+ strapi.documents(CONTENT_TYPE_UID).findMany({
1329
+ limit: pageSize,
1330
+ start,
1331
+ filters: params?.filters
1332
+ }),
1333
+ strapi.documents(CONTENT_TYPE_UID).count({
1334
+ filters: params?.filters
1335
+ })
1336
+ ]);
1337
+ return {
1338
+ data,
1339
+ count: data.length,
1340
+ totalCount
1341
+ };
1342
+ }
1343
+ });
1344
+ const services = {
1345
+ embeddings
1346
+ };
1347
+ const index = {
1348
+ register,
1349
+ bootstrap,
1350
+ destroy,
1351
+ config,
1352
+ controllers,
1353
+ routes,
1354
+ services,
1355
+ contentTypes,
1356
+ policies,
1357
+ middlewares
1358
+ };
1359
+ module.exports = index;