@voicethere/agent 0.5.5 → 0.5.6
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/agent.js +6 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -1
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +3 -0
- package/dist/templates/echo-dc/agent.js +3 -0
- package/dist/templates/game-sync/agent.js +3 -0
- package/dist/templates/recording-consent/agent.js +1003 -0
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +11 -0
- package/dist/templates/registry.js.map +1 -1
- package/dist/templates/voice-showcase/agent.js +3 -0
- package/dist/templates/voice-starter/agent.js +3 -0
- package/dist/templates/webhooks/agent.js +3 -0
- package/dist/templates/webhooks-redis/agent.js +3 -0
- package/package.json +1 -1
- package/templates/README.md +10 -4
- package/templates/recording-consent/agent.ts +162 -0
- package/templates/recording-consent/conversation.ts +300 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EA4HpD,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAMrE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,eAAe,CAEnE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAMnE;AAED,wBAAgB,yBAAyB,IAAI,uBAAuB,EAAE,CAErE"}
|
|
@@ -51,6 +51,17 @@ export const AGENT_TEMPLATES = [
|
|
|
51
51
|
"voice-showcase/fun-facts.ts",
|
|
52
52
|
],
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
id: "recording-consent",
|
|
56
|
+
entry: "recording-consent/agent.ts",
|
|
57
|
+
kind: "product",
|
|
58
|
+
seedOnCreate: true,
|
|
59
|
+
description: "Recording consent flow — ask consent, pause capture for PII, resume when allowed.",
|
|
60
|
+
sourceFiles: [
|
|
61
|
+
"recording-consent/agent.ts",
|
|
62
|
+
"recording-consent/conversation.ts",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
54
65
|
{
|
|
55
66
|
id: "webhooks",
|
|
56
67
|
entry: "webhooks.ts",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -191,6 +191,7 @@ function parseDataChannelPayload(raw) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
194
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
194
195
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
196
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
197
|
function handleRecordingControlAck(message) {
|
|
@@ -340,6 +341,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
340
341
|
case "session_start":
|
|
341
342
|
endedSessionIds.delete(message.sessionId);
|
|
342
343
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
344
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
343
345
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
344
346
|
if (sessionStartInitDelayMs > 0) {
|
|
345
347
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -384,6 +386,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
384
386
|
case "session_end":
|
|
385
387
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
386
388
|
peerEnvBySessionId.delete(message.sessionId);
|
|
389
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
387
390
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
388
391
|
sessionId: message.sessionId
|
|
389
392
|
});
|
|
@@ -191,6 +191,7 @@ function parseDataChannelPayload(raw) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
194
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
194
195
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
196
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
197
|
function handleRecordingControlAck(message) {
|
|
@@ -340,6 +341,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
340
341
|
case "session_start":
|
|
341
342
|
endedSessionIds.delete(message.sessionId);
|
|
342
343
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
344
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
343
345
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
344
346
|
if (sessionStartInitDelayMs > 0) {
|
|
345
347
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -384,6 +386,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
384
386
|
case "session_end":
|
|
385
387
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
386
388
|
peerEnvBySessionId.delete(message.sessionId);
|
|
389
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
387
390
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
388
391
|
sessionId: message.sessionId
|
|
389
392
|
});
|
|
@@ -194,6 +194,7 @@ function parseDataChannelPayload(raw) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
197
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
197
198
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
198
199
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
199
200
|
function handleRecordingControlAck(message) {
|
|
@@ -343,6 +344,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
343
344
|
case "session_start":
|
|
344
345
|
endedSessionIds.delete(message.sessionId);
|
|
345
346
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
347
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
346
348
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
347
349
|
if (sessionStartInitDelayMs > 0) {
|
|
348
350
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -387,6 +389,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
387
389
|
case "session_end":
|
|
388
390
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
389
391
|
peerEnvBySessionId.delete(message.sessionId);
|
|
392
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
390
393
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
391
394
|
sessionId: message.sessionId
|
|
392
395
|
});
|
|
@@ -10946,6 +10946,7 @@ function parseDataChannelPayload(raw) {
|
|
|
10946
10946
|
}
|
|
10947
10947
|
}
|
|
10948
10948
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
10949
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
10949
10950
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
10950
10951
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
10951
10952
|
function handleRecordingControlAck(message) {
|
|
@@ -11095,6 +11096,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
11095
11096
|
case "session_start":
|
|
11096
11097
|
endedSessionIds.delete(message.sessionId);
|
|
11097
11098
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
11099
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
11098
11100
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
11099
11101
|
if (sessionStartInitDelayMs > 0) {
|
|
11100
11102
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -11139,6 +11141,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
11139
11141
|
case "session_end":
|
|
11140
11142
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
11141
11143
|
peerEnvBySessionId.delete(message.sessionId);
|
|
11144
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
11142
11145
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
11143
11146
|
sessionId: message.sessionId
|
|
11144
11147
|
});
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
|
|
16
16
|
## Product vs e2e
|
|
17
17
|
|
|
18
|
-
| Kind | Dashboard create
|
|
19
|
-
| ----------- |
|
|
20
|
-
| **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `game-sync`, `voice-showcase`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
|
|
21
|
-
| **e2e** | No
|
|
18
|
+
| Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
|
|
19
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
|
|
20
|
+
| **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `game-sync`, `voice-showcase`, `recording-consent`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
|
|
21
|
+
| **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
|
|
22
22
|
|
|
23
23
|
Product templates always set `seedOnCreate: true`. CI fails if a product template is missing its prebuilt bundle after `npm run build`.
|
|
24
24
|
|
|
@@ -67,6 +67,12 @@ Conversational voice demo for landing and dashboard previews — greets the user
|
|
|
67
67
|
|
|
68
68
|
Sources: `voice-showcase/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine), `weather.ts`, `recipes.ts`, `fun-facts.ts`.
|
|
69
69
|
|
|
70
|
+
### `recording-consent/` (`recording-consent`)
|
|
71
|
+
|
|
72
|
+
Demonstrates conversation recording consent on connect: asks whether recording is OK when the project has recording enabled, pauses capture while collecting name and date of birth, then resumes only if the customer consented. When project recording is off, logs a warning and never calls `startRecording`. Voice finals and typed chat share the same handler.
|
|
73
|
+
|
|
74
|
+
Sources: `recording-consent/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine).
|
|
75
|
+
|
|
70
76
|
### `webhooks.ts` (`webhooks`)
|
|
71
77
|
|
|
72
78
|
Inbound HTTP webhook sample — verifies `x-agent-webhook-signature` HMAC on the **raw body** with `AGENT_WEBHOOK_SIGNING_SECRET`, then `JSON.parse` and fans out to connected sessions via DataChannel + `speak`.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording consent template — ask consent, pause for PII, resume when allowed.
|
|
3
|
+
*
|
|
4
|
+
* Build:
|
|
5
|
+
* npx @voicethere/agent build --entry templates/recording-consent/agent.ts
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
agentLog,
|
|
9
|
+
defineAgent,
|
|
10
|
+
parseChatText,
|
|
11
|
+
pauseRecording,
|
|
12
|
+
resumeRecording,
|
|
13
|
+
sendToClient,
|
|
14
|
+
speak,
|
|
15
|
+
stopRecording,
|
|
16
|
+
type SpeechEvent,
|
|
17
|
+
} from "@voicethere/agent";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
beginSession,
|
|
21
|
+
handleUtterance,
|
|
22
|
+
type ConversationState,
|
|
23
|
+
type ConversationTurnResult,
|
|
24
|
+
type OutboundMessage,
|
|
25
|
+
type RecordingAction,
|
|
26
|
+
} from "./conversation.js";
|
|
27
|
+
|
|
28
|
+
let sessions = new Map<string, ConversationState>();
|
|
29
|
+
/** Per-session consent flag (shared-child safe — not one process-wide boolean). */
|
|
30
|
+
let consentBySessionId = new Map<string, boolean>();
|
|
31
|
+
|
|
32
|
+
function getState(sessionId: string): ConversationState {
|
|
33
|
+
const state = sessions.get(sessionId);
|
|
34
|
+
if (!state) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`recording-consent: missing session state for ${sessionId}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function relaySpeechEvent(sessionId: string, event: SpeechEvent): void {
|
|
43
|
+
sendToClient(sessionId, {
|
|
44
|
+
type: "agent_event",
|
|
45
|
+
event: event.type,
|
|
46
|
+
text: event.text,
|
|
47
|
+
raw: event,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function deliverMessages(sessionId: string, messages: OutboundMessage[]): void {
|
|
52
|
+
for (const message of messages) {
|
|
53
|
+
sendToClient(sessionId, message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function speakLines(sessionId: string, lines: string[]): void {
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
speak(sessionId, line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function applyRecordingAction(
|
|
64
|
+
sessionId: string,
|
|
65
|
+
action: RecordingAction,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
if (!action) return;
|
|
68
|
+
switch (action) {
|
|
69
|
+
case "pause":
|
|
70
|
+
await pauseRecording(sessionId);
|
|
71
|
+
break;
|
|
72
|
+
case "stop":
|
|
73
|
+
await stopRecording(sessionId);
|
|
74
|
+
break;
|
|
75
|
+
case "start":
|
|
76
|
+
case "resume":
|
|
77
|
+
await resumeRecording(sessionId);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function applyTurn(
|
|
83
|
+
sessionId: string,
|
|
84
|
+
result: ConversationTurnResult,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
sessions.set(sessionId, result.state);
|
|
87
|
+
if (result.state.consent !== undefined) {
|
|
88
|
+
consentBySessionId.set(sessionId, result.state.consent);
|
|
89
|
+
}
|
|
90
|
+
if (result.warnRecordingDisabled) {
|
|
91
|
+
agentLog(
|
|
92
|
+
"warn",
|
|
93
|
+
"Project conversation recording is disabled; skipping consent and will not call startRecording",
|
|
94
|
+
sessionId,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
speakLines(sessionId, result.speakLines);
|
|
98
|
+
deliverMessages(sessionId, result.messages);
|
|
99
|
+
await applyRecordingAction(sessionId, result.recordingAction);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function onUserText(sessionId: string, text: string): Promise<void> {
|
|
103
|
+
const state = getState(sessionId);
|
|
104
|
+
const result = handleUtterance(state, text);
|
|
105
|
+
await applyTurn(sessionId, result);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
defineAgent({
|
|
109
|
+
onAgentStart() {
|
|
110
|
+
sessions = new Map();
|
|
111
|
+
consentBySessionId = new Map();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onSessionStart({ sessionId, recordingAvailable }) {
|
|
115
|
+
const result = beginSession(recordingAvailable);
|
|
116
|
+
sessions.set(sessionId, result.state);
|
|
117
|
+
if (result.warnRecordingDisabled) {
|
|
118
|
+
agentLog(
|
|
119
|
+
"warn",
|
|
120
|
+
"Project conversation recording is disabled; skipping consent and will not call startRecording",
|
|
121
|
+
sessionId,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
speakLines(sessionId, result.speakLines);
|
|
125
|
+
deliverMessages(sessionId, result.messages);
|
|
126
|
+
sendToClient(sessionId, {
|
|
127
|
+
type: "agent_event",
|
|
128
|
+
event: "session_start",
|
|
129
|
+
sessionId,
|
|
130
|
+
});
|
|
131
|
+
agentLog(
|
|
132
|
+
"info",
|
|
133
|
+
`recording-consent session_start ${sessionId} recordingAvailable=${recordingAvailable}`,
|
|
134
|
+
sessionId,
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
onSpeechEvent({ sessionId }, event: SpeechEvent) {
|
|
139
|
+
relaySpeechEvent(sessionId, event);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
onUserSpeechFinal({ sessionId, text }) {
|
|
143
|
+
void onUserText(sessionId, text);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
onDataChannelMessage(ctx) {
|
|
147
|
+
const text = parseChatText(ctx.message);
|
|
148
|
+
if (!text) return;
|
|
149
|
+
void onUserText(ctx.sessionId, text);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
onSessionEnd({ sessionId }) {
|
|
153
|
+
sessions.delete(sessionId);
|
|
154
|
+
consentBySessionId.delete(sessionId);
|
|
155
|
+
sendToClient(sessionId, {
|
|
156
|
+
type: "agent_event",
|
|
157
|
+
event: "session_end",
|
|
158
|
+
sessionId,
|
|
159
|
+
});
|
|
160
|
+
agentLog("info", `recording-consent session_end ${sessionId}`, sessionId);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure conversation state machine for the recording-consent template.
|
|
3
|
+
* Tests import this module directly — no defineAgent dependency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const CONSENT_PROMPT =
|
|
7
|
+
"This call may be recorded for quality purposes. Is that OK?";
|
|
8
|
+
|
|
9
|
+
export const NAME_PROMPT = "May I have your name please?";
|
|
10
|
+
|
|
11
|
+
export const BIRTHDATE_PROMPT = "And your date of birth?";
|
|
12
|
+
|
|
13
|
+
export const RECORDING_DISABLED_SKIP_MESSAGE =
|
|
14
|
+
"Conversation recording is not enabled for this project.";
|
|
15
|
+
|
|
16
|
+
export type ConversationPhase =
|
|
17
|
+
"awaitingConsent" | "awaitingName" | "awaitingBirthdate" | "complete";
|
|
18
|
+
|
|
19
|
+
export type RecordingAction = "pause" | "stop" | "start" | "resume" | null;
|
|
20
|
+
|
|
21
|
+
export interface ConversationState {
|
|
22
|
+
phase: ConversationPhase;
|
|
23
|
+
recordingAvailable: boolean;
|
|
24
|
+
consent?: boolean;
|
|
25
|
+
/** True when consent was skipped because project recording is off. */
|
|
26
|
+
consentSkipped: boolean;
|
|
27
|
+
name?: string;
|
|
28
|
+
birthdate?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OutboundMessage {
|
|
32
|
+
type: "chat_reply" | "agent_event";
|
|
33
|
+
text?: string;
|
|
34
|
+
event?: string;
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConversationTurnResult {
|
|
39
|
+
state: ConversationState;
|
|
40
|
+
speakLines: string[];
|
|
41
|
+
messages: OutboundMessage[];
|
|
42
|
+
recordingAction: RecordingAction;
|
|
43
|
+
/** When true, agent should warn that project recording is disabled. */
|
|
44
|
+
warnRecordingDisabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createInitialState(
|
|
48
|
+
recordingAvailable: boolean,
|
|
49
|
+
): ConversationState {
|
|
50
|
+
if (recordingAvailable) {
|
|
51
|
+
return {
|
|
52
|
+
phase: "awaitingConsent",
|
|
53
|
+
recordingAvailable,
|
|
54
|
+
consentSkipped: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
phase: "awaitingName",
|
|
59
|
+
recordingAvailable,
|
|
60
|
+
consentSkipped: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function speakAndChat(text: string): {
|
|
65
|
+
speakLines: string[];
|
|
66
|
+
messages: OutboundMessage[];
|
|
67
|
+
} {
|
|
68
|
+
return {
|
|
69
|
+
speakLines: [text],
|
|
70
|
+
messages: [{ type: "chat_reply", text }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function beginSession(
|
|
75
|
+
recordingAvailable: boolean,
|
|
76
|
+
): ConversationTurnResult {
|
|
77
|
+
const state = createInitialState(recordingAvailable);
|
|
78
|
+
if (recordingAvailable) {
|
|
79
|
+
const prompt = speakAndChat(CONSENT_PROMPT);
|
|
80
|
+
return {
|
|
81
|
+
state,
|
|
82
|
+
speakLines: prompt.speakLines,
|
|
83
|
+
messages: prompt.messages,
|
|
84
|
+
recordingAction: null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const skip = speakAndChat(RECORDING_DISABLED_SKIP_MESSAGE);
|
|
88
|
+
const name = speakAndChat(NAME_PROMPT);
|
|
89
|
+
return {
|
|
90
|
+
state,
|
|
91
|
+
speakLines: [...skip.speakLines, ...name.speakLines],
|
|
92
|
+
messages: [...skip.messages, ...name.messages],
|
|
93
|
+
recordingAction: null,
|
|
94
|
+
warnRecordingDisabled: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isConsentNo(utterance: string): boolean {
|
|
99
|
+
const lower = utterance.toLowerCase().trim();
|
|
100
|
+
if (/\bnot\s+ok(?:ay)?\b/i.test(lower)) return true;
|
|
101
|
+
const noPhrases = [
|
|
102
|
+
"no",
|
|
103
|
+
"nope",
|
|
104
|
+
"nah",
|
|
105
|
+
"don't",
|
|
106
|
+
"do not",
|
|
107
|
+
"decline",
|
|
108
|
+
"refuse",
|
|
109
|
+
];
|
|
110
|
+
if (noPhrases.some((p) => lower === p || lower.startsWith(`${p} `))) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return /\b(no|nope|nah)\b/i.test(utterance) && !/\bknow\b/i.test(utterance);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isConsentYes(utterance: string): boolean {
|
|
117
|
+
if (isConsentNo(utterance)) return false;
|
|
118
|
+
const lower = utterance.toLowerCase().trim();
|
|
119
|
+
const yesPhrases = [
|
|
120
|
+
"yes",
|
|
121
|
+
"yeah",
|
|
122
|
+
"yep",
|
|
123
|
+
"sure",
|
|
124
|
+
"ok",
|
|
125
|
+
"okay",
|
|
126
|
+
"that's fine",
|
|
127
|
+
"that is fine",
|
|
128
|
+
"go ahead",
|
|
129
|
+
"fine",
|
|
130
|
+
"absolutely",
|
|
131
|
+
];
|
|
132
|
+
if (yesPhrases.some((p) => lower === p || lower.startsWith(`${p} `))) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return /\b(yes|yeah|yep|sure|ok|okay)\b/i.test(utterance);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function extractName(utterance: string): string | null {
|
|
139
|
+
const trimmed = utterance.trim();
|
|
140
|
+
const patterns = [/(?:my name is|i'm|i am|call me)\s+(.+)/i];
|
|
141
|
+
for (const pattern of patterns) {
|
|
142
|
+
const match = trimmed.match(pattern);
|
|
143
|
+
if (match?.[1]) {
|
|
144
|
+
return sanitizeToken(match[1], 40);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (trimmed.length > 0 && trimmed.length <= 60) {
|
|
148
|
+
return sanitizeToken(trimmed, 40);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function extractBirthdate(utterance: string): string | null {
|
|
154
|
+
const trimmed = utterance.trim();
|
|
155
|
+
const iso = trimmed.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
156
|
+
if (iso?.[1]) return iso[1];
|
|
157
|
+
const slash = trimmed.match(/\b(\d{1,2}\/\d{1,2}\/\d{2,4})\b/);
|
|
158
|
+
if (slash?.[1]) return slash[1];
|
|
159
|
+
const spoken = trimmed.match(
|
|
160
|
+
/\b(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}\b/i,
|
|
161
|
+
);
|
|
162
|
+
if (spoken?.[0]) return spoken[0];
|
|
163
|
+
if (trimmed.length >= 4 && trimmed.length <= 40) {
|
|
164
|
+
return sanitizeToken(trimmed, 40);
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sanitizeToken(raw: string, maxLen: number): string {
|
|
170
|
+
let value = raw
|
|
171
|
+
.trim()
|
|
172
|
+
.replace(/[.,!?;:]+$/g, "")
|
|
173
|
+
.trim();
|
|
174
|
+
if (value.length > maxLen) {
|
|
175
|
+
value = value.slice(0, maxLen).trim();
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function askNameAgain(state: ConversationState): ConversationTurnResult {
|
|
181
|
+
const prompt = speakAndChat(
|
|
182
|
+
"Sorry, I didn't catch your name. May I have your name please?",
|
|
183
|
+
);
|
|
184
|
+
return {
|
|
185
|
+
state,
|
|
186
|
+
speakLines: prompt.speakLines,
|
|
187
|
+
messages: prompt.messages,
|
|
188
|
+
recordingAction: null,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function askBirthdateAgain(state: ConversationState): ConversationTurnResult {
|
|
193
|
+
const prompt = speakAndChat(
|
|
194
|
+
"Sorry, I didn't catch your date of birth. Could you repeat it?",
|
|
195
|
+
);
|
|
196
|
+
return {
|
|
197
|
+
state,
|
|
198
|
+
speakLines: prompt.speakLines,
|
|
199
|
+
messages: prompt.messages,
|
|
200
|
+
recordingAction: null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function finishAfterPii(state: ConversationState): ConversationTurnResult {
|
|
205
|
+
const next: ConversationState = { ...state, phase: "complete" };
|
|
206
|
+
const thankYou = speakAndChat(
|
|
207
|
+
`Thank you, ${state.name}. We have your date of birth on file.`,
|
|
208
|
+
);
|
|
209
|
+
let recordingAction: RecordingAction = null;
|
|
210
|
+
if (state.consent === true && state.recordingAvailable) {
|
|
211
|
+
recordingAction = "resume";
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
state: next,
|
|
215
|
+
speakLines: thankYou.speakLines,
|
|
216
|
+
messages: thankYou.messages,
|
|
217
|
+
recordingAction,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function handleUtterance(
|
|
222
|
+
state: ConversationState,
|
|
223
|
+
utterance: string,
|
|
224
|
+
): ConversationTurnResult {
|
|
225
|
+
switch (state.phase) {
|
|
226
|
+
case "awaitingConsent": {
|
|
227
|
+
if (isConsentNo(utterance)) {
|
|
228
|
+
const next: ConversationState = {
|
|
229
|
+
...state,
|
|
230
|
+
phase: "awaitingName",
|
|
231
|
+
consent: false,
|
|
232
|
+
};
|
|
233
|
+
const name = speakAndChat(NAME_PROMPT);
|
|
234
|
+
return {
|
|
235
|
+
state: next,
|
|
236
|
+
speakLines: name.speakLines,
|
|
237
|
+
messages: name.messages,
|
|
238
|
+
recordingAction: "stop",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (isConsentYes(utterance)) {
|
|
242
|
+
const next: ConversationState = {
|
|
243
|
+
...state,
|
|
244
|
+
phase: "awaitingName",
|
|
245
|
+
consent: true,
|
|
246
|
+
};
|
|
247
|
+
const name = speakAndChat(NAME_PROMPT);
|
|
248
|
+
return {
|
|
249
|
+
state: next,
|
|
250
|
+
speakLines: name.speakLines,
|
|
251
|
+
messages: name.messages,
|
|
252
|
+
recordingAction: "pause",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const retry = speakAndChat(
|
|
256
|
+
"Please say yes or no — may we record this conversation?",
|
|
257
|
+
);
|
|
258
|
+
return {
|
|
259
|
+
state,
|
|
260
|
+
speakLines: retry.speakLines,
|
|
261
|
+
messages: retry.messages,
|
|
262
|
+
recordingAction: null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case "awaitingName": {
|
|
266
|
+
const name = extractName(utterance);
|
|
267
|
+
if (!name) {
|
|
268
|
+
return askNameAgain(state);
|
|
269
|
+
}
|
|
270
|
+
const next: ConversationState = {
|
|
271
|
+
...state,
|
|
272
|
+
phase: "awaitingBirthdate",
|
|
273
|
+
name,
|
|
274
|
+
};
|
|
275
|
+
const birthdate = speakAndChat(BIRTHDATE_PROMPT);
|
|
276
|
+
return {
|
|
277
|
+
state: next,
|
|
278
|
+
speakLines: birthdate.speakLines,
|
|
279
|
+
messages: birthdate.messages,
|
|
280
|
+
recordingAction: null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
case "awaitingBirthdate": {
|
|
284
|
+
const birthdate = extractBirthdate(utterance);
|
|
285
|
+
if (!birthdate) {
|
|
286
|
+
return askBirthdateAgain(state);
|
|
287
|
+
}
|
|
288
|
+
return finishAfterPii({ ...state, birthdate });
|
|
289
|
+
}
|
|
290
|
+
case "complete": {
|
|
291
|
+
const done = speakAndChat("We are all set. How can I help you today?");
|
|
292
|
+
return {
|
|
293
|
+
state,
|
|
294
|
+
speakLines: done.speakLines,
|
|
295
|
+
messages: done.messages,
|
|
296
|
+
recordingAction: null,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|