Skip to content

Commit

Permalink
Expose collect on Voice Call (#706)
Browse files Browse the repository at this point in the history
* add calling.collect api

* changeset

* rename continue with continuous

* better log

* handle start_of_input

* doc line

* fix prompt types

* ignore done on start_of_input
  • Loading branch information
Edoardo Gallo authored Dec 27, 2022
1 parent 45536d5 commit a937768
Show file tree
Hide file tree
Showing 11 changed files with 733 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-pigs-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@signalwire/core': patch
---

Add types for `calling.collect` API
6 changes: 6 additions & 0 deletions .changeset/eight-squids-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@signalwire/realtime-api': minor
'@sw-internal/e2e-realtime-api': patch
---

Expose calling.collect API
118 changes: 118 additions & 0 deletions internal/e2e-realtime-api/src/voiceCollect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import tap from 'tap'
import { Voice } from '@signalwire/realtime-api'
import { createTestRunner } from './utils'

const sleep = (ms = 3000) => {
return new Promise((r) => {
setTimeout(r, ms)
})
}

const handler = () => {
return new Promise<number>(async (resolve, reject) => {
const client = new Voice.Client({
host: process.env.RELAY_HOST || 'relay.swire.io',
project: process.env.RELAY_PROJECT as string,
token: process.env.RELAY_TOKEN as string,
contexts: [process.env.VOICE_CONTEXT as string],
// logLevel: "trace",
debug: {
logWsTraffic: true,
},
})

client.on('call.received', async (call) => {
console.log('Got call', call.id, call.from, call.to, call.direction)

try {
const resultAnswer = await call.answer()
tap.ok(resultAnswer.id, 'Inboud call answered')
tap.equal(
call.id,
resultAnswer.id,
'Call answered gets the same instance'
)

call.on('collect.started', (collect) => {
console.log('>>> collect.started', collect)
})
call.on('collect.updated', (collect) => {
console.log('>>> collect.updated', collect.digits)
})
call.on('collect.ended', (collect) => {
console.log('>>> collect.ended', collect.digits)
})
call.on('collect.failed', (collect) => {
console.log('>>> collect.failed', collect.reason)
})
// call.on('collect.startOfSpeech', (collect) => {})

const callCollect = await call.collect({
initialTimeout: 4.0,
digits: {
max: 4,
digitTimeout: 10,
terminators: '#',
},
partialResults: true,
continuous: false,
sendStartOfInput: true,
startInputTimers: false,
})

await callCollect.ended() // block the script until the collect ended

tap.equal(callCollect.digits, '123', 'Collect the correct digits')
// await callCollect.stop()
// await callCollect.startInputTimers()

await call.hangup()
} catch (error) {
console.error('Error', error)
reject(4)
}
})

const call = await client.dialPhone({
// make an outbound call to an `office` context to trigger the `call.received` event above
to: process.env.VOICE_DIAL_TO_NUMBER as string,
from: process.env.VOICE_DIAL_FROM_NUMBER as string,
timeout: 30,
})
tap.ok(call.id, 'Call resolved')

await sleep(3000)

const sendDigitResult = await call.sendDigits('1w2w3w#')
tap.equal(
call.id,
sendDigitResult.id,
'sendDigit returns the same instance'
)

const waitForParams = ['ended', 'ending', ['ending', 'ended']] as const
const results = await Promise.all(
waitForParams.map((params) => call.waitFor(params as any))
)
waitForParams.forEach((value, i) => {
if (typeof value === 'string') {
tap.ok(results[i], `"${value}": completed successfully.`)
} else {
tap.ok(results[i], `${JSON.stringify(value)}: completed successfully.`)
}
})
resolve(0)
})
}

async function main() {
const runner = createTestRunner({
name: 'Voice Collect E2E',
testHandler: handler,
executionTime: 60_000,
})

await runner.run()
}

main()
122 changes: 122 additions & 0 deletions packages/core/src/types/voiceCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,15 @@ export type CallRecordingUpdated = 'recording.updated'
export type CallRecordingEnded = 'recording.ended'
export type CallRecordingFailed = 'recording.failed'
export type CallPromptStarted = 'prompt.started'
export type CallPromptStartOfInput = 'prompt.startOfInput'
export type CallPromptUpdated = 'prompt.updated'
export type CallPromptEnded = 'prompt.ended'
export type CallPromptFailed = 'prompt.failed'
export type CallCollectStarted = 'collect.started'
export type CallCollectStartOfInput = 'collect.startOfInput'
export type CallCollectUpdated = 'collect.updated'
export type CallCollectEnded = 'collect.ended'
export type CallCollectFailed = 'collect.failed'
export type CallTapStarted = 'tap.started'
export type CallTapEnded = 'tap.ended'
// Not exposed yet to the public-side
Expand Down Expand Up @@ -117,6 +123,10 @@ export type VoiceCallEventNames =
| CallDetectStarted
| CallDetectUpdated
| CallDetectEnded
| CallCollectStarted
| CallCollectUpdated
| CallCollectEnded
| CallCollectFailed

/**
* List of internal events
Expand Down Expand Up @@ -325,6 +335,14 @@ export interface VoiceCallTapAudioMethodParams {
direction: TapDirection
}

export type VoiceCallCollectMethodParams = SpeechOrDigits & {
initialTimeout?: number
partialResults?: boolean
continuous: boolean
sendStartOfInput: boolean
startInputTimers: boolean
}

export type VoiceCallConnectMethodParams =
| VoiceDeviceBuilder
| {
Expand Down Expand Up @@ -551,6 +569,42 @@ export type VoiceCallPromptEntity = OnlyStateProperties<VoiceCallPromptContract>
export type VoiceCallPromptMethods =
OnlyFunctionProperties<VoiceCallPromptContract>

/**
* Public Contract for a VoiceCallCollect
*/
export interface VoiceCallCollectContract {
/** Unique id for this recording */
readonly id: string
/** @ignore */
readonly callId: string
/** @ignore */
readonly controlId: string

readonly type?: CallingCallCollectResult['type']
/** Alias for type in case of errors */
readonly reason?: string
readonly digits?: string
readonly terminator?: string
readonly text?: string
readonly confidence?: number

stop(): Promise<this>
startInputTimers(): Promise<this>
ended(): Promise<this>
}

/**
* VoiceCallCollect properties
*/
export type VoiceCallCollectEntity =
OnlyStateProperties<VoiceCallCollectContract>

/**
* VoiceCallCollect methods
*/
export type VoiceCallCollectMethods =
OnlyFunctionProperties<VoiceCallCollectContract>

/**
* Public Contract for a VoiceCallTap
*/
Expand Down Expand Up @@ -694,6 +748,9 @@ export interface VoiceCallContract<T = any> {
detectDigit(
params?: Omit<VoiceCallDetectDigitParams, 'type'>
): Promise<VoiceCallDetectContract>
collect(
params: VoiceCallCollectMethodParams
): Promise<VoiceCallCollectContract>
}

/**
Expand Down Expand Up @@ -875,6 +932,9 @@ interface CallingCallCollectResultNoInput {
interface CallingCallCollectResultNoMatch {
type: 'no_match'
}
interface CallingCallCollectResultStartOfInput {
type: 'start_of_input'
}
interface CallingCallCollectResultDigit {
type: 'digit'
params: {
Expand All @@ -893,6 +953,7 @@ export type CallingCallCollectResult =
| CallingCallCollectResultError
| CallingCallCollectResultNoInput
| CallingCallCollectResultNoMatch
| CallingCallCollectResultStartOfInput
| CallingCallCollectResultDigit
| CallingCallCollectResultSpeech

Expand Down Expand Up @@ -1129,6 +1190,14 @@ export interface CallPromptStartedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallPromptStarted>
params: CallingCallCollectEventParams & { tag: string }
}
/**
* 'calling.prompt.startOfInput'
* Different from `started` because it's from the server
*/
export interface CallPromptStartOfInputEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallPromptStartOfInput>
params: CallingCallCollectEventParams & { tag: string }
}
/**
* 'calling.prompt.updated'
*/
Expand Down Expand Up @@ -1217,6 +1286,43 @@ export interface CallDetectEndedEvent extends SwEvent {
params: CallingCallDetectEventParams & { tag: string }
}

