@nebutra/support-deflector 0.1.1
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/.nebutra/debug/content-store.jsonl +3 -0
- package/.nebutra/debug/event-log.jsonl +1 -0
- package/.nebutra/debug/support-deflector.jsonl +1 -0
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +16 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +676 -0
- package/README.md +14 -0
- package/dist/chunk-OEGPSIYO.js +158 -0
- package/dist/chunk-OEGPSIYO.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/examples/classify.ts +15 -0
- package/examples/decision.ts +25 -0
- package/examples/quickstart.ts +33 -0
- package/package.json +63 -0
- package/plays/ticket_triage/SKILL.md +36 -0
- package/src/cli.ts +50 -0
- package/src/index.test.ts +106 -0
- package/src/index.ts +273 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @nebutra/support-deflector
|
|
2
|
+
|
|
3
|
+
Status: **WIP**
|
|
4
|
+
|
|
5
|
+
Confidence-gated customer support Play package.
|
|
6
|
+
|
|
7
|
+
The package owns ticket triage decisions, not channel transport. Support-channel
|
|
8
|
+
bridges stay as external adapters.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm support-deflector:doctor
|
|
12
|
+
pnpm support-deflector:quickstart
|
|
13
|
+
pnpm support-deflector:debug
|
|
14
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
5
|
+
import { ContentStore } from "@nebutra/content-store";
|
|
6
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
7
|
+
import { EventLog } from "@nebutra/event-log";
|
|
8
|
+
import { assetId } from "@nebutra/generation-context";
|
|
9
|
+
function requireTenant(explicit, fallback) {
|
|
10
|
+
const tenantId = explicit ?? fallback;
|
|
11
|
+
if (!tenantId?.trim()) {
|
|
12
|
+
throw new CapabilityError("support-deflector", "Support Deflector requires tenant context", {
|
|
13
|
+
suggestion: "Pass tenantId with the ticket or construct SupportDeflector with tenantId.",
|
|
14
|
+
statusCode: 400
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return tenantId;
|
|
18
|
+
}
|
|
19
|
+
function includesAny(text, terms) {
|
|
20
|
+
return terms.some((term) => text.includes(term));
|
|
21
|
+
}
|
|
22
|
+
function classifyTicket(ticket) {
|
|
23
|
+
const text = `${ticket.subject} ${ticket.body}`.toLowerCase();
|
|
24
|
+
const category = includesAny(text, ["bug", "broke", "error", "production"]) ? "bug" : includesAny(text, ["refund", "invoice", "billing", "cancel"]) ? "billing" : includesAny(text, ["how", "setup", "configure"]) ? "how_to" : includesAny(text, ["pricing", "demo", "sales"]) ? "sales" : "other";
|
|
25
|
+
const angry = includesAny(text, ["angry", "furious", "broken", "terrible", "lawsuit"]);
|
|
26
|
+
const positive = includesAny(text, ["thanks", "great", "love"]);
|
|
27
|
+
const highValue = ticket.customer.plan === "enterprise" || includesAny(text, ["enterprise"]);
|
|
28
|
+
return {
|
|
29
|
+
category,
|
|
30
|
+
sentiment: angry ? "angry" : positive ? "positive" : "neutral",
|
|
31
|
+
highValue,
|
|
32
|
+
complaint: angry || includesAny(text, ["complaint", "refund now", "unacceptable"])
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function matchArticle(ticket, articles) {
|
|
36
|
+
const text = `${ticket.subject} ${ticket.body}`.toLowerCase();
|
|
37
|
+
return articles.find(
|
|
38
|
+
(article) => article.title.toLowerCase().split(/\s+/).filter((term) => term.length > 3).some((term) => text.includes(term))
|
|
39
|
+
) ?? articles.find(
|
|
40
|
+
(article) => article.body.toLowerCase().split(/\W+/).filter((term) => term.length > 6).some((term) => text.includes(term))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
function synthesizeReply(ticket, article) {
|
|
44
|
+
const firstSentence = article.body.split(".")[0]?.trim() ?? article.body;
|
|
45
|
+
return {
|
|
46
|
+
subject: `Re: ${ticket.subject}`,
|
|
47
|
+
body: `${firstSentence}. If you want, reply here and we will help with the next step.`,
|
|
48
|
+
citations: [article.id]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function decideTicket(ticket, articles, policy) {
|
|
52
|
+
const classification = classifyTicket(ticket);
|
|
53
|
+
const article = matchArticle(ticket, articles);
|
|
54
|
+
const confidence = article ? classification.category === "other" ? 0.72 : 0.9 : 0.35;
|
|
55
|
+
const shouldEscalate = (policy.escalateOnComplaint ?? true) && classification.complaint ? true : (policy.escalateOnHighValueCustomer ?? true) && classification.highValue;
|
|
56
|
+
if (shouldEscalate) {
|
|
57
|
+
return {
|
|
58
|
+
play: "ticket_triage",
|
|
59
|
+
action: "escalate",
|
|
60
|
+
confidence,
|
|
61
|
+
classification,
|
|
62
|
+
...article ? { reply: synthesizeReply(ticket, article) } : {},
|
|
63
|
+
escalationSummary: `${ticket.customer.email} needs founder attention for ${classification.category}.`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (article && confidence >= policy.autoReplyThreshold) {
|
|
67
|
+
return {
|
|
68
|
+
play: "ticket_triage",
|
|
69
|
+
action: "auto-answer",
|
|
70
|
+
confidence,
|
|
71
|
+
classification,
|
|
72
|
+
reply: synthesizeReply(ticket, article)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
play: "ticket_triage",
|
|
77
|
+
action: "suggest-answer",
|
|
78
|
+
confidence,
|
|
79
|
+
classification,
|
|
80
|
+
...article ? { reply: synthesizeReply(ticket, article) } : {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var SupportDeflector = class _SupportDeflector {
|
|
84
|
+
#tenantId;
|
|
85
|
+
#debugRoot;
|
|
86
|
+
#contentStore;
|
|
87
|
+
#eventLog;
|
|
88
|
+
constructor(options) {
|
|
89
|
+
this.#tenantId = options.tenantId;
|
|
90
|
+
this.#debugRoot = options.debugRoot ?? process.cwd();
|
|
91
|
+
this.#contentStore = options.contentStore;
|
|
92
|
+
this.#eventLog = options.eventLog;
|
|
93
|
+
}
|
|
94
|
+
static async open(root = ".nebutra/support-deflector", options = {}) {
|
|
95
|
+
const tenantId = options.tenantId ?? "local";
|
|
96
|
+
await mkdir(root, { recursive: true });
|
|
97
|
+
const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
|
|
98
|
+
const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
|
|
99
|
+
return new _SupportDeflector({ ...options, tenantId, root, contentStore, eventLog });
|
|
100
|
+
}
|
|
101
|
+
async handleTicket(input) {
|
|
102
|
+
const tenantId = requireTenant(input.ticket.tenantId, this.#tenantId);
|
|
103
|
+
const decision = decideTicket(input.ticket, input.articles, input.policy);
|
|
104
|
+
const artifactPath = `support/tickets/${input.ticket.id}.json`;
|
|
105
|
+
const content = `${JSON.stringify({ ticket: input.ticket, decision }, null, 2)}
|
|
106
|
+
`;
|
|
107
|
+
await this.#contentStore.write(artifactPath, content);
|
|
108
|
+
const eventId = await this.#eventLog.commit({
|
|
109
|
+
traceId: assetId("support_ticket", input.ticket.id),
|
|
110
|
+
kind: "content_write",
|
|
111
|
+
affected: [artifactPath],
|
|
112
|
+
parent: null,
|
|
113
|
+
snapshot: { [artifactPath]: content }
|
|
114
|
+
});
|
|
115
|
+
await this.#debug({
|
|
116
|
+
type: "ticket_decision",
|
|
117
|
+
tenantId,
|
|
118
|
+
ticketId: input.ticket.id,
|
|
119
|
+
action: decision.action,
|
|
120
|
+
eventId
|
|
121
|
+
});
|
|
122
|
+
return { ...decision, eventId };
|
|
123
|
+
}
|
|
124
|
+
async doctor() {
|
|
125
|
+
return {
|
|
126
|
+
capability: "support-deflector",
|
|
127
|
+
ok: true,
|
|
128
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
129
|
+
plays: ["ticket_triage"],
|
|
130
|
+
mode: "confidence-gated",
|
|
131
|
+
channels: [
|
|
132
|
+
{ provider: "local-ticket", ok: true },
|
|
133
|
+
{ provider: "chatwoot-bridge", ok: false },
|
|
134
|
+
{ provider: "email-bridge", ok: false }
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async close() {
|
|
139
|
+
await this.#contentStore.close();
|
|
140
|
+
}
|
|
141
|
+
async #debug(entry) {
|
|
142
|
+
await mkdir(dirname(join(this.#debugRoot, ".nebutra", "debug", "support-deflector.jsonl")), {
|
|
143
|
+
recursive: true
|
|
144
|
+
});
|
|
145
|
+
await appendCapabilityDebug("support-deflector", entry, { root: this.#debugRoot });
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
async function readSupportDeflectorDebug(root = process.cwd(), limit = 20) {
|
|
149
|
+
return readCapabilityDebug("support-deflector", { root, limit });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
classifyTicket,
|
|
154
|
+
decideTicket,
|
|
155
|
+
SupportDeflector,
|
|
156
|
+
readSupportDeflectorDebug
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=chunk-OEGPSIYO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport { ContentStore } from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\nimport { EventLog } from \"@nebutra/event-log\";\nimport { assetId } from \"@nebutra/generation-context\";\n\nexport type TicketCategory = \"billing\" | \"bug\" | \"how_to\" | \"sales\" | \"other\";\nexport type TicketSentiment = \"neutral\" | \"angry\" | \"positive\";\nexport type SupportAction = \"auto-answer\" | \"suggest-answer\" | \"escalate\";\n\nexport interface SupportTicket {\n readonly id: string;\n readonly tenantId?: string;\n readonly customer: {\n readonly id: string;\n readonly email: string;\n readonly plan?: string;\n };\n readonly subject: string;\n readonly body: string;\n}\n\nexport interface KnowledgeArticle {\n readonly id: string;\n readonly title: string;\n readonly body: string;\n}\n\nexport interface TicketClassification {\n readonly category: TicketCategory;\n readonly sentiment: TicketSentiment;\n readonly highValue: boolean;\n readonly complaint: boolean;\n}\n\nexport interface SupportPolicy {\n readonly autoReplyThreshold: number;\n readonly escalateOnComplaint?: boolean;\n readonly escalateOnHighValueCustomer?: boolean;\n}\n\nexport interface SupportReply {\n readonly subject: string;\n readonly body: string;\n readonly citations: readonly string[];\n}\n\nexport interface SupportDecision {\n readonly play: \"ticket_triage\";\n readonly action: SupportAction;\n readonly confidence: number;\n readonly classification: TicketClassification;\n readonly reply?: SupportReply;\n readonly escalationSummary?: string;\n readonly eventId?: string;\n}\n\nexport interface HandleTicketInput {\n readonly ticket: SupportTicket;\n readonly articles: readonly KnowledgeArticle[];\n readonly policy: SupportPolicy;\n}\n\nexport interface SupportDoctorReport {\n readonly capability: \"support-deflector\";\n readonly ok: boolean;\n readonly checkedAt: string;\n readonly plays: readonly string[];\n readonly mode: \"confidence-gated\";\n readonly channels: readonly { readonly provider: string; readonly ok: boolean }[];\n}\n\nexport interface SupportDeflectorOptions {\n readonly tenantId?: string;\n readonly root?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly eventLog?: EventLog;\n}\n\nfunction requireTenant(explicit: string | undefined, fallback: string | undefined): string {\n const tenantId = explicit ?? fallback;\n if (!tenantId?.trim()) {\n throw new CapabilityError(\"support-deflector\", \"Support Deflector requires tenant context\", {\n suggestion: \"Pass tenantId with the ticket or construct SupportDeflector with tenantId.\",\n statusCode: 400,\n });\n }\n return tenantId;\n}\n\nfunction includesAny(text: string, terms: readonly string[]): boolean {\n return terms.some((term) => text.includes(term));\n}\n\nexport function classifyTicket(ticket: SupportTicket): TicketClassification {\n const text = `${ticket.subject} ${ticket.body}`.toLowerCase();\n const category: TicketCategory = includesAny(text, [\"bug\", \"broke\", \"error\", \"production\"])\n ? \"bug\"\n : includesAny(text, [\"refund\", \"invoice\", \"billing\", \"cancel\"])\n ? \"billing\"\n : includesAny(text, [\"how\", \"setup\", \"configure\"])\n ? \"how_to\"\n : includesAny(text, [\"pricing\", \"demo\", \"sales\"])\n ? \"sales\"\n : \"other\";\n const angry = includesAny(text, [\"angry\", \"furious\", \"broken\", \"terrible\", \"lawsuit\"]);\n const positive = includesAny(text, [\"thanks\", \"great\", \"love\"]);\n const highValue = ticket.customer.plan === \"enterprise\" || includesAny(text, [\"enterprise\"]);\n return {\n category,\n sentiment: angry ? \"angry\" : positive ? \"positive\" : \"neutral\",\n highValue,\n complaint: angry || includesAny(text, [\"complaint\", \"refund now\", \"unacceptable\"]),\n };\n}\n\nfunction matchArticle(\n ticket: SupportTicket,\n articles: readonly KnowledgeArticle[],\n): KnowledgeArticle | undefined {\n const text = `${ticket.subject} ${ticket.body}`.toLowerCase();\n return (\n articles.find((article) =>\n article.title\n .toLowerCase()\n .split(/\\s+/)\n .filter((term) => term.length > 3)\n .some((term) => text.includes(term)),\n ) ??\n articles.find((article) =>\n article.body\n .toLowerCase()\n .split(/\\W+/)\n .filter((term) => term.length > 6)\n .some((term) => text.includes(term)),\n )\n );\n}\n\nfunction synthesizeReply(ticket: SupportTicket, article: KnowledgeArticle): SupportReply {\n const firstSentence = article.body.split(\".\")[0]?.trim() ?? article.body;\n return {\n subject: `Re: ${ticket.subject}`,\n body: `${firstSentence}. If you want, reply here and we will help with the next step.`,\n citations: [article.id],\n };\n}\n\nexport function decideTicket(\n ticket: SupportTicket,\n articles: readonly KnowledgeArticle[],\n policy: SupportPolicy,\n): SupportDecision {\n const classification = classifyTicket(ticket);\n const article = matchArticle(ticket, articles);\n const confidence = article ? (classification.category === \"other\" ? 0.72 : 0.9) : 0.35;\n const shouldEscalate =\n (policy.escalateOnComplaint ?? true) && classification.complaint\n ? true\n : (policy.escalateOnHighValueCustomer ?? true) && classification.highValue;\n if (shouldEscalate) {\n return {\n play: \"ticket_triage\",\n action: \"escalate\",\n confidence,\n classification,\n ...(article ? { reply: synthesizeReply(ticket, article) } : {}),\n escalationSummary: `${ticket.customer.email} needs founder attention for ${classification.category}.`,\n };\n }\n if (article && confidence >= policy.autoReplyThreshold) {\n return {\n play: \"ticket_triage\",\n action: \"auto-answer\",\n confidence,\n classification,\n reply: synthesizeReply(ticket, article),\n };\n }\n return {\n play: \"ticket_triage\",\n action: \"suggest-answer\",\n confidence,\n classification,\n ...(article ? { reply: synthesizeReply(ticket, article) } : {}),\n };\n}\n\nexport class SupportDeflector {\n readonly #tenantId: string | undefined;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore;\n readonly #eventLog: EventLog;\n\n private constructor(\n options: SupportDeflectorOptions & { contentStore: ContentStore; eventLog: EventLog },\n ) {\n this.#tenantId = options.tenantId;\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#eventLog = options.eventLog;\n }\n\n static async open(\n root = \".nebutra/support-deflector\",\n options: Omit<SupportDeflectorOptions, \"root\" | \"contentStore\" | \"eventLog\"> = {},\n ): Promise<SupportDeflector> {\n const tenantId = options.tenantId ?? \"local\";\n await mkdir(root, { recursive: true });\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n const eventLog = await EventLog.open(join(root, \"event-log\"), { tenantId });\n return new SupportDeflector({ ...options, tenantId, root, contentStore, eventLog });\n }\n\n async handleTicket(input: HandleTicketInput): Promise<SupportDecision> {\n const tenantId = requireTenant(input.ticket.tenantId, this.#tenantId);\n const decision = decideTicket(input.ticket, input.articles, input.policy);\n const artifactPath = `support/tickets/${input.ticket.id}.json`;\n const content = `${JSON.stringify({ ticket: input.ticket, decision }, null, 2)}\\n`;\n await this.#contentStore.write(artifactPath, content);\n const eventId = await this.#eventLog.commit({\n traceId: assetId(\"support_ticket\", input.ticket.id),\n kind: \"content_write\",\n affected: [artifactPath],\n parent: null,\n snapshot: { [artifactPath]: content },\n });\n await this.#debug({\n type: \"ticket_decision\",\n tenantId,\n ticketId: input.ticket.id,\n action: decision.action,\n eventId,\n });\n return { ...decision, eventId };\n }\n\n async doctor(): Promise<SupportDoctorReport> {\n return {\n capability: \"support-deflector\",\n ok: true,\n checkedAt: new Date().toISOString(),\n plays: [\"ticket_triage\"],\n mode: \"confidence-gated\",\n channels: [\n { provider: \"local-ticket\", ok: true },\n { provider: \"chatwoot-bridge\", ok: false },\n { provider: \"email-bridge\", ok: false },\n ],\n };\n }\n\n async close(): Promise<void> {\n await this.#contentStore.close();\n }\n\n async #debug(entry: Record<string, unknown>): Promise<void> {\n await mkdir(dirname(join(this.#debugRoot, \".nebutra\", \"debug\", \"support-deflector.jsonl\")), {\n recursive: true,\n });\n await appendCapabilityDebug(\"support-deflector\", entry, { root: this.#debugRoot });\n }\n}\n\nexport async function readSupportDeflectorDebug(\n root = process.cwd(),\n limit = 20,\n): Promise<unknown[]> {\n return readCapabilityDebug(\"support-deflector\", { root, limit });\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SupportDeflector,
|
|
3
|
+
readSupportDeflectorDebug
|
|
4
|
+
} from "./chunk-OEGPSIYO.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
var command = process.argv[2] ?? "doctor";
|
|
8
|
+
var root = process.env.SUPPORT_DEFLECTOR_ROOT ?? ".nebutra/support-deflector";
|
|
9
|
+
var tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
|
|
10
|
+
if (command === "doctor") {
|
|
11
|
+
const support = await SupportDeflector.open(root, { tenantId });
|
|
12
|
+
try {
|
|
13
|
+
process.stdout.write(`${JSON.stringify(await support.doctor(), null, 2)}
|
|
14
|
+
`);
|
|
15
|
+
} finally {
|
|
16
|
+
await support.close();
|
|
17
|
+
}
|
|
18
|
+
} else if (command === "quickstart") {
|
|
19
|
+
const support = await SupportDeflector.open(root, { tenantId });
|
|
20
|
+
try {
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
`${JSON.stringify(
|
|
23
|
+
await support.handleTicket({
|
|
24
|
+
ticket: {
|
|
25
|
+
id: "ticket_demo",
|
|
26
|
+
tenantId,
|
|
27
|
+
customer: { id: "customer_demo", email: "demo@example.com", plan: "free" },
|
|
28
|
+
subject: "How do refunds work?",
|
|
29
|
+
body: "Can I get a refund if I cancel this week?"
|
|
30
|
+
},
|
|
31
|
+
articles: [
|
|
32
|
+
{
|
|
33
|
+
id: "kb_refund",
|
|
34
|
+
title: "Refund policy",
|
|
35
|
+
body: "Refunds are available within 14 days. Contact support with your account email."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
policy: { autoReplyThreshold: 0.82 }
|
|
39
|
+
}),
|
|
40
|
+
null,
|
|
41
|
+
2
|
|
42
|
+
)}
|
|
43
|
+
`
|
|
44
|
+
);
|
|
45
|
+
} finally {
|
|
46
|
+
await support.close();
|
|
47
|
+
}
|
|
48
|
+
} else if (command === "debug") {
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
`${JSON.stringify({ capability: "support-deflector", entries: await readSupportDeflectorDebug() }, null, 2)}
|
|
51
|
+
`
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
process.stderr.write(`Unknown support-deflector command: ${command}
|
|
55
|
+
`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { readSupportDeflectorDebug, SupportDeflector } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst root = process.env.SUPPORT_DEFLECTOR_ROOT ?? \".nebutra/support-deflector\";\nconst tenantId = process.env.NEBUTRA_TENANT_ID ?? \"local\";\n\nif (command === \"doctor\") {\n const support = await SupportDeflector.open(root, { tenantId });\n try {\n process.stdout.write(`${JSON.stringify(await support.doctor(), null, 2)}\\n`);\n } finally {\n await support.close();\n }\n} else if (command === \"quickstart\") {\n const support = await SupportDeflector.open(root, { tenantId });\n try {\n process.stdout.write(\n `${JSON.stringify(\n await support.handleTicket({\n ticket: {\n id: \"ticket_demo\",\n tenantId,\n customer: { id: \"customer_demo\", email: \"demo@example.com\", plan: \"free\" },\n subject: \"How do refunds work?\",\n body: \"Can I get a refund if I cancel this week?\",\n },\n articles: [\n {\n id: \"kb_refund\",\n title: \"Refund policy\",\n body: \"Refunds are available within 14 days. Contact support with your account email.\",\n },\n ],\n policy: { autoReplyThreshold: 0.82 },\n }),\n null,\n 2,\n )}\\n`,\n );\n } finally {\n await support.close();\n }\n} else if (command === \"debug\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"support-deflector\", entries: await readSupportDeflectorDebug() }, null, 2)}\\n`,\n );\n} else {\n process.stderr.write(`Unknown support-deflector command: ${command}\\n`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;AAEA,IAAM,UAAU,QAAQ,KAAK,CAAC,KAAK;AACnC,IAAM,OAAO,QAAQ,IAAI,0BAA0B;AACnD,IAAM,WAAW,QAAQ,IAAI,qBAAqB;AAElD,IAAI,YAAY,UAAU;AACxB,QAAM,UAAU,MAAM,iBAAiB,KAAK,MAAM,EAAE,SAAS,CAAC;AAC9D,MAAI;AACF,YAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,MAAM,QAAQ,OAAO,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EAC7E,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF,WAAW,YAAY,cAAc;AACnC,QAAM,UAAU,MAAM,iBAAiB,KAAK,MAAM,EAAE,SAAS,CAAC;AAC9D,MAAI;AACF,YAAQ,OAAO;AAAA,MACb,GAAG,KAAK;AAAA,QACN,MAAM,QAAQ,aAAa;AAAA,UACzB,QAAQ;AAAA,YACN,IAAI;AAAA,YACJ;AAAA,YACA,UAAU,EAAE,IAAI,iBAAiB,OAAO,oBAAoB,MAAM,OAAO;AAAA,YACzE,SAAS;AAAA,YACT,MAAM;AAAA,UACR;AAAA,UACA,UAAU;AAAA,YACR;AAAA,cACE,IAAI;AAAA,cACJ,OAAO;AAAA,cACP,MAAM;AAAA,YACR;AAAA,UACF;AAAA,UACA,QAAQ,EAAE,oBAAoB,KAAK;AAAA,QACrC,CAAC;AAAA,QACD;AAAA,QACA;AAAA,MACF,CAAC;AAAA;AAAA,IACH;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF,WAAW,YAAY,SAAS;AAC9B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,qBAAqB,SAAS,MAAM,0BAA0B,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAC7G;AACF,OAAO;AACL,UAAQ,OAAO,MAAM,sCAAsC,OAAO;AAAA,CAAI;AACtE,UAAQ,WAAW;AACrB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ContentStore } from '@nebutra/content-store';
|
|
2
|
+
import { EventLog } from '@nebutra/event-log';
|
|
3
|
+
|
|
4
|
+
type TicketCategory = "billing" | "bug" | "how_to" | "sales" | "other";
|
|
5
|
+
type TicketSentiment = "neutral" | "angry" | "positive";
|
|
6
|
+
type SupportAction = "auto-answer" | "suggest-answer" | "escalate";
|
|
7
|
+
interface SupportTicket {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly tenantId?: string;
|
|
10
|
+
readonly customer: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly email: string;
|
|
13
|
+
readonly plan?: string;
|
|
14
|
+
};
|
|
15
|
+
readonly subject: string;
|
|
16
|
+
readonly body: string;
|
|
17
|
+
}
|
|
18
|
+
interface KnowledgeArticle {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly body: string;
|
|
22
|
+
}
|
|
23
|
+
interface TicketClassification {
|
|
24
|
+
readonly category: TicketCategory;
|
|
25
|
+
readonly sentiment: TicketSentiment;
|
|
26
|
+
readonly highValue: boolean;
|
|
27
|
+
readonly complaint: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface SupportPolicy {
|
|
30
|
+
readonly autoReplyThreshold: number;
|
|
31
|
+
readonly escalateOnComplaint?: boolean;
|
|
32
|
+
readonly escalateOnHighValueCustomer?: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface SupportReply {
|
|
35
|
+
readonly subject: string;
|
|
36
|
+
readonly body: string;
|
|
37
|
+
readonly citations: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
interface SupportDecision {
|
|
40
|
+
readonly play: "ticket_triage";
|
|
41
|
+
readonly action: SupportAction;
|
|
42
|
+
readonly confidence: number;
|
|
43
|
+
readonly classification: TicketClassification;
|
|
44
|
+
readonly reply?: SupportReply;
|
|
45
|
+
readonly escalationSummary?: string;
|
|
46
|
+
readonly eventId?: string;
|
|
47
|
+
}
|
|
48
|
+
interface HandleTicketInput {
|
|
49
|
+
readonly ticket: SupportTicket;
|
|
50
|
+
readonly articles: readonly KnowledgeArticle[];
|
|
51
|
+
readonly policy: SupportPolicy;
|
|
52
|
+
}
|
|
53
|
+
interface SupportDoctorReport {
|
|
54
|
+
readonly capability: "support-deflector";
|
|
55
|
+
readonly ok: boolean;
|
|
56
|
+
readonly checkedAt: string;
|
|
57
|
+
readonly plays: readonly string[];
|
|
58
|
+
readonly mode: "confidence-gated";
|
|
59
|
+
readonly channels: readonly {
|
|
60
|
+
readonly provider: string;
|
|
61
|
+
readonly ok: boolean;
|
|
62
|
+
}[];
|
|
63
|
+
}
|
|
64
|
+
interface SupportDeflectorOptions {
|
|
65
|
+
readonly tenantId?: string;
|
|
66
|
+
readonly root?: string;
|
|
67
|
+
readonly debugRoot?: string;
|
|
68
|
+
readonly contentStore?: ContentStore;
|
|
69
|
+
readonly eventLog?: EventLog;
|
|
70
|
+
}
|
|
71
|
+
declare function classifyTicket(ticket: SupportTicket): TicketClassification;
|
|
72
|
+
declare function decideTicket(ticket: SupportTicket, articles: readonly KnowledgeArticle[], policy: SupportPolicy): SupportDecision;
|
|
73
|
+
declare class SupportDeflector {
|
|
74
|
+
#private;
|
|
75
|
+
private constructor();
|
|
76
|
+
static open(root?: string, options?: Omit<SupportDeflectorOptions, "root" | "contentStore" | "eventLog">): Promise<SupportDeflector>;
|
|
77
|
+
handleTicket(input: HandleTicketInput): Promise<SupportDecision>;
|
|
78
|
+
doctor(): Promise<SupportDoctorReport>;
|
|
79
|
+
close(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
declare function readSupportDeflectorDebug(root?: string, limit?: number): Promise<unknown[]>;
|
|
82
|
+
|
|
83
|
+
export { type HandleTicketInput, type KnowledgeArticle, type SupportAction, type SupportDecision, SupportDeflector, type SupportDeflectorOptions, type SupportDoctorReport, type SupportPolicy, type SupportReply, type SupportTicket, type TicketCategory, type TicketClassification, type TicketSentiment, classifyTicket, decideTicket, readSupportDeflectorDebug };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SupportDeflector,
|
|
3
|
+
classifyTicket,
|
|
4
|
+
decideTicket,
|
|
5
|
+
readSupportDeflectorDebug
|
|
6
|
+
} from "./chunk-OEGPSIYO.js";
|
|
7
|
+
export {
|
|
8
|
+
SupportDeflector,
|
|
9
|
+
classifyTicket,
|
|
10
|
+
decideTicket,
|
|
11
|
+
readSupportDeflectorDebug
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { classifyTicket } from "../src/index";
|
|
2
|
+
|
|
3
|
+
process.stdout.write(
|
|
4
|
+
`${JSON.stringify(
|
|
5
|
+
classifyTicket({
|
|
6
|
+
id: "ticket_1",
|
|
7
|
+
tenantId: "local",
|
|
8
|
+
customer: { id: "customer_1", email: "a@example.com", plan: "enterprise" },
|
|
9
|
+
subject: "Production bug",
|
|
10
|
+
body: "I am angry because this broke production.",
|
|
11
|
+
}),
|
|
12
|
+
null,
|
|
13
|
+
2,
|
|
14
|
+
)}\n`,
|
|
15
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { decideTicket } from "../src/index";
|
|
2
|
+
|
|
3
|
+
process.stdout.write(
|
|
4
|
+
`${JSON.stringify(
|
|
5
|
+
decideTicket(
|
|
6
|
+
{
|
|
7
|
+
id: "ticket_1",
|
|
8
|
+
tenantId: "local",
|
|
9
|
+
customer: { id: "customer_1", email: "a@example.com", plan: "free" },
|
|
10
|
+
subject: "Refund",
|
|
11
|
+
body: "How do refunds work?",
|
|
12
|
+
},
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
id: "kb_refund",
|
|
16
|
+
title: "Refund policy",
|
|
17
|
+
body: "Refunds are available within 14 days.",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
{ autoReplyThreshold: 0.82 },
|
|
21
|
+
),
|
|
22
|
+
null,
|
|
23
|
+
2,
|
|
24
|
+
)}\n`,
|
|
25
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SupportDeflector } from "../src/index";
|
|
2
|
+
|
|
3
|
+
const support = await SupportDeflector.open(".nebutra/support-deflector-example", {
|
|
4
|
+
tenantId: "local",
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
process.stdout.write(
|
|
9
|
+
`${JSON.stringify(
|
|
10
|
+
await support.handleTicket({
|
|
11
|
+
ticket: {
|
|
12
|
+
id: "ticket_demo",
|
|
13
|
+
tenantId: "local",
|
|
14
|
+
customer: { id: "customer_1", email: "a@example.com", plan: "free" },
|
|
15
|
+
subject: "How do refunds work?",
|
|
16
|
+
body: "Can I get a refund if I cancel this week?",
|
|
17
|
+
},
|
|
18
|
+
articles: [
|
|
19
|
+
{
|
|
20
|
+
id: "kb_refund",
|
|
21
|
+
title: "Refund policy",
|
|
22
|
+
body: "Refunds are available within 14 days. Contact support with your account email.",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
policy: { autoReplyThreshold: 0.82 },
|
|
26
|
+
}),
|
|
27
|
+
null,
|
|
28
|
+
2,
|
|
29
|
+
)}\n`,
|
|
30
|
+
);
|
|
31
|
+
} finally {
|
|
32
|
+
await support.close();
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/support-deflector",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Confidence-gated customer support Play package for founder-owned support automation",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"nebutra": {
|
|
9
|
+
"status": "wip",
|
|
10
|
+
"productionReady": false,
|
|
11
|
+
"surface": "play-product",
|
|
12
|
+
"requires": [
|
|
13
|
+
"@nebutra/play-loader for SKILL.md play parsing",
|
|
14
|
+
"@nebutra/generation-context for founder and brand voice",
|
|
15
|
+
"@nebutra/content-store for support memory file truth",
|
|
16
|
+
"@nebutra/event-log for ticket decisions"
|
|
17
|
+
],
|
|
18
|
+
"gaps": [
|
|
19
|
+
"Channel bridges are represented as adapter handoffs until support-channel connectors are configured",
|
|
20
|
+
"Confidence scoring is deterministic and conservative until historical ticket feedback is available"
|
|
21
|
+
],
|
|
22
|
+
"featureId": "support-deflector",
|
|
23
|
+
"category": "ai",
|
|
24
|
+
"summary": "Support Play product that triages tickets into auto-answer, suggest, or escalate decisions"
|
|
25
|
+
},
|
|
26
|
+
"main": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nebutra/capability-kit": "0.2.0",
|
|
33
|
+
"@nebutra/content-store": "0.1.0",
|
|
34
|
+
"@nebutra/errors": "0.1.0",
|
|
35
|
+
"@nebutra/event-log": "0.1.0",
|
|
36
|
+
"@nebutra/generation-context": "0.1.0",
|
|
37
|
+
"@nebutra/play-loader": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.19.15",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
42
|
+
"tsx": "^4.21.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.1.4"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/support-deflector#readme",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
50
|
+
"directory": "packages/ai/support-deflector"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"typecheck": "tsc --noEmit"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ticket_triage
|
|
3
|
+
kind: play
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: Triage a support ticket into auto-answer, suggest-answer, or escalate
|
|
6
|
+
inputs:
|
|
7
|
+
ticket: { type: object }
|
|
8
|
+
support_policy: { type: object }
|
|
9
|
+
outputs:
|
|
10
|
+
decision: { type: file, path: support/tickets/ticket.json, mime: application/json }
|
|
11
|
+
budget:
|
|
12
|
+
duration_s: 30
|
|
13
|
+
cost_usd: 0.2
|
|
14
|
+
required_skills:
|
|
15
|
+
- knowledge_base.search
|
|
16
|
+
- content_store.write
|
|
17
|
+
- event_log.commit
|
|
18
|
+
sub_agents:
|
|
19
|
+
- role: support_triager
|
|
20
|
+
allowed_skills: [knowledge_base.search, content_store.write]
|
|
21
|
+
- role: escalation_summarizer
|
|
22
|
+
allowed_skills: [content_store.write]
|
|
23
|
+
depends_on_plays: []
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What this play does
|
|
27
|
+
|
|
28
|
+
Classifies a new customer-support ticket, finds relevant knowledge, then returns
|
|
29
|
+
a confidence-gated decision.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
|
|
33
|
+
- Escalate angry, complaint, or high-value customer tickets.
|
|
34
|
+
- Auto-answer only when confidence exceeds policy threshold.
|
|
35
|
+
- Keep the founder in control of medium-confidence replies.
|
|
36
|
+
- Redact personally sensitive data before trace-store emission.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readSupportDeflectorDebug, SupportDeflector } from "./index";
|
|
2
|
+
|
|
3
|
+
const command = process.argv[2] ?? "doctor";
|
|
4
|
+
const root = process.env.SUPPORT_DEFLECTOR_ROOT ?? ".nebutra/support-deflector";
|
|
5
|
+
const tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
|
|
6
|
+
|
|
7
|
+
if (command === "doctor") {
|
|
8
|
+
const support = await SupportDeflector.open(root, { tenantId });
|
|
9
|
+
try {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(await support.doctor(), null, 2)}\n`);
|
|
11
|
+
} finally {
|
|
12
|
+
await support.close();
|
|
13
|
+
}
|
|
14
|
+
} else if (command === "quickstart") {
|
|
15
|
+
const support = await SupportDeflector.open(root, { tenantId });
|
|
16
|
+
try {
|
|
17
|
+
process.stdout.write(
|
|
18
|
+
`${JSON.stringify(
|
|
19
|
+
await support.handleTicket({
|
|
20
|
+
ticket: {
|
|
21
|
+
id: "ticket_demo",
|
|
22
|
+
tenantId,
|
|
23
|
+
customer: { id: "customer_demo", email: "demo@example.com", plan: "free" },
|
|
24
|
+
subject: "How do refunds work?",
|
|
25
|
+
body: "Can I get a refund if I cancel this week?",
|
|
26
|
+
},
|
|
27
|
+
articles: [
|
|
28
|
+
{
|
|
29
|
+
id: "kb_refund",
|
|
30
|
+
title: "Refund policy",
|
|
31
|
+
body: "Refunds are available within 14 days. Contact support with your account email.",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
policy: { autoReplyThreshold: 0.82 },
|
|
35
|
+
}),
|
|
36
|
+
null,
|
|
37
|
+
2,
|
|
38
|
+
)}\n`,
|
|
39
|
+
);
|
|
40
|
+
} finally {
|
|
41
|
+
await support.close();
|
|
42
|
+
}
|
|
43
|
+
} else if (command === "debug") {
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
`${JSON.stringify({ capability: "support-deflector", entries: await readSupportDeflectorDebug() }, null, 2)}\n`,
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
process.stderr.write(`Unknown support-deflector command: ${command}\n`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|