spine-framework-cortex 0.1.18 → 0.2.0

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 +8 -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 -42
  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 -430
  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,649 +0,0 @@
1
- import { createHandler } from './_shared/middleware'
2
- import { adminDb } from './_shared/db'
3
- import { resolveTypeIds, resolveAccountId, resolveAgentId, resolvePromptConfigId } from './_shared/resolve-ids'
4
-
5
- async function resolveIds() {
6
- const [types, kbPlatformAccountId, triageAgentId, promptConfigId] = await Promise.all([
7
- resolveTypeIds([
8
- { kind: 'item', slug: 'support_ticket' },
9
- { kind: 'thread', slug: 'thread' },
10
- { kind: 'message', slug: 'message' },
11
- ]),
12
- resolveAccountId('spine-system'),
13
- resolveAgentId('Support Triage Agent'),
14
- resolvePromptConfigId('support_triage_config'),
15
- ])
16
- return {
17
- SUPPORT_TICKET_TYPE_ID: types['item/support_ticket'],
18
- THREAD_TYPE_ID: types['thread/thread'],
19
- MESSAGE_TYPE_ID: types['message/message'],
20
- KB_PLATFORM_ACCOUNT_ID: kbPlatformAccountId,
21
- TRIAGE_AGENT_ID: triageAgentId,
22
- PROMPT_CONFIG_ID: promptConfigId,
23
- }
24
- }
25
-
26
- /**
27
- * Custom Support Triage Handler
28
- *
29
- * Two actions:
30
- * POST ?action=new_ticket — first message: creates ticket + thread + messages, runs AI
31
- * POST ?action=reply — follow-up message on existing ticket: posts message, runs AI
32
- *
33
- * No direct DB access from custom code — all writes go through admin-data API or
34
- * adminDb (service-role) where the API is not available in server context.
35
- *
36
- * AI response schema (enforced via response_format: json_object):
37
- * {
38
- * public_response: string,
39
- * confidence: number (0-1),
40
- * confidence_reasoning: string,
41
- * escalate: boolean,
42
- * escalation_reason: 'low_confidence' | 'none',
43
- * sources_used: Array<{ type, id, title, relevance }>,
44
- * suggested_title: string (turn 1 only)
45
- * }
46
- */
47
-
48
- // ─── CONSTANTS ────────────────────────────────────────────────────────────────
49
-
50
- const CONFIDENCE_THRESHOLD = 0.75
51
-
52
- // ─── TYPES ────────────────────────────────────────────────────────────────────
53
-
54
- interface TriageEnvelope {
55
- public_response: string
56
- confidence: number
57
- confidence_reasoning: string
58
- escalate: boolean
59
- escalation_reason: 'low_confidence' | 'none'
60
- sources_used: Array<{ type: string; id: string; title: string; relevance: number }>
61
- suggested_title?: string
62
- }
63
-
64
- interface TriageResult {
65
- ticketId: string
66
- threadId: string
67
- publicMessageId: string
68
- internalMessageId: string
69
- public_response: string
70
- confidence: number
71
- escalated: boolean
72
- escalation_reason: string
73
- suggested_title?: string
74
- }
75
-
76
- // ─── HELPERS ──────────────────────────────────────────────────────────────────
77
-
78
- async function getConversationHistory(threadId: string): Promise<any[]> {
79
- const { data: msgs, error } = await adminDb
80
- .from('messages')
81
- .select('content, direction, data, created_at')
82
- .eq('thread_id', threadId)
83
- .eq('visibility', 'public')
84
- .order('created_at', { ascending: true })
85
- .limit(20)
86
- if (error) return []
87
- return msgs || []
88
- }
89
-
90
- type ResolvedIds = Awaited<ReturnType<typeof resolveIds>>
91
-
92
- async function searchKB(query: string, accountId: string, ids: ResolvedIds): Promise<any[]> {
93
- // Search embeddings scoped to: (1) client's own KB articles, and (2) platform-wide KB articles
94
- // Exclude restricted items that belong to a different account
95
- const accountIds = [accountId]
96
- if (accountId !== ids.KB_PLATFORM_ACCOUNT_ID) {
97
- accountIds.push(ids.KB_PLATFORM_ACCOUNT_ID)
98
- }
99
-
100
- const { data: results, error } = await adminDb
101
- .from('embeddings')
102
- .select('document_id, content, metadata, account_id')
103
- .in('account_id', accountIds)
104
- .eq('metadata->>item_type', 'kb_article')
105
- .eq('metadata->>vector_type', 'semantic')
106
- .textSearch('content', query, { type: 'websearch', config: 'english' })
107
- .limit(8)
108
-
109
- if (error) return []
110
-
111
- // For cross-account (platform) results, filter out restricted items
112
- const filtered = (results || []).filter((r: any) => {
113
- if (r.account_id === accountId) return true
114
- return r.metadata?.security_level !== 'restricted'
115
- })
116
-
117
- return filtered.slice(0, 5)
118
- }
119
-
120
- async function getPriorTickets(accountId: string, personId: string, ids: ResolvedIds): Promise<any[]> {
121
- const { data: tickets, error } = await adminDb
122
- .from('items')
123
- .select('id, title, description, created_at')
124
- .eq('type_id', ids.SUPPORT_TICKET_TYPE_ID)
125
- .eq('account_id', accountId)
126
- .eq('created_by', personId)
127
- .order('created_at', { ascending: false })
128
- .limit(5)
129
- if (error) return []
130
- return tickets || []
131
- }
132
-
133
- async function callOpenAI(
134
- systemPrompt: string,
135
- messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>,
136
- model: string,
137
- temperature: number
138
- ): Promise<{ envelope: TriageEnvelope; latency_ms: number; token_usage: any }> {
139
- const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY
140
- const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
141
- const startTime = Date.now()
142
-
143
- if (!apiKey) {
144
- const mockEnvelope: TriageEnvelope = {
145
- public_response: '[Mock] No OPENAI_API_KEY set. This is a mock AI response for local development.',
146
- confidence: 0.85,
147
- confidence_reasoning: 'Mock response — no API key configured.',
148
- escalate: false,
149
- escalation_reason: 'none',
150
- sources_used: [],
151
- suggested_title: 'Mock Support Ticket'
152
- }
153
- return { envelope: mockEnvelope, latency_ms: 0, token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }
154
- }
155
-
156
- const res = await fetch(`${baseUrl}/chat/completions`, {
157
- method: 'POST',
158
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
159
- body: JSON.stringify({
160
- model,
161
- temperature,
162
- response_format: { type: 'json_object' },
163
- messages: [{ role: 'system', content: systemPrompt }, ...messages],
164
- }),
165
- })
166
-
167
- if (!res.ok) {
168
- const err = await res.text()
169
- throw new Error(`OpenAI error ${res.status}: ${err.slice(0, 200)}`)
170
- }
171
-
172
- const result: any = await res.json()
173
- const raw = result.choices?.[0]?.message?.content || '{}'
174
- let envelope: TriageEnvelope
175
-
176
- try {
177
- envelope = JSON.parse(raw)
178
- if (typeof envelope.public_response !== 'string') throw new Error('missing public_response')
179
- if (typeof envelope.confidence !== 'number') throw new Error('missing confidence')
180
- } catch (parseErr: any) {
181
- envelope = {
182
- public_response: "We're looking into this and will have a human response shortly.",
183
- confidence: 0,
184
- confidence_reasoning: `Parse failure: ${parseErr.message}. Raw: ${raw.slice(0, 200)}`,
185
- escalate: true,
186
- escalation_reason: 'low_confidence',
187
- sources_used: [],
188
- }
189
- }
190
-
191
- return {
192
- envelope,
193
- latency_ms: Date.now() - startTime,
194
- token_usage: result.usage || {},
195
- }
196
- }
197
-
198
- function buildSystemPrompt(
199
- agentSystemPrompt: string,
200
- contextTemplate: string,
201
- kbSources: any[],
202
- priorTickets: any[],
203
- history: any[]
204
- ): string {
205
- let prompt = agentSystemPrompt + '\n\n'
206
- prompt += contextTemplate + '\n\n'
207
-
208
- if (kbSources.length > 0) {
209
- prompt += '## Knowledge Base Articles\n'
210
- kbSources.forEach((doc, i) => {
211
- prompt += `[KB${i + 1}] ${doc.content?.slice(0, 500)}\n`
212
- })
213
- prompt += '\n'
214
- }
215
-
216
- if (priorTickets.length > 0) {
217
- prompt += "## Customer's Prior Tickets\n"
218
- priorTickets.forEach((t) => {
219
- prompt += `- ${t.title}: ${t.description?.slice(0, 200) || 'No description'}\n`
220
- })
221
- prompt += '\n'
222
- }
223
-
224
- if (history.length > 0) {
225
- prompt += '## Conversation So Far\n'
226
- history.forEach((m) => {
227
- const role = m.direction === 'inbound' ? 'Customer' : 'Assistant'
228
- prompt += `${role}: ${m.content}\n`
229
- })
230
- prompt += '\n'
231
- }
232
-
233
- prompt += `\nYou MUST respond with a single valid JSON object matching EXACTLY this schema. No markdown, no code fences, no extra text:\n`
234
- prompt += `{
235
- "public_response": "<your response to the customer>",
236
- "confidence": <0.0-1.0>,
237
- "confidence_reasoning": "<one sentence explaining your confidence score>",
238
- "escalate": <true|false>,
239
- "escalation_reason": "<low_confidence|none>",
240
- "sources_used": [{"type": "kb_article|prior_ticket", "id": "<id>", "title": "<title>", "relevance": <0.0-1.0>}],
241
- "suggested_title": "<3-8 word case title — ONLY include on the first turn>"
242
- }`
243
-
244
- return prompt
245
- }
246
-
247
- // ─── SCHEMA LOADER ──────────────────────────────────────────────────────────
248
-
249
- async function loadTypeSchemas(ids: ResolvedIds) {
250
- const { data: types } = await adminDb
251
- .from('types')
252
- .select('id, design_schema, validation_schema')
253
- .in('id', [ids.SUPPORT_TICKET_TYPE_ID, ids.THREAD_TYPE_ID, ids.MESSAGE_TYPE_ID])
254
- const byId = Object.fromEntries((types || []).map((t: any) => [t.id, t]))
255
- return {
256
- ticketSchema: byId[ids.SUPPORT_TICKET_TYPE_ID]?.design_schema || {},
257
- ticketValidation: byId[ids.SUPPORT_TICKET_TYPE_ID]?.validation_schema || {},
258
- threadSchema: byId[ids.THREAD_TYPE_ID]?.design_schema || {},
259
- threadValidation: byId[ids.THREAD_TYPE_ID]?.validation_schema || {},
260
- messageSchema: byId[ids.MESSAGE_TYPE_ID]?.design_schema || {},
261
- messageValidation: byId[ids.MESSAGE_TYPE_ID]?.validation_schema || {},
262
- }
263
- }
264
-
265
- // ─── ACTION: NEW TICKET ───────────────────────────────────────────────────────
266
-
267
- async function handleNewTicket(ctx: any, body: any): Promise<TriageResult> {
268
- const { message } = body
269
- const account_id = ctx.accountId as string
270
- const person_id = ctx.principal?.id as string
271
- if (!message) throw new Error('message is required')
272
- if (!account_id || !person_id) throw new Error('User context (account + person) required')
273
-
274
- const ids = await resolveIds()
275
-
276
- // Load agent + prompt config + type schemas
277
- const [{ data: agent }, { data: promptConfig }, schemas] = await Promise.all([
278
- adminDb.from('ai_agents').select('*').eq('id', ids.TRIAGE_AGENT_ID).single(),
279
- adminDb.from('prompt_configs').select('*').eq('id', ids.PROMPT_CONFIG_ID).single(),
280
- loadTypeSchemas(ids),
281
- ])
282
- if (!agent || !promptConfig) throw new Error('Triage agent configuration not found')
283
-
284
- const modelConfig = agent.model_config || {}
285
- const model = modelConfig.model || process.env.LLM_DEFAULT_MODEL || 'gpt-4o'
286
- const temperature = modelConfig.temperature ?? 0.7
287
-
288
- // CALL A: Run AI (before ticket exists — no history yet)
289
- const kbSources = await searchKB(message, account_id, ids)
290
- const priorTickets = await getPriorTickets(account_id, person_id, ids)
291
- const systemPrompt = buildSystemPrompt(
292
- agent.system_prompt,
293
- promptConfig.context_template.replace('{{user_message}}', message),
294
- kbSources,
295
- priorTickets,
296
- []
297
- )
298
-
299
- const promptMessages = [{ role: 'user' as const, content: message }]
300
- const { envelope, latency_ms, token_usage } = await callOpenAI(systemPrompt, promptMessages, model, temperature)
301
-
302
- const escalate = envelope.escalate || envelope.confidence < (promptConfig.confidence_threshold || CONFIDENCE_THRESHOLD)
303
- const suggestedTitle = envelope.suggested_title || message.slice(0, 80)
304
-
305
- // CALL A continued: Create ticket via adminDb
306
- const { data: ticket, error: ticketError } = await adminDb
307
- .from('items')
308
- .insert({
309
- type_id: ids.SUPPORT_TICKET_TYPE_ID,
310
- account_id,
311
- created_by: person_id,
312
- title: suggestedTitle,
313
- description: message,
314
- status: escalate ? 'human_assigned' : 'ai_responding',
315
- design_schema: schemas.ticketSchema,
316
- validation_schema: schemas.ticketValidation,
317
- data: {
318
- status: escalate ? 'human_assigned' : 'ai_responding',
319
- aim_triage_agent_id: ids.TRIAGE_AGENT_ID,
320
- aim_confidence_threshold: promptConfig.confidence_threshold || CONFIDENCE_THRESHOLD,
321
- aim_confidence_at_response: envelope.confidence,
322
- aim_escalation_reason: escalate ? envelope.escalation_reason : 'none',
323
- },
324
- })
325
- .select('id')
326
- .single()
327
-
328
- if (ticketError || !ticket) throw new Error(`Failed to create ticket: ${ticketError?.message}`)
329
- const ticketId = ticket.id
330
-
331
- // Create external thread with agent routing in data
332
- const { data: thread, error: threadError } = await adminDb
333
- .from('threads')
334
- .insert({
335
- type_id: ids.THREAD_TYPE_ID,
336
- account_id,
337
- target_type: 'items',
338
- target_id: ticketId,
339
- visibility: 'external',
340
- status: 'active',
341
- design_schema: schemas.threadSchema,
342
- validation_schema: schemas.threadValidation,
343
- data: {
344
- agent_id: ids.TRIAGE_AGENT_ID,
345
- prompt_config_id: ids.PROMPT_CONFIG_ID,
346
- },
347
- })
348
- .select('id')
349
- .single()
350
-
351
- if (threadError || !thread) throw new Error(`Failed to create thread: ${threadError?.message}`)
352
- const threadId = thread.id
353
-
354
- // Post customer's inbound message (public)
355
- const nextSeq = 1
356
- const { data: customerMsg, error: custMsgErr } = await adminDb
357
- .from('messages')
358
- .insert({
359
- type_id: ids.MESSAGE_TYPE_ID,
360
- thread_id: threadId,
361
- account_id,
362
- person_id,
363
- content: message,
364
- direction: 'inbound',
365
- visibility: 'public',
366
- sequence: nextSeq,
367
- design_schema: schemas.messageSchema,
368
- validation_schema: schemas.messageValidation,
369
- data: { message_type: 'human' },
370
- })
371
- .select('id')
372
- .single()
373
-
374
- if (custMsgErr || !customerMsg) throw new Error(`Failed to save customer message: ${custMsgErr?.message}`)
375
-
376
- // Post AI public response (outbound, public)
377
- const publicContent = escalate
378
- ? "We're looking into this and will have a human response shortly."
379
- : envelope.public_response
380
-
381
- const { data: publicMsg, error: pubMsgErr } = await adminDb
382
- .from('messages')
383
- .insert({
384
- type_id: ids.MESSAGE_TYPE_ID,
385
- thread_id: threadId,
386
- account_id,
387
- content: publicContent,
388
- direction: 'outbound',
389
- visibility: 'public',
390
- sequence: nextSeq + 1,
391
- design_schema: schemas.messageSchema,
392
- validation_schema: schemas.messageValidation,
393
- data: {
394
- message_type: 'agent',
395
- agent_id: ids.TRIAGE_AGENT_ID,
396
- confidence: envelope.confidence,
397
- escalated: escalate,
398
- },
399
- })
400
- .select('id')
401
- .single()
402
-
403
- if (pubMsgErr || !publicMsg) throw new Error(`Failed to save public AI message: ${pubMsgErr?.message}`)
404
-
405
- // Post internal AI note (outbound, internal) — full audit data
406
- const internalPayload = {
407
- turn: 1,
408
- prompt_sent: [{ role: 'system', content: systemPrompt }, ...promptMessages],
409
- kb_sources_retrieved: kbSources.map((s) => ({ id: s.document_id, content: s.content?.slice(0, 100), relevance: null })),
410
- prior_tickets_retrieved: priorTickets.map((t) => ({ id: t.id, title: t.title })),
411
- ai_raw_response: envelope,
412
- confidence: envelope.confidence,
413
- confidence_reasoning: envelope.confidence_reasoning,
414
- escalation_decision: escalate ? envelope.escalation_reason : 'none',
415
- model,
416
- temperature,
417
- latency_ms,
418
- token_usage,
419
- suggested_title: suggestedTitle,
420
- }
421
-
422
- const { data: internalMsg, error: intMsgErr } = await adminDb
423
- .from('messages')
424
- .insert({
425
- type_id: ids.MESSAGE_TYPE_ID,
426
- thread_id: threadId,
427
- account_id,
428
- content: `[AI Internal] Turn 1 — Confidence: ${envelope.confidence.toFixed(2)} — ${envelope.confidence_reasoning}`,
429
- direction: 'outbound',
430
- visibility: 'internal',
431
- sequence: nextSeq + 2,
432
- design_schema: schemas.messageSchema,
433
- validation_schema: schemas.messageValidation,
434
- data: {
435
- message_type: 'agent_internal',
436
- ai_internal: internalPayload,
437
- },
438
- })
439
- .select('id')
440
- .single()
441
-
442
- if (intMsgErr || !internalMsg) throw new Error(`Failed to save internal AI message: ${intMsgErr?.message}`)
443
-
444
- return {
445
- ticketId,
446
- threadId,
447
- publicMessageId: publicMsg.id,
448
- internalMessageId: internalMsg.id,
449
- public_response: publicContent,
450
- confidence: envelope.confidence,
451
- escalated: escalate,
452
- escalation_reason: escalate ? envelope.escalation_reason : 'none',
453
- suggested_title: suggestedTitle,
454
- }
455
- }
456
-
457
- // ─── ACTION: REPLY (multi-turn) ───────────────────────────────────────────────
458
-
459
- async function handleReply(ctx: any, body: any): Promise<TriageResult> {
460
- const { message, thread_id, ticket_id } = body
461
- const account_id = ctx.accountId as string
462
- const person_id = ctx.principal?.id as string
463
- if (!message || !thread_id || !ticket_id) throw new Error('message, thread_id, and ticket_id are required')
464
- if (!account_id || !person_id) throw new Error('User context (account + person) required')
465
-
466
- const ids = await resolveIds()
467
-
468
- // Load agent + prompt config + type schemas
469
- const [{ data: agent }, { data: promptConfig }, schemas] = await Promise.all([
470
- adminDb.from('ai_agents').select('*').eq('id', ids.TRIAGE_AGENT_ID).single(),
471
- adminDb.from('prompt_configs').select('*').eq('id', ids.PROMPT_CONFIG_ID).single(),
472
- loadTypeSchemas(ids),
473
- ])
474
- if (!agent || !promptConfig) throw new Error('Triage agent configuration not found')
475
-
476
- const modelConfig = agent.model_config || {}
477
- const model = modelConfig.model || process.env.LLM_DEFAULT_MODEL || 'gpt-4o'
478
- const temperature = modelConfig.temperature ?? 0.7
479
-
480
- // Load existing conversation history (public messages only)
481
- const history = await getConversationHistory(thread_id)
482
- const turnNumber = history.filter((m) => m.direction === 'inbound').length + 1
483
-
484
- // Get current message count for sequencing
485
- const { count: msgCount } = await adminDb
486
- .from('messages')
487
- .select('id', { count: 'exact', head: true })
488
- .eq('thread_id', thread_id)
489
- const nextSeq = (msgCount || 0) + 1
490
-
491
- // Post customer message first (so it shows immediately)
492
- const { data: customerMsg, error: custMsgErr } = await adminDb
493
- .from('messages')
494
- .insert({
495
- type_id: ids.MESSAGE_TYPE_ID,
496
- thread_id,
497
- account_id,
498
- person_id,
499
- content: message,
500
- direction: 'inbound',
501
- visibility: 'public',
502
- sequence: nextSeq,
503
- design_schema: schemas.messageSchema,
504
- validation_schema: schemas.messageValidation,
505
- data: { message_type: 'human' },
506
- })
507
- .select('id')
508
- .single()
509
-
510
- if (custMsgErr || !customerMsg) throw new Error(`Failed to save customer message: ${custMsgErr?.message}`)
511
-
512
- // Run AI with full history context
513
- const kbSources = await searchKB(message, account_id, ids)
514
- const priorTickets = await getPriorTickets(account_id, person_id, ids)
515
- const systemPrompt = buildSystemPrompt(
516
- agent.system_prompt,
517
- promptConfig.context_template.replace('{{user_message}}', message),
518
- kbSources,
519
- priorTickets,
520
- history
521
- )
522
-
523
- const promptMessages: Array<{ role: 'user' | 'assistant'; content: string }> = [
524
- ...history.map((m) => ({
525
- role: (m.direction === 'inbound' ? 'user' : 'assistant') as 'user' | 'assistant',
526
- content: m.content,
527
- })),
528
- { role: 'user', content: message },
529
- ]
530
-
531
- const { envelope, latency_ms, token_usage } = await callOpenAI(systemPrompt, promptMessages, model, temperature)
532
-
533
- const escalate = envelope.escalate || envelope.confidence < (promptConfig.confidence_threshold || CONFIDENCE_THRESHOLD)
534
- const publicContent = escalate
535
- ? "We're looking into this and will have a human response shortly."
536
- : envelope.public_response
537
-
538
- // Post AI public response
539
- const { data: publicMsg, error: pubMsgErr } = await adminDb
540
- .from('messages')
541
- .insert({
542
- type_id: ids.MESSAGE_TYPE_ID,
543
- thread_id,
544
- account_id,
545
- content: publicContent,
546
- direction: 'outbound',
547
- visibility: 'public',
548
- sequence: nextSeq + 1,
549
- design_schema: schemas.messageSchema,
550
- validation_schema: schemas.messageValidation,
551
- data: {
552
- message_type: 'agent',
553
- agent_id: ids.TRIAGE_AGENT_ID,
554
- confidence: envelope.confidence,
555
- escalated: escalate,
556
- },
557
- })
558
- .select('id')
559
- .single()
560
-
561
- if (pubMsgErr || !publicMsg) throw new Error(`Failed to save public AI message: ${pubMsgErr?.message}`)
562
-
563
- // Post internal note
564
- const internalPayload = {
565
- turn: turnNumber,
566
- prompt_sent: [{ role: 'system', content: systemPrompt }, ...promptMessages],
567
- kb_sources_retrieved: kbSources.map((s) => ({ id: s.document_id, content: s.content?.slice(0, 100), relevance: null })),
568
- prior_tickets_retrieved: priorTickets.map((t) => ({ id: t.id, title: t.title })),
569
- ai_raw_response: envelope,
570
- confidence: envelope.confidence,
571
- confidence_reasoning: envelope.confidence_reasoning,
572
- escalation_decision: escalate ? envelope.escalation_reason : 'none',
573
- model,
574
- temperature,
575
- latency_ms,
576
- token_usage,
577
- }
578
-
579
- const { data: internalMsg, error: intMsgErr } = await adminDb
580
- .from('messages')
581
- .insert({
582
- type_id: ids.MESSAGE_TYPE_ID,
583
- thread_id,
584
- account_id,
585
- content: `[AI Internal] Turn ${turnNumber} — Confidence: ${envelope.confidence.toFixed(2)} — ${envelope.confidence_reasoning}`,
586
- direction: 'outbound',
587
- visibility: 'internal',
588
- sequence: nextSeq + 2,
589
- design_schema: schemas.messageSchema,
590
- validation_schema: schemas.messageValidation,
591
- data: {
592
- message_type: 'agent_internal',
593
- ai_internal: internalPayload,
594
- },
595
- })
596
- .select('id')
597
- .single()
598
-
599
- if (intMsgErr || !internalMsg) throw new Error(`Failed to save internal AI message: ${intMsgErr?.message}`)
600
-
601
- // Update ticket status if escalated
602
- if (escalate) {
603
- const { data: currentTicket } = await adminDb
604
- .from('items')
605
- .select('data')
606
- .eq('id', ticket_id)
607
- .single()
608
-
609
- await adminDb
610
- .from('items')
611
- .update({
612
- status: 'human_assigned',
613
- data: {
614
- ...(currentTicket?.data || {}),
615
- status: 'human_assigned',
616
- aim_confidence_at_response: envelope.confidence,
617
- aim_escalation_reason: envelope.escalation_reason,
618
- },
619
- updated_at: new Date().toISOString(),
620
- })
621
- .eq('id', ticket_id)
622
- }
623
-
624
- return {
625
- ticketId: ticket_id,
626
- threadId: thread_id,
627
- publicMessageId: publicMsg.id,
628
- internalMessageId: internalMsg.id,
629
- public_response: publicContent,
630
- confidence: envelope.confidence,
631
- escalated: escalate,
632
- escalation_reason: escalate ? envelope.escalation_reason : 'none',
633
- }
634
- }
635
-
636
- // ─── HANDLER ──────────────────────────────────────────────────────────────────
637
-
638
- export const handler = createHandler(async (ctx, body) => {
639
- const action = ctx.query?.action
640
-
641
- switch (action) {
642
- case 'new_ticket':
643
- return await handleNewTicket(ctx, body)
644
- case 'reply':
645
- return await handleReply(ctx, body)
646
- default:
647
- throw new Error(`Unknown action: ${action}. Use 'new_ticket' or 'reply'.`)
648
- }
649
- })