/**
* 'calling.collect.started'
*/
export interface CallCollectStartedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallCollectStarted>
params: CallingCallCollectEventParams & { tag: string }
}
/**
* 'calling.collect.startOfInput'
* Different from `started` because it's from the server
*/
export interface CallCollectStartOfInputEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallCollectStartOfInput>
params: CallingCallCollectEventParams & { tag: string }
}
/**
* 'calling.collect.updated'
*/
export interface CallCollectUpdatedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallCollectUpdated>
params: CallingCallCollectEventParams & { tag: string }
}
/**
* 'calling.collect.ended'
*/
export interface CallCollectEndedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallCollectEnded>
params: CallingCallCollectEventParams & { tag: string }
}
/**
* 'calling.collect.failed'
*/
export interface CallCollectFailedEvent extends SwEvent {
event_type: ToInternalVoiceEvent<CallCollectFailed>
params: CallingCallCollectEventParams & { tag: string }
}

// interface VoiceCallStateEvent {
// call_id: string
// node_id: string
Expand Down Expand Up @@ -1265,6 +1371,7 @@ export type VoiceCallEvent =
| CallRecordingEndedEvent
| CallRecordingFailedEvent
| CallPromptStartedEvent
| CallPromptStartOfInputEvent
| CallPromptUpdatedEvent
| CallPromptEndedEvent
| CallPromptFailedEvent
Expand All @@ -1277,6 +1384,11 @@ export type VoiceCallEvent =
| CallDetectStartedEvent
| CallDetectUpdatedEvent
| CallDetectEndedEvent
| CallCollectStartedEvent
| CallCollectStartOfInputEvent
| CallCollectUpdatedEvent
| CallCollectEndedEvent
| CallCollectFailedEvent

