spine-framework-cortex 0.2.21 → 0.2.22
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 +224 -439
- package/functions/custom_kb-embeddings.ts +129 -22
- package/functions/custom_support-redaction.ts +115 -0
- package/functions/custom_support-solution.ts +104 -0
- package/manifest.json +9 -5
- package/package.json +1 -1
- package/pages/support/RedactionReview.tsx +36 -18
- package/pages/support/TicketDetailPage.tsx +127 -129
- package/seed/ai-agents.json +98 -0
- package/seed/pipelines.json +29 -0
- package/seed/prompt-configs.json +84 -0
- package/LICENSE.md +0 -193
- package/README.md +0 -46
- package/functions/custom_cortex-handler.ts +0 -35
- package/functions/custom_kb-chunker-test.ts +0 -364
- package/functions/custom_kb-ingestion.ts +0 -447
- package/functions/custom_tag_management.ts +0 -314
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
import { createHandler } from './_shared/middleware'
|
|
2
|
-
import { adminDb } from './_shared/db'
|
|
3
|
-
import { create as adminCreate, update as adminUpdate } from './admin-data'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
interface ParsedChunk {
|
|
7
|
-
identifier: string
|
|
8
|
-
chunk_id: string
|
|
9
|
-
version: string
|
|
10
|
-
hash: string
|
|
11
|
-
macro: string
|
|
12
|
-
micro: string
|
|
13
|
-
inputs: Record<string, string>
|
|
14
|
-
outputs: string
|
|
15
|
-
depends_on: string[]
|
|
16
|
-
depended_by: string[]
|
|
17
|
-
side_effects: string[]
|
|
18
|
-
tags: string[]
|
|
19
|
-
code: string
|
|
20
|
-
metadata: {
|
|
21
|
-
chunk_id: string
|
|
22
|
-
file_path: string
|
|
23
|
-
line_start: number
|
|
24
|
-
line_end: number
|
|
25
|
-
chunk_type: 'function' | 'class' | 'interface' | 'config' | 'object'
|
|
26
|
-
purpose: string
|
|
27
|
-
hash: string
|
|
28
|
-
dependencies: string[]
|
|
29
|
-
dependents: string[]
|
|
30
|
-
source: {
|
|
31
|
-
source_type: 'core'
|
|
32
|
-
ref: string
|
|
33
|
-
line_start: number
|
|
34
|
-
line_end: number
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface IngestionRequest {
|
|
40
|
-
chunks: ParsedChunk[]
|
|
41
|
-
force_update?: boolean
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface IngestionResponse {
|
|
45
|
-
success: boolean
|
|
46
|
-
items_created: number
|
|
47
|
-
items_updated: number
|
|
48
|
-
embeddings_generated: number
|
|
49
|
-
errors: string[]
|
|
50
|
-
skipped: string[]
|
|
51
|
-
item_ids: string[]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Build a human-readable HTML description for developer consumption
|
|
55
|
-
function buildDescriptionHtml(chunk: ParsedChunk, cleanCode: string): string {
|
|
56
|
-
const sections: string[] = []
|
|
57
|
-
|
|
58
|
-
// Purpose — always present
|
|
59
|
-
sections.push(`<p><strong>Purpose</strong><br/>${chunk.macro}</p>`)
|
|
60
|
-
|
|
61
|
-
if (chunk.micro && chunk.micro !== chunk.macro) {
|
|
62
|
-
sections.push(`<p>${chunk.micro}</p>`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Parameters
|
|
66
|
-
if (chunk.inputs && Object.keys(chunk.inputs).length > 0) {
|
|
67
|
-
const rows = Object.entries(chunk.inputs)
|
|
68
|
-
.map(([name, desc]) => `<code>${name}</code> — ${desc}`)
|
|
69
|
-
.join('<br/>')
|
|
70
|
-
sections.push(`<p><strong>Parameters</strong><br/>${rows}</p>`)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Returns
|
|
74
|
-
if (chunk.outputs) {
|
|
75
|
-
sections.push(`<p><strong>Returns</strong><br/>${chunk.outputs}</p>`)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Dependencies
|
|
79
|
-
if (chunk.depends_on && chunk.depends_on.length > 0) {
|
|
80
|
-
const depList = chunk.depends_on.map(d => `<code>${d}</code>`).join(', ')
|
|
81
|
-
sections.push(`<p><strong>Dependencies</strong><br/>${depList}</p>`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Side Effects
|
|
85
|
-
if (chunk.side_effects && chunk.side_effects.length > 0) {
|
|
86
|
-
const effectList = chunk.side_effects.join('<br/>')
|
|
87
|
-
sections.push(`<p><strong>Side Effects</strong><br/>${effectList}</p>`)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Source location
|
|
91
|
-
sections.push(`<p><strong>Source</strong><br/><code>${chunk.metadata.file_path}</code> lines ${chunk.metadata.line_start}–${chunk.metadata.line_end}</p>`)
|
|
92
|
-
|
|
93
|
-
// Code block
|
|
94
|
-
const escapedCode = cleanCode.trim().replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
95
|
-
sections.push(`<pre><code>${escapedCode}</code></pre>`)
|
|
96
|
-
|
|
97
|
-
return sections.join('\n')
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Convert parsed chunk to KB article data
|
|
101
|
-
function chunkToKBArticle(chunk: ParsedChunk): any {
|
|
102
|
-
const title = chunk.identifier.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
103
|
-
|
|
104
|
-
// Extract clean code (remove docblock if present)
|
|
105
|
-
let cleanCode = chunk.code
|
|
106
|
-
const docblockMatch = chunk.code.match(/^\/\*\*[\s\S]*?\*\/\s*/)
|
|
107
|
-
if (docblockMatch) {
|
|
108
|
-
cleanCode = chunk.code.substring(docblockMatch[0].length)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const descriptionHtml = buildDescriptionHtml(chunk, cleanCode)
|
|
112
|
-
|
|
113
|
-
const searchKeywords = [
|
|
114
|
-
chunk.identifier,
|
|
115
|
-
chunk.macro,
|
|
116
|
-
chunk.micro,
|
|
117
|
-
...chunk.tags,
|
|
118
|
-
chunk.metadata.chunk_type,
|
|
119
|
-
'typescript'
|
|
120
|
-
].filter(Boolean)
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
type_id: 'ce1e50b6-473e-4581-ba0c-e944f47cb240', // kb_article type
|
|
124
|
-
title,
|
|
125
|
-
status: 'published',
|
|
126
|
-
description: descriptionHtml,
|
|
127
|
-
data: {
|
|
128
|
-
kb_type: 'code_chunk',
|
|
129
|
-
priority: 'medium',
|
|
130
|
-
audience: ['developer', 'ai_system'],
|
|
131
|
-
tags: [...chunk.tags, chunk.metadata.chunk_type, 'typescript', 'core'],
|
|
132
|
-
search_keywords: searchKeywords,
|
|
133
|
-
category: 'technical',
|
|
134
|
-
security_level: 'internal',
|
|
135
|
-
source_info: {
|
|
136
|
-
source_type: 'automated_ingestion',
|
|
137
|
-
author: 'chunk-parser-v1.0',
|
|
138
|
-
ingestion_timestamp: new Date().toISOString(),
|
|
139
|
-
original_source: chunk.metadata.file_path
|
|
140
|
-
},
|
|
141
|
-
code_metadata: {
|
|
142
|
-
chunk_id: chunk.chunk_id,
|
|
143
|
-
identifier: chunk.identifier,
|
|
144
|
-
version: chunk.version,
|
|
145
|
-
hash: chunk.hash,
|
|
146
|
-
macro: chunk.macro,
|
|
147
|
-
micro: chunk.micro,
|
|
148
|
-
inputs: chunk.inputs,
|
|
149
|
-
outputs: chunk.outputs,
|
|
150
|
-
depends_on: chunk.depends_on,
|
|
151
|
-
depended_by: chunk.depended_by,
|
|
152
|
-
side_effects: chunk.side_effects,
|
|
153
|
-
file_path: chunk.metadata.file_path,
|
|
154
|
-
line_start: chunk.metadata.line_start,
|
|
155
|
-
line_end: chunk.metadata.line_end,
|
|
156
|
-
chunk_type: chunk.metadata.chunk_type,
|
|
157
|
-
language: 'typescript',
|
|
158
|
-
code: cleanCode.trim()
|
|
159
|
-
},
|
|
160
|
-
related_articles: []
|
|
161
|
-
},
|
|
162
|
-
is_active: true,
|
|
163
|
-
created_by: 'c230fe01-edf4-4e03-b455-c9cbac22b699' // System Admin
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Check if chunk already exists
|
|
168
|
-
async function findExistingChunk(ctx: any, chunkId: string): Promise<any | null> {
|
|
169
|
-
const { data } = await ctx.db
|
|
170
|
-
.from('items')
|
|
171
|
-
.select('*')
|
|
172
|
-
.eq('type_id', 'ce1e50b6-473e-4581-ba0c-e944f47cb240')
|
|
173
|
-
.filter('data->>kb_type', 'eq', 'code_chunk')
|
|
174
|
-
.filter('data->code_metadata->>chunk_id', 'eq', chunkId)
|
|
175
|
-
.maybeSingle()
|
|
176
|
-
|
|
177
|
-
return data
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Create or update KB article item using standard Spine handlers
|
|
181
|
-
async function upsertKBArticle(ctx: any, chunk: ParsedChunk, forceUpdate: boolean = false): Promise<{ created: boolean; id: string }> {
|
|
182
|
-
const existing = await findExistingChunk(ctx, chunk.chunk_id)
|
|
183
|
-
const kbData = chunkToKBArticle(chunk)
|
|
184
|
-
|
|
185
|
-
if (existing) {
|
|
186
|
-
if (!forceUpdate) {
|
|
187
|
-
throw new Error(`Chunk ${chunk.chunk_id} already exists (use force_update to override)`)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Update existing item via direct admin-data import (ctx passed through — nested call, no HTTP)
|
|
191
|
-
const ctxWithQuery = { ...ctx, query: { ...ctx.query, entity: 'items', id: existing.id } }
|
|
192
|
-
await adminUpdate(ctxWithQuery, {
|
|
193
|
-
title: kbData.title,
|
|
194
|
-
status: kbData.status,
|
|
195
|
-
description: kbData.description,
|
|
196
|
-
data: kbData.data,
|
|
197
|
-
is_active: kbData.is_active
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
return { created: false, id: existing.id }
|
|
201
|
-
} else {
|
|
202
|
-
// Create new item via direct admin-data import (ctx passed through — nested call, no HTTP)
|
|
203
|
-
const result: any = await adminCreate(ctx, {
|
|
204
|
-
entity: 'items',
|
|
205
|
-
type_id: kbData.type_id,
|
|
206
|
-
title: kbData.title,
|
|
207
|
-
status: kbData.status,
|
|
208
|
-
description: kbData.description,
|
|
209
|
-
data: kbData.data,
|
|
210
|
-
is_active: kbData.is_active
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const id = result?.id
|
|
214
|
-
if (!id) throw new Error('Failed to create KB article: no ID returned')
|
|
215
|
-
return { created: true, id }
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Build plain-text semantic content for embedding — developer understanding focus
|
|
220
|
-
function buildSemanticContent(chunk: ParsedChunk): string {
|
|
221
|
-
const inputSummary = chunk.inputs && Object.keys(chunk.inputs).length > 0
|
|
222
|
-
? 'Parameters: ' + Object.entries(chunk.inputs).map(([k, v]) => `${k} (${v})`).join(', ')
|
|
223
|
-
: ''
|
|
224
|
-
const sideEffects = chunk.side_effects && chunk.side_effects.length > 0
|
|
225
|
-
? 'Side effects: ' + chunk.side_effects.join(', ')
|
|
226
|
-
: ''
|
|
227
|
-
const deps = chunk.depends_on && chunk.depends_on.length > 0
|
|
228
|
-
? 'Depends on: ' + chunk.depends_on.join(', ')
|
|
229
|
-
: ''
|
|
230
|
-
|
|
231
|
-
return [
|
|
232
|
-
chunk.identifier,
|
|
233
|
-
chunk.macro,
|
|
234
|
-
chunk.micro,
|
|
235
|
-
inputSummary,
|
|
236
|
-
chunk.outputs ? `Returns: ${chunk.outputs}` : '',
|
|
237
|
-
deps,
|
|
238
|
-
sideEffects,
|
|
239
|
-
`Tags: ${chunk.tags.join(', ')}`,
|
|
240
|
-
`File: ${chunk.metadata.file_path} lines ${chunk.metadata.line_start}-${chunk.metadata.line_end}`
|
|
241
|
-
].filter(Boolean).join('\n')
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Generate embeddings for a KB item — semantic (understanding) + structure (metadata)
|
|
245
|
-
async function generateEmbeddings(ctx: any, itemId: string, chunk: ParsedChunk): Promise<void> {
|
|
246
|
-
const embeddingTypes = ['semantic', 'structure']
|
|
247
|
-
|
|
248
|
-
for (const vectorType of embeddingTypes) {
|
|
249
|
-
let content = ''
|
|
250
|
-
let metadata: any = {
|
|
251
|
-
vector_type: vectorType,
|
|
252
|
-
item_type: 'kb_article',
|
|
253
|
-
chunk_id: chunk.chunk_id,
|
|
254
|
-
version: chunk.version,
|
|
255
|
-
kb_type: 'code_chunk'
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
switch (vectorType) {
|
|
259
|
-
case 'semantic':
|
|
260
|
-
content = buildSemanticContent(chunk)
|
|
261
|
-
metadata.chunk_type = chunk.metadata.chunk_type
|
|
262
|
-
metadata.file_path = chunk.metadata.file_path
|
|
263
|
-
break
|
|
264
|
-
|
|
265
|
-
case 'structure':
|
|
266
|
-
content = JSON.stringify({
|
|
267
|
-
identifier: chunk.identifier,
|
|
268
|
-
chunk_type: chunk.metadata.chunk_type,
|
|
269
|
-
file_path: chunk.metadata.file_path,
|
|
270
|
-
line_start: chunk.metadata.line_start,
|
|
271
|
-
line_end: chunk.metadata.line_end,
|
|
272
|
-
version: chunk.version,
|
|
273
|
-
tags: chunk.tags,
|
|
274
|
-
depends_on: chunk.depends_on,
|
|
275
|
-
depended_by: chunk.depended_by,
|
|
276
|
-
inputs: Object.keys(chunk.inputs || {}),
|
|
277
|
-
has_side_effects: (chunk.side_effects || []).length > 0
|
|
278
|
-
})
|
|
279
|
-
metadata.tags = chunk.tags
|
|
280
|
-
metadata.file_path = chunk.metadata.file_path
|
|
281
|
-
metadata.depends_on = chunk.depends_on
|
|
282
|
-
break
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const embeddingVector = await generateEmbeddingVector(content)
|
|
287
|
-
await ctx.db.from('embeddings').insert({
|
|
288
|
-
model_id: 'text-embedding-3-small',
|
|
289
|
-
document_id: itemId,
|
|
290
|
-
chunk_index: vectorType === 'semantic' ? 0 : 1,
|
|
291
|
-
content,
|
|
292
|
-
embedding: embeddingVector,
|
|
293
|
-
metadata
|
|
294
|
-
})
|
|
295
|
-
} catch (error) {
|
|
296
|
-
console.error(`Failed to generate ${vectorType} embedding for ${chunk.chunk_id}:`, error)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Generate real embedding vector via OpenAI text-embedding-3-small
|
|
302
|
-
async function generateEmbeddingVector(content: string): Promise<number[]> {
|
|
303
|
-
const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY
|
|
304
|
-
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
|
305
|
-
|
|
306
|
-
if (!apiKey) {
|
|
307
|
-
throw new Error('No OPENAI_API_KEY configured — cannot generate embeddings')
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const res = await fetch(`${baseUrl}/embeddings`, {
|
|
311
|
-
method: 'POST',
|
|
312
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
313
|
-
body: JSON.stringify({
|
|
314
|
-
model: 'text-embedding-3-small',
|
|
315
|
-
input: content,
|
|
316
|
-
}),
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
if (!res.ok) {
|
|
320
|
-
const err = await res.text()
|
|
321
|
-
throw new Error(`OpenAI embeddings error ${res.status}: ${err.slice(0, 200)}`)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const result: any = await res.json()
|
|
325
|
-
return result.data?.[0]?.embedding || []
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Main ingestion handler
|
|
329
|
-
async function handleIngestChunks(ctx: any, chunks: ParsedChunk[], forceUpdate: boolean = false): Promise<IngestionResponse> {
|
|
330
|
-
const response: IngestionResponse = {
|
|
331
|
-
success: true,
|
|
332
|
-
items_created: 0,
|
|
333
|
-
items_updated: 0,
|
|
334
|
-
embeddings_generated: 0,
|
|
335
|
-
errors: [],
|
|
336
|
-
skipped: [],
|
|
337
|
-
item_ids: []
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
for (const chunk of chunks) {
|
|
341
|
-
try {
|
|
342
|
-
// Validate chunk
|
|
343
|
-
if (!chunk.chunk_id || !chunk.code || !chunk.metadata) {
|
|
344
|
-
response.errors.push(`Invalid chunk data for ${chunk.identifier}`)
|
|
345
|
-
continue
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Create/update KB article
|
|
349
|
-
const { created, id } = await upsertKBArticle(ctx, chunk, forceUpdate)
|
|
350
|
-
|
|
351
|
-
if (created) {
|
|
352
|
-
response.items_created++
|
|
353
|
-
} else {
|
|
354
|
-
response.items_updated++
|
|
355
|
-
}
|
|
356
|
-
response.item_ids.push(id)
|
|
357
|
-
|
|
358
|
-
// Generate embeddings
|
|
359
|
-
await generateEmbeddings(ctx, id, chunk)
|
|
360
|
-
response.embeddings_generated++
|
|
361
|
-
|
|
362
|
-
} catch (error) {
|
|
363
|
-
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
364
|
-
console.error(`KB ingestion error for ${chunk.identifier}:`, error)
|
|
365
|
-
|
|
366
|
-
if (errorMessage.includes('already exists')) {
|
|
367
|
-
response.skipped.push(`${chunk.identifier}: ${errorMessage}`)
|
|
368
|
-
} else {
|
|
369
|
-
response.errors.push(`${chunk.identifier}: ${errorMessage}`)
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Determine overall success
|
|
375
|
-
if (response.errors.length > 0 && response.items_created === 0 && response.items_updated === 0) {
|
|
376
|
-
response.success = false
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return response
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export const handler = createHandler(async (ctx, body) => {
|
|
383
|
-
const { action } = ctx.query || {}
|
|
384
|
-
const method = ctx.query?.method || 'POST'
|
|
385
|
-
|
|
386
|
-
switch (action) {
|
|
387
|
-
case 'ingest':
|
|
388
|
-
if (method === 'POST') {
|
|
389
|
-
return await handleIngestChunks(ctx, body.chunks, body.force_update)
|
|
390
|
-
}
|
|
391
|
-
break
|
|
392
|
-
|
|
393
|
-
case 'status':
|
|
394
|
-
if (method === 'GET' || method === 'POST') {
|
|
395
|
-
const existing = await findExistingChunk(ctx, body.chunk_id)
|
|
396
|
-
|
|
397
|
-
if (!existing) {
|
|
398
|
-
return { found: false }
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Get embedding count
|
|
402
|
-
const { data: embeddings } = await adminDb
|
|
403
|
-
.from('embeddings')
|
|
404
|
-
.select('metadata->>vector_type')
|
|
405
|
-
.eq('document_id', existing.id)
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
found: true,
|
|
409
|
-
item: existing,
|
|
410
|
-
embeddings: embeddings?.length || 0,
|
|
411
|
-
vector_types: embeddings?.map(e => e.metadata?.vector_type) || []
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
break
|
|
415
|
-
|
|
416
|
-
case 'delete':
|
|
417
|
-
if (method === 'DELETE' || method === 'POST') {
|
|
418
|
-
const existing = await findExistingChunk(ctx, body.chunk_id)
|
|
419
|
-
|
|
420
|
-
if (!existing) {
|
|
421
|
-
throw new Error('Chunk not found')
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Delete embeddings first
|
|
425
|
-
await adminDb
|
|
426
|
-
.from('embeddings')
|
|
427
|
-
.delete()
|
|
428
|
-
.eq('document_id', existing.id)
|
|
429
|
-
|
|
430
|
-
// Delete the item
|
|
431
|
-
await adminDb
|
|
432
|
-
.from('items')
|
|
433
|
-
.delete()
|
|
434
|
-
.eq('id', existing.id)
|
|
435
|
-
|
|
436
|
-
return { deleted: true }
|
|
437
|
-
}
|
|
438
|
-
break
|
|
439
|
-
|
|
440
|
-
default:
|
|
441
|
-
if (method === 'POST') {
|
|
442
|
-
return await handleIngestChunks(ctx, body.chunks, body.force_update)
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
throw new Error('Invalid action or method')
|
|
447
|
-
})
|