spine-framework-cortex 0.2.20 → 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 +292 -0
- package/functions/custom_cortex-chunks.ts +52 -0
- package/functions/custom_kb-chunker.ts +576 -0
- package/functions/custom_kb-embeddings.ts +579 -0
- package/functions/custom_support-redaction.ts +115 -0
- package/functions/custom_support-solution.ts +104 -0
- package/manifest.json +10 -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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHandler } from './_shared/middleware'
|
|
2
|
+
import { adminDb } from './_shared/db'
|
|
3
|
+
import { resolveTypeId, resolveAgentId, resolvePromptConfigId } from './_shared/resolve-ids'
|
|
4
|
+
import { runAgent } from './_shared/index'
|
|
5
|
+
import { create } from './admin-data'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @module custom_support-solution
|
|
9
|
+
* @layer custom/cortex
|
|
10
|
+
* @stability custom
|
|
11
|
+
*
|
|
12
|
+
* Solution AI — internal AI collaborator for support agents working a case.
|
|
13
|
+
* Powered by core's runAgent() with the 'solution_ai' prompt config.
|
|
14
|
+
* Never visible to the customer (all messages are visibility='internal').
|
|
15
|
+
*
|
|
16
|
+
* Two actions:
|
|
17
|
+
* POST ?action=start — ensures an internal AI thread exists for the ticket,
|
|
18
|
+
* sends the first message, returns thread + agent message.
|
|
19
|
+
* POST ?action=message — sends a follow-up message on the existing AI thread.
|
|
20
|
+
*
|
|
21
|
+
* Response: { threadId, agentMessageId, content }
|
|
22
|
+
*
|
|
23
|
+
* The AI thread is distinct from the regular internal thread: it is tagged with
|
|
24
|
+
* data.thread_purpose = 'solution_ai' so the UI can find it deterministically.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ─── ACTION: START ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
async function handleStart(ctx: any, body: any) {
|
|
30
|
+
const { ticket_id, message } = body
|
|
31
|
+
if (!ticket_id) throw new Error('ticket_id is required')
|
|
32
|
+
if (!message) throw new Error('message is required')
|
|
33
|
+
|
|
34
|
+
const [threadTypeId, solutionAgentId, promptConfigId] = await Promise.all([
|
|
35
|
+
resolveTypeId('thread', 'thread'),
|
|
36
|
+
resolveAgentId('Solution AI Agent'),
|
|
37
|
+
resolvePromptConfigId('solution_ai'),
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
// Find existing solution AI thread for this ticket
|
|
41
|
+
const { data: existing } = await adminDb
|
|
42
|
+
.from('threads')
|
|
43
|
+
.select('id')
|
|
44
|
+
.eq('target_type', 'items')
|
|
45
|
+
.eq('target_id', ticket_id)
|
|
46
|
+
.eq('visibility', 'internal')
|
|
47
|
+
.eq('data->>thread_purpose', 'solution_ai')
|
|
48
|
+
.maybeSingle()
|
|
49
|
+
|
|
50
|
+
let threadId = existing?.id
|
|
51
|
+
|
|
52
|
+
if (!threadId) {
|
|
53
|
+
const thread = await create(ctx, {
|
|
54
|
+
entity: 'threads',
|
|
55
|
+
type_id: threadTypeId,
|
|
56
|
+
target_type: 'items',
|
|
57
|
+
target_id: ticket_id,
|
|
58
|
+
visibility: 'internal',
|
|
59
|
+
status: 'active',
|
|
60
|
+
data: {
|
|
61
|
+
thread_purpose: 'solution_ai',
|
|
62
|
+
agent_id: solutionAgentId,
|
|
63
|
+
prompt_config_id: promptConfigId,
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
if (!thread?.id) throw new Error('Failed to create solution AI thread')
|
|
67
|
+
threadId = thread.id
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const agentMsg = await runAgent(threadId, message, ctx)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
threadId,
|
|
74
|
+
agentMessageId: agentMsg?.id,
|
|
75
|
+
content: agentMsg?.content || '',
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── ACTION: MESSAGE ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async function handleMessage(ctx: any, body: any) {
|
|
82
|
+
const { thread_id, message } = body
|
|
83
|
+
if (!thread_id) throw new Error('thread_id is required')
|
|
84
|
+
if (!message) throw new Error('message is required')
|
|
85
|
+
|
|
86
|
+
const agentMsg = await runAgent(thread_id, message, ctx)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
threadId: thread_id,
|
|
90
|
+
agentMessageId: agentMsg?.id,
|
|
91
|
+
content: agentMsg?.content || '',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── HANDLER ──────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export const handler = createHandler(async (ctx, body) => {
|
|
98
|
+
const action = (ctx as any).query?.action
|
|
99
|
+
switch (action) {
|
|
100
|
+
case 'start': return handleStart(ctx, body)
|
|
101
|
+
case 'message': return handleMessage(ctx, body)
|
|
102
|
+
default: throw new Error(`Unknown action: ${action}. Use 'start' or 'message'.`)
|
|
103
|
+
}
|
|
104
|
+
})
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "Cortex",
|
|
3
3
|
"slug": "cortex",
|
|
4
4
|
"description": "Unified workspace for CRM, Support, Community, and Knowledge Base",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.22",
|
|
6
6
|
"app_type": "full",
|
|
7
7
|
"route_prefix": "/cortex",
|
|
8
8
|
"required_roles": ["support", "support_admin"],
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"/crm/activity",
|
|
20
20
|
"/support",
|
|
21
21
|
"/support/:id",
|
|
22
|
+
"/support/:id/kb-review",
|
|
22
23
|
"/community",
|
|
23
24
|
"/kb",
|
|
24
25
|
"/kb/editor",
|
|
@@ -86,13 +87,17 @@
|
|
|
86
87
|
],
|
|
87
88
|
"features": ["crm", "support", "community", "kb", "courses", "intelligence"],
|
|
88
89
|
"dependencies": ["items", "accounts", "pipelines", "integrations"],
|
|
90
|
+
"prod_domain": "cortex.spine-framework.com",
|
|
89
91
|
"entry_point": "./index.tsx",
|
|
90
92
|
"sidebar_component": "./components/CortexSidebar.tsx",
|
|
91
93
|
"seed": [
|
|
92
|
-
{ "file": "seed/roles.json", "table": "roles",
|
|
93
|
-
{ "file": "seed/types.json", "table": "types",
|
|
94
|
-
{ "file": "seed/link-types.json", "table": "link_types",
|
|
95
|
-
{ "file": "seed/
|
|
94
|
+
{ "file": "seed/roles.json", "table": "roles", "conflict": "app_id,slug" },
|
|
95
|
+
{ "file": "seed/types.json", "table": "types", "conflict": "app_id,kind,slug" },
|
|
96
|
+
{ "file": "seed/link-types.json", "table": "link_types", "conflict": "app_id,slug" },
|
|
97
|
+
{ "file": "seed/ai-agents.json", "table": "ai_agents", "conflict": "app_id,name", "inject_app_id": true },
|
|
98
|
+
{ "file": "seed/prompt-configs.json", "table": "prompt_configs","conflict": "app_id,slug", "inject_app_id": true },
|
|
99
|
+
{ "file": "seed/pipelines.json", "table": "pipelines", "conflict": "app_id,name", "inject_app_id": true },
|
|
100
|
+
{ "file": "seed/type_permissions.json", "table": "types", "conflict": "kind,slug", "inject_app_id": false, "permissions_patch": true }
|
|
96
101
|
],
|
|
97
102
|
"registration": {
|
|
98
103
|
"enabled": true,
|
package/package.json
CHANGED
|
@@ -39,13 +39,19 @@ interface Ticket {
|
|
|
39
39
|
title: string
|
|
40
40
|
description?: string
|
|
41
41
|
data?: {
|
|
42
|
+
// Case analysis fields (populated by custom_case_analysis)
|
|
43
|
+
ca_reported_issue?: string
|
|
44
|
+
ca_true_problem?: string
|
|
45
|
+
ca_final_solution?: string
|
|
46
|
+
ca_diagnostic_steps?: string[]
|
|
47
|
+
ca_solution_steps?: string[]
|
|
48
|
+
// Legacy fields
|
|
42
49
|
ai_metadata?: {
|
|
43
50
|
problem_statement?: string
|
|
44
51
|
solution_path?: string
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
53
|
+
// KB tracking
|
|
54
|
+
kb_proposed_kb_id?: string
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -118,12 +124,11 @@ export default function RedactionReview() {
|
|
|
118
124
|
setArticleTitle(`KB: ${ticketData.title}`)
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
// Trigger redaction analysis via
|
|
122
|
-
const analysisRes = await apiFetch('/
|
|
127
|
+
// Trigger redaction analysis via KB Redaction Agent
|
|
128
|
+
const analysisRes = await apiFetch('/.netlify/functions/custom_support-redaction?action=analyze', {
|
|
123
129
|
method: 'POST',
|
|
124
130
|
headers: { 'Content-Type': 'application/json' },
|
|
125
131
|
body: JSON.stringify({
|
|
126
|
-
action: 'run_redaction_analysis',
|
|
127
132
|
ticket_id: id,
|
|
128
133
|
content: buildContentForAnalysis(ticketData),
|
|
129
134
|
}),
|
|
@@ -155,16 +160,33 @@ export default function RedactionReview() {
|
|
|
155
160
|
})
|
|
156
161
|
}, [id])
|
|
157
162
|
|
|
158
|
-
// Build content from ticket for analysis
|
|
163
|
+
// Build content from ticket for redaction analysis.
|
|
164
|
+
// Prefers case analysis fields (ca_*) populated by custom_case_analysis,
|
|
165
|
+
// falling back to legacy ai_metadata fields.
|
|
159
166
|
function buildContentForAnalysis(ticket: Ticket): string {
|
|
167
|
+
const d = ticket.data || {}
|
|
160
168
|
const parts: string[] = []
|
|
161
169
|
if (ticket.title) parts.push(`Title: ${ticket.title}`)
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
if (d.ca_reported_issue) {
|
|
171
|
+
parts.push(`Reported Issue: ${d.ca_reported_issue}`)
|
|
172
|
+
} else if (ticket.description) {
|
|
173
|
+
parts.push(`Description: ${ticket.description}`)
|
|
174
|
+
}
|
|
175
|
+
if (d.ca_true_problem) {
|
|
176
|
+
parts.push(`Root Cause: ${d.ca_true_problem}`)
|
|
177
|
+
} else if (d.ai_metadata?.problem_statement) {
|
|
178
|
+
parts.push(`Problem: ${d.ai_metadata.problem_statement}`)
|
|
179
|
+
}
|
|
180
|
+
if (d.ca_diagnostic_steps?.length) {
|
|
181
|
+
parts.push(`Diagnostic Steps:\n${d.ca_diagnostic_steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`)
|
|
165
182
|
}
|
|
166
|
-
if (
|
|
167
|
-
parts.push(`Solution
|
|
183
|
+
if (d.ca_solution_steps?.length) {
|
|
184
|
+
parts.push(`Solution Steps:\n${d.ca_solution_steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`)
|
|
185
|
+
}
|
|
186
|
+
if (d.ca_final_solution) {
|
|
187
|
+
parts.push(`Final Solution: ${d.ca_final_solution}`)
|
|
188
|
+
} else if (d.ai_metadata?.solution_path) {
|
|
189
|
+
parts.push(`Solution: ${d.ai_metadata.solution_path}`)
|
|
168
190
|
}
|
|
169
191
|
return parts.join('\n\n')
|
|
170
192
|
}
|
|
@@ -279,16 +301,12 @@ export default function RedactionReview() {
|
|
|
279
301
|
|
|
280
302
|
// Update ticket with KB reference
|
|
281
303
|
await apiFetch(`/api/admin-data?action=update&entity=items&id=${id}`, {
|
|
282
|
-
method: '
|
|
304
|
+
method: 'PATCH',
|
|
283
305
|
headers: { 'Content-Type': 'application/json' },
|
|
284
306
|
body: JSON.stringify({
|
|
285
307
|
data: {
|
|
286
308
|
...ticket?.data,
|
|
287
|
-
|
|
288
|
-
...ticket?.data?.postmortem,
|
|
289
|
-
kb_generated: true,
|
|
290
|
-
kb_draft_id: kbId,
|
|
291
|
-
},
|
|
309
|
+
kb_proposed_kb_id: kbId,
|
|
292
310
|
},
|
|
293
311
|
}),
|
|
294
312
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { apiFetch } from '@core/lib/api'
|
|
4
4
|
import { Button } from '@core/components/ui/button'
|
|
@@ -7,7 +7,8 @@ import { Badge } from '@core/components/ui/badge'
|
|
|
7
7
|
import { Skeleton } from '@core/components/ui/skeleton'
|
|
8
8
|
import { ScrollArea } from '@core/components/ui/scroll-area'
|
|
9
9
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@core/components/ui/tabs'
|
|
10
|
-
import {
|
|
10
|
+
import { Textarea } from '@core/components/ui/textarea'
|
|
11
|
+
import { ArrowLeft, Send, Lock, Globe, Bot, CheckCircle, AlertCircle, FileText, Building2, CreditCard, Package, BookOpen, Eye, EyeOff, Brain, Clock, TrendingUp, Tag as TagIcon, Sparkles } from 'lucide-react'
|
|
11
12
|
import { useAuth } from '@core/contexts/AuthContext'
|
|
12
13
|
|
|
13
14
|
interface Ticket {
|
|
@@ -367,145 +368,143 @@ function CaseDataPanel({ ticket }: { ticket: Ticket | null }) {
|
|
|
367
368
|
)
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
// AI
|
|
371
|
-
|
|
372
|
-
if (!ticket?.data?.aim_confidence_threshold && !ticket?.data?.aim_confidence_at_response && !ticket?.data?.aim_escalation_reason) {
|
|
373
|
-
return (
|
|
374
|
-
<div className="h-full flex flex-col">
|
|
375
|
-
<div className="px-4 py-2 border-b border-border bg-purple-50">
|
|
376
|
-
<p className="text-xs font-medium uppercase tracking-wide text-purple-700">AI Analysis</p>
|
|
377
|
-
</div>
|
|
378
|
-
<div className="flex-1 p-4 flex items-center justify-center text-sm text-muted-foreground">
|
|
379
|
-
No AI metadata available for this ticket.
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
)
|
|
383
|
-
}
|
|
371
|
+
// Solution AI Panel — internal AI collaborator for support agents
|
|
372
|
+
interface AIMessage { role: 'user' | 'ai'; content: string }
|
|
384
373
|
|
|
385
|
-
|
|
386
|
-
const
|
|
374
|
+
function SolutionAIPanel({ ticket, onGenerateKB }: { ticket: Ticket | null; onGenerateKB?: () => void }) {
|
|
375
|
+
const [messages, setMessages] = useState<AIMessage[]>([])
|
|
376
|
+
const [input, setInput] = useState('')
|
|
377
|
+
const [sending, setSending] = useState(false)
|
|
378
|
+
const [threadId, setThreadId] = useState<string | null>(null)
|
|
379
|
+
const bottomRef = useRef<HTMLDivElement>(null)
|
|
380
|
+
const ai = ticket?.data
|
|
381
|
+
const isResolved = ticket?.status === 'resolved' || ticket?.status === 'closed'
|
|
382
|
+
|
|
383
|
+
// Auto-scroll to bottom when new messages arrive
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
386
|
+
}, [messages])
|
|
387
|
+
|
|
388
|
+
const sendMessage = async () => {
|
|
389
|
+
if (!input.trim() || !ticket?.id || sending) return
|
|
390
|
+
const userText = input.trim()
|
|
391
|
+
setInput('')
|
|
392
|
+
setMessages(prev => [...prev, { role: 'user', content: userText }])
|
|
393
|
+
setSending(true)
|
|
394
|
+
try {
|
|
395
|
+
const isFirst = !threadId
|
|
396
|
+
const res = await apiFetch(
|
|
397
|
+
`/.netlify/functions/custom_support-solution?action=${isFirst ? 'start' : 'message'}`,
|
|
398
|
+
{
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
401
|
+
body: JSON.stringify(isFirst
|
|
402
|
+
? { ticket_id: ticket.id, message: userText }
|
|
403
|
+
: { thread_id: threadId, message: userText }
|
|
404
|
+
),
|
|
405
|
+
}
|
|
406
|
+
).then(r => r.json())
|
|
407
|
+
|
|
408
|
+
if (res.threadId && !threadId) setThreadId(res.threadId)
|
|
409
|
+
if (res.content) setMessages(prev => [...prev, { role: 'ai', content: res.content }])
|
|
410
|
+
} catch (err) {
|
|
411
|
+
setMessages(prev => [...prev, { role: 'ai', content: 'Error contacting Solution AI. Please try again.' }])
|
|
412
|
+
} finally {
|
|
413
|
+
setSending(false)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
387
416
|
|
|
388
417
|
return (
|
|
389
418
|
<div className="h-full flex flex-col">
|
|
390
|
-
|
|
391
|
-
|
|
419
|
+
{/* Header */}
|
|
420
|
+
<div className="px-4 py-2 border-b border-border bg-purple-50 shrink-0">
|
|
421
|
+
<div className="flex items-center justify-between">
|
|
422
|
+
<div className="flex items-center gap-1.5">
|
|
423
|
+
<Sparkles className="h-3.5 w-3.5 text-purple-600" />
|
|
424
|
+
<p className="text-xs font-medium uppercase tracking-wide text-purple-700">Solution AI</p>
|
|
425
|
+
</div>
|
|
426
|
+
<p className="text-xs text-purple-500">Internal only</p>
|
|
427
|
+
</div>
|
|
392
428
|
</div>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
429
|
+
|
|
430
|
+
{/* Triage metadata strip */}
|
|
431
|
+
{(ai?.aim_confidence_at_response || ai?.aim_escalation_reason) && (
|
|
432
|
+
<div className="px-3 py-1.5 border-b border-border bg-muted/40 shrink-0 flex items-center gap-3 text-xs text-muted-foreground">
|
|
396
433
|
{ai.aim_confidence_at_response && (
|
|
397
|
-
<
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<span className={`font-mono font-medium ${ai.aim_confidence_at_response >= 0.75 ? 'text-green-600' : 'text-amber-600'}`}>
|
|
401
|
-
{Math.round(ai.aim_confidence_at_response * 100)}%
|
|
402
|
-
</span>
|
|
403
|
-
</div>
|
|
404
|
-
{ai.aim_confidence_threshold && (
|
|
405
|
-
<p className="text-xs text-muted-foreground">Threshold: {Math.round(ai.aim_confidence_threshold * 100)}%</p>
|
|
406
|
-
)}
|
|
407
|
-
</div>
|
|
434
|
+
<span className={ai.aim_confidence_at_response >= 0.75 ? 'text-green-600' : 'text-amber-600'}>
|
|
435
|
+
Triage confidence: {Math.round(ai.aim_confidence_at_response * 100)}%
|
|
436
|
+
</span>
|
|
408
437
|
)}
|
|
409
|
-
|
|
410
438
|
{ai.aim_escalation_reason && ai.aim_escalation_reason !== 'none' && (
|
|
411
|
-
<
|
|
412
|
-
<AlertCircle className="h-
|
|
413
|
-
|
|
414
|
-
<p className="font-medium text-amber-700">Escalated</p>
|
|
415
|
-
<p className="text-amber-600 text-xs">{ai.aim_escalation_reason.replace('_', ' ')}</p>
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
)}
|
|
419
|
-
|
|
420
|
-
{/* Problem Statement */}
|
|
421
|
-
{ai.aim_problem_statement && (
|
|
422
|
-
<div className="space-y-1">
|
|
423
|
-
<p className="text-xs font-medium text-muted-foreground uppercase">Problem</p>
|
|
424
|
-
<p className="text-sm bg-muted p-2 rounded">{ai.aim_problem_statement}</p>
|
|
425
|
-
</div>
|
|
439
|
+
<span className="text-amber-600 flex items-center gap-1">
|
|
440
|
+
<AlertCircle className="h-3 w-3" /> Escalated
|
|
441
|
+
</span>
|
|
426
442
|
)}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
|
|
446
|
+
{/* Chat history */}
|
|
447
|
+
<ScrollArea className="flex-1 px-3 py-2">
|
|
448
|
+
{messages.length === 0 ? (
|
|
449
|
+
<div className="flex flex-col items-center justify-center h-32 text-center text-muted-foreground text-xs gap-2">
|
|
450
|
+
<Bot className="h-6 w-6" />
|
|
451
|
+
<p>Ask Solution AI for help with this case.</p>
|
|
452
|
+
<p className="text-xs opacity-70">Searches KB · recalls similar cases · drafts replies</p>
|
|
453
|
+
</div>
|
|
454
|
+
) : (
|
|
455
|
+
<div className="space-y-2">
|
|
456
|
+
{messages.map((msg, i) => (
|
|
457
|
+
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
458
|
+
<div className={`max-w-[90%] rounded-lg px-3 py-2 text-xs whitespace-pre-wrap ${
|
|
459
|
+
msg.role === 'user'
|
|
460
|
+
? 'bg-primary text-primary-foreground'
|
|
461
|
+
: 'bg-muted text-foreground'
|
|
462
|
+
}`}>
|
|
463
|
+
{msg.content}
|
|
464
|
+
</div>
|
|
444
465
|
</div>
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
<>
|
|
451
|
-
<Separator />
|
|
452
|
-
<div className="space-y-3">
|
|
453
|
-
<p className="text-xs font-medium text-muted-foreground uppercase">Post-mortem</p>
|
|
454
|
-
|
|
455
|
-
{postmortem?.root_cause_category && (
|
|
456
|
-
<p className="text-sm">
|
|
457
|
-
<span className="text-muted-foreground">Root cause:</span>{' '}
|
|
458
|
-
{postmortem.root_cause_category}
|
|
459
|
-
</p>
|
|
460
|
-
)}
|
|
461
|
-
|
|
462
|
-
{postmortem?.resolution_time_minutes && (
|
|
463
|
-
<p className="text-sm">
|
|
464
|
-
<span className="text-muted-foreground">Resolution time:</span>{' '}
|
|
465
|
-
{Math.round(postmortem.resolution_time_minutes)} min
|
|
466
|
-
</p>
|
|
467
|
-
)}
|
|
468
|
-
|
|
469
|
-
{postmortem?.customer_satisfaction !== undefined && (
|
|
470
|
-
<div className="flex items-center gap-2">
|
|
471
|
-
<span className="text-sm text-muted-foreground">Satisfaction:</span>
|
|
472
|
-
<div className="flex">
|
|
473
|
-
{[1, 2, 3, 4, 5].map((star) => (
|
|
474
|
-
<span
|
|
475
|
-
key={star}
|
|
476
|
-
className={`text-lg ${star <= (postmortem.customer_satisfaction || 0) ? 'text-amber-400' : 'text-gray-300'}`}
|
|
477
|
-
>
|
|
478
|
-
★
|
|
479
|
-
</span>
|
|
480
|
-
))}
|
|
481
|
-
</div>
|
|
482
|
-
</div>
|
|
483
|
-
)}
|
|
484
|
-
|
|
485
|
-
{/* KB Generation */}
|
|
486
|
-
<div className="pt-2">
|
|
487
|
-
{postmortem?.kb_generated ? (
|
|
488
|
-
<div className="flex items-center gap-2 text-green-600 text-sm">
|
|
489
|
-
<CheckCircle className="h-4 w-4" />
|
|
490
|
-
KB article created
|
|
491
|
-
</div>
|
|
492
|
-
) : (
|
|
493
|
-
<Button
|
|
494
|
-
variant="outline"
|
|
495
|
-
size="sm"
|
|
496
|
-
className="w-full gap-1 text-xs"
|
|
497
|
-
onClick={onGenerateKB}
|
|
498
|
-
>
|
|
499
|
-
<BookOpen className="h-3 w-3" />
|
|
500
|
-
Generate KB Article
|
|
501
|
-
</Button>
|
|
502
|
-
)}
|
|
466
|
+
))}
|
|
467
|
+
{sending && (
|
|
468
|
+
<div className="flex justify-start">
|
|
469
|
+
<div className="bg-muted rounded-lg px-3 py-2 text-xs text-muted-foreground animate-pulse">
|
|
470
|
+
Thinking…
|
|
503
471
|
</div>
|
|
504
472
|
</div>
|
|
505
|
-
|
|
473
|
+
)}
|
|
474
|
+
<div ref={bottomRef} />
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</ScrollArea>
|
|
478
|
+
|
|
479
|
+
{/* KB generation (resolved only) */}
|
|
480
|
+
{isResolved && (
|
|
481
|
+
<div className="px-3 py-2 border-t border-border shrink-0">
|
|
482
|
+
{ai?.kb_proposed_kb_id ? (
|
|
483
|
+
<div className="flex items-center gap-2 text-green-600 text-xs">
|
|
484
|
+
<CheckCircle className="h-3.5 w-3.5" /> KB article created
|
|
485
|
+
</div>
|
|
486
|
+
) : (
|
|
487
|
+
<Button variant="outline" size="sm" className="w-full gap-1 text-xs" onClick={onGenerateKB}>
|
|
488
|
+
<BookOpen className="h-3 w-3" /> Generate KB Article
|
|
489
|
+
</Button>
|
|
506
490
|
)}
|
|
507
491
|
</div>
|
|
508
|
-
|
|
492
|
+
)}
|
|
493
|
+
|
|
494
|
+
{/* Input */}
|
|
495
|
+
<div className="px-3 py-2 border-t border-border shrink-0 flex gap-2">
|
|
496
|
+
<Textarea
|
|
497
|
+
value={input}
|
|
498
|
+
onChange={e => setInput(e.target.value)}
|
|
499
|
+
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() } }}
|
|
500
|
+
placeholder="Ask Solution AI…"
|
|
501
|
+
className="flex-1 text-xs resize-none min-h-[36px] max-h-[100px]"
|
|
502
|
+
rows={1}
|
|
503
|
+
/>
|
|
504
|
+
<Button size="sm" onClick={sendMessage} disabled={!input.trim() || sending} className="shrink-0">
|
|
505
|
+
<Send className="h-3.5 w-3.5" />
|
|
506
|
+
</Button>
|
|
507
|
+
</div>
|
|
509
508
|
</div>
|
|
510
509
|
)
|
|
511
510
|
}
|
|
@@ -838,7 +837,6 @@ export default function TicketDetailPage() {
|
|
|
838
837
|
}
|
|
839
838
|
|
|
840
839
|
const handleGenerateKB = () => {
|
|
841
|
-
// Trigger KB generation flow - would navigate to redaction review
|
|
842
840
|
navigate(`/cortex/support/${id}/kb-review`)
|
|
843
841
|
}
|
|
844
842
|
|
|
@@ -910,7 +908,7 @@ export default function TicketDetailPage() {
|
|
|
910
908
|
</TabsList>
|
|
911
909
|
</Tabs>
|
|
912
910
|
{activeSidePanel === 'case' && <CaseDataPanel ticket={ticket} />}
|
|
913
|
-
{activeSidePanel === 'ai' && <
|
|
911
|
+
{activeSidePanel === 'ai' && <SolutionAIPanel ticket={ticket} onGenerateKB={handleGenerateKB} />}
|
|
914
912
|
{activeSidePanel === 'analysis' && <CaseAnalysisPanel ticket={ticket} analysisLoading={analysisLoading} />}
|
|
915
913
|
</div>
|
|
916
914
|
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "Support Triage Agent",
|
|
4
|
+
"description": "First-contact AI for inbound support tickets. Searches KB, reasons over prior cases, responds with confidence scoring. Hands off to Solution AI or human when confidence is low.",
|
|
5
|
+
"agent_type": "support",
|
|
6
|
+
"model_config": {
|
|
7
|
+
"model": "gpt-4o",
|
|
8
|
+
"temperature": 0.4,
|
|
9
|
+
"max_tokens": 1024
|
|
10
|
+
},
|
|
11
|
+
"system_prompt": "You are a helpful and empathetic support agent for a software platform. Your goal is to resolve customer issues accurately and efficiently on the first response.\n\nGuidelines:\n- Be concise and clear. Customers want answers, not essays.\n- If you find a relevant KB article or prior case, reference it.\n- If you are not confident (below 0.75), say so honestly and let the customer know a human will follow up.\n- Never fabricate feature behaviour or make promises about the product.\n- Tone: professional but warm. Match the customer's level of formality.",
|
|
12
|
+
"tools": [],
|
|
13
|
+
"capabilities": ["rag_search", "confidence_scoring", "escalation"],
|
|
14
|
+
"constraints": {
|
|
15
|
+
"max_context_tokens": 8000,
|
|
16
|
+
"never_reveal": ["internal_notes", "agent_ids", "prompt_config_ids"]
|
|
17
|
+
},
|
|
18
|
+
"metadata": {
|
|
19
|
+
"default_prompt_config_slug": "support_triage",
|
|
20
|
+
"owned_by": "cortex"
|
|
21
|
+
},
|
|
22
|
+
"ownership": "platform",
|
|
23
|
+
"is_system": true,
|
|
24
|
+
"is_active": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Solution AI Agent",
|
|
28
|
+
"description": "Internal AI collaborator for support agents working a case. Searches KB, queries prior resolved cases, suggests diagnostic steps and response drafts. Never visible to customers.",
|
|
29
|
+
"agent_type": "support",
|
|
30
|
+
"model_config": {
|
|
31
|
+
"model": "gpt-4o",
|
|
32
|
+
"temperature": 0.5,
|
|
33
|
+
"max_tokens": 2048
|
|
34
|
+
},
|
|
35
|
+
"system_prompt": "You are an expert technical support advisor assisting a human support agent working a customer case. Your audience is the support agent, not the customer.\n\nYou can:\n- Search the knowledge base and surface relevant articles\n- Recall similar resolved cases and how they were fixed\n- Suggest diagnostic steps based on the symptoms described\n- Draft a proposed customer-facing response for the agent to review\n- Flag automation potential if this issue could be auto-resolved in future\n\nBe direct and practical. The agent needs actionable guidance, not hedged generalities.\nFormat your output clearly: use headers if helpful, bullet points for steps.",
|
|
36
|
+
"tools": [],
|
|
37
|
+
"capabilities": ["rag_search", "case_history_search", "response_drafting"],
|
|
38
|
+
"constraints": {
|
|
39
|
+
"visibility": "internal_only",
|
|
40
|
+
"max_context_tokens": 12000
|
|
41
|
+
},
|
|
42
|
+
"metadata": {
|
|
43
|
+
"default_prompt_config_slug": "solution_ai",
|
|
44
|
+
"owned_by": "cortex"
|
|
45
|
+
},
|
|
46
|
+
"ownership": "platform",
|
|
47
|
+
"is_system": true,
|
|
48
|
+
"is_active": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Case Resolution Analysis Agent",
|
|
52
|
+
"description": "Post-resolution analyst. Given a closed ticket and its full conversation (including internal AI collaboration thread), produces a structured postmortem: root cause, diagnostic path, solution, automation potential, and KB candidacy flag.",
|
|
53
|
+
"agent_type": "analysis",
|
|
54
|
+
"model_config": {
|
|
55
|
+
"model": "gpt-4o",
|
|
56
|
+
"temperature": 0.2,
|
|
57
|
+
"max_tokens": 2048
|
|
58
|
+
},
|
|
59
|
+
"system_prompt": "You are a case analysis specialist. When given a resolved support ticket — including the customer conversation, the internal support-agent collaboration thread, and any escalation notes — you produce a structured postmortem.\n\nYour output must be a JSON object with these exact fields:\n- reported_issue: what the customer described (their words)\n- true_problem: the actual root cause (may differ significantly from how it was reported)\n- diagnostic_steps: ordered array of steps taken to identify the root cause\n- solution_steps: ordered array of steps taken to resolve it\n- final_solution: one-sentence concise answer\n- customer_temperature: 'positive' | 'neutral' | 'frustrated' | 'escalated'\n- time_to_resolution: estimated minutes from first message to resolution\n- escalation_required: boolean\n- back_and_forth_count: integer — number of customer messages\n- sentiment_progression: array of sentiment labels across the conversation\n- automation_potential: 'high' | 'medium' | 'low' — could this be auto-resolved next time?\n- kb_candidate: boolean — would a KB article have prevented this ticket?\n- suggested_tags: array of tag objects { slug, name, purpose, category, applicable_to }\n- confidence_score: 0.0-1.0\n- analysis_summary: 2-3 sentence plain English summary for the ticket record",
|
|
60
|
+
"tools": [],
|
|
61
|
+
"capabilities": ["structured_output", "sentiment_analysis", "case_classification"],
|
|
62
|
+
"constraints": {
|
|
63
|
+
"output_format": "json",
|
|
64
|
+
"max_context_tokens": 16000
|
|
65
|
+
},
|
|
66
|
+
"metadata": {
|
|
67
|
+
"default_prompt_config_slug": "case_analysis_prompt",
|
|
68
|
+
"owned_by": "cortex"
|
|
69
|
+
},
|
|
70
|
+
"ownership": "platform",
|
|
71
|
+
"is_system": true,
|
|
72
|
+
"is_active": true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "KB Redaction Agent",
|
|
76
|
+
"description": "Prepares resolved cases for publication as KB articles. Identifies PII, account-specific details, internal references, and confidential data. Produces redaction suggestions and a generalised, customer-facing rewrite.",
|
|
77
|
+
"agent_type": "content",
|
|
78
|
+
"model_config": {
|
|
79
|
+
"model": "gpt-4o",
|
|
80
|
+
"temperature": 0.1,
|
|
81
|
+
"max_tokens": 3000
|
|
82
|
+
},
|
|
83
|
+
"system_prompt": "You are a knowledge base editorial agent. You receive the raw text of a resolved support case and must prepare it for public publication as a KB article.\n\nYour tasks:\n1. IDENTIFY all text that should be redacted before publishing. Categories:\n - pii: names, emails, phone numbers, addresses, usernames\n - account_specific: account names, account IDs, organisation-specific data\n - confidential: internal pricing, unreleased features, internal process details\n - internal_reference: ticket IDs, internal tool names, employee names\n\n2. GENERALISE the solution: rewrite the content to be universally applicable, removing all account-specific context while preserving the diagnostic and solution logic.\n\nOutput must be a JSON object:\n{\n \"original_content\": \"<the input text unchanged>\",\n \"redacted_content\": \"<fully redacted and generalised version>\",\n \"suggestions\": [\n {\n \"id\": \"<uuid>\",\n \"start_index\": <int>,\n \"end_index\": <int>,\n \"original_text\": \"<exact text to redact>\",\n \"redacted_text\": \"<replacement text, e.g. [Account Name]>\",\n \"sensitivity_level\": \"high|medium|low\",\n \"reasoning\": \"<one sentence why>\",\n \"category\": \"pii|confidential|account_specific|internal_reference\"\n }\n ],\n \"confidence_score\": 0.0-1.0,\n \"processing_metadata\": { \"model_used\": \"<model>\", \"temperature\": 0.1, \"tokens_consumed\": <int> }\n}",
|
|
84
|
+
"tools": [],
|
|
85
|
+
"capabilities": ["structured_output", "redaction", "content_generalisation"],
|
|
86
|
+
"constraints": {
|
|
87
|
+
"output_format": "json",
|
|
88
|
+
"max_context_tokens": 12000
|
|
89
|
+
},
|
|
90
|
+
"metadata": {
|
|
91
|
+
"default_prompt_config_slug": "kb_generator",
|
|
92
|
+
"owned_by": "cortex"
|
|
93
|
+
},
|
|
94
|
+
"ownership": "platform",
|
|
95
|
+
"is_system": true,
|
|
96
|
+
"is_active": true
|
|
97
|
+
}
|
|
98
|
+
]
|