export type VoiceCallEventParams =
// Server Event Params
Expand All @@ -1300,6 +1412,7 @@ export type VoiceCallEventParams =
| CallRecordingEndedEvent['params']
| CallRecordingFailedEvent['params']
| CallPromptStartedEvent['params']
| CallPromptStartOfInputEvent['params']
| CallPromptUpdatedEvent['params']
| CallPromptEndedEvent['params']
| CallPromptFailedEvent['params']
Expand All @@ -1312,6 +1425,11 @@ export type VoiceCallEventParams =
| CallDetectStartedEvent['params']
| CallDetectUpdatedEvent['params']
| CallDetectEndedEvent['params']
| CallCollectStartedEvent['params']
| CallCollectStartOfInputEvent['params']
| CallCollectUpdatedEvent['params']
| CallCollectEndedEvent['params']
| CallCollectFailedEvent['params']

export type VoiceCallAction = MapToPubSubShape<VoiceCallEvent>

Expand All @@ -1336,6 +1454,9 @@ export type VoiceCallJSONRPCMethod =
| 'calling.send_digits'
| 'calling.detect'
| 'calling.detect.stop'
| 'calling.collect'
| 'calling.collect.stop'
| 'calling.collect.start_input_timers'

export type CallingTransformType =
| 'voiceCallReceived'
Expand All @@ -1346,3 +1467,4 @@ export type CallingTransformType =
| 'voiceCallConnect'
| 'voiceCallState'
| 'voiceCallDetect'
| 'voiceCallCollect'
16 changes: 15 additions & 1 deletion packages/realtime-api/src/types/voice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ import type {
CallPromptFailed,
CallTapStarted,
CallTapEnded,
CallCollectStarted,
CallCollectStartOfInput,
CallCollectUpdated,
CallCollectEnded,
CallCollectFailed,
} from '@signalwire/core'
import type { Call } from '../voice/Call'
import type { CallPlayback } from '../voice/CallPlayback'
import type { CallRecording } from '../voice/CallRecording'
import type { CallPrompt } from '../voice/CallPrompt'
import type { CallTap } from '../voice/CallTap'
import type { CallCollect } from '../voice/CallCollect'

export type RealTimeCallApiEventsHandlerMapping = Record<
CallReceived,
Expand All @@ -41,7 +47,15 @@ export type RealTimeCallApiEventsHandlerMapping = Record<
CallPromptStarted | CallPromptUpdated | CallPromptEnded | CallPromptFailed,
(prompt: CallPrompt) => void
> &
Record<CallTapStarted | CallTapEnded, (tap: CallTap) => void>
Record<CallTapStarted | CallTapEnded, (tap: CallTap) => void> &
Record<
| CallCollectStarted
| CallCollectStartOfInput
| CallCollectUpdated
| CallCollectEnded
| CallCollectFailed,
(callCollect: CallCollect) => void
>

export type RealTimeCallApiEvents = {
[k in keyof RealTimeCallApiEventsHandlerMapping]: RealTimeCallApiEventsHandlerMapping[k]
Expand Down
Loading

0 comments on commit a937768

Please sign in to comment.