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,507 @@
|
|
|
1
|
+
import { createHandler } from './_shared/middleware'
|
|
2
|
+
import { adminDb } from './_shared/db'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom Case Analysis Handler
|
|
6
|
+
*
|
|
7
|
+
* Action: POST ?action=analyze_ticket
|
|
8
|
+
*
|
|
9
|
+
* Analyzes resolved support tickets to extract insights, identify root causes,
|
|
10
|
+
* and suggest improvements. Creates case_analysis items and manages tags.
|
|
11
|
+
*
|
|
12
|
+
* Process:
|
|
13
|
+
* 1. Fetch ticket data, threads, and messages
|
|
14
|
+
* 2. Run AI analysis with structured prompt
|
|
15
|
+
* 3. Process tag suggestions (check existence, create if needed)
|
|
16
|
+
* 4. Create case_analysis item with results
|
|
17
|
+
* 5. Update ticket with analysis summary and tags
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const ANALYSIS_AGENT_ID = 'case_analysis_agent' // Will be looked up by name
|
|
23
|
+
const PROMPT_CONFIG_ID = 'case_analysis_prompt' // Will be looked up by slug
|
|
24
|
+
const SUPPORT_TICKET_TYPE_ID = '82320862-a99c-4a84-b7ed-c2832cf519cd' // From existing triage
|
|
25
|
+
const CASE_ANALYSIS_TYPE_ID = 'case_analysis' // Will be looked up by slug
|
|
26
|
+
const TAG_TYPE_ID = 'tag' // Will be looked up by slug
|
|
27
|
+
|
|
28
|
+
// ─── TYPES ────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface AnalysisRequest {
|
|
31
|
+
ticket_id: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AnalysisResult {
|
|
35
|
+
ticketId: string
|
|
36
|
+
caseAnalysisId: string
|
|
37
|
+
analysisData: any
|
|
38
|
+
createdTags: string[]
|
|
39
|
+
confidence: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── HELPERS ──────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
async function getTicketData(ticketId: string): Promise<any> {
|
|
45
|
+
const { data: ticket, error } = await adminDb
|
|
46
|
+
.from('items')
|
|
47
|
+
.select('*')
|
|
48
|
+
.eq('id', ticketId)
|
|
49
|
+
.eq('type_id', SUPPORT_TICKET_TYPE_ID)
|
|
50
|
+
.single()
|
|
51
|
+
|
|
52
|
+
if (error || !ticket) throw new Error(`Ticket not found: ${error?.message}`)
|
|
53
|
+
return ticket
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getConversationHistory(ticketId: string): Promise<any[]> {
|
|
57
|
+
// Get threads for this ticket
|
|
58
|
+
const { data: threads, error: threadError } = await adminDb
|
|
59
|
+
.from('threads')
|
|
60
|
+
.select('id')
|
|
61
|
+
.eq('target_type', 'items')
|
|
62
|
+
.eq('target_id', ticketId)
|
|
63
|
+
|
|
64
|
+
if (threadError || !threads || threads.length === 0) return []
|
|
65
|
+
|
|
66
|
+
const threadId = threads[0].id
|
|
67
|
+
|
|
68
|
+
// Get messages for this thread
|
|
69
|
+
const { data: messages, error: msgError } = await adminDb
|
|
70
|
+
.from('messages')
|
|
71
|
+
.select('content, direction, data, created_at, person_id')
|
|
72
|
+
.eq('thread_id', threadId)
|
|
73
|
+
.eq('visibility', 'public')
|
|
74
|
+
.order('created_at', { ascending: true })
|
|
75
|
+
.limit(50)
|
|
76
|
+
|
|
77
|
+
if (msgError) return []
|
|
78
|
+
return messages || []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function getAgentAndPrompt(): Promise<{ agent: any; promptConfig: any }> {
|
|
82
|
+
const [{ data: agent }, { data: promptConfig }] = await Promise.all([
|
|
83
|
+
adminDb.from('ai_agents').select('*').eq('name', 'Case Resolution Analysis Agent').single(),
|
|
84
|
+
adminDb.from('prompt_configs').select('*').eq('slug', 'case_analysis_prompt').single()
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
if (!agent || !promptConfig) {
|
|
88
|
+
throw new Error('Case analysis agent or prompt configuration not found')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { agent, promptConfig }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function callOpenAI(
|
|
95
|
+
systemPrompt: string,
|
|
96
|
+
messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>,
|
|
97
|
+
model: string,
|
|
98
|
+
temperature: number
|
|
99
|
+
): Promise<{ envelope: any; latency_ms: number; token_usage: any }> {
|
|
100
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY
|
|
101
|
+
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
|
102
|
+
const startTime = Date.now()
|
|
103
|
+
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
const mockEnvelope = {
|
|
106
|
+
reported_issue: 'Mock reported issue - no API key configured',
|
|
107
|
+
true_problem: 'Mock true problem - no API key configured',
|
|
108
|
+
diagnostic_steps: ['Mock step 1', 'Mock step 2'],
|
|
109
|
+
solution_steps: ['Mock solution step 1', 'Mock solution step 2'],
|
|
110
|
+
final_solution: 'Mock final solution - no API key configured',
|
|
111
|
+
customer_temperature: 'neutral',
|
|
112
|
+
time_to_resolution: 60,
|
|
113
|
+
escalation_required: false,
|
|
114
|
+
back_and_forth_count: 2,
|
|
115
|
+
sentiment_progression: ['neutral', 'neutral', 'neutral'],
|
|
116
|
+
automation_potential: 'medium',
|
|
117
|
+
kb_candidate: false,
|
|
118
|
+
suggested_tags: [],
|
|
119
|
+
confidence_score: 0.5,
|
|
120
|
+
analysis_summary: 'Mock analysis - no API key configured'
|
|
121
|
+
}
|
|
122
|
+
return { envelope: mockEnvelope, latency_ms: 0, token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
model,
|
|
130
|
+
temperature,
|
|
131
|
+
response_format: { type: 'json_object' },
|
|
132
|
+
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
133
|
+
}),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const err = await res.text()
|
|
138
|
+
throw new Error(`OpenAI error ${res.status}: ${err.slice(0, 200)}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result: any = await res.json()
|
|
142
|
+
const raw = result.choices?.[0]?.message?.content || '{}'
|
|
143
|
+
let envelope: any
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
envelope = JSON.parse(raw)
|
|
147
|
+
} catch (parseErr: any) {
|
|
148
|
+
envelope = {
|
|
149
|
+
reported_issue: 'Parse failure',
|
|
150
|
+
true_problem: 'Parse failure',
|
|
151
|
+
diagnostic_steps: ['Parse failure'],
|
|
152
|
+
solution_steps: ['Parse failure'],
|
|
153
|
+
final_solution: `Parse failure: ${parseErr.message}`,
|
|
154
|
+
customer_temperature: 'neutral',
|
|
155
|
+
time_to_resolution: 0,
|
|
156
|
+
escalation_required: true,
|
|
157
|
+
back_and_forth_count: 0,
|
|
158
|
+
sentiment_progression: ['neutral'],
|
|
159
|
+
automation_potential: 'low',
|
|
160
|
+
kb_candidate: false,
|
|
161
|
+
suggested_tags: [],
|
|
162
|
+
confidence_score: 0,
|
|
163
|
+
analysis_summary: `Parse failure: ${parseErr.message}. Raw: ${raw.slice(0, 200)}`
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
envelope,
|
|
169
|
+
latency_ms: Date.now() - startTime,
|
|
170
|
+
token_usage: result.usage || {},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function getOrCreateTag(tagData: any, accountId: string): Promise<string> {
|
|
175
|
+
const { slug, name, purpose, category, applicable_to } = tagData
|
|
176
|
+
|
|
177
|
+
// Check if tag already exists
|
|
178
|
+
const { data: existingTag } = await adminDb
|
|
179
|
+
.from('items')
|
|
180
|
+
.select('id')
|
|
181
|
+
.eq('type_id', TAG_TYPE_ID)
|
|
182
|
+
.eq('data->>slug', slug)
|
|
183
|
+
.single()
|
|
184
|
+
|
|
185
|
+
if (existingTag) {
|
|
186
|
+
// Increment usage count
|
|
187
|
+
await adminDb
|
|
188
|
+
.from('items')
|
|
189
|
+
.update({
|
|
190
|
+
data: adminDb.sql`jsonb_set(data, '{usage_count}', COALESCE((data->>'usage_count')::int, 0) + 1)`,
|
|
191
|
+
updated_at: new Date().toISOString()
|
|
192
|
+
})
|
|
193
|
+
.eq('id', existingTag.id)
|
|
194
|
+
|
|
195
|
+
return existingTag.id
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Get tag type ID
|
|
199
|
+
const { data: tagType } = await adminDb
|
|
200
|
+
.from('types')
|
|
201
|
+
.select('id')
|
|
202
|
+
.eq('slug', 'tag')
|
|
203
|
+
.single()
|
|
204
|
+
|
|
205
|
+
if (!tagType) throw new Error('Tag type not found')
|
|
206
|
+
|
|
207
|
+
// Create new tag
|
|
208
|
+
const { data: newTag, error } = await adminDb
|
|
209
|
+
.from('items')
|
|
210
|
+
.insert({
|
|
211
|
+
type_id: tagType.id,
|
|
212
|
+
account_id: accountId,
|
|
213
|
+
title: name,
|
|
214
|
+
description: purpose,
|
|
215
|
+
data: {
|
|
216
|
+
slug,
|
|
217
|
+
name,
|
|
218
|
+
purpose,
|
|
219
|
+
applicable_to: applicable_to || ['ticket'],
|
|
220
|
+
category,
|
|
221
|
+
usage_count: 1
|
|
222
|
+
},
|
|
223
|
+
status: 'active'
|
|
224
|
+
})
|
|
225
|
+
.select('id')
|
|
226
|
+
.single()
|
|
227
|
+
|
|
228
|
+
if (error || !newTag) throw new Error(`Failed to create tag: ${error?.message}`)
|
|
229
|
+
return newTag.id
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function createCaseAnalysis(
|
|
233
|
+
ticketId: string,
|
|
234
|
+
analysisData: any,
|
|
235
|
+
confidence: number,
|
|
236
|
+
agentId: string,
|
|
237
|
+
accountId: string
|
|
238
|
+
): Promise<string> {
|
|
239
|
+
// Get case_analysis type ID
|
|
240
|
+
const { data: caseAnalysisType } = await adminDb
|
|
241
|
+
.from('types')
|
|
242
|
+
.select('id')
|
|
243
|
+
.eq('slug', 'case_analysis')
|
|
244
|
+
.single()
|
|
245
|
+
|
|
246
|
+
if (!caseAnalysisType) throw new Error('Case analysis type not found')
|
|
247
|
+
|
|
248
|
+
const { data: caseAnalysis, error } = await adminDb
|
|
249
|
+
.from('items')
|
|
250
|
+
.insert({
|
|
251
|
+
type_id: caseAnalysisType.id,
|
|
252
|
+
account_id: accountId,
|
|
253
|
+
title: `Analysis for Ticket ${ticketId.slice(0, 8)}`,
|
|
254
|
+
data: {
|
|
255
|
+
ticket_id: ticketId,
|
|
256
|
+
analysis_data: analysisData,
|
|
257
|
+
confidence_score: confidence,
|
|
258
|
+
analysis_timestamp: new Date().toISOString(),
|
|
259
|
+
ai_agent_id: agentId
|
|
260
|
+
},
|
|
261
|
+
status: 'completed'
|
|
262
|
+
})
|
|
263
|
+
.select('id')
|
|
264
|
+
.single()
|
|
265
|
+
|
|
266
|
+
if (error || !caseAnalysis) throw new Error(`Failed to create case analysis: ${error?.message}`)
|
|
267
|
+
|
|
268
|
+
// Create entity link from case analysis to ticket
|
|
269
|
+
await createEntityLink(
|
|
270
|
+
caseAnalysis.id,
|
|
271
|
+
'items',
|
|
272
|
+
ticketId,
|
|
273
|
+
'items',
|
|
274
|
+
'analyzed_by',
|
|
275
|
+
accountId
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return caseAnalysis.id
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function createEntityLink(
|
|
282
|
+
sourceId: string,
|
|
283
|
+
sourceType: string,
|
|
284
|
+
targetId: string,
|
|
285
|
+
targetType: string,
|
|
286
|
+
linkTypeSlug: string,
|
|
287
|
+
accountId: string
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
// Get link type ID
|
|
290
|
+
const { data: linkType } = await adminDb
|
|
291
|
+
.from('link_types')
|
|
292
|
+
.select('id')
|
|
293
|
+
.eq('slug', linkTypeSlug)
|
|
294
|
+
.single()
|
|
295
|
+
|
|
296
|
+
if (!linkType) throw new Error(`Link type '${linkTypeSlug}' not found`)
|
|
297
|
+
|
|
298
|
+
// Get link type ID for the links table
|
|
299
|
+
const { data: linkItemType } = await adminDb
|
|
300
|
+
.from('types')
|
|
301
|
+
.select('id')
|
|
302
|
+
.eq('slug', 'link')
|
|
303
|
+
.single()
|
|
304
|
+
|
|
305
|
+
if (!linkItemType) throw new Error('Link type not found')
|
|
306
|
+
|
|
307
|
+
// Check if link already exists
|
|
308
|
+
const { data: existingLink } = await adminDb
|
|
309
|
+
.from('links')
|
|
310
|
+
.select('id')
|
|
311
|
+
.eq('source_id', sourceId)
|
|
312
|
+
.eq('source_type', sourceType)
|
|
313
|
+
.eq('target_id', targetId)
|
|
314
|
+
.eq('target_type', targetType)
|
|
315
|
+
.eq('link_type_id', linkType.id)
|
|
316
|
+
.single()
|
|
317
|
+
|
|
318
|
+
if (existingLink) return // Link already exists
|
|
319
|
+
|
|
320
|
+
// Create the link
|
|
321
|
+
const { error } = await adminDb
|
|
322
|
+
.from('links')
|
|
323
|
+
.insert({
|
|
324
|
+
type_id: linkItemType.id, // Required field
|
|
325
|
+
link_type_id: linkType.id,
|
|
326
|
+
account_id: accountId,
|
|
327
|
+
source_type: sourceType,
|
|
328
|
+
source_id: sourceId,
|
|
329
|
+
target_type: targetType,
|
|
330
|
+
target_id: targetId,
|
|
331
|
+
link_type: linkTypeSlug,
|
|
332
|
+
created_at: new Date().toISOString(),
|
|
333
|
+
updated_at: new Date().toISOString()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
if (error) throw new Error(`Failed to create entity link: ${error?.message}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function createTagLinks(
|
|
340
|
+
ticketId: string,
|
|
341
|
+
tagIds: string[],
|
|
342
|
+
accountId: string
|
|
343
|
+
): Promise<void> {
|
|
344
|
+
for (const tagId of tagIds) {
|
|
345
|
+
await createEntityLink(
|
|
346
|
+
ticketId,
|
|
347
|
+
'items',
|
|
348
|
+
tagId,
|
|
349
|
+
'items',
|
|
350
|
+
'tagged_with',
|
|
351
|
+
accountId
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function updateTicketWithAnalysis(
|
|
357
|
+
ticketId: string,
|
|
358
|
+
analysisData: any,
|
|
359
|
+
tagIds: string[]
|
|
360
|
+
): Promise<void> {
|
|
361
|
+
// Get current ticket data
|
|
362
|
+
const { data: ticket, error: fetchError } = await adminDb
|
|
363
|
+
.from('items')
|
|
364
|
+
.select('data')
|
|
365
|
+
.eq('id', ticketId)
|
|
366
|
+
.single()
|
|
367
|
+
|
|
368
|
+
if (fetchError || !ticket) {
|
|
369
|
+
throw new Error(`Ticket not found: ${ticketId}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Map analysis data to ca_ prefixed field names to match design schema
|
|
373
|
+
const mappedAnalysisData = {
|
|
374
|
+
ca_reported_issue: analysisData.reported_issue,
|
|
375
|
+
ca_true_problem: analysisData.true_problem,
|
|
376
|
+
ca_final_solution: analysisData.final_solution,
|
|
377
|
+
ca_diagnostic_steps: analysisData.diagnostic_steps,
|
|
378
|
+
ca_solution_steps: analysisData.solution_steps,
|
|
379
|
+
ca_analysis_tags: tagIds,
|
|
380
|
+
ca_time_to_resolution: analysisData.time_to_resolution,
|
|
381
|
+
ca_back_and_forth_count: analysisData.back_and_forth_count,
|
|
382
|
+
ca_escalation_required: analysisData.escalation_required,
|
|
383
|
+
ca_automation_potential: analysisData.automation_potential,
|
|
384
|
+
ca_customer_temperature: analysisData.customer_temperature,
|
|
385
|
+
ca_kb_candidate: analysisData.kb_candidate,
|
|
386
|
+
ca_sentiment_progression: analysisData.sentiment_progression,
|
|
387
|
+
analysis_timestamp: new Date().toISOString()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Update ticket with analysis data using simple object merge
|
|
391
|
+
const { error } = await adminDb
|
|
392
|
+
.from('items')
|
|
393
|
+
.update({
|
|
394
|
+
data: {
|
|
395
|
+
...ticket.data,
|
|
396
|
+
...mappedAnalysisData
|
|
397
|
+
},
|
|
398
|
+
updated_at: new Date().toISOString()
|
|
399
|
+
})
|
|
400
|
+
.eq('id', ticketId)
|
|
401
|
+
|
|
402
|
+
if (error) {
|
|
403
|
+
throw new Error(`Failed to update ticket with analysis: ${error.message}`)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── ACTION: ANALYZE TICKET ─────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
async function handleAnalyzeTicket(ctx: any, body: AnalysisRequest): Promise<AnalysisResult> {
|
|
410
|
+
const { ticket_id } = body
|
|
411
|
+
const account_id = ctx.accountId as string
|
|
412
|
+
|
|
413
|
+
if (!ticket_id) throw new Error('ticket_id is required')
|
|
414
|
+
if (!account_id) throw new Error('Account context required')
|
|
415
|
+
|
|
416
|
+
// Load ticket and conversation data
|
|
417
|
+
const [ticket, conversationHistory, { agent, promptConfig }] = await Promise.all([
|
|
418
|
+
getTicketData(ticket_id),
|
|
419
|
+
getConversationHistory(ticket_id),
|
|
420
|
+
getAgentAndPrompt()
|
|
421
|
+
])
|
|
422
|
+
|
|
423
|
+
// Verify ticket is resolved
|
|
424
|
+
if (ticket.status !== 'resolved') {
|
|
425
|
+
throw new Error('Ticket must be resolved before analysis')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const modelConfig = agent.model_config || {}
|
|
429
|
+
const model = modelConfig.model || process.env.LLM_DEFAULT_MODEL || 'gpt-4o'
|
|
430
|
+
const temperature = modelConfig.temperature ?? 0.3
|
|
431
|
+
|
|
432
|
+
// Build analysis prompt
|
|
433
|
+
const contextTemplate = promptConfig.context_template
|
|
434
|
+
.replace('{{ticket_title}}', ticket.title || '')
|
|
435
|
+
.replace('{{ticket_description}}', ticket.description || '')
|
|
436
|
+
.replace('{{created_at}}', ticket.created_at || '')
|
|
437
|
+
.replace('{{resolved_at}}', ticket.updated_at || '')
|
|
438
|
+
.replace('{{status}}', ticket.data?.status || ticket.status || '')
|
|
439
|
+
.replace('{{priority}}', ticket.data?.priority || 'medium')
|
|
440
|
+
|
|
441
|
+
const conversationText = conversationHistory.map(msg => {
|
|
442
|
+
const role = msg.direction === 'inbound' ? 'Customer' : 'Agent'
|
|
443
|
+
return `${role}: ${msg.content}`
|
|
444
|
+
}).join('\n')
|
|
445
|
+
|
|
446
|
+
const fullPrompt = contextTemplate.replace('{{conversation_history}}', conversationText)
|
|
447
|
+
|
|
448
|
+
// Run AI analysis
|
|
449
|
+
const promptMessages = [{ role: 'user' as const, content: fullPrompt }]
|
|
450
|
+
const { envelope, latency_ms, token_usage } = await callOpenAI(
|
|
451
|
+
agent.system_prompt,
|
|
452
|
+
promptMessages,
|
|
453
|
+
model,
|
|
454
|
+
temperature
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
// Process suggested tags
|
|
458
|
+
const suggestedTags = envelope.suggested_tags || []
|
|
459
|
+
const createdTagIds: string[] = []
|
|
460
|
+
|
|
461
|
+
for (const tagData of suggestedTags) {
|
|
462
|
+
try {
|
|
463
|
+
const tagId = await getOrCreateTag(tagData, account_id)
|
|
464
|
+
createdTagIds.push(tagId)
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error(`Failed to create tag ${tagData.slug}:`, err)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Create case analysis record
|
|
471
|
+
const caseAnalysisId = await createCaseAnalysis(
|
|
472
|
+
ticket_id,
|
|
473
|
+
envelope,
|
|
474
|
+
envelope.confidence_score || 0.5,
|
|
475
|
+
agent.id,
|
|
476
|
+
account_id
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
// Update ticket with analysis data
|
|
480
|
+
await updateTicketWithAnalysis(ticket_id, envelope, createdTagIds)
|
|
481
|
+
|
|
482
|
+
// Create entity links for tags
|
|
483
|
+
if (createdTagIds.length > 0) {
|
|
484
|
+
await createTagLinks(ticket_id, createdTagIds, account_id)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
ticketId: ticket_id,
|
|
489
|
+
caseAnalysisId,
|
|
490
|
+
analysisData: envelope,
|
|
491
|
+
createdTags: createdTagIds,
|
|
492
|
+
confidence: envelope.confidence_score || 0.5
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─── HANDLER ──────────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
499
|
+
const action = ctx.query?.action
|
|
500
|
+
|
|
501
|
+
switch (action) {
|
|
502
|
+
case 'analyze_ticket':
|
|
503
|
+
return await handleAnalyzeTicket(ctx, body)
|
|
504
|
+
default:
|
|
505
|
+
throw new Error(`Unknown action: ${action}. Use 'analyze_ticket'.`)
|
|
506
|
+
}
|
|
507
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHandler } from './_shared/middleware'
|
|
2
|
+
import { adminDb } from './_shared/db'
|
|
3
|
+
|
|
4
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
5
|
+
const { action } = ctx.query || {}
|
|
6
|
+
const method = ctx.query?.method || 'GET'
|
|
7
|
+
|
|
8
|
+
switch (action) {
|
|
9
|
+
case 'list':
|
|
10
|
+
if (method === 'GET') {
|
|
11
|
+
try {
|
|
12
|
+
// Read chunks from the project root directory
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
const chunksPath = path.join(process.cwd(), 'chunks.json')
|
|
16
|
+
const fileContent = fs.readFileSync(chunksPath, 'utf8')
|
|
17
|
+
const data = JSON.parse(fileContent)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
chunks: data.chunks || [],
|
|
21
|
+
total: data.chunks?.length || 0,
|
|
22
|
+
loaded_at: new Date().toISOString()
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Failed to load chunks: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
if (method === 'GET') {
|
|
32
|
+
try {
|
|
33
|
+
// Read chunks from the project root directory
|
|
34
|
+
const fs = require('fs')
|
|
35
|
+
const path = require('path')
|
|
36
|
+
const chunksPath = path.join(process.cwd(), 'chunks.json')
|
|
37
|
+
const fileContent = fs.readFileSync(chunksPath, 'utf8')
|
|
38
|
+
const data = JSON.parse(fileContent)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
chunks: data.chunks || [],
|
|
42
|
+
total: data.chunks?.length || 0,
|
|
43
|
+
loaded_at: new Date().toISOString()
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Failed to load chunks: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error('Invalid action or method')
|
|
52
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cortex Webhook Handler
|
|
3
|
+
*
|
|
4
|
+
* Convention: custom_*.ts files are assembled into /functions/
|
|
5
|
+
* and loaded by integration-routes via: import('./custom_cortex-handler')
|
|
6
|
+
*
|
|
7
|
+
* Receives: (sanitizedData, context, event)
|
|
8
|
+
* Returns: plain text or object
|
|
9
|
+
*/
|
|
10
|
+
export default async function cortexHandler(
|
|
11
|
+
data: Record<string, any>,
|
|
12
|
+
ctx: {
|
|
13
|
+
integrationId: string
|
|
14
|
+
accountId: string
|
|
15
|
+
slug: string
|
|
16
|
+
principal: { id: string; type: string; accountId: string }
|
|
17
|
+
requestId: string
|
|
18
|
+
headers: Record<string, string>
|
|
19
|
+
},
|
|
20
|
+
event: {
|
|
21
|
+
httpMethod: string
|
|
22
|
+
headers: Record<string, string>
|
|
23
|
+
body: any
|
|
24
|
+
path: string
|
|
25
|
+
queryStringParameters: Record<string, string>
|
|
26
|
+
}
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
console.log(`[${ctx.requestId}] Cortex handler received:`, {
|
|
29
|
+
testText: data['test-text'],
|
|
30
|
+
integrationId: ctx.integrationId,
|
|
31
|
+
accountId: ctx.accountId
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return data['test-text']
|
|
35
|
+
}
|