@absolutejs/voice 0.0.22-beta.27 → 0.0.22-beta.29
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/angular/index.js +4 -0
- package/dist/angular/voice-stream.service.d.ts +2 -0
- package/dist/handoff.d.ts +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +276 -9
- package/dist/react/index.js +4 -0
- package/dist/react/useVoiceController.d.ts +1 -0
- package/dist/react/useVoiceStream.d.ts +1 -0
- package/dist/testing/index.js +268 -5
- package/dist/trace.d.ts +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/vue/index.js +4 -0
- package/dist/vue/useVoiceStream.d.ts +2 -0
- package/package.json +1 -1
package/dist/angular/index.js
CHANGED
|
@@ -596,6 +596,7 @@ class VoiceStreamService {
|
|
|
596
596
|
const stream = createVoiceStream(path, options);
|
|
597
597
|
const assistantAudioSignal = signal([]);
|
|
598
598
|
const assistantTextsSignal = signal([]);
|
|
599
|
+
const callSignal = signal(null);
|
|
599
600
|
const errorSignal = signal(null);
|
|
600
601
|
const isConnectedSignal = signal(false);
|
|
601
602
|
const partialSignal = signal("");
|
|
@@ -605,6 +606,7 @@ class VoiceStreamService {
|
|
|
605
606
|
const sync = () => {
|
|
606
607
|
assistantAudioSignal.set([...stream.assistantAudio]);
|
|
607
608
|
assistantTextsSignal.set([...stream.assistantTexts]);
|
|
609
|
+
callSignal.set(stream.call);
|
|
608
610
|
errorSignal.set(stream.error);
|
|
609
611
|
isConnectedSignal.set(stream.isConnected);
|
|
610
612
|
partialSignal.set(stream.partial);
|
|
@@ -617,6 +619,8 @@ class VoiceStreamService {
|
|
|
617
619
|
return {
|
|
618
620
|
assistantAudio: computed(() => assistantAudioSignal()),
|
|
619
621
|
assistantTexts: computed(() => assistantTextsSignal()),
|
|
622
|
+
call: computed(() => callSignal()),
|
|
623
|
+
callControl: (message) => stream.callControl(message),
|
|
620
624
|
close: () => {
|
|
621
625
|
unsubscribe();
|
|
622
626
|
stream.close();
|
|
@@ -8,6 +8,8 @@ export declare class VoiceStreamService {
|
|
|
8
8
|
turnId?: string;
|
|
9
9
|
}[]>;
|
|
10
10
|
assistantTexts: import("@angular/core").Signal<string[]>;
|
|
11
|
+
call: import("@angular/core").Signal<import("..").VoiceCallLifecycleState | null>;
|
|
12
|
+
callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
|
|
11
13
|
close: () => void;
|
|
12
14
|
endTurn: () => void;
|
|
13
15
|
error: import("@angular/core").Signal<string | null>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { VoiceHandoffAction, VoiceHandoffAdapter, VoiceHandoffConfig, VoiceHandoffInput, VoiceHandoffResult, VoiceSessionRecord } from './types';
|
|
2
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
3
|
+
export type VoiceHandoffDelivery = VoiceHandoffResult & {
|
|
4
|
+
adapterId: string;
|
|
5
|
+
adapterKind?: string;
|
|
6
|
+
};
|
|
7
|
+
export type VoiceHandoffFanoutResult = {
|
|
8
|
+
action: VoiceHandoffAction;
|
|
9
|
+
deliveries: Record<string, VoiceHandoffDelivery>;
|
|
10
|
+
status: VoiceHandoffResult['status'];
|
|
11
|
+
};
|
|
12
|
+
export type VoiceWebhookHandoffAdapterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
13
|
+
actions?: VoiceHandoffAction[];
|
|
14
|
+
body?: (input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<Record<string, unknown>>;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
id: string;
|
|
18
|
+
kind?: string;
|
|
19
|
+
method?: 'POST' | 'PUT' | 'PATCH';
|
|
20
|
+
signingSecret?: string;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
url: string;
|
|
23
|
+
};
|
|
24
|
+
export type VoiceTwilioRedirectHandoffAdapterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
25
|
+
accountSid: string;
|
|
26
|
+
actions?: VoiceHandoffAction[];
|
|
27
|
+
authToken: string;
|
|
28
|
+
buildTwiML?: (input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<string>;
|
|
29
|
+
callSid?: string | ((input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<string | undefined>);
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
id?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare const deliverVoiceHandoff: <TContext, TSession extends VoiceSessionRecord, TResult>(input: {
|
|
35
|
+
config?: VoiceHandoffConfig<TContext, TSession, TResult>;
|
|
36
|
+
handoff: VoiceHandoffInput<TContext, TSession, TResult>;
|
|
37
|
+
}) => Promise<VoiceHandoffFanoutResult | undefined>;
|
|
38
|
+
export declare const createVoiceWebhookHandoffAdapter: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceWebhookHandoffAdapterOptions<TContext, TSession, TResult>) => VoiceHandoffAdapter<TContext, TSession, TResult>;
|
|
39
|
+
export declare const createVoiceTwilioRedirectHandoffAdapter: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceTwilioRedirectHandoffAdapterOptions<TContext, TSession, TResult>) => VoiceHandoffAdapter<TContext, TSession, TResult>;
|
|
40
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { createVoiceS3ReviewStore } from './s3Store';
|
|
|
14
14
|
export { createVoiceMemoryStore } from './memoryStore';
|
|
15
15
|
export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks } from './opsSinks';
|
|
16
16
|
export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature } from './opsWebhook';
|
|
17
|
+
export { createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff } from './handoff';
|
|
17
18
|
export { createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
|
|
18
19
|
export { assignVoiceOpsTask, applyVoiceOpsTaskAssignmentRule, applyVoiceOpsTaskPolicy, buildVoiceOpsTaskFromReview, buildVoiceOpsTaskFromSLABreach, claimVoiceOpsTask, completeVoiceOpsTask, createVoiceExternalObjectMap, createVoiceExternalObjectMapId, createVoiceCallCompletedEvent, createVoiceTaskSLABreachedEvent, deadLetterVoiceOpsTask, deliverVoiceIntegrationEvent, failVoiceOpsTask, hasVoiceOpsTaskSLABreach, heartbeatVoiceOpsTask, isVoiceOpsTaskOverdue, markVoiceOpsTaskSLABreached, matchesVoiceOpsTaskAssignmentRule, resolveVoiceOpsTaskAgeBucket, createVoiceIntegrationEvent, createVoiceReviewSavedEvent, resolveVoiceOpsTaskAssignment, resolveVoiceOpsTaskPolicy, requeueVoiceOpsTask, createVoiceTaskCreatedEvent, createVoiceTaskUpdatedEvent, listVoiceOpsTasks, reopenVoiceOpsTask, startVoiceOpsTask, summarizeVoiceOpsTaskAnalytics, summarizeVoiceOpsTasks, withVoiceIntegrationEventId, withVoiceOpsTaskId } from './ops';
|
|
19
20
|
export { createVoiceSession } from './session';
|
|
@@ -40,6 +41,7 @@ export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPrese
|
|
|
40
41
|
export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
|
|
41
42
|
export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
|
|
42
43
|
export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
|
|
44
|
+
export type { VoiceHandoffDelivery, VoiceHandoffFanoutResult, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
|
|
43
45
|
export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
|
|
44
46
|
export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
|
|
45
47
|
export type { StoredVoiceTraceEvent, VoiceTraceEvaluation, VoiceTraceEvaluationOptions, VoiceTraceEvent, VoiceTraceEventFilter, VoiceTraceEventStore, VoiceTraceEventType, VoiceTraceIssue, VoiceTraceIssueSeverity, VoiceTraceHTTPSinkOptions, VoiceTracePruneFilter, VoiceTracePruneOptions, VoiceTracePruneResult, VoiceTraceRedactionConfig, VoiceTraceRedactionOptions, VoiceTraceRedactionReplacement, VoiceResolvedTraceRedactionOptions, VoiceTraceSink, VoiceTraceSinkDeliveryQueueStatus, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryResult, VoiceTraceSinkDeliveryStatus, VoiceTraceSinkDeliveryStore, VoiceTraceSinkFanoutResult, VoiceTraceSinkStoreOptions, VoiceTraceSummary } from './trace';
|
package/dist/index.js
CHANGED
|
@@ -2992,6 +2992,214 @@ var toVoiceSessionSummary = (session) => ({
|
|
|
2992
2992
|
// src/session.ts
|
|
2993
2993
|
import { Buffer } from "buffer";
|
|
2994
2994
|
|
|
2995
|
+
// src/handoff.ts
|
|
2996
|
+
var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2997
|
+
var signHandoffBody = async (input) => {
|
|
2998
|
+
const encoder = new TextEncoder;
|
|
2999
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
3000
|
+
hash: "SHA-256",
|
|
3001
|
+
name: "HMAC"
|
|
3002
|
+
}, false, ["sign"]);
|
|
3003
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
3004
|
+
return `sha256=${toHex3(new Uint8Array(signature))}`;
|
|
3005
|
+
};
|
|
3006
|
+
var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
3007
|
+
var createSkippedDelivery = (adapter) => ({
|
|
3008
|
+
adapterId: adapter.id,
|
|
3009
|
+
adapterKind: adapter.kind,
|
|
3010
|
+
status: "skipped"
|
|
3011
|
+
});
|
|
3012
|
+
var aggregateHandoffStatus = (deliveries) => {
|
|
3013
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
3014
|
+
if (statuses.some((status) => status === "failed")) {
|
|
3015
|
+
return "failed";
|
|
3016
|
+
}
|
|
3017
|
+
if (statuses.some((status) => status === "delivered")) {
|
|
3018
|
+
return "delivered";
|
|
3019
|
+
}
|
|
3020
|
+
return "skipped";
|
|
3021
|
+
};
|
|
3022
|
+
var defaultWebhookBody = (input) => ({
|
|
3023
|
+
action: input.action,
|
|
3024
|
+
metadata: input.metadata,
|
|
3025
|
+
reason: input.reason,
|
|
3026
|
+
result: input.result,
|
|
3027
|
+
session: {
|
|
3028
|
+
id: input.session.id,
|
|
3029
|
+
scenarioId: input.session.scenarioId,
|
|
3030
|
+
status: input.session.status
|
|
3031
|
+
},
|
|
3032
|
+
source: "absolutejs-voice",
|
|
3033
|
+
target: input.target
|
|
3034
|
+
});
|
|
3035
|
+
var deliverVoiceHandoff = async (input) => {
|
|
3036
|
+
if (!input.config || input.config.adapters.length === 0) {
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
const deliveries = {};
|
|
3040
|
+
for (const adapter of input.config.adapters) {
|
|
3041
|
+
if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
|
|
3042
|
+
deliveries[adapter.id] = createSkippedDelivery(adapter);
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
try {
|
|
3046
|
+
const result = await adapter.handoff(input.handoff);
|
|
3047
|
+
deliveries[adapter.id] = {
|
|
3048
|
+
...result,
|
|
3049
|
+
adapterId: adapter.id,
|
|
3050
|
+
adapterKind: adapter.kind
|
|
3051
|
+
};
|
|
3052
|
+
} catch (error) {
|
|
3053
|
+
deliveries[adapter.id] = {
|
|
3054
|
+
adapterId: adapter.id,
|
|
3055
|
+
adapterKind: adapter.kind,
|
|
3056
|
+
error: toErrorMessage2(error),
|
|
3057
|
+
status: "failed"
|
|
3058
|
+
};
|
|
3059
|
+
if (input.config.failMode === "throw") {
|
|
3060
|
+
throw error;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return {
|
|
3065
|
+
action: input.handoff.action,
|
|
3066
|
+
deliveries,
|
|
3067
|
+
status: aggregateHandoffStatus(deliveries)
|
|
3068
|
+
};
|
|
3069
|
+
};
|
|
3070
|
+
var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
3071
|
+
actions: options.actions,
|
|
3072
|
+
handoff: async (input) => {
|
|
3073
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3074
|
+
if (typeof fetchImpl !== "function") {
|
|
3075
|
+
return {
|
|
3076
|
+
deliveredTo: options.url,
|
|
3077
|
+
error: "Handoff delivery failed: fetch is not available in this runtime.",
|
|
3078
|
+
status: "failed"
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
|
|
3082
|
+
const headers = {
|
|
3083
|
+
"content-type": "application/json",
|
|
3084
|
+
...options.headers
|
|
3085
|
+
};
|
|
3086
|
+
if (options.signingSecret) {
|
|
3087
|
+
const timestamp = String(Date.now());
|
|
3088
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
3089
|
+
headers["x-absolutejs-signature"] = await signHandoffBody({
|
|
3090
|
+
body,
|
|
3091
|
+
secret: options.signingSecret,
|
|
3092
|
+
timestamp
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
3096
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
3097
|
+
try {
|
|
3098
|
+
const response = await fetchImpl(options.url, {
|
|
3099
|
+
body,
|
|
3100
|
+
headers,
|
|
3101
|
+
method: options.method ?? "POST",
|
|
3102
|
+
signal: controller?.signal
|
|
3103
|
+
});
|
|
3104
|
+
if (!response.ok) {
|
|
3105
|
+
return {
|
|
3106
|
+
deliveredTo: options.url,
|
|
3107
|
+
error: `Handoff delivery failed with response ${response.status}.`,
|
|
3108
|
+
status: "failed"
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
return {
|
|
3112
|
+
deliveredAt: Date.now(),
|
|
3113
|
+
deliveredTo: options.url,
|
|
3114
|
+
status: "delivered"
|
|
3115
|
+
};
|
|
3116
|
+
} finally {
|
|
3117
|
+
if (timeout) {
|
|
3118
|
+
clearTimeout(timeout);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
},
|
|
3122
|
+
id: options.id,
|
|
3123
|
+
kind: options.kind ?? "webhook"
|
|
3124
|
+
});
|
|
3125
|
+
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
3126
|
+
var defaultTwilioTransferTwiML = (input) => {
|
|
3127
|
+
if (!input.target) {
|
|
3128
|
+
return "<Response><Hangup /></Response>";
|
|
3129
|
+
}
|
|
3130
|
+
return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
|
|
3131
|
+
};
|
|
3132
|
+
var resolveTwilioCallSid = async (resolver, input) => {
|
|
3133
|
+
if (typeof resolver === "function") {
|
|
3134
|
+
return resolver(input);
|
|
3135
|
+
}
|
|
3136
|
+
if (typeof resolver === "string" && resolver.length > 0) {
|
|
3137
|
+
return resolver;
|
|
3138
|
+
}
|
|
3139
|
+
const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
|
|
3140
|
+
const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
|
|
3141
|
+
const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
|
|
3142
|
+
return metadataSid ?? sessionSid;
|
|
3143
|
+
};
|
|
3144
|
+
var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
3145
|
+
actions: options.actions ?? ["transfer"],
|
|
3146
|
+
handoff: async (input) => {
|
|
3147
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3148
|
+
const callSid = await resolveTwilioCallSid(options.callSid, input);
|
|
3149
|
+
if (!callSid) {
|
|
3150
|
+
return {
|
|
3151
|
+
error: "Twilio handoff requires a callSid.",
|
|
3152
|
+
status: "failed"
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
if (typeof fetchImpl !== "function") {
|
|
3156
|
+
return {
|
|
3157
|
+
error: "Twilio handoff failed: fetch is not available in this runtime.",
|
|
3158
|
+
status: "failed"
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
|
|
3162
|
+
const body = new URLSearchParams({
|
|
3163
|
+
Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
|
|
3164
|
+
});
|
|
3165
|
+
const auth = btoa(`${options.accountSid}:${options.authToken}`);
|
|
3166
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
3167
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
3168
|
+
try {
|
|
3169
|
+
const response = await fetchImpl(url, {
|
|
3170
|
+
body,
|
|
3171
|
+
headers: {
|
|
3172
|
+
authorization: `Basic ${auth}`,
|
|
3173
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
3174
|
+
},
|
|
3175
|
+
method: "POST",
|
|
3176
|
+
signal: controller?.signal
|
|
3177
|
+
});
|
|
3178
|
+
if (!response.ok) {
|
|
3179
|
+
return {
|
|
3180
|
+
deliveredTo: url,
|
|
3181
|
+
error: `Twilio handoff failed with response ${response.status}.`,
|
|
3182
|
+
status: "failed"
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
return {
|
|
3186
|
+
deliveredAt: Date.now(),
|
|
3187
|
+
deliveredTo: url,
|
|
3188
|
+
metadata: {
|
|
3189
|
+
callSid
|
|
3190
|
+
},
|
|
3191
|
+
status: "delivered"
|
|
3192
|
+
};
|
|
3193
|
+
} finally {
|
|
3194
|
+
if (timeout) {
|
|
3195
|
+
clearTimeout(timeout);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
3199
|
+
id: options.id ?? "twilio-redirect",
|
|
3200
|
+
kind: "twilio-redirect"
|
|
3201
|
+
});
|
|
3202
|
+
|
|
2995
3203
|
// src/turnDetection.ts
|
|
2996
3204
|
var DEFAULT_SILENCE_MS = 700;
|
|
2997
3205
|
var DEFAULT_SPEECH_THRESHOLD = 0.015;
|
|
@@ -3400,6 +3608,34 @@ var createVoiceSession = (options) => {
|
|
|
3400
3608
|
type: "call_lifecycle"
|
|
3401
3609
|
});
|
|
3402
3610
|
};
|
|
3611
|
+
const runHandoff = async (input) => {
|
|
3612
|
+
const result = await deliverVoiceHandoff({
|
|
3613
|
+
config: options.handoff,
|
|
3614
|
+
handoff: {
|
|
3615
|
+
action: input.action,
|
|
3616
|
+
api,
|
|
3617
|
+
context: options.context,
|
|
3618
|
+
metadata: input.metadata,
|
|
3619
|
+
reason: input.reason,
|
|
3620
|
+
result: input.result,
|
|
3621
|
+
session: input.session,
|
|
3622
|
+
target: input.target
|
|
3623
|
+
}
|
|
3624
|
+
});
|
|
3625
|
+
if (!result) {
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
await appendTrace({
|
|
3629
|
+
metadata: input.metadata,
|
|
3630
|
+
payload: {
|
|
3631
|
+
...result,
|
|
3632
|
+
reason: input.reason,
|
|
3633
|
+
target: input.target
|
|
3634
|
+
},
|
|
3635
|
+
session: input.session,
|
|
3636
|
+
type: "call.handoff"
|
|
3637
|
+
});
|
|
3638
|
+
};
|
|
3403
3639
|
const readSession = async () => options.store.getOrCreate(options.id);
|
|
3404
3640
|
const writeSession = async (mutate) => {
|
|
3405
3641
|
const session = await options.store.getOrCreate(options.id);
|
|
@@ -3679,6 +3915,14 @@ var createVoiceSession = (options) => {
|
|
|
3679
3915
|
type: "call.lifecycle"
|
|
3680
3916
|
});
|
|
3681
3917
|
await sendCallLifecycle(session);
|
|
3918
|
+
await runHandoff({
|
|
3919
|
+
action: "transfer",
|
|
3920
|
+
metadata: input.metadata,
|
|
3921
|
+
reason: input.reason,
|
|
3922
|
+
result: input.result,
|
|
3923
|
+
session,
|
|
3924
|
+
target: input.target
|
|
3925
|
+
});
|
|
3682
3926
|
await completeInternal(input.result, {
|
|
3683
3927
|
disposition: "transferred",
|
|
3684
3928
|
invokeOnComplete: false,
|
|
@@ -3705,6 +3949,13 @@ var createVoiceSession = (options) => {
|
|
|
3705
3949
|
type: "call.lifecycle"
|
|
3706
3950
|
});
|
|
3707
3951
|
await sendCallLifecycle(session);
|
|
3952
|
+
await runHandoff({
|
|
3953
|
+
action: "escalate",
|
|
3954
|
+
metadata: input.metadata,
|
|
3955
|
+
reason: input.reason,
|
|
3956
|
+
result: input.result,
|
|
3957
|
+
session
|
|
3958
|
+
});
|
|
3708
3959
|
await completeInternal(input.result, {
|
|
3709
3960
|
disposition: "escalated",
|
|
3710
3961
|
invokeOnComplete: false,
|
|
@@ -3728,6 +3979,12 @@ var createVoiceSession = (options) => {
|
|
|
3728
3979
|
type: "call.lifecycle"
|
|
3729
3980
|
});
|
|
3730
3981
|
await sendCallLifecycle(session);
|
|
3982
|
+
await runHandoff({
|
|
3983
|
+
action: "no-answer",
|
|
3984
|
+
metadata: input?.metadata,
|
|
3985
|
+
result: input?.result,
|
|
3986
|
+
session
|
|
3987
|
+
});
|
|
3731
3988
|
await completeInternal(input?.result, {
|
|
3732
3989
|
disposition: "no-answer",
|
|
3733
3990
|
invokeOnComplete: false,
|
|
@@ -3750,6 +4007,12 @@ var createVoiceSession = (options) => {
|
|
|
3750
4007
|
type: "call.lifecycle"
|
|
3751
4008
|
});
|
|
3752
4009
|
await sendCallLifecycle(session);
|
|
4010
|
+
await runHandoff({
|
|
4011
|
+
action: "voicemail",
|
|
4012
|
+
metadata: input?.metadata,
|
|
4013
|
+
result: input?.result,
|
|
4014
|
+
session
|
|
4015
|
+
});
|
|
3753
4016
|
await completeInternal(input?.result, {
|
|
3754
4017
|
disposition: "voicemail",
|
|
3755
4018
|
invokeOnComplete: false,
|
|
@@ -4922,6 +5185,7 @@ var voice = (config) => {
|
|
|
4922
5185
|
audioConditioning: sessionOptions.audioConditioning,
|
|
4923
5186
|
context,
|
|
4924
5187
|
id: sessionId,
|
|
5188
|
+
handoff: config.handoff,
|
|
4925
5189
|
languageStrategy: config.languageStrategy,
|
|
4926
5190
|
lexicon,
|
|
4927
5191
|
logger: sessionOptions.logger,
|
|
@@ -5117,7 +5381,7 @@ var voice = (config) => {
|
|
|
5117
5381
|
};
|
|
5118
5382
|
// src/agent.ts
|
|
5119
5383
|
var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
|
|
5120
|
-
var
|
|
5384
|
+
var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
|
|
5121
5385
|
var createHistoryMessages = (session, turn) => {
|
|
5122
5386
|
const messages = [];
|
|
5123
5387
|
for (const previousTurn of session.turns) {
|
|
@@ -5298,7 +5562,7 @@ var createVoiceAgent = (options) => {
|
|
|
5298
5562
|
toolCallId: toolCall.id
|
|
5299
5563
|
});
|
|
5300
5564
|
} catch (error) {
|
|
5301
|
-
const errorMessage =
|
|
5565
|
+
const errorMessage = toErrorMessage3(error);
|
|
5302
5566
|
toolResults.push({
|
|
5303
5567
|
error: errorMessage,
|
|
5304
5568
|
status: "error",
|
|
@@ -6563,7 +6827,7 @@ var sleep3 = async (delayMs) => {
|
|
|
6563
6827
|
}
|
|
6564
6828
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
6565
6829
|
};
|
|
6566
|
-
var
|
|
6830
|
+
var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6567
6831
|
var signVoiceTraceSinkBody = async (input) => {
|
|
6568
6832
|
const encoder = new TextEncoder;
|
|
6569
6833
|
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
@@ -6572,7 +6836,7 @@ var signVoiceTraceSinkBody = async (input) => {
|
|
|
6572
6836
|
}, false, ["sign"]);
|
|
6573
6837
|
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
6574
6838
|
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
6575
|
-
return `sha256=${
|
|
6839
|
+
return `sha256=${toHex4(new Uint8Array(signature))}`;
|
|
6576
6840
|
};
|
|
6577
6841
|
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
6578
6842
|
if (input.response) {
|
|
@@ -8937,7 +9201,7 @@ var createVoiceMemoryStore = () => {
|
|
|
8937
9201
|
};
|
|
8938
9202
|
// src/opsWebhook.ts
|
|
8939
9203
|
import { Elysia as Elysia5 } from "elysia";
|
|
8940
|
-
var
|
|
9204
|
+
var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8941
9205
|
var signVoiceOpsWebhookBody = async (input) => {
|
|
8942
9206
|
const encoder = new TextEncoder;
|
|
8943
9207
|
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
@@ -8945,7 +9209,7 @@ var signVoiceOpsWebhookBody = async (input) => {
|
|
|
8945
9209
|
name: "HMAC"
|
|
8946
9210
|
}, false, ["sign"]);
|
|
8947
9211
|
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
8948
|
-
return `sha256=${
|
|
9212
|
+
return `sha256=${toHex5(new Uint8Array(signature))}`;
|
|
8949
9213
|
};
|
|
8950
9214
|
var timingSafeEqual = (left, right) => {
|
|
8951
9215
|
const encoder = new TextEncoder;
|
|
@@ -10630,7 +10894,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
|
|
|
10630
10894
|
import { Buffer as Buffer2 } from "buffer";
|
|
10631
10895
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
10632
10896
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
10633
|
-
var
|
|
10897
|
+
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
10634
10898
|
var normalizeOnTurn2 = (handler) => {
|
|
10635
10899
|
if (handler.length > 1) {
|
|
10636
10900
|
const directHandler = handler;
|
|
@@ -10826,8 +11090,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
10826
11090
|
}
|
|
10827
11091
|
});
|
|
10828
11092
|
var createTwilioVoiceResponse = (options) => {
|
|
10829
|
-
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${
|
|
10830
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${
|
|
11093
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
11094
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
|
|
10831
11095
|
};
|
|
10832
11096
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
10833
11097
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
|
@@ -11119,13 +11383,16 @@ export {
|
|
|
11119
11383
|
deliverVoiceTraceEventsToSinks,
|
|
11120
11384
|
deliverVoiceIntegrationEventToSinks,
|
|
11121
11385
|
deliverVoiceIntegrationEvent,
|
|
11386
|
+
deliverVoiceHandoff,
|
|
11122
11387
|
decodeTwilioMulawBase64,
|
|
11123
11388
|
deadLetterVoiceOpsTask,
|
|
11124
11389
|
createVoiceZendeskTicketUpdateSink,
|
|
11125
11390
|
createVoiceZendeskTicketSyncSinks,
|
|
11126
11391
|
createVoiceZendeskTicketSink,
|
|
11392
|
+
createVoiceWebhookHandoffAdapter,
|
|
11127
11393
|
createVoiceWebhookDeliveryWorkerLoop,
|
|
11128
11394
|
createVoiceWebhookDeliveryWorker,
|
|
11395
|
+
createVoiceTwilioRedirectHandoffAdapter,
|
|
11129
11396
|
createVoiceTraceSinkStore,
|
|
11130
11397
|
createVoiceTraceSinkDeliveryWorkerLoop,
|
|
11131
11398
|
createVoiceTraceSinkDeliveryWorker,
|
package/dist/react/index.js
CHANGED
|
@@ -589,6 +589,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
589
589
|
var EMPTY_SNAPSHOT = {
|
|
590
590
|
assistantAudio: [],
|
|
591
591
|
assistantTexts: [],
|
|
592
|
+
call: null,
|
|
592
593
|
error: null,
|
|
593
594
|
isConnected: false,
|
|
594
595
|
partial: "",
|
|
@@ -606,6 +607,7 @@ var useVoiceStream = (path, options = {}) => {
|
|
|
606
607
|
const snapshot = useSyncExternalStore(stream.subscribe, stream.getSnapshot, stream.getServerSnapshot) ?? EMPTY_SNAPSHOT;
|
|
607
608
|
return {
|
|
608
609
|
...snapshot,
|
|
610
|
+
callControl: (message) => stream.callControl(message),
|
|
609
611
|
close: () => stream.close(),
|
|
610
612
|
endTurn: () => stream.endTurn(),
|
|
611
613
|
sendAudio: (audio) => stream.sendAudio(audio)
|
|
@@ -1248,6 +1250,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1248
1250
|
var EMPTY_SNAPSHOT2 = {
|
|
1249
1251
|
assistantAudio: [],
|
|
1250
1252
|
assistantTexts: [],
|
|
1253
|
+
call: null,
|
|
1251
1254
|
error: null,
|
|
1252
1255
|
isConnected: false,
|
|
1253
1256
|
isRecording: false,
|
|
@@ -1268,6 +1271,7 @@ var useVoiceController = (path, options = {}) => {
|
|
|
1268
1271
|
return {
|
|
1269
1272
|
...snapshot,
|
|
1270
1273
|
bindHTMX: controller.bindHTMX,
|
|
1274
|
+
callControl: (message) => controller.callControl(message),
|
|
1271
1275
|
close: () => controller.close(),
|
|
1272
1276
|
endTurn: () => controller.endTurn(),
|
|
1273
1277
|
sendAudio: (audio) => controller.sendAudio(audio),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { VoiceControllerOptions } from '../types';
|
|
2
2
|
export declare const useVoiceController: <TResult = unknown>(path: string, options?: VoiceControllerOptions) => {
|
|
3
3
|
bindHTMX: (options: import("..").VoiceHTMXBindingOptions) => () => void;
|
|
4
|
+
callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
|
|
4
5
|
close: () => void;
|
|
5
6
|
endTurn: () => void;
|
|
6
7
|
sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { VoiceConnectionOptions } from '../types';
|
|
2
2
|
export declare const useVoiceStream: <TResult = unknown>(path: string, options?: VoiceConnectionOptions) => {
|
|
3
|
+
callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
|
|
3
4
|
close: () => void;
|
|
4
5
|
endTurn: () => void;
|
|
5
6
|
sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
|
package/dist/testing/index.js
CHANGED
|
@@ -4459,6 +4459,214 @@ var createVoiceMemoryStore = () => {
|
|
|
4459
4459
|
// src/session.ts
|
|
4460
4460
|
import { Buffer } from "buffer";
|
|
4461
4461
|
|
|
4462
|
+
// src/handoff.ts
|
|
4463
|
+
var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4464
|
+
var signHandoffBody = async (input) => {
|
|
4465
|
+
const encoder = new TextEncoder;
|
|
4466
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
4467
|
+
hash: "SHA-256",
|
|
4468
|
+
name: "HMAC"
|
|
4469
|
+
}, false, ["sign"]);
|
|
4470
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
|
|
4471
|
+
return `sha256=${toHex(new Uint8Array(signature))}`;
|
|
4472
|
+
};
|
|
4473
|
+
var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
4474
|
+
var createSkippedDelivery = (adapter) => ({
|
|
4475
|
+
adapterId: adapter.id,
|
|
4476
|
+
adapterKind: adapter.kind,
|
|
4477
|
+
status: "skipped"
|
|
4478
|
+
});
|
|
4479
|
+
var aggregateHandoffStatus = (deliveries) => {
|
|
4480
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
4481
|
+
if (statuses.some((status) => status === "failed")) {
|
|
4482
|
+
return "failed";
|
|
4483
|
+
}
|
|
4484
|
+
if (statuses.some((status) => status === "delivered")) {
|
|
4485
|
+
return "delivered";
|
|
4486
|
+
}
|
|
4487
|
+
return "skipped";
|
|
4488
|
+
};
|
|
4489
|
+
var defaultWebhookBody = (input) => ({
|
|
4490
|
+
action: input.action,
|
|
4491
|
+
metadata: input.metadata,
|
|
4492
|
+
reason: input.reason,
|
|
4493
|
+
result: input.result,
|
|
4494
|
+
session: {
|
|
4495
|
+
id: input.session.id,
|
|
4496
|
+
scenarioId: input.session.scenarioId,
|
|
4497
|
+
status: input.session.status
|
|
4498
|
+
},
|
|
4499
|
+
source: "absolutejs-voice",
|
|
4500
|
+
target: input.target
|
|
4501
|
+
});
|
|
4502
|
+
var deliverVoiceHandoff = async (input) => {
|
|
4503
|
+
if (!input.config || input.config.adapters.length === 0) {
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
const deliveries = {};
|
|
4507
|
+
for (const adapter of input.config.adapters) {
|
|
4508
|
+
if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
|
|
4509
|
+
deliveries[adapter.id] = createSkippedDelivery(adapter);
|
|
4510
|
+
continue;
|
|
4511
|
+
}
|
|
4512
|
+
try {
|
|
4513
|
+
const result = await adapter.handoff(input.handoff);
|
|
4514
|
+
deliveries[adapter.id] = {
|
|
4515
|
+
...result,
|
|
4516
|
+
adapterId: adapter.id,
|
|
4517
|
+
adapterKind: adapter.kind
|
|
4518
|
+
};
|
|
4519
|
+
} catch (error) {
|
|
4520
|
+
deliveries[adapter.id] = {
|
|
4521
|
+
adapterId: adapter.id,
|
|
4522
|
+
adapterKind: adapter.kind,
|
|
4523
|
+
error: toErrorMessage(error),
|
|
4524
|
+
status: "failed"
|
|
4525
|
+
};
|
|
4526
|
+
if (input.config.failMode === "throw") {
|
|
4527
|
+
throw error;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
return {
|
|
4532
|
+
action: input.handoff.action,
|
|
4533
|
+
deliveries,
|
|
4534
|
+
status: aggregateHandoffStatus(deliveries)
|
|
4535
|
+
};
|
|
4536
|
+
};
|
|
4537
|
+
var createVoiceWebhookHandoffAdapter = (options) => ({
|
|
4538
|
+
actions: options.actions,
|
|
4539
|
+
handoff: async (input) => {
|
|
4540
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4541
|
+
if (typeof fetchImpl !== "function") {
|
|
4542
|
+
return {
|
|
4543
|
+
deliveredTo: options.url,
|
|
4544
|
+
error: "Handoff delivery failed: fetch is not available in this runtime.",
|
|
4545
|
+
status: "failed"
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
|
|
4549
|
+
const headers = {
|
|
4550
|
+
"content-type": "application/json",
|
|
4551
|
+
...options.headers
|
|
4552
|
+
};
|
|
4553
|
+
if (options.signingSecret) {
|
|
4554
|
+
const timestamp = String(Date.now());
|
|
4555
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
4556
|
+
headers["x-absolutejs-signature"] = await signHandoffBody({
|
|
4557
|
+
body,
|
|
4558
|
+
secret: options.signingSecret,
|
|
4559
|
+
timestamp
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
4563
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
4564
|
+
try {
|
|
4565
|
+
const response = await fetchImpl(options.url, {
|
|
4566
|
+
body,
|
|
4567
|
+
headers,
|
|
4568
|
+
method: options.method ?? "POST",
|
|
4569
|
+
signal: controller?.signal
|
|
4570
|
+
});
|
|
4571
|
+
if (!response.ok) {
|
|
4572
|
+
return {
|
|
4573
|
+
deliveredTo: options.url,
|
|
4574
|
+
error: `Handoff delivery failed with response ${response.status}.`,
|
|
4575
|
+
status: "failed"
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
return {
|
|
4579
|
+
deliveredAt: Date.now(),
|
|
4580
|
+
deliveredTo: options.url,
|
|
4581
|
+
status: "delivered"
|
|
4582
|
+
};
|
|
4583
|
+
} finally {
|
|
4584
|
+
if (timeout) {
|
|
4585
|
+
clearTimeout(timeout);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
},
|
|
4589
|
+
id: options.id,
|
|
4590
|
+
kind: options.kind ?? "webhook"
|
|
4591
|
+
});
|
|
4592
|
+
var escapeXml = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
4593
|
+
var defaultTwilioTransferTwiML = (input) => {
|
|
4594
|
+
if (!input.target) {
|
|
4595
|
+
return "<Response><Hangup /></Response>";
|
|
4596
|
+
}
|
|
4597
|
+
return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
|
|
4598
|
+
};
|
|
4599
|
+
var resolveTwilioCallSid = async (resolver, input) => {
|
|
4600
|
+
if (typeof resolver === "function") {
|
|
4601
|
+
return resolver(input);
|
|
4602
|
+
}
|
|
4603
|
+
if (typeof resolver === "string" && resolver.length > 0) {
|
|
4604
|
+
return resolver;
|
|
4605
|
+
}
|
|
4606
|
+
const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
|
|
4607
|
+
const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
|
|
4608
|
+
const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
|
|
4609
|
+
return metadataSid ?? sessionSid;
|
|
4610
|
+
};
|
|
4611
|
+
var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
|
|
4612
|
+
actions: options.actions ?? ["transfer"],
|
|
4613
|
+
handoff: async (input) => {
|
|
4614
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4615
|
+
const callSid = await resolveTwilioCallSid(options.callSid, input);
|
|
4616
|
+
if (!callSid) {
|
|
4617
|
+
return {
|
|
4618
|
+
error: "Twilio handoff requires a callSid.",
|
|
4619
|
+
status: "failed"
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
if (typeof fetchImpl !== "function") {
|
|
4623
|
+
return {
|
|
4624
|
+
error: "Twilio handoff failed: fetch is not available in this runtime.",
|
|
4625
|
+
status: "failed"
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
|
|
4629
|
+
const body = new URLSearchParams({
|
|
4630
|
+
Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
|
|
4631
|
+
});
|
|
4632
|
+
const auth = btoa(`${options.accountSid}:${options.authToken}`);
|
|
4633
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
|
|
4634
|
+
const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
4635
|
+
try {
|
|
4636
|
+
const response = await fetchImpl(url, {
|
|
4637
|
+
body,
|
|
4638
|
+
headers: {
|
|
4639
|
+
authorization: `Basic ${auth}`,
|
|
4640
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
4641
|
+
},
|
|
4642
|
+
method: "POST",
|
|
4643
|
+
signal: controller?.signal
|
|
4644
|
+
});
|
|
4645
|
+
if (!response.ok) {
|
|
4646
|
+
return {
|
|
4647
|
+
deliveredTo: url,
|
|
4648
|
+
error: `Twilio handoff failed with response ${response.status}.`,
|
|
4649
|
+
status: "failed"
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
return {
|
|
4653
|
+
deliveredAt: Date.now(),
|
|
4654
|
+
deliveredTo: url,
|
|
4655
|
+
metadata: {
|
|
4656
|
+
callSid
|
|
4657
|
+
},
|
|
4658
|
+
status: "delivered"
|
|
4659
|
+
};
|
|
4660
|
+
} finally {
|
|
4661
|
+
if (timeout) {
|
|
4662
|
+
clearTimeout(timeout);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
},
|
|
4666
|
+
id: options.id ?? "twilio-redirect",
|
|
4667
|
+
kind: "twilio-redirect"
|
|
4668
|
+
});
|
|
4669
|
+
|
|
4462
4670
|
// src/logger.ts
|
|
4463
4671
|
var noop2 = () => {};
|
|
4464
4672
|
var createNoopLogger = () => ({
|
|
@@ -4763,6 +4971,34 @@ var createVoiceSession = (options) => {
|
|
|
4763
4971
|
type: "call_lifecycle"
|
|
4764
4972
|
});
|
|
4765
4973
|
};
|
|
4974
|
+
const runHandoff = async (input) => {
|
|
4975
|
+
const result = await deliverVoiceHandoff({
|
|
4976
|
+
config: options.handoff,
|
|
4977
|
+
handoff: {
|
|
4978
|
+
action: input.action,
|
|
4979
|
+
api,
|
|
4980
|
+
context: options.context,
|
|
4981
|
+
metadata: input.metadata,
|
|
4982
|
+
reason: input.reason,
|
|
4983
|
+
result: input.result,
|
|
4984
|
+
session: input.session,
|
|
4985
|
+
target: input.target
|
|
4986
|
+
}
|
|
4987
|
+
});
|
|
4988
|
+
if (!result) {
|
|
4989
|
+
return;
|
|
4990
|
+
}
|
|
4991
|
+
await appendTrace({
|
|
4992
|
+
metadata: input.metadata,
|
|
4993
|
+
payload: {
|
|
4994
|
+
...result,
|
|
4995
|
+
reason: input.reason,
|
|
4996
|
+
target: input.target
|
|
4997
|
+
},
|
|
4998
|
+
session: input.session,
|
|
4999
|
+
type: "call.handoff"
|
|
5000
|
+
});
|
|
5001
|
+
};
|
|
4766
5002
|
const readSession = async () => options.store.getOrCreate(options.id);
|
|
4767
5003
|
const writeSession = async (mutate) => {
|
|
4768
5004
|
const session = await options.store.getOrCreate(options.id);
|
|
@@ -5042,6 +5278,14 @@ var createVoiceSession = (options) => {
|
|
|
5042
5278
|
type: "call.lifecycle"
|
|
5043
5279
|
});
|
|
5044
5280
|
await sendCallLifecycle(session);
|
|
5281
|
+
await runHandoff({
|
|
5282
|
+
action: "transfer",
|
|
5283
|
+
metadata: input.metadata,
|
|
5284
|
+
reason: input.reason,
|
|
5285
|
+
result: input.result,
|
|
5286
|
+
session,
|
|
5287
|
+
target: input.target
|
|
5288
|
+
});
|
|
5045
5289
|
await completeInternal(input.result, {
|
|
5046
5290
|
disposition: "transferred",
|
|
5047
5291
|
invokeOnComplete: false,
|
|
@@ -5068,6 +5312,13 @@ var createVoiceSession = (options) => {
|
|
|
5068
5312
|
type: "call.lifecycle"
|
|
5069
5313
|
});
|
|
5070
5314
|
await sendCallLifecycle(session);
|
|
5315
|
+
await runHandoff({
|
|
5316
|
+
action: "escalate",
|
|
5317
|
+
metadata: input.metadata,
|
|
5318
|
+
reason: input.reason,
|
|
5319
|
+
result: input.result,
|
|
5320
|
+
session
|
|
5321
|
+
});
|
|
5071
5322
|
await completeInternal(input.result, {
|
|
5072
5323
|
disposition: "escalated",
|
|
5073
5324
|
invokeOnComplete: false,
|
|
@@ -5091,6 +5342,12 @@ var createVoiceSession = (options) => {
|
|
|
5091
5342
|
type: "call.lifecycle"
|
|
5092
5343
|
});
|
|
5093
5344
|
await sendCallLifecycle(session);
|
|
5345
|
+
await runHandoff({
|
|
5346
|
+
action: "no-answer",
|
|
5347
|
+
metadata: input?.metadata,
|
|
5348
|
+
result: input?.result,
|
|
5349
|
+
session
|
|
5350
|
+
});
|
|
5094
5351
|
await completeInternal(input?.result, {
|
|
5095
5352
|
disposition: "no-answer",
|
|
5096
5353
|
invokeOnComplete: false,
|
|
@@ -5113,6 +5370,12 @@ var createVoiceSession = (options) => {
|
|
|
5113
5370
|
type: "call.lifecycle"
|
|
5114
5371
|
});
|
|
5115
5372
|
await sendCallLifecycle(session);
|
|
5373
|
+
await runHandoff({
|
|
5374
|
+
action: "voicemail",
|
|
5375
|
+
metadata: input?.metadata,
|
|
5376
|
+
result: input?.result,
|
|
5377
|
+
session
|
|
5378
|
+
});
|
|
5116
5379
|
await completeInternal(input?.result, {
|
|
5117
5380
|
disposition: "voicemail",
|
|
5118
5381
|
invokeOnComplete: false,
|
|
@@ -6490,7 +6753,7 @@ var createVoiceCallReviewFromLiveTelephonyReport = (report, options = {}) => {
|
|
|
6490
6753
|
}
|
|
6491
6754
|
};
|
|
6492
6755
|
};
|
|
6493
|
-
var
|
|
6756
|
+
var toErrorMessage2 = (error) => {
|
|
6494
6757
|
if (typeof error === "string" && error.trim().length > 0) {
|
|
6495
6758
|
return error;
|
|
6496
6759
|
}
|
|
@@ -6577,7 +6840,7 @@ var createVoiceCallReviewRecorder = (options = {}) => {
|
|
|
6577
6840
|
};
|
|
6578
6841
|
},
|
|
6579
6842
|
recordError: (error) => {
|
|
6580
|
-
const message =
|
|
6843
|
+
const message = toErrorMessage2(error);
|
|
6581
6844
|
errors.push(message);
|
|
6582
6845
|
push("turn", "error", {
|
|
6583
6846
|
reason: message
|
|
@@ -7286,7 +7549,7 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
|
7286
7549
|
import { Buffer as Buffer2 } from "buffer";
|
|
7287
7550
|
var TWILIO_MULAW_SAMPLE_RATE = 8000;
|
|
7288
7551
|
var VOICE_PCM_SAMPLE_RATE = 16000;
|
|
7289
|
-
var
|
|
7552
|
+
var escapeXml2 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
7290
7553
|
var normalizeOnTurn = (handler) => {
|
|
7291
7554
|
if (handler.length > 1) {
|
|
7292
7555
|
const directHandler = handler;
|
|
@@ -7482,8 +7745,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
|
|
|
7482
7745
|
}
|
|
7483
7746
|
});
|
|
7484
7747
|
var createTwilioVoiceResponse = (options) => {
|
|
7485
|
-
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${
|
|
7486
|
-
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${
|
|
7748
|
+
const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
|
|
7749
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
|
|
7487
7750
|
};
|
|
7488
7751
|
var createTwilioMediaStreamBridge = (socket, options) => {
|
|
7489
7752
|
const runtimePreset = resolveVoiceRuntimePreset(options.preset);
|
package/dist/trace.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type VoiceTraceEventType = 'assistant.guardrail' | 'assistant.memory' | 'assistant.run' | 'agent.handoff' | 'agent.model' | 'agent.result' | 'agent.tool' | 'call.lifecycle' | 'session.error' | 'turn.assistant' | 'turn.committed' | 'turn.cost' | 'turn.transcript';
|
|
1
|
+
export type VoiceTraceEventType = 'assistant.guardrail' | 'assistant.memory' | 'assistant.run' | 'agent.handoff' | 'agent.model' | 'agent.result' | 'agent.tool' | 'call.handoff' | 'call.lifecycle' | 'session.error' | 'turn.assistant' | 'turn.committed' | 'turn.cost' | 'turn.transcript';
|
|
2
2
|
export type VoiceTraceEvent<TPayload extends Record<string, unknown> = Record<string, unknown>> = {
|
|
3
3
|
at: number;
|
|
4
4
|
id?: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -269,6 +269,35 @@ export type VoiceCallLifecycleState = {
|
|
|
269
269
|
lastEventAt: number;
|
|
270
270
|
startedAt: number;
|
|
271
271
|
};
|
|
272
|
+
export type VoiceHandoffAction = 'escalate' | 'no-answer' | 'transfer' | 'voicemail';
|
|
273
|
+
export type VoiceHandoffStatus = 'delivered' | 'failed' | 'skipped';
|
|
274
|
+
export type VoiceHandoffResult = {
|
|
275
|
+
deliveredAt?: number;
|
|
276
|
+
deliveredTo?: string;
|
|
277
|
+
error?: string;
|
|
278
|
+
metadata?: Record<string, unknown>;
|
|
279
|
+
status: VoiceHandoffStatus;
|
|
280
|
+
};
|
|
281
|
+
export type VoiceHandoffInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
282
|
+
action: VoiceHandoffAction;
|
|
283
|
+
api: VoiceSessionHandle<TContext, TSession, TResult>;
|
|
284
|
+
context: TContext;
|
|
285
|
+
metadata?: Record<string, unknown>;
|
|
286
|
+
reason?: string;
|
|
287
|
+
result?: TResult;
|
|
288
|
+
session: TSession;
|
|
289
|
+
target?: string;
|
|
290
|
+
};
|
|
291
|
+
export type VoiceHandoffAdapter<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
292
|
+
actions?: VoiceHandoffAction[];
|
|
293
|
+
handoff: (input: VoiceHandoffInput<TContext, TSession, TResult>) => Promise<VoiceHandoffResult> | VoiceHandoffResult;
|
|
294
|
+
id: string;
|
|
295
|
+
kind?: string;
|
|
296
|
+
};
|
|
297
|
+
export type VoiceHandoffConfig<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
298
|
+
adapters: VoiceHandoffAdapter<TContext, TSession, TResult>[];
|
|
299
|
+
failMode?: 'record' | 'throw';
|
|
300
|
+
};
|
|
272
301
|
export type VoiceSessionStore<TSession extends VoiceSessionRecord = VoiceSessionRecord> = SessionStore<TSession, VoiceSessionSummary>;
|
|
273
302
|
export type VoiceLogger = {
|
|
274
303
|
debug?: (message: string, meta?: Record<string, unknown>) => void;
|
|
@@ -567,6 +596,7 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
|
|
|
567
596
|
audioConditioning?: VoiceAudioConditioningConfig;
|
|
568
597
|
logger?: VoiceLogger;
|
|
569
598
|
htmx?: boolean | VoiceHTMXConfig<TSession, NoInfer<TResult>>;
|
|
599
|
+
handoff?: VoiceHandoffConfig<TContext, TSession, TResult>;
|
|
570
600
|
ops?: VoiceRuntimeOpsConfig<TContext, TSession, TResult>;
|
|
571
601
|
trace?: VoiceTraceEventStore;
|
|
572
602
|
} & VoiceRouteConfig<TContext, TSession, TResult>;
|
|
@@ -588,6 +618,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
|
|
|
588
618
|
sttLifecycle: VoiceSTTLifecycle;
|
|
589
619
|
turnDetection: VoiceResolvedTurnDetectionConfig;
|
|
590
620
|
audioConditioning?: VoiceResolvedAudioConditioningConfig;
|
|
621
|
+
handoff?: VoiceHandoffConfig<TContext, TSession, TResult>;
|
|
591
622
|
route: VoiceNormalizedRouteConfig<TContext, TSession, TResult>;
|
|
592
623
|
logger?: VoiceLogger;
|
|
593
624
|
};
|
package/dist/vue/index.js
CHANGED
|
@@ -590,6 +590,7 @@ var useVoiceStream = (path, options = {}) => {
|
|
|
590
590
|
const stream = createVoiceStream(path, options);
|
|
591
591
|
const assistantAudio = shallowRef([]);
|
|
592
592
|
const assistantTexts = shallowRef([]);
|
|
593
|
+
const call = shallowRef(null);
|
|
593
594
|
const error = ref(null);
|
|
594
595
|
const isConnected = ref(false);
|
|
595
596
|
const partial = ref("");
|
|
@@ -599,6 +600,7 @@ var useVoiceStream = (path, options = {}) => {
|
|
|
599
600
|
const sync = () => {
|
|
600
601
|
assistantAudio.value = [...stream.assistantAudio];
|
|
601
602
|
assistantTexts.value = [...stream.assistantTexts];
|
|
603
|
+
call.value = stream.call;
|
|
602
604
|
error.value = stream.error;
|
|
603
605
|
isConnected.value = stream.isConnected;
|
|
604
606
|
partial.value = stream.partial;
|
|
@@ -616,6 +618,8 @@ var useVoiceStream = (path, options = {}) => {
|
|
|
616
618
|
return {
|
|
617
619
|
assistantAudio,
|
|
618
620
|
assistantTexts,
|
|
621
|
+
call,
|
|
622
|
+
callControl: (message) => stream.callControl(message),
|
|
619
623
|
close: () => destroy(),
|
|
620
624
|
endTurn: () => stream.endTurn(),
|
|
621
625
|
error,
|
|
@@ -12,6 +12,8 @@ export declare const useVoiceStream: <TResult = unknown>(path: string, options?:
|
|
|
12
12
|
turnId?: string;
|
|
13
13
|
}[]>;
|
|
14
14
|
assistantTexts: import("vue").ShallowRef<string[], string[]>;
|
|
15
|
+
call: import("vue").ShallowRef<import("..").VoiceCallLifecycleState | null, import("..").VoiceCallLifecycleState | null>;
|
|
16
|
+
callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
|
|
15
17
|
close: () => void;
|
|
16
18
|
endTurn: () => void;
|
|
17
19
|
error: import("vue").Ref<string | null, string | null>;
|