Skip to content

Commit a8a3e48

Browse files
feat: add web phone call for cal ai (calcom#23527)
* feat: add web phone call for cal ai * fix: web call * refactor: improvements * chore * refactor: web call improvements * chore: error message * refactor: improvements * fix: err * refactor: feedback and improvements * fix: type errors * refactor: hasAvailable * chore: use orgid * fix: types --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com>
1 parent 2e96600 commit a8a3e48

22 files changed

Lines changed: 1043 additions & 44 deletions

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
"react-use-intercom": "1.5.1",
140140
"recoil": "^0.7.7",
141141
"remove-markdown": "^0.5.0",
142+
"retell-client-js-sdk": "^2.0.0",
142143
"retell-sdk": "^4.40.0",
143144
"rrule": "^2.7.1",
144145
"sanitize-html": "^2.10.0",

apps/web/public/icons/sprite.svg

Lines changed: 22 additions & 0 deletions
Loading

apps/web/public/static/locales/en/common.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,7 @@
860860
"phone_number_for_ai_call": "Phone number (for our agent to call)",
861861
"buy_a_phone_number_or_import_one_you_already_have": "Buy a phone number or import one you already have on twilio",
862862
"test_agent": "Test Agent",
863+
"test_web_call": "Test Web Call",
863864
"phone_numbers": "Phone Numbers",
864865
"unsubscribe": "Unsubscribe",
865866
"delete_workflow_step": "Delete Workflow Step",
@@ -1479,6 +1480,24 @@
14791480
"app_removed_successfully": "App removed successfully",
14801481
"error_removing_app": "Error removing app",
14811482
"web_conference": "Web conference",
1483+
"web_call": "Web call",
1484+
"start_web_call": "Start web call",
1485+
"end_call": "End call",
1486+
"test_your_agent_with_web_call": "Test your agent with a web call without requiring a phone number",
1487+
"phone_call": "Phone call",
1488+
"connecting_to_agent": "Connecting to agent...",
1489+
"call_active": "Call active",
1490+
"call_ended": "Call ended",
1491+
"call_error": "Call error",
1492+
"ready_to_start_call": "Ready to start call",
1493+
"failed_to_start_web_call_try_again": "Failed to start web call. Please try again.",
1494+
"call_encountered_error_try_again": "Call encountered an error. Please try again.",
1495+
"failed_initialize_web_call_microphone_permissions": "Failed to initialize web call. Please ensure microphone permissions are granted.",
1496+
"start_call_to_see_conversation": "Start a call to see the conversation",
1497+
"waiting_for_conversation": "Waiting for conversation...",
1498+
"ai_agent": "AI Agent",
1499+
"live": "Live",
1500+
"connecting": "Connecting...",
14821501
"requires_confirmation": "Requires confirmation",
14831502
"requires_confirmation_threshold": "Requires confirmation if booked with < {{time}} $t({{unit}}_timeUnit) notice",
14841503
"may_require_confirmation": "May require confirmation",
@@ -1714,6 +1733,7 @@
17141733
"transcription_enabled": "Transcriptions are enabled now",
17151734
"transcription_stopped": "Transcriptions are stopped now",
17161735
"download_transcript": "Download Transcript",
1736+
"transcript": "Transcript",
17171737
"recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download",
17181738
"transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour",
17191739
"you_can_download_transcript_from_attachments": "You can also download transcript from attachments",
@@ -3622,6 +3642,11 @@
36223642
"send_another_message": "Send another message",
36233643
"sending": "Sending",
36243644
"no_variables_found": "No variables found",
3645+
"credits_required": "Credits Required",
3646+
"web_call_credits_info": "Web calls consume credits. Make sure you have enough credits to start a call.",
3647+
"web_call_no_credits": "Web calls require credits. You have 0 credits remaining.",
3648+
"purchase_credits": "Purchase Credits",
3649+
"credits_required_tooltip": "This action requires credits to proceed",
36253650
"api_key_name_too_long": "Name must not exceed {{max}} characters",
36263651
"auto_charge_for_last_minute_cancellation": "Auto charge no-show fee for last minute cancellations",
36273652
"before_scheduled_start_time": "Before scheduled start time",

packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,21 @@ export interface AIPhoneServiceProvider<T extends AIPhoneServiceProviderType = A
327327
message: string;
328328
}>;
329329

330+
/**
331+
* Create a web call
332+
*/
333+
createWebCall(params: {
334+
agentId: string;
335+
userId: number;
336+
teamId?: number;
337+
timeZone: string;
338+
eventTypeId: number;
339+
}): Promise<{
340+
callId: string;
341+
accessToken: string;
342+
agentId: string;
343+
}>;
344+
330345
/**
331346
* Update tools from event type ID
332347
*/

packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,20 @@ export class RetellAIPhoneServiceProvider
266266
return await this.service.createTestCall(params);
267267
}
268268

