@contractspec/lib.support-bot 1.57.0 → 1.58.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.
- package/dist/bot/auto-responder.d.ts +19 -23
- package/dist/bot/auto-responder.d.ts.map +1 -1
- package/dist/bot/auto-responder.js +86 -68
- package/dist/bot/feedback-loop.d.ts +13 -17
- package/dist/bot/feedback-loop.d.ts.map +1 -1
- package/dist/bot/feedback-loop.js +38 -34
- package/dist/bot/index.d.ts +4 -4
- package/dist/bot/index.d.ts.map +1 -0
- package/dist/bot/index.js +268 -4
- package/dist/bot/tools.d.ts +9 -13
- package/dist/bot/tools.d.ts.map +1 -1
- package/dist/bot/tools.js +118 -123
- package/dist/browser/bot/auto-responder.js +101 -0
- package/dist/browser/bot/feedback-loop.js +38 -0
- package/dist/browser/bot/index.js +268 -0
- package/dist/browser/bot/tools.js +131 -0
- package/dist/browser/index.js +517 -0
- package/dist/browser/rag/index.js +65 -0
- package/dist/browser/rag/ticket-resolver.js +65 -0
- package/dist/browser/spec.js +33 -0
- package/dist/browser/tickets/classifier.js +156 -0
- package/dist/browser/tickets/index.js +156 -0
- package/dist/browser/types.js +0 -0
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +518 -9
- package/dist/node/bot/auto-responder.js +101 -0
- package/dist/node/bot/feedback-loop.js +38 -0
- package/dist/node/bot/index.js +268 -0
- package/dist/node/bot/tools.js +131 -0
- package/dist/node/index.js +517 -0
- package/dist/node/rag/index.js +65 -0
- package/dist/node/rag/ticket-resolver.js +65 -0
- package/dist/node/spec.js +33 -0
- package/dist/node/tickets/classifier.js +156 -0
- package/dist/node/tickets/index.js +156 -0
- package/dist/node/types.js +0 -0
- package/dist/rag/index.d.ts +2 -2
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/index.js +66 -3
- package/dist/rag/ticket-resolver.d.ts +17 -21
- package/dist/rag/ticket-resolver.d.ts.map +1 -1
- package/dist/rag/ticket-resolver.js +65 -63
- package/dist/spec.d.ts +7 -11
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +31 -32
- package/dist/tickets/classifier.d.ts +18 -22
- package/dist/tickets/classifier.d.ts.map +1 -1
- package/dist/tickets/classifier.js +153 -195
- package/dist/tickets/index.d.ts +2 -2
- package/dist/tickets/index.d.ts.map +1 -0
- package/dist/tickets/index.js +156 -2
- package/dist/types.d.ts +62 -66
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +127 -38
- package/dist/bot/auto-responder.js.map +0 -1
- package/dist/bot/feedback-loop.js.map +0 -1
- package/dist/bot/tools.js.map +0 -1
- package/dist/rag/ticket-resolver.js.map +0 -1
- package/dist/spec.js.map +0 -1
- package/dist/tickets/classifier.js.map +0 -1
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
llm?: LLMProvider;
|
|
8
|
-
llmModel?: string;
|
|
1
|
+
import type { LLMProvider } from '@contractspec/lib.contracts/integrations/providers/llm';
|
|
2
|
+
import type { SupportTicket, TicketCategory, TicketClassification } from '../types';
|
|
3
|
+
export interface TicketClassifierOptions {
|
|
4
|
+
keywords?: Partial<Record<TicketCategory, string[]>>;
|
|
5
|
+
llm?: LLMProvider;
|
|
6
|
+
llmModel?: string;
|
|
9
7
|
}
|
|
10
|
-
declare class TicketClassifier {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
export declare class TicketClassifier {
|
|
9
|
+
private readonly keywords;
|
|
10
|
+
private readonly llm?;
|
|
11
|
+
private readonly llmModel?;
|
|
12
|
+
constructor(options?: TicketClassifierOptions);
|
|
13
|
+
classify(ticket: SupportTicket): Promise<TicketClassification>;
|
|
14
|
+
private heuristicClassification;
|
|
15
|
+
private detectCategory;
|
|
16
|
+
private detectPriority;
|
|
17
|
+
private detectSentiment;
|
|
18
|
+
private extractIntents;
|
|
19
|
+
private estimateConfidence;
|
|
22
20
|
}
|
|
23
|
-
//#endregion
|
|
24
|
-
export { TicketClassifier, TicketClassifierOptions };
|
|
25
21
|
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classifier.d.ts","
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../src/tickets/classifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wDAAwD,CAAC;AAC1F,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EAGd,oBAAoB,EACrB,MAAM,UAAU,CAAC;AAyBlB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACrD,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;gBAEvB,OAAO,CAAC,EAAE,uBAAuB;IASvC,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAiDpE,OAAO,CAAC,uBAAuB;IAqB/B,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,kBAAkB;CAW3B"}
|
|
@@ -1,199 +1,157 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
],
|
|
11
|
-
technical: [
|
|
12
|
-
"bug",
|
|
13
|
-
"error",
|
|
14
|
-
"crash",
|
|
15
|
-
"issue",
|
|
16
|
-
"failed",
|
|
17
|
-
"timeout"
|
|
18
|
-
],
|
|
19
|
-
product: [
|
|
20
|
-
"feature",
|
|
21
|
-
"roadmap",
|
|
22
|
-
"idea",
|
|
23
|
-
"request",
|
|
24
|
-
"feedback"
|
|
25
|
-
],
|
|
26
|
-
account: [
|
|
27
|
-
"login",
|
|
28
|
-
"password",
|
|
29
|
-
"2fa",
|
|
30
|
-
"account",
|
|
31
|
-
"profile",
|
|
32
|
-
"email change"
|
|
33
|
-
],
|
|
34
|
-
compliance: [
|
|
35
|
-
"kyc",
|
|
36
|
-
"aml",
|
|
37
|
-
"compliance",
|
|
38
|
-
"regulation",
|
|
39
|
-
"gdpr"
|
|
40
|
-
],
|
|
41
|
-
other: []
|
|
1
|
+
// @bun
|
|
2
|
+
// src/tickets/classifier.ts
|
|
3
|
+
var CATEGORY_KEYWORDS = {
|
|
4
|
+
billing: ["invoice", "payout", "refund", "charge", "billing", "payment"],
|
|
5
|
+
technical: ["bug", "error", "crash", "issue", "failed", "timeout"],
|
|
6
|
+
product: ["feature", "roadmap", "idea", "request", "feedback"],
|
|
7
|
+
account: ["login", "password", "2fa", "account", "profile", "email change"],
|
|
8
|
+
compliance: ["kyc", "aml", "compliance", "regulation", "gdpr"],
|
|
9
|
+
other: []
|
|
42
10
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"today",
|
|
49
|
-
"right away"
|
|
50
|
-
],
|
|
51
|
-
high: [
|
|
52
|
-
"high priority",
|
|
53
|
-
"blocking",
|
|
54
|
-
"major",
|
|
55
|
-
"critical"
|
|
56
|
-
],
|
|
57
|
-
medium: ["soon", "next few days"],
|
|
58
|
-
low: [
|
|
59
|
-
"nice to have",
|
|
60
|
-
"when possible",
|
|
61
|
-
"later"
|
|
62
|
-
]
|
|
11
|
+
var PRIORITY_HINTS = {
|
|
12
|
+
urgent: ["urgent", "asap", "immediately", "today", "right away"],
|
|
13
|
+
high: ["high priority", "blocking", "major", "critical"],
|
|
14
|
+
medium: ["soon", "next few days"],
|
|
15
|
+
low: ["nice to have", "when possible", "later"]
|
|
63
16
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"thank you"
|
|
70
|
-
],
|
|
71
|
-
neutral: [
|
|
72
|
-
"question",
|
|
73
|
-
"wonder",
|
|
74
|
-
"curious"
|
|
75
|
-
],
|
|
76
|
-
negative: [
|
|
77
|
-
"unhappy",
|
|
78
|
-
"bad",
|
|
79
|
-
"terrible",
|
|
80
|
-
"awful",
|
|
81
|
-
"angry"
|
|
82
|
-
],
|
|
83
|
-
frustrated: [
|
|
84
|
-
"furious",
|
|
85
|
-
"frustrated",
|
|
86
|
-
"fed up",
|
|
87
|
-
"ridiculous"
|
|
88
|
-
]
|
|
89
|
-
};
|
|
90
|
-
var TicketClassifier = class {
|
|
91
|
-
keywords;
|
|
92
|
-
llm;
|
|
93
|
-
llmModel;
|
|
94
|
-
constructor(options) {
|
|
95
|
-
this.keywords = {
|
|
96
|
-
...CATEGORY_KEYWORDS,
|
|
97
|
-
...options?.keywords ?? {}
|
|
98
|
-
};
|
|
99
|
-
this.llm = options?.llm;
|
|
100
|
-
this.llmModel = options?.llmModel;
|
|
101
|
-
}
|
|
102
|
-
async classify(ticket) {
|
|
103
|
-
const heuristics = this.heuristicClassification(ticket);
|
|
104
|
-
if (!this.llm) return heuristics;
|
|
105
|
-
try {
|
|
106
|
-
const content = (await this.llm.chat([{
|
|
107
|
-
role: "system",
|
|
108
|
-
content: [{
|
|
109
|
-
type: "text",
|
|
110
|
-
text: "Classify the support ticket."
|
|
111
|
-
}]
|
|
112
|
-
}, {
|
|
113
|
-
role: "user",
|
|
114
|
-
content: [{
|
|
115
|
-
type: "text",
|
|
116
|
-
text: JSON.stringify({
|
|
117
|
-
subject: ticket.subject,
|
|
118
|
-
body: ticket.body,
|
|
119
|
-
channel: ticket.channel
|
|
120
|
-
})
|
|
121
|
-
}]
|
|
122
|
-
}], {
|
|
123
|
-
responseFormat: "json",
|
|
124
|
-
model: this.llmModel
|
|
125
|
-
})).message.content.find((part) => "text" in part);
|
|
126
|
-
if (content && "text" in content) {
|
|
127
|
-
const parsed = JSON.parse(content.text);
|
|
128
|
-
return {
|
|
129
|
-
...heuristics,
|
|
130
|
-
...parsed,
|
|
131
|
-
intents: parsed.intents ?? heuristics.intents,
|
|
132
|
-
tags: parsed.tags ?? heuristics.tags
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
} catch {}
|
|
136
|
-
return heuristics;
|
|
137
|
-
}
|
|
138
|
-
heuristicClassification(ticket) {
|
|
139
|
-
const text = `${ticket.subject}\n${ticket.body}`.toLowerCase();
|
|
140
|
-
const category = this.detectCategory(text);
|
|
141
|
-
const priority = this.detectPriority(text);
|
|
142
|
-
const sentiment = this.detectSentiment(text);
|
|
143
|
-
const intents = this.extractIntents(text);
|
|
144
|
-
const tags = intents.slice(0, 3);
|
|
145
|
-
const confidence = this.estimateConfidence(category, priority, sentiment);
|
|
146
|
-
return {
|
|
147
|
-
ticketId: ticket.id,
|
|
148
|
-
category,
|
|
149
|
-
priority,
|
|
150
|
-
sentiment,
|
|
151
|
-
intents,
|
|
152
|
-
tags,
|
|
153
|
-
confidence,
|
|
154
|
-
escalationRequired: priority === "urgent" || category === "compliance"
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
detectCategory(text) {
|
|
158
|
-
for (const [category, keywords] of Object.entries(this.keywords)) if (keywords.some((keyword) => text.includes(keyword))) return category;
|
|
159
|
-
return "other";
|
|
160
|
-
}
|
|
161
|
-
detectPriority(text) {
|
|
162
|
-
for (const priority of [
|
|
163
|
-
"urgent",
|
|
164
|
-
"high",
|
|
165
|
-
"medium",
|
|
166
|
-
"low"
|
|
167
|
-
]) if (PRIORITY_HINTS[priority].some((word) => text.includes(word))) return priority;
|
|
168
|
-
return "medium";
|
|
169
|
-
}
|
|
170
|
-
detectSentiment(text) {
|
|
171
|
-
for (const sentiment of [
|
|
172
|
-
"frustrated",
|
|
173
|
-
"negative",
|
|
174
|
-
"neutral",
|
|
175
|
-
"positive"
|
|
176
|
-
]) if (SENTIMENT_HINTS[sentiment].some((word) => text.includes(word))) return sentiment;
|
|
177
|
-
return "neutral";
|
|
178
|
-
}
|
|
179
|
-
extractIntents(text) {
|
|
180
|
-
const intents = [];
|
|
181
|
-
if (text.includes("refund") || text.includes("chargeback")) intents.push("refund");
|
|
182
|
-
if (text.includes("payout")) intents.push("payout");
|
|
183
|
-
if (text.includes("login")) intents.push("login-help");
|
|
184
|
-
if (text.includes("feature")) intents.push("feature-request");
|
|
185
|
-
if (text.includes("bug") || text.includes("error")) intents.push("bug-report");
|
|
186
|
-
return intents.length ? intents : ["general"];
|
|
187
|
-
}
|
|
188
|
-
estimateConfidence(category, priority, sentiment) {
|
|
189
|
-
let base = .6;
|
|
190
|
-
if (category !== "other") base += .1;
|
|
191
|
-
if (priority === "urgent" || priority === "low") base += .05;
|
|
192
|
-
if (sentiment === "frustrated") base -= .05;
|
|
193
|
-
return Math.min(.95, Math.max(.4, Number(base.toFixed(2))));
|
|
194
|
-
}
|
|
17
|
+
var SENTIMENT_HINTS = {
|
|
18
|
+
positive: ["love", "great", "awesome", "thank you"],
|
|
19
|
+
neutral: ["question", "wonder", "curious"],
|
|
20
|
+
negative: ["unhappy", "bad", "terrible", "awful", "angry"],
|
|
21
|
+
frustrated: ["furious", "frustrated", "fed up", "ridiculous"]
|
|
195
22
|
};
|
|
196
23
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
24
|
+
class TicketClassifier {
|
|
25
|
+
keywords;
|
|
26
|
+
llm;
|
|
27
|
+
llmModel;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.keywords = {
|
|
30
|
+
...CATEGORY_KEYWORDS,
|
|
31
|
+
...options?.keywords ?? {}
|
|
32
|
+
};
|
|
33
|
+
this.llm = options?.llm;
|
|
34
|
+
this.llmModel = options?.llmModel;
|
|
35
|
+
}
|
|
36
|
+
async classify(ticket) {
|
|
37
|
+
const heuristics = this.heuristicClassification(ticket);
|
|
38
|
+
if (!this.llm)
|
|
39
|
+
return heuristics;
|
|
40
|
+
try {
|
|
41
|
+
const llmResult = await this.llm.chat([
|
|
42
|
+
{
|
|
43
|
+
role: "system",
|
|
44
|
+
content: [{ type: "text", text: "Classify the support ticket." }]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: "user",
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: "text",
|
|
51
|
+
text: JSON.stringify({
|
|
52
|
+
subject: ticket.subject,
|
|
53
|
+
body: ticket.body,
|
|
54
|
+
channel: ticket.channel
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
], {
|
|
60
|
+
responseFormat: "json",
|
|
61
|
+
model: this.llmModel
|
|
62
|
+
});
|
|
63
|
+
const content = llmResult.message.content.find((part) => ("text" in part));
|
|
64
|
+
if (content && "text" in content) {
|
|
65
|
+
const parsed = JSON.parse(content.text);
|
|
66
|
+
return {
|
|
67
|
+
...heuristics,
|
|
68
|
+
...parsed,
|
|
69
|
+
intents: parsed.intents ?? heuristics.intents,
|
|
70
|
+
tags: parsed.tags ?? heuristics.tags
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
return heuristics;
|
|
75
|
+
}
|
|
76
|
+
heuristicClassification(ticket) {
|
|
77
|
+
const text = `${ticket.subject}
|
|
78
|
+
${ticket.body}`.toLowerCase();
|
|
79
|
+
const category = this.detectCategory(text);
|
|
80
|
+
const priority = this.detectPriority(text);
|
|
81
|
+
const sentiment = this.detectSentiment(text);
|
|
82
|
+
const intents = this.extractIntents(text);
|
|
83
|
+
const tags = intents.slice(0, 3);
|
|
84
|
+
const confidence = this.estimateConfidence(category, priority, sentiment);
|
|
85
|
+
return {
|
|
86
|
+
ticketId: ticket.id,
|
|
87
|
+
category,
|
|
88
|
+
priority,
|
|
89
|
+
sentiment,
|
|
90
|
+
intents,
|
|
91
|
+
tags,
|
|
92
|
+
confidence,
|
|
93
|
+
escalationRequired: priority === "urgent" || category === "compliance"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
detectCategory(text) {
|
|
97
|
+
for (const [category, keywords] of Object.entries(this.keywords)) {
|
|
98
|
+
if (keywords.some((keyword) => text.includes(keyword))) {
|
|
99
|
+
return category;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return "other";
|
|
103
|
+
}
|
|
104
|
+
detectPriority(text) {
|
|
105
|
+
for (const priority of [
|
|
106
|
+
"urgent",
|
|
107
|
+
"high",
|
|
108
|
+
"medium",
|
|
109
|
+
"low"
|
|
110
|
+
]) {
|
|
111
|
+
if (PRIORITY_HINTS[priority].some((word) => text.includes(word))) {
|
|
112
|
+
return priority;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return "medium";
|
|
116
|
+
}
|
|
117
|
+
detectSentiment(text) {
|
|
118
|
+
for (const sentiment of [
|
|
119
|
+
"frustrated",
|
|
120
|
+
"negative",
|
|
121
|
+
"neutral",
|
|
122
|
+
"positive"
|
|
123
|
+
]) {
|
|
124
|
+
if (SENTIMENT_HINTS[sentiment].some((word) => text.includes(word))) {
|
|
125
|
+
return sentiment;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return "neutral";
|
|
129
|
+
}
|
|
130
|
+
extractIntents(text) {
|
|
131
|
+
const intents = [];
|
|
132
|
+
if (text.includes("refund") || text.includes("chargeback"))
|
|
133
|
+
intents.push("refund");
|
|
134
|
+
if (text.includes("payout"))
|
|
135
|
+
intents.push("payout");
|
|
136
|
+
if (text.includes("login"))
|
|
137
|
+
intents.push("login-help");
|
|
138
|
+
if (text.includes("feature"))
|
|
139
|
+
intents.push("feature-request");
|
|
140
|
+
if (text.includes("bug") || text.includes("error"))
|
|
141
|
+
intents.push("bug-report");
|
|
142
|
+
return intents.length ? intents : ["general"];
|
|
143
|
+
}
|
|
144
|
+
estimateConfidence(category, priority, sentiment) {
|
|
145
|
+
let base = 0.6;
|
|
146
|
+
if (category !== "other")
|
|
147
|
+
base += 0.1;
|
|
148
|
+
if (priority === "urgent" || priority === "low")
|
|
149
|
+
base += 0.05;
|
|
150
|
+
if (sentiment === "frustrated")
|
|
151
|
+
base -= 0.05;
|
|
152
|
+
return Math.min(0.95, Math.max(0.4, Number(base.toFixed(2))));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
TicketClassifier
|
|
157
|
+
};
|
package/dist/tickets/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export * from './classifier';
|
|
2
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tickets/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
package/dist/tickets/index.js
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/tickets/classifier.ts
|
|
3
|
+
var CATEGORY_KEYWORDS = {
|
|
4
|
+
billing: ["invoice", "payout", "refund", "charge", "billing", "payment"],
|
|
5
|
+
technical: ["bug", "error", "crash", "issue", "failed", "timeout"],
|
|
6
|
+
product: ["feature", "roadmap", "idea", "request", "feedback"],
|
|
7
|
+
account: ["login", "password", "2fa", "account", "profile", "email change"],
|
|
8
|
+
compliance: ["kyc", "aml", "compliance", "regulation", "gdpr"],
|
|
9
|
+
other: []
|
|
10
|
+
};
|
|
11
|
+
var PRIORITY_HINTS = {
|
|
12
|
+
urgent: ["urgent", "asap", "immediately", "today", "right away"],
|
|
13
|
+
high: ["high priority", "blocking", "major", "critical"],
|
|
14
|
+
medium: ["soon", "next few days"],
|
|
15
|
+
low: ["nice to have", "when possible", "later"]
|
|
16
|
+
};
|
|
17
|
+
var SENTIMENT_HINTS = {
|
|
18
|
+
positive: ["love", "great", "awesome", "thank you"],
|
|
19
|
+
neutral: ["question", "wonder", "curious"],
|
|
20
|
+
negative: ["unhappy", "bad", "terrible", "awful", "angry"],
|
|
21
|
+
frustrated: ["furious", "frustrated", "fed up", "ridiculous"]
|
|
22
|
+
};
|
|
2
23
|
|
|
3
|
-
|
|
24
|
+
class TicketClassifier {
|
|
25
|
+
keywords;
|
|
26
|
+
llm;
|
|
27
|
+
llmModel;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.keywords = {
|
|
30
|
+
...CATEGORY_KEYWORDS,
|
|
31
|
+
...options?.keywords ?? {}
|
|
32
|
+
};
|
|
33
|
+
this.llm = options?.llm;
|
|
34
|
+
this.llmModel = options?.llmModel;
|
|
35
|
+
}
|
|
36
|
+
async classify(ticket) {
|
|
37
|
+
const heuristics = this.heuristicClassification(ticket);
|
|
38
|
+
if (!this.llm)
|
|
39
|
+
return heuristics;
|
|
40
|
+
try {
|
|
41
|
+
const llmResult = await this.llm.chat([
|
|
42
|
+
{
|
|
43
|
+
role: "system",
|
|
44
|
+
content: [{ type: "text", text: "Classify the support ticket." }]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: "user",
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: "text",
|
|
51
|
+
text: JSON.stringify({
|
|
52
|
+
subject: ticket.subject,
|
|
53
|
+
body: ticket.body,
|
|
54
|
+
channel: ticket.channel
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
], {
|
|
60
|
+
responseFormat: "json",
|
|
61
|
+
model: this.llmModel
|
|
62
|
+
});
|
|
63
|
+
const content = llmResult.message.content.find((part) => ("text" in part));
|
|
64
|
+
if (content && "text" in content) {
|
|
65
|
+
const parsed = JSON.parse(content.text);
|
|
66
|
+
return {
|
|
67
|
+
...heuristics,
|
|
68
|
+
...parsed,
|
|
69
|
+
intents: parsed.intents ?? heuristics.intents,
|
|
70
|
+
tags: parsed.tags ?? heuristics.tags
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
return heuristics;
|
|
75
|
+
}
|
|
76
|
+
heuristicClassification(ticket) {
|
|
77
|
+
const text = `${ticket.subject}
|
|
78
|
+
${ticket.body}`.toLowerCase();
|
|
79
|
+
const category = this.detectCategory(text);
|
|
80
|
+
const priority = this.detectPriority(text);
|
|
81
|
+
const sentiment = this.detectSentiment(text);
|
|
82
|
+
const intents = this.extractIntents(text);
|
|
83
|
+
const tags = intents.slice(0, 3);
|
|
84
|
+
const confidence = this.estimateConfidence(category, priority, sentiment);
|
|
85
|
+
return {
|
|
86
|
+
ticketId: ticket.id,
|
|
87
|
+
category,
|
|
88
|
+
priority,
|
|
89
|
+
sentiment,
|
|
90
|
+
intents,
|
|
91
|
+
tags,
|
|
92
|
+
confidence,
|
|
93
|
+
escalationRequired: priority === "urgent" || category === "compliance"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
detectCategory(text) {
|
|
97
|
+
for (const [category, keywords] of Object.entries(this.keywords)) {
|
|
98
|
+
if (keywords.some((keyword) => text.includes(keyword))) {
|
|
99
|
+
return category;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return "other";
|
|
103
|
+
}
|
|
104
|
+
detectPriority(text) {
|
|
105
|
+
for (const priority of [
|
|
106
|
+
"urgent",
|
|
107
|
+
"high",
|
|
108
|
+
"medium",
|
|
109
|
+
"low"
|
|
110
|
+
]) {
|
|
111
|
+
if (PRIORITY_HINTS[priority].some((word) => text.includes(word))) {
|
|
112
|
+
return priority;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return "medium";
|
|
116
|
+
}
|
|
117
|
+
detectSentiment(text) {
|
|
118
|
+
for (const sentiment of [
|
|
119
|
+
"frustrated",
|
|
120
|
+
"negative",
|
|
121
|
+
"neutral",
|
|
122
|
+
"positive"
|
|
123
|
+
]) {
|
|
124
|
+
if (SENTIMENT_HINTS[sentiment].some((word) => text.includes(word))) {
|
|
125
|
+
return sentiment;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return "neutral";
|
|
129
|
+
}
|
|
130
|
+
extractIntents(text) {
|
|
131
|
+
const intents = [];
|
|
132
|
+
if (text.includes("refund") || text.includes("chargeback"))
|
|
133
|
+
intents.push("refund");
|
|
134
|
+
if (text.includes("payout"))
|
|
135
|
+
intents.push("payout");
|
|
136
|
+
if (text.includes("login"))
|
|
137
|
+
intents.push("login-help");
|
|
138
|
+
if (text.includes("feature"))
|
|
139
|
+
intents.push("feature-request");
|
|
140
|
+
if (text.includes("bug") || text.includes("error"))
|
|
141
|
+
intents.push("bug-report");
|
|
142
|
+
return intents.length ? intents : ["general"];
|
|
143
|
+
}
|
|
144
|
+
estimateConfidence(category, priority, sentiment) {
|
|
145
|
+
let base = 0.6;
|
|
146
|
+
if (category !== "other")
|
|
147
|
+
base += 0.1;
|
|
148
|
+
if (priority === "urgent" || priority === "low")
|
|
149
|
+
base += 0.05;
|
|
150
|
+
if (sentiment === "frustrated")
|
|
151
|
+
base -= 0.05;
|
|
152
|
+
return Math.min(0.95, Math.max(0.4, Number(base.toFixed(2))));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
TicketClassifier
|
|
157
|
+
};
|