spine-framework-cortex 0.2.23 → 0.2.25

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.
@@ -0,0 +1,447 @@
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
+ })
@@ -36,7 +36,7 @@ async function handleAnalyze(ctx: any, body: any) {
36
36
  if (!content) throw new Error('content is required')
37
37
 
38
38
  const [threadTypeId, agentId, promptConfigId] = await Promise.all([
39
- resolveTypeId('thread', 'thread'),
39
+ resolveTypeId('thread', 'support_thread'),
40
40
  resolveAgentId('KB Redaction Agent'),
41
41
  resolvePromptConfigId('kb_generator'),
42
42
  ])
@@ -32,7 +32,7 @@ async function handleStart(ctx: any, body: any) {
32
32
  if (!message) throw new Error('message is required')
33
33
 
34
34
  const [threadTypeId, solutionAgentId, promptConfigId] = await Promise.all([
35
- resolveTypeId('thread', 'thread'),
35
+ resolveTypeId('thread', 'support_thread'),
36
36
  resolveAgentId('Solution AI Agent'),
37
37
  resolvePromptConfigId('solution_ai'),
38
38
  ])