269+
async createWebCall(params: {
270+
agentId: string;
271+
userId: number;
272+
teamId?: number;
273+
timeZone: string;
274+
eventTypeId: number;
275+
}): Promise<{
276+
callId: string;
277+
accessToken: string;
278+
agentId: string;
279+
}> {
280+
return await this.service.createWebCall(params);
281+
}
282+
269283
async updateToolsFromAgentId(
270284
agentId: string,
271285
data: { eventTypeId: number | null; timeZone: string; userId: number | null; teamId?: number | null }

packages/features/calAIPhone/providers/retellAI/RetellAIService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,16 @@ export class RetellAIService {
220220
return this.callService.createTestCall(params);
221221
}
222222

223+
async createWebCall(params: {
224+
agentId: string;
225+
userId: number;
226+
teamId?: number;
227+
timeZone: string;
228+
eventTypeId: number;
229+
}) {
230+
return this.callService.createWebCall(params);
231+
}
232+
223233
async generatePhoneNumberCheckoutSession(params: {
224234
userId: number;
225235
teamId?: number;

packages/features/calAIPhone/providers/retellAI/RetellSDKClient.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
RetellAgent,
1313
CreatePhoneNumberParams,
1414
CreatePhoneCallParams,
15+
CreateWebCallParams,
1516
ImportPhoneNumberParams,
1617
} from "./types";
1718

@@ -234,4 +235,17 @@ export class RetellSDKClient implements RetellAIRepository {
234235
throw error;
235236
}
236237
}
238+
239+
async createWebCall(data: CreateWebCallParams) {
240+
try {
241+
const response = await this.client.call.createWebCall({
242+
agent_id: data.agentId,
243+
retell_llm_dynamic_variables: data.dynamicVariables,
244+
});
245+
return response;
246+
} catch (error) {
247+
this.logger.error("Failed to create web call", { error });
248+
throw error;
249+
}
250+
}
237251
}

packages/features/calAIPhone/providers/retellAI/services/CallService.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,93 @@ export class CallService {
166166
};
167167
}
168168

169+
async createWebCall({
170+
agentId,
171+
userId,
172+
teamId,
173+
timeZone,
174+
eventTypeId,
175+
}: {
176+
agentId: string;
177+
userId: number;
178+
teamId?: number;
179+
timeZone: string;
180+
eventTypeId: number;
181+
}) {
182+
if (!agentId?.trim()) {
183+
throw new HttpError({
184+
statusCode: 400,
185+
message: "Agent ID is required and cannot be empty",
186+
});
187+
}
188+
189+
await this.validateCreditsForTestCall({ userId, teamId });
190+
191+
await checkRateLimitAndThrowError({
192+
rateLimitingType: "core",
193+
identifier: `web-call:${userId}`,
194+
});
195+
196+
const agent = await this.agentRepository.findByIdWithCallAccess({
197+
id: agentId,
198+
userId,
199+
});
200+
201+
if (!agent) {
202+
throw new HttpError({
203+
statusCode: 404,
204+
message: "Agent not found or you don't have permission to use it.",
205+
});
206+
}
207+
208+
if (!agent.providerAgentId) {
209+
throw new HttpError({
210+
statusCode: 400,
211+
message: "Agent provider ID not found.",
212+
});
213+
}
214+
215+
const dynamicVariables = {
216+
EVENT_NAME: "Web Call Test with Agent",
217+
EVENT_DATE: "Monday, January 15, 2025",
218+
EVENT_TIME: "2:00 PM",
219+
EVENT_END_TIME: "2:30 PM",
220+
TIMEZONE: timeZone,
221+
LOCATION: "Web Call",
222+
ORGANIZER_NAME: "Cal.com AI Agent",
223+
ATTENDEE_NAME: "Test User",
224+
ATTENDEE_FIRST_NAME: "Test",
225+
ATTENDEE_LAST_NAME: "User",
226+
ATTENDEE_EMAIL: "testuser@example.com",
227+
ATTENDEE_TIMEZONE: timeZone,
228+
ADDITIONAL_NOTES: "This is a test web call to verify the AI phone agent",
229+
EVENT_START_TIME_IN_ATTENDEE_TIMEZONE: "2:00 PM",
230+
EVENT_END_TIME_IN_ATTENDEE_TIMEZONE: "2:30 PM",
231+
eventTypeId: eventTypeId.toString(),
232+
};
233+
234+
try {
235+
const webCall = await this.retellRepository.createWebCall({
236+
agentId: agent.providerAgentId,
237+
dynamicVariables,
238+
});
239+
return {
240+
callId: webCall.call_id,
241+
accessToken: webCall.access_token,
242+
agentId: webCall.agent_id,
243+
};
244+
} catch (error) {
245+
this.logger.error("Failed to create web call in external AI service", {
246+
agentId: agent.providerAgentId,
247+
error,
248+
});
249+
throw new HttpError({
250+
statusCode: 500,
251+
message: "Failed to create web call",
252+
});
253+
}
254+
}
255+
169256
private async validateCreditsForTestCall({ userId, teamId }: { userId: number; teamId?: number }) {
170257
try {
171258
const { CreditService } = await import("@calcom/features/ee/billing/credit-service");

packages/features/calAIPhone/providers/retellAI/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ export type CreatePhoneCallParams = {
8686
toNumber: string;
8787
dynamicVariables?: RetellDynamicVariables;
8888
};
89+
90+
export type CreateWebCallParams = {
91+
agentId: string;
92+
dynamicVariables?: RetellDynamicVariables;
93+
};
8994
export type UpdatePhoneNumberParams = Retell.PhoneNumberUpdateParams;
9095
export type ImportPhoneNumberParams = Retell.PhoneNumberImportParams;
9196
export type RetellLLMGeneralTools = Retell.LlmCreateParams["general_tools"];
@@ -171,4 +176,7 @@ export interface RetellAIRepository {
171176

172177
// Call operations
173178
createPhoneCall(data: CreatePhoneCallParams): Promise<RetellCall>;
179+
createWebCall(
180+
data: CreateWebCallParams
181+
): Promise<{ call_id: string; access_token: string; agent_id: string }>;
174182
}

0 commit comments

Comments
 (0)