spine-framework-cortex 0.1.19 → 0.2.1

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