@skillrecordings/cli 0.1.0 → 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.
- package/bin/skill.mjs +27 -0
- package/dist/chunk-2NCCVTEE.js +22342 -0
- package/dist/chunk-2NCCVTEE.js.map +1 -0
- package/dist/chunk-3E3GYSZR.js +7071 -0
- package/dist/chunk-3E3GYSZR.js.map +1 -0
- package/dist/chunk-F4EM72IH.js +86 -0
- package/dist/chunk-F4EM72IH.js.map +1 -0
- package/dist/chunk-FGP7KUQW.js +432 -0
- package/dist/chunk-FGP7KUQW.js.map +1 -0
- package/dist/chunk-H3D6VCME.js +55 -0
- package/dist/chunk-H3D6VCME.js.map +1 -0
- package/dist/chunk-HK3PEWFD.js +208 -0
- package/dist/chunk-HK3PEWFD.js.map +1 -0
- package/dist/chunk-KEV3QKXP.js +4495 -0
- package/dist/chunk-KEV3QKXP.js.map +1 -0
- package/dist/chunk-MG37YDAK.js +882 -0
- package/dist/chunk-MG37YDAK.js.map +1 -0
- package/dist/chunk-MLNDSBZ4.js +482 -0
- package/dist/chunk-MLNDSBZ4.js.map +1 -0
- package/dist/chunk-N2WIV2JV.js +22 -0
- package/dist/chunk-N2WIV2JV.js.map +1 -0
- package/dist/chunk-PWWRCN5W.js +2067 -0
- package/dist/chunk-PWWRCN5W.js.map +1 -0
- package/dist/chunk-SKHBM3XP.js +7746 -0
- package/dist/chunk-SKHBM3XP.js.map +1 -0
- package/dist/chunk-WFANXVQG.js +64 -0
- package/dist/chunk-WFANXVQG.js.map +1 -0
- package/dist/chunk-WYKL32C3.js +275 -0
- package/dist/chunk-WYKL32C3.js.map +1 -0
- package/dist/chunk-ZNF7XD2S.js +134 -0
- package/dist/chunk-ZNF7XD2S.js.map +1 -0
- package/dist/config-AUAIYDSI.js +20 -0
- package/dist/config-AUAIYDSI.js.map +1 -0
- package/dist/fileFromPath-XN7LXIBI.js +134 -0
- package/dist/fileFromPath-XN7LXIBI.js.map +1 -0
- package/dist/getMachineId-bsd-KW2E7VK3.js +42 -0
- package/dist/getMachineId-bsd-KW2E7VK3.js.map +1 -0
- package/dist/getMachineId-darwin-ROXJUJX5.js +42 -0
- package/dist/getMachineId-darwin-ROXJUJX5.js.map +1 -0
- package/dist/getMachineId-linux-KVZEHQSU.js +34 -0
- package/dist/getMachineId-linux-KVZEHQSU.js.map +1 -0
- package/dist/getMachineId-unsupported-PPRILPPA.js +25 -0
- package/dist/getMachineId-unsupported-PPRILPPA.js.map +1 -0
- package/dist/getMachineId-win-IIF36LEJ.js +44 -0
- package/dist/getMachineId-win-IIF36LEJ.js.map +1 -0
- package/dist/index.js +112703 -0
- package/dist/index.js.map +1 -0
- package/dist/lib-R6DEEJCP.js +7623 -0
- package/dist/lib-R6DEEJCP.js.map +1 -0
- package/dist/pipeline-IAVVAKTU.js +120 -0
- package/dist/pipeline-IAVVAKTU.js.map +1 -0
- package/dist/query-NTP5NVXN.js +25 -0
- package/dist/query-NTP5NVXN.js.map +1 -0
- package/dist/routing-BAEPFB7V.js +390 -0
- package/dist/routing-BAEPFB7V.js.map +1 -0
- package/dist/stripe-lookup-charge-EPRUMZDL.js +56 -0
- package/dist/stripe-lookup-charge-EPRUMZDL.js.map +1 -0
- package/dist/stripe-payment-history-SJPKA63N.js +67 -0
- package/dist/stripe-payment-history-SJPKA63N.js.map +1 -0
- package/dist/stripe-subscription-status-L4Z65GB3.js +58 -0
- package/dist/stripe-subscription-status-L4Z65GB3.js.map +1 -0
- package/dist/stripe-verify-refund-FZDKCIUQ.js +54 -0
- package/dist/stripe-verify-refund-FZDKCIUQ.js.map +1 -0
- package/dist/support-memory-WSG7SDKG.js +10 -0
- package/dist/support-memory-WSG7SDKG.js.map +1 -0
- package/package.json +10 -7
- package/.env.encrypted +0 -0
- package/CHANGELOG.md +0 -35
- package/data/tt-archive-dataset.json +0 -1
- package/data/validate-test-dataset.json +0 -97
- package/docs/CLI-AUTH.md +0 -504
- package/preload.ts +0 -18
- package/src/__tests__/init.test.ts +0 -74
- package/src/alignment-test.ts +0 -64
- package/src/check-apps.ts +0 -16
- package/src/commands/auth/decrypt.ts +0 -123
- package/src/commands/auth/encrypt.ts +0 -81
- package/src/commands/auth/index.ts +0 -50
- package/src/commands/auth/keygen.ts +0 -41
- package/src/commands/auth/status.ts +0 -164
- package/src/commands/axiom/forensic.ts +0 -868
- package/src/commands/axiom/index.ts +0 -697
- package/src/commands/build-dataset.ts +0 -311
- package/src/commands/db-status.ts +0 -47
- package/src/commands/deploys.ts +0 -219
- package/src/commands/eval-local/compare.ts +0 -171
- package/src/commands/eval-local/health.ts +0 -212
- package/src/commands/eval-local/index.ts +0 -76
- package/src/commands/eval-local/real-tools.ts +0 -416
- package/src/commands/eval-local/run.ts +0 -1168
- package/src/commands/eval-local/score-production.ts +0 -256
- package/src/commands/eval-local/seed.ts +0 -276
- package/src/commands/eval-pipeline/index.ts +0 -53
- package/src/commands/eval-pipeline/real-tools.ts +0 -492
- package/src/commands/eval-pipeline/run.ts +0 -1316
- package/src/commands/eval-pipeline/seed.ts +0 -395
- package/src/commands/eval-prompt.ts +0 -496
- package/src/commands/eval.test.ts +0 -253
- package/src/commands/eval.ts +0 -108
- package/src/commands/faq-classify.ts +0 -460
- package/src/commands/faq-cluster.ts +0 -135
- package/src/commands/faq-extract.ts +0 -249
- package/src/commands/faq-mine.ts +0 -432
- package/src/commands/faq-review.ts +0 -426
- package/src/commands/front/index.ts +0 -351
- package/src/commands/front/pull-conversations.ts +0 -275
- package/src/commands/front/tags.ts +0 -825
- package/src/commands/front-cache.ts +0 -1277
- package/src/commands/front-stats.ts +0 -75
- package/src/commands/health.test.ts +0 -82
- package/src/commands/health.ts +0 -362
- package/src/commands/init.test.ts +0 -89
- package/src/commands/init.ts +0 -106
- package/src/commands/inngest/client.ts +0 -294
- package/src/commands/inngest/events.ts +0 -296
- package/src/commands/inngest/investigate.ts +0 -382
- package/src/commands/inngest/runs.ts +0 -149
- package/src/commands/inngest/signal.ts +0 -143
- package/src/commands/kb-sync.ts +0 -498
- package/src/commands/memory/find.ts +0 -135
- package/src/commands/memory/get.ts +0 -87
- package/src/commands/memory/index.ts +0 -97
- package/src/commands/memory/stats.ts +0 -163
- package/src/commands/memory/store.ts +0 -49
- package/src/commands/memory/vote.ts +0 -159
- package/src/commands/pipeline.ts +0 -127
- package/src/commands/responses.ts +0 -856
- package/src/commands/tools.ts +0 -293
- package/src/commands/wizard.ts +0 -319
- package/src/index.ts +0 -172
- package/src/lib/crypto.ts +0 -56
- package/src/lib/env-loader.ts +0 -206
- package/src/lib/onepassword.ts +0 -137
- package/src/test-agent-local.ts +0 -115
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -10
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BASE_DRAFT_PROMPT,
|
|
3
|
+
addDecisionComment,
|
|
4
|
+
addSupportComment,
|
|
5
|
+
applyMultipleTags,
|
|
6
|
+
applyTag,
|
|
7
|
+
archiveConversation,
|
|
8
|
+
buildCategoryPrompt,
|
|
9
|
+
classify,
|
|
10
|
+
classifyThread,
|
|
11
|
+
computeMessageSignals,
|
|
12
|
+
computeThreadSignals,
|
|
13
|
+
createArchiveStep,
|
|
14
|
+
createCommentStep,
|
|
15
|
+
createTagStep,
|
|
16
|
+
draft,
|
|
17
|
+
extractEmail,
|
|
18
|
+
extractSignals,
|
|
19
|
+
fastClassify,
|
|
20
|
+
fastClassifyThread,
|
|
21
|
+
formatApprovalComment,
|
|
22
|
+
formatAuditComment,
|
|
23
|
+
formatContextForPrompt,
|
|
24
|
+
formatDecisionComment,
|
|
25
|
+
formatEscalationComment,
|
|
26
|
+
formatIssues,
|
|
27
|
+
formatMinimalComment,
|
|
28
|
+
formatSupportComment,
|
|
29
|
+
gather,
|
|
30
|
+
getPromptForCategory,
|
|
31
|
+
getRoutingRules,
|
|
32
|
+
getTagsForCategory,
|
|
33
|
+
getThreadRoutingRules,
|
|
34
|
+
hasIssueType,
|
|
35
|
+
isThreadResolved,
|
|
36
|
+
llmClassifyThread,
|
|
37
|
+
recordEscalationConfirmed,
|
|
38
|
+
recordRoutingOutcome,
|
|
39
|
+
recordShouldHaveEscalated,
|
|
40
|
+
recordUnnecessaryEscalation,
|
|
41
|
+
route,
|
|
42
|
+
routeThread,
|
|
43
|
+
routeThreadWithMemory,
|
|
44
|
+
routeWithMemory,
|
|
45
|
+
runPipeline,
|
|
46
|
+
runThreadPipeline,
|
|
47
|
+
shouldArchive,
|
|
48
|
+
shouldEscalate,
|
|
49
|
+
shouldRespond,
|
|
50
|
+
shouldSilence,
|
|
51
|
+
shouldSupportTeammate,
|
|
52
|
+
storeDraftCorrection,
|
|
53
|
+
storeDraftSuccess,
|
|
54
|
+
validate,
|
|
55
|
+
validateSync
|
|
56
|
+
} from "./chunk-3E3GYSZR.js";
|
|
57
|
+
import "./chunk-KEV3QKXP.js";
|
|
58
|
+
import "./chunk-HK3PEWFD.js";
|
|
59
|
+
import "./chunk-WYKL32C3.js";
|
|
60
|
+
import "./chunk-MLNDSBZ4.js";
|
|
61
|
+
import "./chunk-H3D6VCME.js";
|
|
62
|
+
import "./chunk-MG37YDAK.js";
|
|
63
|
+
import "./chunk-WFANXVQG.js";
|
|
64
|
+
export {
|
|
65
|
+
BASE_DRAFT_PROMPT,
|
|
66
|
+
addDecisionComment,
|
|
67
|
+
addSupportComment,
|
|
68
|
+
applyMultipleTags,
|
|
69
|
+
applyTag,
|
|
70
|
+
archiveConversation,
|
|
71
|
+
buildCategoryPrompt,
|
|
72
|
+
classify,
|
|
73
|
+
classifyThread,
|
|
74
|
+
computeMessageSignals,
|
|
75
|
+
computeThreadSignals,
|
|
76
|
+
createArchiveStep,
|
|
77
|
+
createCommentStep,
|
|
78
|
+
createTagStep,
|
|
79
|
+
draft,
|
|
80
|
+
extractEmail,
|
|
81
|
+
extractSignals,
|
|
82
|
+
fastClassify,
|
|
83
|
+
fastClassifyThread,
|
|
84
|
+
formatApprovalComment,
|
|
85
|
+
formatAuditComment,
|
|
86
|
+
formatContextForPrompt,
|
|
87
|
+
formatDecisionComment,
|
|
88
|
+
formatEscalationComment,
|
|
89
|
+
formatIssues,
|
|
90
|
+
formatMinimalComment,
|
|
91
|
+
formatSupportComment,
|
|
92
|
+
gather,
|
|
93
|
+
getPromptForCategory,
|
|
94
|
+
getRoutingRules,
|
|
95
|
+
getTagsForCategory,
|
|
96
|
+
getThreadRoutingRules,
|
|
97
|
+
hasIssueType,
|
|
98
|
+
isThreadResolved,
|
|
99
|
+
llmClassifyThread,
|
|
100
|
+
recordEscalationConfirmed,
|
|
101
|
+
recordRoutingOutcome,
|
|
102
|
+
recordShouldHaveEscalated,
|
|
103
|
+
recordUnnecessaryEscalation,
|
|
104
|
+
route,
|
|
105
|
+
routeThread,
|
|
106
|
+
routeThreadWithMemory,
|
|
107
|
+
routeWithMemory,
|
|
108
|
+
runPipeline,
|
|
109
|
+
runThreadPipeline,
|
|
110
|
+
shouldArchive,
|
|
111
|
+
shouldEscalate,
|
|
112
|
+
shouldRespond,
|
|
113
|
+
shouldSilence,
|
|
114
|
+
shouldSupportTeammate,
|
|
115
|
+
storeDraftCorrection,
|
|
116
|
+
storeDraftSuccess,
|
|
117
|
+
validate,
|
|
118
|
+
validateSync
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=pipeline-IAVVAKTU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
citeMemories,
|
|
3
|
+
formatMemoriesCompact,
|
|
4
|
+
formatMemoriesForPrompt,
|
|
5
|
+
formatMemoriesForValidation,
|
|
6
|
+
queryCorrectedMemories,
|
|
7
|
+
queryMemoriesForSituation,
|
|
8
|
+
queryMemoriesForStage,
|
|
9
|
+
recordCitationOutcome
|
|
10
|
+
} from "./chunk-HK3PEWFD.js";
|
|
11
|
+
import "./chunk-WYKL32C3.js";
|
|
12
|
+
import "./chunk-MLNDSBZ4.js";
|
|
13
|
+
import "./chunk-MG37YDAK.js";
|
|
14
|
+
import "./chunk-WFANXVQG.js";
|
|
15
|
+
export {
|
|
16
|
+
citeMemories,
|
|
17
|
+
formatMemoriesCompact,
|
|
18
|
+
formatMemoriesForPrompt,
|
|
19
|
+
formatMemoriesForValidation,
|
|
20
|
+
queryCorrectedMemories,
|
|
21
|
+
queryMemoriesForSituation,
|
|
22
|
+
queryMemoriesForStage,
|
|
23
|
+
recordCitationOutcome
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=query-NTP5NVXN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyMessage
|
|
3
|
+
} from "./chunk-ZNF7XD2S.js";
|
|
4
|
+
import "./chunk-H3D6VCME.js";
|
|
5
|
+
import "./chunk-MG37YDAK.js";
|
|
6
|
+
import {
|
|
7
|
+
init_esm_shims
|
|
8
|
+
} from "./chunk-WFANXVQG.js";
|
|
9
|
+
|
|
10
|
+
// ../core/src/evals/routing.ts
|
|
11
|
+
init_esm_shims();
|
|
12
|
+
|
|
13
|
+
// ../core/src/router/cache.ts
|
|
14
|
+
init_esm_shims();
|
|
15
|
+
var RouterCache = class {
|
|
16
|
+
decisionCache = /* @__PURE__ */ new Map();
|
|
17
|
+
config;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Retrieves a cached routing decision for a message.
|
|
23
|
+
*
|
|
24
|
+
* @param messageId - Unique message identifier (format: "conv-id:msg-id")
|
|
25
|
+
* @returns Cached decision if found and not expired, null otherwise
|
|
26
|
+
*/
|
|
27
|
+
getDecision(messageId) {
|
|
28
|
+
const entry = this.decisionCache.get(messageId);
|
|
29
|
+
if (!entry) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (this.isExpired(entry)) {
|
|
33
|
+
this.decisionCache.delete(messageId);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return entry.decision;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Caches a routing decision for a message.
|
|
40
|
+
*
|
|
41
|
+
* @param messageId - Unique message identifier
|
|
42
|
+
* @param decision - RouterDecision to cache
|
|
43
|
+
*/
|
|
44
|
+
setDecision(messageId, decision) {
|
|
45
|
+
this.decisionCache.set(messageId, {
|
|
46
|
+
decision,
|
|
47
|
+
timestamp: Date.now()
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Invalidates all cached decisions for a conversation.
|
|
52
|
+
* Triggered on new inbound messages to ensure fresh routing.
|
|
53
|
+
*
|
|
54
|
+
* @param conversationId - Conversation identifier
|
|
55
|
+
*/
|
|
56
|
+
invalidateConversation(conversationId) {
|
|
57
|
+
const prefix = `${conversationId}:`;
|
|
58
|
+
for (const messageId of this.decisionCache.keys()) {
|
|
59
|
+
if (messageId.startsWith(prefix)) {
|
|
60
|
+
this.decisionCache.delete(messageId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
isExpired(entry) {
|
|
65
|
+
const age = Date.now() - entry.timestamp;
|
|
66
|
+
return age >= this.config.decisionTtlMs;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ../core/src/router/message-router.ts
|
|
71
|
+
init_esm_shims();
|
|
72
|
+
|
|
73
|
+
// ../core/src/router/canned.ts
|
|
74
|
+
init_esm_shims();
|
|
75
|
+
|
|
76
|
+
// ../core/src/router/rules.ts
|
|
77
|
+
init_esm_shims();
|
|
78
|
+
function matchRules(message, sender, rules) {
|
|
79
|
+
if (rules.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const sortedRules = [...rules].sort((a, b) => a.priority - b.priority);
|
|
83
|
+
for (const rule of sortedRules) {
|
|
84
|
+
const matches = matchRule(rule, message, sender);
|
|
85
|
+
if (matches) {
|
|
86
|
+
return {
|
|
87
|
+
ruleId: rule.id,
|
|
88
|
+
action: rule.action,
|
|
89
|
+
response: rule.response,
|
|
90
|
+
cannedResponseId: rule.cannedResponseId
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function matchRule(rule, message, sender) {
|
|
97
|
+
switch (rule.type) {
|
|
98
|
+
case "regex":
|
|
99
|
+
return matchRegex(rule.pattern, message);
|
|
100
|
+
case "keyword":
|
|
101
|
+
return matchKeyword(rule.pattern, message);
|
|
102
|
+
case "sender_domain":
|
|
103
|
+
return matchSenderDomain(rule.pattern, sender);
|
|
104
|
+
case "sender_pattern":
|
|
105
|
+
return matchSenderPattern(rule.pattern, sender);
|
|
106
|
+
default:
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function matchRegex(pattern, message) {
|
|
111
|
+
try {
|
|
112
|
+
const regex = new RegExp(pattern, "i");
|
|
113
|
+
return regex.test(message);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function matchKeyword(pattern, message) {
|
|
119
|
+
const lowerMessage = message.toLowerCase();
|
|
120
|
+
const lowerPattern = pattern.toLowerCase();
|
|
121
|
+
if (lowerPattern.includes("|")) {
|
|
122
|
+
const keywords = lowerPattern.split("|").map((k) => k.trim());
|
|
123
|
+
return keywords.some((keyword) => lowerMessage.includes(keyword));
|
|
124
|
+
}
|
|
125
|
+
return lowerMessage.includes(lowerPattern);
|
|
126
|
+
}
|
|
127
|
+
function matchSenderDomain(pattern, sender) {
|
|
128
|
+
const emailMatch = sender.match(/@(.+)$/);
|
|
129
|
+
if (!emailMatch || !emailMatch[1]) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const senderDomain = emailMatch[1].toLowerCase();
|
|
133
|
+
const lowerPattern = pattern.toLowerCase();
|
|
134
|
+
if (lowerPattern.startsWith("*.")) {
|
|
135
|
+
const baseDomain = lowerPattern.slice(2);
|
|
136
|
+
return senderDomain.endsWith(baseDomain);
|
|
137
|
+
}
|
|
138
|
+
return senderDomain === lowerPattern;
|
|
139
|
+
}
|
|
140
|
+
function matchSenderPattern(pattern, sender) {
|
|
141
|
+
const lowerSender = sender.toLowerCase();
|
|
142
|
+
const lowerPattern = pattern.toLowerCase();
|
|
143
|
+
const regexPattern = lowerPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
144
|
+
try {
|
|
145
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
146
|
+
return regex.test(lowerSender);
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ../core/src/router/message-router.ts
|
|
153
|
+
async function routeMessage(message, context) {
|
|
154
|
+
const cacheKey = `${context.conversationId}:${context.messageId}`;
|
|
155
|
+
const cached = context.cache.getDecision(cacheKey);
|
|
156
|
+
if (cached) {
|
|
157
|
+
return cached;
|
|
158
|
+
}
|
|
159
|
+
let decision;
|
|
160
|
+
const ruleMatch = matchRules(message, context.sender, context.rules);
|
|
161
|
+
if (ruleMatch) {
|
|
162
|
+
if (ruleMatch.action === "route_to_canned" && ruleMatch.cannedResponseId) {
|
|
163
|
+
decision = {
|
|
164
|
+
route: "canned",
|
|
165
|
+
reason: `Rule ${ruleMatch.ruleId} matched, routing to canned response ${ruleMatch.cannedResponseId}`,
|
|
166
|
+
confidence: 1,
|
|
167
|
+
category: "rule-based",
|
|
168
|
+
ruleId: ruleMatch.ruleId,
|
|
169
|
+
cannedResponseId: ruleMatch.cannedResponseId
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
decision = {
|
|
173
|
+
route: "rule",
|
|
174
|
+
reason: `Matched rule ${ruleMatch.ruleId} with action ${ruleMatch.action}`,
|
|
175
|
+
confidence: 1,
|
|
176
|
+
category: "rule-based",
|
|
177
|
+
ruleId: ruleMatch.ruleId
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
context.cache.setDecision(cacheKey, decision);
|
|
181
|
+
return decision;
|
|
182
|
+
}
|
|
183
|
+
if (context.cannedResponses && context.cannedResponses.length > 0) {
|
|
184
|
+
const cannedMatch = matchCannedResponse2(message, context.cannedResponses);
|
|
185
|
+
if (cannedMatch) {
|
|
186
|
+
decision = {
|
|
187
|
+
route: "canned",
|
|
188
|
+
reason: `Matched canned response pattern: ${cannedMatch.pattern}`,
|
|
189
|
+
confidence: 0.9,
|
|
190
|
+
// High confidence for pattern match, but not rule-level certainty
|
|
191
|
+
category: "canned",
|
|
192
|
+
cannedResponseId: cannedMatch.id
|
|
193
|
+
};
|
|
194
|
+
context.cache.setDecision(cacheKey, decision);
|
|
195
|
+
return decision;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const classifierResult = await classifyMessage(message, {
|
|
199
|
+
recentMessages: context.recentMessages
|
|
200
|
+
});
|
|
201
|
+
if (classifierResult.confidence < 0.7) {
|
|
202
|
+
decision = {
|
|
203
|
+
route: "agent",
|
|
204
|
+
reason: `Classifier confidence too low (${classifierResult.confidence}): ${classifierResult.reasoning}`,
|
|
205
|
+
confidence: classifierResult.confidence,
|
|
206
|
+
category: classifierResult.category
|
|
207
|
+
};
|
|
208
|
+
} else {
|
|
209
|
+
decision = {
|
|
210
|
+
route: "classifier",
|
|
211
|
+
reason: classifierResult.reasoning,
|
|
212
|
+
confidence: classifierResult.confidence,
|
|
213
|
+
category: classifierResult.category
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
context.cache.setDecision(cacheKey, decision);
|
|
217
|
+
return decision;
|
|
218
|
+
}
|
|
219
|
+
function matchCannedResponse2(message, cannedResponses) {
|
|
220
|
+
const lowerMessage = message.toLowerCase();
|
|
221
|
+
for (const canned of cannedResponses) {
|
|
222
|
+
const lowerPattern = canned.pattern.toLowerCase();
|
|
223
|
+
if (lowerPattern.includes("|")) {
|
|
224
|
+
const keywords = lowerPattern.split("|").map((k) => k.trim());
|
|
225
|
+
if (keywords.some((keyword) => lowerMessage.includes(keyword))) {
|
|
226
|
+
return canned;
|
|
227
|
+
}
|
|
228
|
+
} else if (lowerMessage.includes(lowerPattern)) {
|
|
229
|
+
return canned;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ../core/src/evals/routing.ts
|
|
236
|
+
var DEFAULT_GATES = {
|
|
237
|
+
minPrecision: 0.92,
|
|
238
|
+
minRecall: 0.95,
|
|
239
|
+
maxFpRate: 0.03,
|
|
240
|
+
maxFnRate: 0.02
|
|
241
|
+
};
|
|
242
|
+
async function evalRouting(dataset, gates) {
|
|
243
|
+
const mergedGates = { ...DEFAULT_GATES, ...gates };
|
|
244
|
+
let tp = 0;
|
|
245
|
+
let fp = 0;
|
|
246
|
+
let fn = 0;
|
|
247
|
+
let tn = 0;
|
|
248
|
+
const categoryStats = /* @__PURE__ */ new Map();
|
|
249
|
+
const latencies = [];
|
|
250
|
+
let totalTokens = 0;
|
|
251
|
+
const cache = new RouterCache({
|
|
252
|
+
decisionTtlMs: 3e5,
|
|
253
|
+
contextTtlMs: 3e5
|
|
254
|
+
});
|
|
255
|
+
for (const datapoint of dataset) {
|
|
256
|
+
const startTime = Date.now();
|
|
257
|
+
const decision = await routeMessage(datapoint.message, {
|
|
258
|
+
conversationId: `eval-${Math.random()}`,
|
|
259
|
+
messageId: `eval-msg-${Math.random()}`,
|
|
260
|
+
sender: "[EMAIL]",
|
|
261
|
+
rules: [],
|
|
262
|
+
cache
|
|
263
|
+
});
|
|
264
|
+
const latency2 = Date.now() - startTime;
|
|
265
|
+
latencies.push(latency2);
|
|
266
|
+
totalTokens += Math.ceil(datapoint.message.length / 4);
|
|
267
|
+
if (!categoryStats.has(datapoint.expectedCategory)) {
|
|
268
|
+
categoryStats.set(datapoint.expectedCategory, {
|
|
269
|
+
tp: 0,
|
|
270
|
+
fp: 0,
|
|
271
|
+
fn: 0,
|
|
272
|
+
tn: 0
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const catStats = categoryStats.get(datapoint.expectedCategory);
|
|
276
|
+
const outcome = classifyOutcome(decision, datapoint);
|
|
277
|
+
if (outcome === "tp") {
|
|
278
|
+
tp++;
|
|
279
|
+
catStats.tp++;
|
|
280
|
+
} else if (outcome === "fp") {
|
|
281
|
+
fp++;
|
|
282
|
+
catStats.fp++;
|
|
283
|
+
} else if (outcome === "fn") {
|
|
284
|
+
fn++;
|
|
285
|
+
catStats.fn++;
|
|
286
|
+
} else {
|
|
287
|
+
tn++;
|
|
288
|
+
catStats.tn++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
|
|
292
|
+
const recall = tp + fn > 0 ? tp / (tp + fn) : 0;
|
|
293
|
+
const fpRate = fp + tn > 0 ? fp / (fp + tn) : 0;
|
|
294
|
+
const fnRate = fn + tp > 0 ? fn / (fn + tp) : 0;
|
|
295
|
+
const byCategory = {};
|
|
296
|
+
for (const [category, stats] of categoryStats.entries()) {
|
|
297
|
+
const catPrecision = stats.tp + stats.fp > 0 ? stats.tp / (stats.tp + stats.fp) : 0;
|
|
298
|
+
const catRecall = stats.tp + stats.fn > 0 ? stats.tp / (stats.tp + stats.fn) : 0;
|
|
299
|
+
const catF1 = catPrecision + catRecall > 0 ? 2 * catPrecision * catRecall / (catPrecision + catRecall) : 0;
|
|
300
|
+
byCategory[category] = {
|
|
301
|
+
tp: stats.tp,
|
|
302
|
+
fp: stats.fp,
|
|
303
|
+
fn: stats.fn,
|
|
304
|
+
tn: stats.tn,
|
|
305
|
+
precision: catPrecision,
|
|
306
|
+
recall: catRecall,
|
|
307
|
+
f1: catF1,
|
|
308
|
+
count: stats.tp + stats.fp + stats.fn + stats.tn
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const sortedLatencies = latencies.sort((a, b) => a - b);
|
|
312
|
+
const p50 = percentile(sortedLatencies, 50);
|
|
313
|
+
const p95 = percentile(sortedLatencies, 95);
|
|
314
|
+
const p99 = percentile(sortedLatencies, 99);
|
|
315
|
+
const estimatedUsd = totalTokens / 1e6 * 0.25;
|
|
316
|
+
const cost = {
|
|
317
|
+
tokens: totalTokens,
|
|
318
|
+
estimatedUsd
|
|
319
|
+
};
|
|
320
|
+
const latency = {
|
|
321
|
+
p50,
|
|
322
|
+
p95,
|
|
323
|
+
p99
|
|
324
|
+
};
|
|
325
|
+
let passed = true;
|
|
326
|
+
if (precision < mergedGates.minPrecision) {
|
|
327
|
+
passed = false;
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Precision ${precision.toFixed(4)} below threshold ${mergedGates.minPrecision}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (recall < mergedGates.minRecall) {
|
|
333
|
+
passed = false;
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Recall ${recall.toFixed(4)} below threshold ${mergedGates.minRecall}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (fpRate > mergedGates.maxFpRate) {
|
|
339
|
+
passed = false;
|
|
340
|
+
throw new Error(
|
|
341
|
+
`False positive rate ${fpRate.toFixed(4)} above threshold ${mergedGates.maxFpRate}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (fnRate > mergedGates.maxFnRate) {
|
|
345
|
+
passed = false;
|
|
346
|
+
throw new Error(
|
|
347
|
+
`False negative rate ${fnRate.toFixed(4)} above threshold ${mergedGates.maxFnRate}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
precision,
|
|
352
|
+
recall,
|
|
353
|
+
fpRate,
|
|
354
|
+
fnRate,
|
|
355
|
+
byCategory,
|
|
356
|
+
cost,
|
|
357
|
+
latency,
|
|
358
|
+
passed
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function classifyOutcome(decision, expected) {
|
|
362
|
+
const routeMatch = decision.route === expected.expectedRoute;
|
|
363
|
+
const categoryMatch = decision.category === expected.expectedCategory;
|
|
364
|
+
const isAutoResponse = ["rule", "canned", "classifier"].includes(
|
|
365
|
+
decision.route
|
|
366
|
+
);
|
|
367
|
+
const shouldAutoRespond = ["rule", "canned", "classifier"].includes(
|
|
368
|
+
expected.expectedRoute
|
|
369
|
+
);
|
|
370
|
+
if (routeMatch && categoryMatch) {
|
|
371
|
+
return "tp";
|
|
372
|
+
} else if (isAutoResponse && !shouldAutoRespond) {
|
|
373
|
+
return "fp";
|
|
374
|
+
} else if (!isAutoResponse && shouldAutoRespond) {
|
|
375
|
+
return "fn";
|
|
376
|
+
} else if (!isAutoResponse && !shouldAutoRespond) {
|
|
377
|
+
return "tn";
|
|
378
|
+
} else {
|
|
379
|
+
return "fp";
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function percentile(sortedValues, p) {
|
|
383
|
+
if (sortedValues.length === 0) return 0;
|
|
384
|
+
const index = Math.ceil(p / 100 * sortedValues.length) - 1;
|
|
385
|
+
return sortedValues[Math.max(0, index)] ?? 0;
|
|
386
|
+
}
|
|
387
|
+
export {
|
|
388
|
+
evalRouting
|
|
389
|
+
};
|
|
390
|
+
//# sourceMappingURL=routing-BAEPFB7V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../core/src/evals/routing.ts","../../core/src/router/cache.ts","../../core/src/router/message-router.ts","../../core/src/router/canned.ts","../../core/src/router/rules.ts"],"sourcesContent":["import { RouterCache } from '../router/cache'\nimport { routeMessage } from '../router/message-router'\nimport type { RouteType } from '../router/types'\nimport type {\n CategoryMetrics,\n CostMetrics,\n EvalDatapoint,\n EvalGates,\n EvalReport,\n LatencyMetrics,\n} from './types'\n\nexport type {\n EvalDatapoint,\n EvalGates,\n EvalReport,\n CategoryMetrics,\n} from './types'\n\n/**\n * Default regression gates from Phase 8 spec.\n */\nconst DEFAULT_GATES: Required<EvalGates> = {\n minPrecision: 0.92,\n minRecall: 0.95,\n maxFpRate: 0.03,\n maxFnRate: 0.02,\n}\n\n/**\n * Evaluate routing performance against a labeled dataset.\n *\n * @param dataset - Array of labeled evaluation datapoints\n * @param gates - Optional regression gates (uses defaults from Phase 8 spec)\n * @returns EvalReport with metrics and gate status\n * @throws Error if any gate threshold is violated\n *\n * @example\n * ```typescript\n * const dataset = [\n * { message: 'I want a refund', expectedCategory: 'refund', expectedRoute: 'rule' }\n * ]\n * const report = await evalRouting(dataset)\n * console.log(`Precision: ${report.precision}, Recall: ${report.recall}`)\n * ```\n */\nexport async function evalRouting(\n dataset: EvalDatapoint[],\n gates?: EvalGates\n): Promise<EvalReport> {\n const mergedGates = { ...DEFAULT_GATES, ...gates }\n\n // Track metrics\n let tp = 0\n let fp = 0\n let fn = 0\n let tn = 0\n const categoryStats = new Map<\n string,\n { tp: number; fp: number; fn: number; tn: number }\n >()\n const latencies: number[] = []\n let totalTokens = 0\n\n // Create a mock cache and context for routing\n const cache = new RouterCache({\n decisionTtlMs: 300000,\n contextTtlMs: 300000,\n })\n\n for (const datapoint of dataset) {\n const startTime = Date.now()\n\n // Route the message\n const decision = await routeMessage(datapoint.message, {\n conversationId: `eval-${Math.random()}`,\n messageId: `eval-msg-${Math.random()}`,\n sender: '[EMAIL]',\n rules: [],\n cache,\n })\n\n const latency = Date.now() - startTime\n latencies.push(latency)\n\n // Estimate tokens (rough approximation: 1 token per 4 chars)\n totalTokens += Math.ceil(datapoint.message.length / 4)\n\n // Initialize category stats if needed\n if (!categoryStats.has(datapoint.expectedCategory)) {\n categoryStats.set(datapoint.expectedCategory, {\n tp: 0,\n fp: 0,\n fn: 0,\n tn: 0,\n })\n }\n const catStats = categoryStats.get(datapoint.expectedCategory)!\n\n // Classify outcome\n const outcome = classifyOutcome(decision, datapoint)\n\n // Update counters\n if (outcome === 'tp') {\n tp++\n catStats.tp++\n } else if (outcome === 'fp') {\n fp++\n catStats.fp++\n } else if (outcome === 'fn') {\n fn++\n catStats.fn++\n } else {\n tn++\n catStats.tn++\n }\n }\n\n // Calculate overall metrics\n const precision = tp + fp > 0 ? tp / (tp + fp) : 0\n const recall = tp + fn > 0 ? tp / (tp + fn) : 0\n const fpRate = fp + tn > 0 ? fp / (fp + tn) : 0\n const fnRate = fn + tp > 0 ? fn / (fn + tp) : 0\n\n // Calculate per-category breakdown\n const byCategory: Record<string, CategoryMetrics> = {}\n for (const [category, stats] of categoryStats.entries()) {\n const catPrecision =\n stats.tp + stats.fp > 0 ? stats.tp / (stats.tp + stats.fp) : 0\n const catRecall =\n stats.tp + stats.fn > 0 ? stats.tp / (stats.tp + stats.fn) : 0\n const catF1 =\n catPrecision + catRecall > 0\n ? (2 * catPrecision * catRecall) / (catPrecision + catRecall)\n : 0\n\n byCategory[category] = {\n tp: stats.tp,\n fp: stats.fp,\n fn: stats.fn,\n tn: stats.tn,\n precision: catPrecision,\n recall: catRecall,\n f1: catF1,\n count: stats.tp + stats.fp + stats.fn + stats.tn,\n }\n }\n\n // Calculate latency percentiles\n const sortedLatencies = latencies.sort((a, b) => a - b)\n const p50 = percentile(sortedLatencies, 50)\n const p95 = percentile(sortedLatencies, 95)\n const p99 = percentile(sortedLatencies, 99)\n\n // Estimate cost (rough approximation: $0.25 per 1M tokens for Haiku)\n const estimatedUsd = (totalTokens / 1_000_000) * 0.25\n\n const cost: CostMetrics = {\n tokens: totalTokens,\n estimatedUsd,\n }\n\n const latency: LatencyMetrics = {\n p50,\n p95,\n p99,\n }\n\n // Check gates\n let passed = true\n if (precision < mergedGates.minPrecision) {\n passed = false\n throw new Error(\n `Precision ${precision.toFixed(4)} below threshold ${mergedGates.minPrecision}`\n )\n }\n if (recall < mergedGates.minRecall) {\n passed = false\n throw new Error(\n `Recall ${recall.toFixed(4)} below threshold ${mergedGates.minRecall}`\n )\n }\n if (fpRate > mergedGates.maxFpRate) {\n passed = false\n throw new Error(\n `False positive rate ${fpRate.toFixed(4)} above threshold ${mergedGates.maxFpRate}`\n )\n }\n if (fnRate > mergedGates.maxFnRate) {\n passed = false\n throw new Error(\n `False negative rate ${fnRate.toFixed(4)} above threshold ${mergedGates.maxFnRate}`\n )\n }\n\n return {\n precision,\n recall,\n fpRate,\n fnRate,\n byCategory,\n cost,\n latency,\n passed,\n }\n}\n\n/**\n * Classify routing decision outcome for metrics.\n */\nfunction classifyOutcome(\n decision: { route: RouteType; category: string },\n expected: { expectedRoute: RouteType; expectedCategory: string }\n): 'tp' | 'fp' | 'fn' | 'tn' {\n const routeMatch = decision.route === expected.expectedRoute\n const categoryMatch = decision.category === expected.expectedCategory\n\n const isAutoResponse = ['rule', 'canned', 'classifier'].includes(\n decision.route\n )\n const shouldAutoRespond = ['rule', 'canned', 'classifier'].includes(\n expected.expectedRoute\n )\n\n if (routeMatch && categoryMatch) {\n return 'tp' // Perfect match\n } else if (isAutoResponse && !shouldAutoRespond) {\n return 'fp' // Auto-responded when shouldn't have\n } else if (!isAutoResponse && shouldAutoRespond) {\n return 'fn' // Didn't respond when should have\n } else if (!isAutoResponse && !shouldAutoRespond) {\n return 'tn' // Correctly no response\n } else {\n return 'fp' // Route mismatch but both are auto-response types\n }\n}\n\n/**\n * Calculate percentile from sorted array.\n */\nfunction percentile(sortedValues: number[], p: number): number {\n if (sortedValues.length === 0) return 0\n const index = Math.ceil((p / 100) * sortedValues.length) - 1\n return sortedValues[Math.max(0, index)] ?? 0\n}\n","import type { RouterDecision } from './types'\n\n/**\n * Configuration for RouterCache TTL policies.\n */\nexport interface CacheConfig {\n /** Time-to-live for routing decisions (default: 1 hour) */\n decisionTtlMs: number\n /** Time-to-live for conversation context (default: 24 hours) */\n contextTtlMs: number\n}\n\ninterface CacheEntry {\n decision: RouterDecision\n timestamp: number\n}\n\n/**\n * In-memory cache for routing decisions with TTL-based expiration.\n *\n * Provides:\n * - Per-message decision caching (1hr TTL)\n * - Per-conversation context caching (24hr TTL)\n * - Idempotency for duplicate Front webhook events\n * - Automatic invalidation on new messages\n *\n * @example\n * ```typescript\n * const cache = new RouterCache({\n * decisionTtlMs: 60 * 60 * 1000, // 1 hour\n * contextTtlMs: 24 * 60 * 60 * 1000 // 24 hours\n * })\n *\n * // Cache a decision\n * cache.setDecision('msg-123', decision)\n *\n * // Retrieve cached decision (null if expired or not found)\n * const cached = cache.getDecision('msg-123')\n *\n * // Invalidate all decisions for a conversation\n * cache.invalidateConversation('conv-abc')\n * ```\n */\nexport class RouterCache {\n private readonly decisionCache: Map<string, CacheEntry> = new Map()\n private readonly config: CacheConfig\n\n constructor(config: CacheConfig) {\n this.config = config\n }\n\n /**\n * Retrieves a cached routing decision for a message.\n *\n * @param messageId - Unique message identifier (format: \"conv-id:msg-id\")\n * @returns Cached decision if found and not expired, null otherwise\n */\n getDecision(messageId: string): RouterDecision | null {\n const entry = this.decisionCache.get(messageId)\n\n if (!entry) {\n return null\n }\n\n if (this.isExpired(entry)) {\n this.decisionCache.delete(messageId)\n return null\n }\n\n return entry.decision\n }\n\n /**\n * Caches a routing decision for a message.\n *\n * @param messageId - Unique message identifier\n * @param decision - RouterDecision to cache\n */\n setDecision(messageId: string, decision: RouterDecision): void {\n this.decisionCache.set(messageId, {\n decision,\n timestamp: Date.now(),\n })\n }\n\n /**\n * Invalidates all cached decisions for a conversation.\n * Triggered on new inbound messages to ensure fresh routing.\n *\n * @param conversationId - Conversation identifier\n */\n invalidateConversation(conversationId: string): void {\n const prefix = `${conversationId}:`\n\n for (const messageId of this.decisionCache.keys()) {\n if (messageId.startsWith(prefix)) {\n this.decisionCache.delete(messageId)\n }\n }\n }\n\n private isExpired(entry: CacheEntry): boolean {\n const age = Date.now() - entry.timestamp\n return age >= this.config.decisionTtlMs\n }\n}\n","import type { RouterCache } from './cache'\nimport { matchCannedResponse as matchCannedResponseVector } from './canned'\nimport { classifyMessage } from './classifier'\nimport { matchRules } from './rules'\nimport type { RouterDecision, Rule } from './types'\n\nexport type { RouterDecision, Rule }\nexport { matchCannedResponse, interpolateTemplate } from './canned'\n\n/**\n * Canned response definition for pattern-based template matching.\n */\nexport interface CannedResponse {\n id: string\n pattern: string\n response: string\n}\n\n/**\n * Context required for routing decisions.\n */\nexport interface RoutingContext {\n conversationId: string\n messageId: string\n sender: string\n rules: Rule[]\n cache: RouterCache\n cannedResponses?: CannedResponse[]\n recentMessages?: string[]\n}\n\n/**\n * Route incoming message through the decision pipeline.\n *\n * Pipeline order (with early exit):\n * 1. Cache check\n * 2. Rule matching\n * 3. Canned response matching\n * 4. Classifier\n * 5. Agent fallback (if classifier confidence < 0.7)\n *\n * @param message - Incoming message content\n * @param context - Routing context with rules, cache, sender info\n * @returns RouterDecision with route type, confidence, and metadata\n *\n * @example\n * ```typescript\n * const decision = await routeMessage('I want a refund', {\n * conversationId: 'conv-123',\n * messageId: 'msg-456',\n * sender: '[EMAIL]',\n * rules: [{ id: 'refund', priority: 1, type: 'keyword', pattern: 'refund', action: 'escalate' }],\n * cache: new RouterCache({ decisionTtlMs: [PHONE], contextTtlMs: [PHONE] })\n * })\n * // => { route: 'rule', ruleId: 'refund', confidence: 1.0, ... }\n * ```\n */\nexport async function routeMessage(\n message: string,\n context: RoutingContext\n): Promise<RouterDecision> {\n const cacheKey = `${context.conversationId}:${context.messageId}`\n\n // 1. Check cache first\n const cached = context.cache.getDecision(cacheKey)\n if (cached) {\n return cached\n }\n\n let decision: RouterDecision\n\n // 2. Check rules\n const ruleMatch = matchRules(message, context.sender, context.rules)\n if (ruleMatch) {\n if (ruleMatch.action === 'route_to_canned' && ruleMatch.cannedResponseId) {\n decision = {\n route: 'canned',\n reason: `Rule ${ruleMatch.ruleId} matched, routing to canned response ${ruleMatch.cannedResponseId}`,\n confidence: 1.0,\n category: 'rule-based',\n ruleId: ruleMatch.ruleId,\n cannedResponseId: ruleMatch.cannedResponseId,\n }\n } else {\n decision = {\n route: 'rule',\n reason: `Matched rule ${ruleMatch.ruleId} with action ${ruleMatch.action}`,\n confidence: 1.0,\n category: 'rule-based',\n ruleId: ruleMatch.ruleId,\n }\n }\n context.cache.setDecision(cacheKey, decision)\n return decision\n }\n\n // 3. Check canned responses\n if (context.cannedResponses && context.cannedResponses.length > 0) {\n const cannedMatch = matchCannedResponse(message, context.cannedResponses)\n if (cannedMatch) {\n decision = {\n route: 'canned',\n reason: `Matched canned response pattern: ${cannedMatch.pattern}`,\n confidence: 0.9, // High confidence for pattern match, but not rule-level certainty\n category: 'canned',\n cannedResponseId: cannedMatch.id,\n }\n context.cache.setDecision(cacheKey, decision)\n return decision\n }\n }\n\n // 4. Use classifier\n const classifierResult = await classifyMessage(message, {\n recentMessages: context.recentMessages,\n })\n\n // 5. Fallback to agent if classifier confidence is low\n if (classifierResult.confidence < 0.7) {\n decision = {\n route: 'agent',\n reason: `Classifier confidence too low (${classifierResult.confidence}): ${classifierResult.reasoning}`,\n confidence: classifierResult.confidence,\n category: classifierResult.category,\n }\n } else {\n decision = {\n route: 'classifier',\n reason: classifierResult.reasoning,\n confidence: classifierResult.confidence,\n category: classifierResult.category,\n }\n }\n\n context.cache.setDecision(cacheKey, decision)\n return decision\n}\n\n/**\n * Match message against canned response patterns.\n * Uses simple case-insensitive keyword matching.\n */\nfunction matchCannedResponse(\n message: string,\n cannedResponses: CannedResponse[]\n): CannedResponse | null {\n const lowerMessage = message.toLowerCase()\n\n for (const canned of cannedResponses) {\n const lowerPattern = canned.pattern.toLowerCase()\n\n // Support pipe-separated OR patterns like rules\n if (lowerPattern.includes('|')) {\n const keywords = lowerPattern.split('|').map((k) => k.trim())\n if (keywords.some((keyword) => lowerMessage.includes(keyword))) {\n return canned\n }\n } else if (lowerMessage.includes(lowerPattern)) {\n return canned\n }\n }\n\n return null\n}\n","import { queryVectors } from '../vector/client'\n\n/**\n * Result of matching against canned responses\n */\nexport interface CannedMatch {\n /** Whether a match was found above threshold */\n matched: boolean\n /** The matched response text (if matched) */\n response?: string\n /** ID of the matched template (if matched) */\n templateId?: string\n /** Similarity score from vector search (if matched) */\n similarity?: number\n}\n\n/**\n * Match a message against canned responses using vector similarity.\n *\n * Searches the vector store for type='response' documents filtered by appId.\n * Returns a match if the top result exceeds the similarity threshold.\n *\n * @param message - The message to match against canned responses\n * @param appId - App ID to filter responses\n * @param threshold - Minimum similarity score (default: 0.92)\n * @returns CannedMatch with match status and response if found\n *\n * @example\n * ```ts\n * const match = await matchCannedResponse(\n * 'I want a refund please',\n * 'totaltypescript',\n * 0.92\n * )\n *\n * if (match.matched) {\n * console.log(match.response) // Canned response text\n * console.log(match.templateId) // resp-refund-standard\n * console.log(match.similarity) // 0.95\n * }\n * ```\n */\nexport async function matchCannedResponse(\n message: string,\n appId: string,\n threshold: number = 0.92\n): Promise<CannedMatch> {\n // Query vector store for type='response' docs filtered by appId\n const results = await queryVectors({\n data: message,\n topK: 1,\n includeMetadata: true,\n includeData: true,\n filter: `appId = \"${appId}\" AND type = \"response\"`,\n })\n\n // No results or no data in result\n if (results.length === 0 || !results[0]?.data) {\n return { matched: false }\n }\n\n const topResult = results[0]\n const score = topResult.score\n\n // Check if score meets threshold\n if (score < threshold) {\n return { matched: false }\n }\n\n return {\n matched: true,\n response: topResult.data,\n templateId: topResult.id,\n similarity: score,\n }\n}\n\n/**\n * Interpolate variables in a canned response template.\n *\n * Replaces {{variable_name}} placeholders with provided values.\n * Preserves template syntax for missing variables.\n *\n * @param template - Template string with {{variable}} placeholders\n * @param variables - Object mapping variable names to values\n * @returns Interpolated string\n *\n * @example\n * ```ts\n * const template = 'Hi {{customer_name}}, your {{product_name}} is ready.'\n * const result = interpolateTemplate(template, {\n * customer_name: 'Alice',\n * product_name: 'Total TypeScript'\n * })\n * // => 'Hi Alice, your Total TypeScript is ready.'\n * ```\n */\nexport function interpolateTemplate(\n template: string,\n variables: Record<string, string>\n): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, varName) => {\n return variables[varName] ?? match\n })\n}\n","import type { Rule, RuleMatch } from './types'\n\nexport type { Rule, RuleMatch }\n\n/**\n * Match incoming message and sender against rules.\n *\n * Rules are evaluated in priority order (lower number = higher priority).\n * Returns the first matching rule or null if no rules match.\n *\n * Supports three rule types:\n * - `regex`: Match message content using regular expressions (case insensitive)\n * - `keyword`: Match message content using keywords or keyword lists (case insensitive)\n * - `sender_domain`: Match sender email domain (supports wildcards like *.example.com)\n *\n * @param message - The message content to match against\n * @param sender - The sender email address\n * @param rules - Array of rules to evaluate\n * @returns Matching rule with action details, or null if no match\n *\n * @example\n * ```typescript\n * const rules: Rule[] = [\n * {\n * id: 'refund-escalation',\n * priority: 1,\n * type: 'keyword',\n * pattern: 'refund|cancel',\n * action: 'escalate'\n * }\n * ]\n *\n * const match = matchRules('I want a refund', '[EMAIL]', rules)\n * // => { ruleId: 'refund-escalation', action: 'escalate', ... }\n * ```\n */\nexport function matchRules(\n message: string,\n sender: string,\n rules: Rule[]\n): RuleMatch | null {\n if (rules.length === 0) {\n return null\n }\n\n // Sort by priority (lower number = higher priority)\n const sortedRules = [...rules].sort((a, b) => a.priority - b.priority)\n\n for (const rule of sortedRules) {\n const matches = matchRule(rule, message, sender)\n if (matches) {\n return {\n ruleId: rule.id,\n action: rule.action,\n response: rule.response,\n cannedResponseId: rule.cannedResponseId,\n }\n }\n }\n\n return null\n}\n\n/**\n * Check if a single rule matches the message/sender.\n */\nfunction matchRule(rule: Rule, message: string, sender: string): boolean {\n switch (rule.type) {\n case 'regex':\n return matchRegex(rule.pattern, message)\n case 'keyword':\n return matchKeyword(rule.pattern, message)\n case 'sender_domain':\n return matchSenderDomain(rule.pattern, sender)\n case 'sender_pattern':\n return matchSenderPattern(rule.pattern, sender)\n default:\n return false\n }\n}\n\n/**\n * Match regex pattern against message (case insensitive).\n */\nfunction matchRegex(pattern: string, message: string): boolean {\n try {\n const regex = new RegExp(pattern, 'i')\n return regex.test(message)\n } catch {\n // Invalid regex - don't match\n return false\n }\n}\n\n/**\n * Match keyword(s) against message (case insensitive).\n * Pattern can contain multiple keywords separated by | (OR).\n */\nfunction matchKeyword(pattern: string, message: string): boolean {\n const lowerMessage = message.toLowerCase()\n const lowerPattern = pattern.toLowerCase()\n\n // If pattern contains |, it's an OR operation\n if (lowerPattern.includes('|')) {\n const keywords = lowerPattern.split('|').map((k) => k.trim())\n return keywords.some((keyword) => lowerMessage.includes(keyword))\n }\n\n return lowerMessage.includes(lowerPattern)\n}\n\n/**\n * Match sender domain pattern.\n * Supports exact domain matching and wildcard subdomain (*.example.com).\n */\nfunction matchSenderDomain(pattern: string, sender: string): boolean {\n // Extract domain from email address\n const emailMatch = sender.match(/@(.+)$/)\n if (!emailMatch || !emailMatch[1]) {\n return false\n }\n\n const senderDomain = emailMatch[1].toLowerCase()\n const lowerPattern = pattern.toLowerCase()\n\n // Wildcard subdomain matching (*.example.com)\n if (lowerPattern.startsWith('*.')) {\n const baseDomain = lowerPattern.slice(2)\n return senderDomain.endsWith(baseDomain)\n }\n\n // Exact domain matching\n return senderDomain === lowerPattern\n}\n\n/**\n * Match sender email against a pattern (case insensitive).\n * Supports wildcards: * matches any characters.\n * Example: \"mailer-daemon@*\" matches \"[EMAIL]\"\n */\nfunction matchSenderPattern(pattern: string, sender: string): boolean {\n const lowerSender = sender.toLowerCase()\n const lowerPattern = pattern.toLowerCase()\n\n // Convert wildcard pattern to regex\n // Escape regex special chars except *, then replace * with .*\n const regexPattern = lowerPattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n\n try {\n const regex = new RegExp(`^${regexPattern}$`, 'i')\n return regex.test(lowerSender)\n } catch {\n return false\n }\n}\n"],"mappings":"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","names":["matchCannedResponse","latency"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stripe_esm_node_default
|
|
3
|
+
} from "./chunk-SKHBM3XP.js";
|
|
4
|
+
import {
|
|
5
|
+
createTool
|
|
6
|
+
} from "./chunk-F4EM72IH.js";
|
|
7
|
+
import {
|
|
8
|
+
init_esm_shims
|
|
9
|
+
} from "./chunk-WFANXVQG.js";
|
|
10
|
+
|
|
11
|
+
// ../core/src/tools/stripe-lookup-charge.ts
|
|
12
|
+
init_esm_shims();
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var lookupCharge = createTool({
|
|
15
|
+
name: "lookup_charge",
|
|
16
|
+
description: "Lookup a specific charge by ID from Stripe Connect. Read-only - provides context for support conversations. Returns charge details including amount, status, and refund information.",
|
|
17
|
+
parameters: z.object({
|
|
18
|
+
/**
|
|
19
|
+
* Stripe charge ID to lookup (format: ch_...)
|
|
20
|
+
*/
|
|
21
|
+
chargeId: z.string().min(1, "Charge ID required").regex(/^ch_/, "Charge ID must start with ch_")
|
|
22
|
+
}),
|
|
23
|
+
execute: async ({ chargeId }, context) => {
|
|
24
|
+
if (!context.appConfig.stripeAccountId) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Stripe account not connected for app: ${context.appConfig.id}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const stripe = new stripe_esm_node_default(process.env.STRIPE_SECRET_KEY, {
|
|
30
|
+
apiVersion: "2025-02-24.acacia"
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
const charge = await stripe.charges.retrieve(chargeId, {
|
|
34
|
+
stripeAccount: context.appConfig.stripeAccountId
|
|
35
|
+
});
|
|
36
|
+
const chargeDetails = {
|
|
37
|
+
id: charge.id,
|
|
38
|
+
amount: charge.amount,
|
|
39
|
+
currency: charge.currency,
|
|
40
|
+
status: charge.status,
|
|
41
|
+
refunded: charge.refunded,
|
|
42
|
+
customer: typeof charge.customer === "string" ? charge.customer : null,
|
|
43
|
+
description: charge.description,
|
|
44
|
+
created: charge.created
|
|
45
|
+
};
|
|
46
|
+
return chargeDetails;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
49
|
+
throw new Error(`Failed to lookup charge: ${message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
export {
|
|
54
|
+
lookupCharge
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=stripe-lookup-charge-EPRUMZDL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../core/src/tools/stripe-lookup-charge.ts"],"sourcesContent":["import Stripe from 'stripe'\nimport { z } from 'zod'\nimport { createTool } from './create-tool'\n\n/**\n * Formatted charge information.\n */\nexport interface ChargeDetails {\n /**\n * Stripe charge ID\n */\n id: string\n /**\n * Amount in cents\n */\n amount: number\n /**\n * Currency code (e.g., 'usd')\n */\n currency: string\n /**\n * Charge status\n */\n status: string\n /**\n * Whether the charge has been refunded\n */\n refunded: boolean\n /**\n * Stripe customer ID\n */\n customer: string | null\n /**\n * Charge description\n */\n description: string | null\n /**\n * Unix timestamp of charge creation\n */\n created: number\n}\n\n/**\n * Lookup a specific charge by ID via Stripe Connect.\n *\n * IMPORTANT: This is a QUERY-ONLY tool. It does NOT execute financial actions.\n * We use Stripe Connect to query charge data from the app's connected account.\n *\n * Architecture: Platform queries for context → Apps execute actions\n *\n * Use cases:\n * - Agent needs charge details for support conversation\n * - Verifying charge status\n * - Checking refund status for a specific charge\n *\n * @example\n * ```typescript\n * const charge = await lookupCharge.execute(\n * { chargeId: 'ch_abc123' },\n * context\n * )\n * ```\n */\nexport const lookupCharge = createTool({\n name: 'lookup_charge',\n description:\n 'Lookup a specific charge by ID from Stripe Connect. Read-only - provides context for support conversations. Returns charge details including amount, status, and refund information.',\n parameters: z.object({\n /**\n * Stripe charge ID to lookup (format: ch_...)\n */\n chargeId: z\n .string()\n .min(1, 'Charge ID required')\n .regex(/^ch_/, 'Charge ID must start with ch_'),\n }),\n\n execute: async ({ chargeId }, context) => {\n // Verify Stripe account is connected\n if (!context.appConfig.stripeAccountId) {\n throw new Error(\n `Stripe account not connected for app: ${context.appConfig.id}`\n )\n }\n\n // Initialize Stripe client\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n apiVersion: '2025-02-24.acacia',\n })\n\n try {\n // Query charge from connected account\n // Using stripeAccount header to query the connected app's Stripe account\n const charge = await stripe.charges.retrieve(chargeId, {\n stripeAccount: context.appConfig.stripeAccountId,\n })\n\n // Format charge for agent context\n const chargeDetails: ChargeDetails = {\n id: charge.id,\n amount: charge.amount,\n currency: charge.currency,\n status: charge.status,\n refunded: charge.refunded,\n customer: typeof charge.customer === 'string' ? charge.customer : null,\n description: charge.description,\n created: charge.created,\n }\n\n return chargeDetails\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n throw new Error(`Failed to lookup charge: ${message}`)\n }\n },\n})\n"],"mappings":";;;;;;;;;;;AAAA;AACA,SAAS,SAAS;AA8DX,IAAM,eAAe,WAAW;AAAA,EACrC,MAAM;AAAA,EACN,aACE;AAAA,EACF,YAAY,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,IAInB,UAAU,EACP,OAAO,EACP,IAAI,GAAG,oBAAoB,EAC3B,MAAM,QAAQ,+BAA+B;AAAA,EAClD,CAAC;AAAA,EAED,SAAS,OAAO,EAAE,SAAS,GAAG,YAAY;AAExC,QAAI,CAAC,QAAQ,UAAU,iBAAiB;AACtC,YAAM,IAAI;AAAA,QACR,yCAAyC,QAAQ,UAAU,EAAE;AAAA,MAC/D;AAAA,IACF;AAGA,UAAM,SAAS,IAAI,wBAAO,QAAQ,IAAI,mBAAoB;AAAA,MACxD,YAAY;AAAA,IACd,CAAC;AAED,QAAI;AAGF,YAAM,SAAS,MAAM,OAAO,QAAQ,SAAS,UAAU;AAAA,QACrD,eAAe,QAAQ,UAAU;AAAA,MACnC,CAAC;AAGD,YAAM,gBAA+B;AAAA,QACnC,IAAI,OAAO;AAAA,QACX,QAAQ,OAAO;AAAA,QACf,UAAU,OAAO;AAAA,QACjB,QAAQ,OAAO;AAAA,QACf,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AAAA,QAClE,aAAa,OAAO;AAAA,QACpB,SAAS,OAAO;AAAA,MAClB;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,YAAM,IAAI,MAAM,4BAA4B,OAAO,EAAE;AAAA,IACvD;AAAA,EACF;AACF,CAAC;","names":[]}
|