spine-framework-cortex 0.2.19 → 0.2.21
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/functions/custom_case_analysis.ts +507 -0
- package/functions/custom_cortex-chunks.ts +52 -0
- package/functions/custom_cortex-handler.ts +35 -0
- package/functions/custom_kb-chunker-test.ts +364 -0
- package/functions/custom_kb-chunker.ts +576 -0
- package/functions/custom_kb-embeddings.ts +472 -0
- package/functions/custom_kb-ingestion.ts +447 -0
- package/functions/custom_tag_management.ts +314 -0
- package/manifest.json +1 -0
- package/package.json +1 -1
- package/pages/courses/CoursesPage.tsx +1 -1
- package/pages/kb/KBEditorPage.tsx +1 -1
- package/pages/support/RedactionReview.tsx +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { createHandler } from './_shared/middleware'
|
|
2
|
+
import { adminDb } from './_shared/db'
|
|
3
|
+
import { chunkArticle, htmlToPlainText } from './custom_kb-chunker'
|
|
4
|
+
|
|
5
|
+
interface KBEmbeddingRequest {
|
|
6
|
+
item_id: string
|
|
7
|
+
vector_types: ('semantic' | 'structure' | 'code')[]
|
|
8
|
+
force_regenerate?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface KBEmbeddingResponse {
|
|
12
|
+
success: boolean
|
|
13
|
+
embeddings_created: number
|
|
14
|
+
embeddings_updated: number
|
|
15
|
+
errors: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Generate semantic embedding (full content)
|
|
19
|
+
function decodeHtmlContent(html: string): string {
|
|
20
|
+
return html
|
|
21
|
+
.replace(/<[^>]+>/g, ' ')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/&/g, '&')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, "'")
|
|
27
|
+
.replace(/\s+/g, ' ')
|
|
28
|
+
.trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Build semantic content from structured code_metadata — developer understanding focus
|
|
32
|
+
function buildSemanticContent(item: any): string {
|
|
33
|
+
const cm = item.data?.code_metadata || {}
|
|
34
|
+
const inputs = cm.inputs && Object.keys(cm.inputs).length > 0
|
|
35
|
+
? 'Parameters: ' + Object.entries(cm.inputs).map(([k, v]) => `${k} (${v})`).join(', ')
|
|
36
|
+
: ''
|
|
37
|
+
const deps = cm.depends_on && cm.depends_on.length > 0
|
|
38
|
+
? 'Depends on: ' + cm.depends_on.join(', ')
|
|
39
|
+
: ''
|
|
40
|
+
const sideEffects = cm.side_effects && cm.side_effects.length > 0
|
|
41
|
+
? 'Side effects: ' + cm.side_effects.join(', ')
|
|
42
|
+
: ''
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
cm.identifier || item.title || '',
|
|
46
|
+
cm.macro || '',
|
|
47
|
+
cm.micro || '',
|
|
48
|
+
inputs,
|
|
49
|
+
cm.outputs ? `Returns: ${cm.outputs}` : '',
|
|
50
|
+
deps,
|
|
51
|
+
sideEffects,
|
|
52
|
+
`Tags: ${(item.data?.tags || []).join(', ')}`,
|
|
53
|
+
cm.file_path ? `File: ${cm.file_path} lines ${cm.line_start}-${cm.line_end}` : ''
|
|
54
|
+
].filter(Boolean).join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Build structure content from metadata — for filtering/faceted search
|
|
58
|
+
function buildStructureContent(item: any): string {
|
|
59
|
+
const cm = item.data?.code_metadata || {}
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
identifier: cm.identifier || '',
|
|
62
|
+
chunk_type: cm.chunk_type || '',
|
|
63
|
+
file_path: cm.file_path || '',
|
|
64
|
+
line_start: cm.line_start,
|
|
65
|
+
line_end: cm.line_end,
|
|
66
|
+
version: cm.version || '',
|
|
67
|
+
tags: item.data?.tags || [],
|
|
68
|
+
depends_on: cm.depends_on || [],
|
|
69
|
+
depended_by: cm.depended_by || [],
|
|
70
|
+
inputs: Object.keys(cm.inputs || {}),
|
|
71
|
+
has_side_effects: (cm.side_effects || []).length > 0
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate real embedding vector via OpenAI text-embedding-3-small (single input)
|
|
76
|
+
async function generateEmbeddingVector(content: string): Promise<string> {
|
|
77
|
+
const vectors = await generateEmbeddingBatch([content])
|
|
78
|
+
return vectors[0]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Batch-generate embedding vectors — one API call for multiple inputs
|
|
82
|
+
async function generateEmbeddingBatch(inputs: string[]): Promise<string[]> {
|
|
83
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY
|
|
84
|
+
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
|
85
|
+
|
|
86
|
+
if (!apiKey) {
|
|
87
|
+
throw new Error('No OPENAI_API_KEY configured — cannot generate embeddings')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (inputs.length === 0) return []
|
|
91
|
+
|
|
92
|
+
const res = await fetch(`${baseUrl}/embeddings`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
model: 'text-embedding-3-small',
|
|
97
|
+
input: inputs,
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const err = await res.text()
|
|
103
|
+
throw new Error(`OpenAI embeddings error ${res.status}: ${err.slice(0, 200)}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result: any = await res.json()
|
|
107
|
+
// OpenAI returns embeddings sorted by index
|
|
108
|
+
const sorted = (result.data || []).sort((a: any, b: any) => a.index - b.index)
|
|
109
|
+
return sorted.map((d: any) => `[${d.embedding.join(',')}]`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Insert an embedding record (used during batch writes)
|
|
113
|
+
async function insertEmbedding(
|
|
114
|
+
itemId: string,
|
|
115
|
+
vectorType: string,
|
|
116
|
+
chunkIndex: number,
|
|
117
|
+
content: string,
|
|
118
|
+
embedding: string,
|
|
119
|
+
metadata: any
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
await adminDb
|
|
122
|
+
.from('embeddings')
|
|
123
|
+
.insert({
|
|
124
|
+
account_id: '12acec9b-8451-40e7-80d5-e80c4e2fc0de', // Master account
|
|
125
|
+
model_id: 'text-embedding-3-small',
|
|
126
|
+
document_id: itemId,
|
|
127
|
+
chunk_index: chunkIndex,
|
|
128
|
+
content: content,
|
|
129
|
+
embedding: embedding,
|
|
130
|
+
metadata: {
|
|
131
|
+
vector_type: vectorType,
|
|
132
|
+
item_type: 'kb_article',
|
|
133
|
+
...metadata
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Delete all existing embeddings for a document (called before re-embedding)
|
|
139
|
+
async function deleteExistingEmbeddings(itemId: string): Promise<number> {
|
|
140
|
+
const { data } = await adminDb
|
|
141
|
+
.from('embeddings')
|
|
142
|
+
.delete()
|
|
143
|
+
.eq('document_id', itemId)
|
|
144
|
+
.eq('metadata->>item_type', 'kb_article')
|
|
145
|
+
.select('id')
|
|
146
|
+
|
|
147
|
+
return data?.length || 0
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Build article-level structure content for non-code articles
|
|
151
|
+
function buildArticleStructureContent(item: any): string {
|
|
152
|
+
return JSON.stringify({
|
|
153
|
+
title: item.title || '',
|
|
154
|
+
kb_type: item.data?.kb_type || '',
|
|
155
|
+
category: item.data?.category || '',
|
|
156
|
+
tags: item.data?.tags || [],
|
|
157
|
+
audience: item.data?.audience || [],
|
|
158
|
+
security_level: item.data?.security_level || '',
|
|
159
|
+
priority: item.data?.priority || '',
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Determine if an item is a code chunk (ingested) vs a manual article
|
|
164
|
+
function isCodeChunk(item: any): boolean {
|
|
165
|
+
return !!(item.data?.code_metadata?.chunk_id || item.data?.code_metadata?.file_path)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Main handler for generating KB embeddings
|
|
169
|
+
async function handleGenerateEmbeddings(
|
|
170
|
+
itemId: string,
|
|
171
|
+
vectorTypes: string[],
|
|
172
|
+
forceRegenerate: boolean = false
|
|
173
|
+
): Promise<KBEmbeddingResponse> {
|
|
174
|
+
const response: KBEmbeddingResponse = {
|
|
175
|
+
success: true,
|
|
176
|
+
embeddings_created: 0,
|
|
177
|
+
embeddings_updated: 0,
|
|
178
|
+
errors: []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Get the KB item
|
|
183
|
+
const { data: item, error: itemError } = await adminDb
|
|
184
|
+
.from('items')
|
|
185
|
+
.select('*')
|
|
186
|
+
.eq('id', itemId)
|
|
187
|
+
.eq('type_id', 'ce1e50b6-473e-4581-ba0c-e944f47cb240') // kb_article type
|
|
188
|
+
.single()
|
|
189
|
+
|
|
190
|
+
if (itemError || !item) {
|
|
191
|
+
throw new Error('KB item not found')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if embeddings already exist
|
|
195
|
+
if (!forceRegenerate) {
|
|
196
|
+
const { data: existing } = await adminDb
|
|
197
|
+
.from('embeddings')
|
|
198
|
+
.select('id')
|
|
199
|
+
.eq('document_id', itemId)
|
|
200
|
+
.eq('metadata->>item_type', 'kb_article')
|
|
201
|
+
.limit(1)
|
|
202
|
+
|
|
203
|
+
if (existing && existing.length > 0) {
|
|
204
|
+
response.errors.push('Embeddings already exist (use force_regenerate to override)')
|
|
205
|
+
response.success = false
|
|
206
|
+
return response
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Delete existing embeddings before regenerating
|
|
211
|
+
const deleted = await deleteExistingEmbeddings(itemId)
|
|
212
|
+
if (deleted > 0) {
|
|
213
|
+
response.embeddings_updated = deleted
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Code chunks: legacy path (single semantic + single structure) ──
|
|
217
|
+
if (isCodeChunk(item)) {
|
|
218
|
+
const semanticContent = buildSemanticContent(item)
|
|
219
|
+
const structureContent = buildStructureContent(item)
|
|
220
|
+
|
|
221
|
+
const vectors = await generateEmbeddingBatch([semanticContent, structureContent])
|
|
222
|
+
|
|
223
|
+
await Promise.all([
|
|
224
|
+
insertEmbedding(itemId, 'semantic', 0, semanticContent, vectors[0], {
|
|
225
|
+
kb_type: item.data?.kb_type,
|
|
226
|
+
chunk_id: item.data?.code_metadata?.chunk_id,
|
|
227
|
+
chunk_type: item.data?.code_metadata?.chunk_type,
|
|
228
|
+
file_path: item.data?.code_metadata?.file_path,
|
|
229
|
+
}),
|
|
230
|
+
insertEmbedding(itemId, 'structure', 0, structureContent, vectors[1], {
|
|
231
|
+
kb_type: item.data?.kb_type,
|
|
232
|
+
tags: item.data?.tags,
|
|
233
|
+
file_path: item.data?.code_metadata?.file_path,
|
|
234
|
+
depends_on: item.data?.code_metadata?.depends_on,
|
|
235
|
+
}),
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
response.embeddings_created = 2
|
|
239
|
+
return response
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Article path: chunk → batch embed → parallel write ──
|
|
243
|
+
const articleContent = item.description || ''
|
|
244
|
+
const chunks = chunkArticle(articleContent, {
|
|
245
|
+
articleTitle: item.title || 'Untitled',
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// Build all texts to embed: N semantic chunks + 1 structure
|
|
249
|
+
const structureContent = buildArticleStructureContent(item)
|
|
250
|
+
const allTexts = [...chunks.map(c => c.content), structureContent]
|
|
251
|
+
|
|
252
|
+
// Single batched API call to OpenAI
|
|
253
|
+
const allVectors = await generateEmbeddingBatch(allTexts)
|
|
254
|
+
|
|
255
|
+
// Parallel DB writes
|
|
256
|
+
const writePromises: Promise<void>[] = []
|
|
257
|
+
|
|
258
|
+
// Semantic embeddings — one per chunk
|
|
259
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
260
|
+
writePromises.push(
|
|
261
|
+
insertEmbedding(itemId, 'semantic', i, chunks[i].content, allVectors[i], {
|
|
262
|
+
kb_type: item.data?.kb_type,
|
|
263
|
+
chunk_index: i,
|
|
264
|
+
chunk_total: chunks.length,
|
|
265
|
+
section_path: chunks[i].sectionPath,
|
|
266
|
+
})
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Structure embedding — article-level, single vector
|
|
271
|
+
writePromises.push(
|
|
272
|
+
insertEmbedding(itemId, 'structure', 0, structureContent, allVectors[chunks.length], {
|
|
273
|
+
kb_type: item.data?.kb_type,
|
|
274
|
+
tags: item.data?.tags,
|
|
275
|
+
category: item.data?.category,
|
|
276
|
+
audience: item.data?.audience,
|
|
277
|
+
})
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
await Promise.all(writePromises)
|
|
281
|
+
response.embeddings_created = chunks.length + 1
|
|
282
|
+
|
|
283
|
+
} catch (error) {
|
|
284
|
+
response.success = false
|
|
285
|
+
response.errors = [error instanceof Error ? error.message : String(error)]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return response
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Generate embedding vector for a query string via OpenAI
|
|
292
|
+
async function generateQueryEmbedding(text: string): Promise<number[]> {
|
|
293
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY
|
|
294
|
+
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
|
295
|
+
|
|
296
|
+
if (!apiKey) {
|
|
297
|
+
throw new Error('No OPENAI_API_KEY configured — vector search unavailable')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const res = await fetch(`${baseUrl}/embeddings`, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
model: 'text-embedding-3-small',
|
|
305
|
+
input: text,
|
|
306
|
+
}),
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
const err = await res.text()
|
|
311
|
+
throw new Error(`OpenAI embeddings error ${res.status}: ${err.slice(0, 200)}`)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result: any = await res.json()
|
|
315
|
+
return result.data?.[0]?.embedding || []
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Platform KB account
|
|
319
|
+
const KB_PLATFORM_ACCOUNT_ID = '12acec9b-8451-40e7-80d5-e80c4e2fc0de'
|
|
320
|
+
|
|
321
|
+
// Search embeddings via vector similarity
|
|
322
|
+
async function handleSearchEmbeddings(
|
|
323
|
+
query: string,
|
|
324
|
+
accountId: string | null,
|
|
325
|
+
vectorType: string = 'semantic',
|
|
326
|
+
limit: number = 8
|
|
327
|
+
): Promise<any[]> {
|
|
328
|
+
if (!query || query.trim().length < 2) return []
|
|
329
|
+
|
|
330
|
+
// Generate embedding for the search query
|
|
331
|
+
const queryEmbedding = await generateQueryEmbedding(query.trim())
|
|
332
|
+
|
|
333
|
+
// Build account filter: user's account + platform KB
|
|
334
|
+
const accountIds = accountId
|
|
335
|
+
? [accountId, KB_PLATFORM_ACCOUNT_ID]
|
|
336
|
+
: [KB_PLATFORM_ACCOUNT_ID]
|
|
337
|
+
|
|
338
|
+
// Call the match_embeddings RPC — fetch extra to account for chunk deduplication
|
|
339
|
+
const { data: matches, error } = await adminDb.rpc('match_embeddings', {
|
|
340
|
+
query_embedding: queryEmbedding,
|
|
341
|
+
match_count: limit * 3,
|
|
342
|
+
filter_account_ids: accountIds,
|
|
343
|
+
filter_vector_type: vectorType,
|
|
344
|
+
similarity_threshold: 0.15,
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
if (error) throw new Error(`Vector search failed: ${error.message}`)
|
|
348
|
+
|
|
349
|
+
// Filter by similarity threshold + restricted cross-account results
|
|
350
|
+
const SIMILARITY_THRESHOLD = 0.15
|
|
351
|
+
const filtered = (matches || []).filter((r: any) => {
|
|
352
|
+
if (r.similarity < SIMILARITY_THRESHOLD) return false
|
|
353
|
+
if (r.account_id === accountId) return true
|
|
354
|
+
return r.metadata?.security_level !== 'restricted'
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Deduplicate by document_id — keep only the best-matching chunk per article
|
|
358
|
+
const bestByDoc = new Map<string, any>()
|
|
359
|
+
for (const r of filtered) {
|
|
360
|
+
const existing = bestByDoc.get(r.document_id)
|
|
361
|
+
if (!existing || r.similarity > existing.similarity) {
|
|
362
|
+
bestByDoc.set(r.document_id, r)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const deduped = [...bestByDoc.values()]
|
|
366
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
367
|
+
.slice(0, limit)
|
|
368
|
+
|
|
369
|
+
// Enrich with item title from the items table
|
|
370
|
+
const docIds = deduped.map((r: any) => r.document_id)
|
|
371
|
+
if (docIds.length === 0) return []
|
|
372
|
+
|
|
373
|
+
const { data: items } = await adminDb
|
|
374
|
+
.from('items')
|
|
375
|
+
.select('id, title, description, status, data')
|
|
376
|
+
.in('id', docIds)
|
|
377
|
+
|
|
378
|
+
const itemMap = new Map((items || []).map((i: any) => [i.id, i]))
|
|
379
|
+
|
|
380
|
+
return deduped.map((r: any) => {
|
|
381
|
+
const item = itemMap.get(r.document_id)
|
|
382
|
+
return {
|
|
383
|
+
id: r.document_id,
|
|
384
|
+
title: item?.title || '',
|
|
385
|
+
description: item?.description || '',
|
|
386
|
+
status: item?.status || '',
|
|
387
|
+
data: item?.data || {},
|
|
388
|
+
similarity: r.similarity,
|
|
389
|
+
matched_section: r.metadata?.section_path || null,
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Delete embeddings for an item
|
|
395
|
+
async function handleDeleteEmbeddings(itemId: string): Promise<{ deleted: number }> {
|
|
396
|
+
const { data, error } = await adminDb
|
|
397
|
+
.from('embeddings')
|
|
398
|
+
.delete()
|
|
399
|
+
.eq('document_id', itemId)
|
|
400
|
+
.eq('metadata->>item_type', 'kb_article')
|
|
401
|
+
|
|
402
|
+
if (error) throw error
|
|
403
|
+
|
|
404
|
+
return { deleted: data?.length || 0 }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export const handler = createHandler(async (ctx: any, body: any) => {
|
|
408
|
+
const { action } = ctx.query || {}
|
|
409
|
+
const method = ctx.query?.method || 'POST'
|
|
410
|
+
|
|
411
|
+
switch (action) {
|
|
412
|
+
case 'generate':
|
|
413
|
+
if (method === 'POST') {
|
|
414
|
+
const ids: string[] = body.item_ids || (body.item_id ? [body.item_id] : [])
|
|
415
|
+
let totalCreated = 0, totalUpdated = 0
|
|
416
|
+
const allErrors: string[] = []
|
|
417
|
+
for (const id of ids) {
|
|
418
|
+
const r = await handleGenerateEmbeddings(id, body.vector_types, body.force_regenerate)
|
|
419
|
+
totalCreated += r.embeddings_created
|
|
420
|
+
totalUpdated += r.embeddings_updated
|
|
421
|
+
allErrors.push(...r.errors)
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
success: allErrors.length === 0 || totalCreated > 0 || totalUpdated > 0,
|
|
425
|
+
embeddings_created: totalCreated,
|
|
426
|
+
embeddings_updated: totalUpdated,
|
|
427
|
+
errors: allErrors
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
break
|
|
431
|
+
|
|
432
|
+
case 'search':
|
|
433
|
+
if (method === 'GET' || method === 'POST') {
|
|
434
|
+
const q = body.query || ctx.query?.q || ''
|
|
435
|
+
const acctId = body.account_id || ctx.principal?.account_id || null
|
|
436
|
+
return await handleSearchEmbeddings(
|
|
437
|
+
q,
|
|
438
|
+
acctId,
|
|
439
|
+
body.vector_type || 'semantic',
|
|
440
|
+
body.limit || 8
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
break
|
|
444
|
+
|
|
445
|
+
case 'delete':
|
|
446
|
+
if (method === 'DELETE' || method === 'POST') {
|
|
447
|
+
return await handleDeleteEmbeddings(body.item_id)
|
|
448
|
+
}
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
default:
|
|
452
|
+
if (method === 'POST') {
|
|
453
|
+
const ids: string[] = body.item_ids || (body.item_id ? [body.item_id] : [])
|
|
454
|
+
let totalCreated = 0, totalUpdated = 0
|
|
455
|
+
const allErrors: string[] = []
|
|
456
|
+
for (const id of ids) {
|
|
457
|
+
const r = await handleGenerateEmbeddings(id, body.vector_types, body.force_regenerate)
|
|
458
|
+
totalCreated += r.embeddings_created
|
|
459
|
+
totalUpdated += r.embeddings_updated
|
|
460
|
+
allErrors.push(...r.errors)
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
success: allErrors.length === 0 || totalCreated > 0 || totalUpdated > 0,
|
|
464
|
+
embeddings_created: totalCreated,
|
|
465
|
+
embeddings_updated: totalUpdated,
|
|
466
|
+
errors: allErrors
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
throw new Error('Invalid action or method')
|
|
472
|
+
})
|