Skip to content

Commit c42e803

Browse files
chore: local testing retell (calcom#23577)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
1 parent 0233c3a commit c42e803

4 files changed

Lines changed: 63 additions & 13 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,11 @@ UNKEY_ROOT_KEY=
386386
# Used for Cal.ai Voice AI Agents
387387
# https://retellai.com
388388
RETELL_AI_KEY=
389+
# Local testing overrides for Retell AI
390+
RETELL_AI_TEST_MODE=false
391+
# JSON mapping of local to production event type IDs (e.g. {"50": 747280} without any string)
392+
RETELL_AI_TEST_EVENT_TYPE_MAP=
393+
RETELL_AI_TEST_CAL_API_KEY=
389394

390395
# Used for buying phone number for cal ai voice agent
391396
STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID=

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { v4 as uuidv4 } from "uuid";
22

3+
import { RETELL_AI_TEST_MODE, RETELL_AI_TEST_EVENT_TYPE_MAP } from "@calcom/lib/constants";
34
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
45
import { HttpError } from "@calcom/lib/http-error";
56
import logger from "@calcom/lib/logger";
@@ -81,6 +82,13 @@ export class AgentService {
8182
});
8283
}
8384

85+
let eventTypeId = data.eventTypeId;
86+
87+
if (RETELL_AI_TEST_MODE && RETELL_AI_TEST_EVENT_TYPE_MAP) {
88+
const mappedId = RETELL_AI_TEST_EVENT_TYPE_MAP[String(data.eventTypeId)];
89+
eventTypeId = mappedId ? Number(mappedId) : data.eventTypeId;
90+
}
91+
8492
try {
8593
const agent = await this.getAgent(agentId);
8694
const llmId = getLlmId(agent);
@@ -99,8 +107,8 @@ export class AgentService {
99107

100108
const existing = llmDetails?.general_tools ?? [];
101109

102-
const hasCheck = existing.some((t) => t.name === `check_availability_${data.eventTypeId}`);
103-
const hasBook = existing.some((t) => t.name === `book_appointment_${data.eventTypeId}`);
110+
const hasCheck = existing.some((t) => t.name === `check_availability_${eventTypeId}`);
111+
const hasBook = existing.some((t) => t.name === `book_appointment_${eventTypeId}`);
104112
// If both already exist and end_call also exists, nothing to do
105113
const hasEndCallAlready = existing.some((t) => t.type === "end_call");
106114
if (hasCheck && hasBook && hasEndCallAlready) {
@@ -113,27 +121,29 @@ export class AgentService {
113121
)?.cal_api_key;
114122

115123
const apiKey =
116-
reusableKey ??
117-
(await this.createApiKey({
118-
userId: data.userId,
119-
teamId: data.teamId || undefined,
120-
}));
124+
RETELL_AI_TEST_MODE && process.env.RETELL_AI_TEST_CAL_API_KEY
125+
? process.env.RETELL_AI_TEST_CAL_API_KEY
126+
: reusableKey ??
127+
(await this.createApiKey({
128+
userId: data.userId,
129+
teamId: data.teamId || undefined,
130+
}));
121131

122132
const newEventTools: NonNullable<AIPhoneServiceTools<AIPhoneServiceProviderType.RETELL_AI>> = [];
123133
if (!hasCheck) {
124134
newEventTools.push({
125-
name: `check_availability_${data.eventTypeId}`,
135+
name: `check_availability_${eventTypeId}`,
126136
type: "check_availability_cal",
127-
event_type_id: data.eventTypeId,
137+
event_type_id: eventTypeId,
128138
cal_api_key: apiKey,
129139
timezone: data.timeZone,
130140
});
131141
}
132142
if (!hasBook) {
133143
newEventTools.push({
134-
name: `book_appointment_${data.eventTypeId}`,
144+
name: `book_appointment_${eventTypeId}`,
135145
type: "book_appointment_cal",
136-
event_type_id: data.eventTypeId,
146+
event_type_id: eventTypeId,
137147
cal_api_key: apiKey,
138148
timezone: data.timeZone,
139149
});
@@ -180,6 +190,15 @@ export class AgentService {
180190
return;
181191
}
182192

193+
let mappedEventTypeIds = eventTypeIds;
194+
195+
if (RETELL_AI_TEST_MODE && RETELL_AI_TEST_EVENT_TYPE_MAP) {
196+
mappedEventTypeIds = eventTypeIds.map((id) => {
197+
const mappedId = RETELL_AI_TEST_EVENT_TYPE_MAP[String(id)];
198+
return mappedId ? Number(mappedId) : id;
199+
});
200+
}
201+
183202
try {
184203
const agent = await this.getAgent(agentId);
185204
const llmId = getLlmId(agent);
@@ -199,7 +218,7 @@ export class AgentService {
199218

200219
const existing = llmDetails?.general_tools ?? [];
201220

202-
const toolNamesToRemove = eventTypeIds.flatMap((eventTypeId) => [
221+
const toolNamesToRemove = mappedEventTypeIds.flatMap((eventTypeId) => [
203222
`check_availability_${eventTypeId}`,
204223
`book_appointment_${eventTypeId}`,
205224
]);
@@ -212,6 +231,7 @@ export class AgentService {
212231
agentId,
213232
llmId,
214233
removedEventTypes: eventTypeIds,
234+
mappedEventTypes: RETELL_AI_TEST_MODE ? mappedEventTypeIds : undefined,
215235
toolsRemoved: existing.length - filteredTools.length,
216236
});
217237
}
@@ -240,6 +260,15 @@ export class AgentService {
240260
});
241261
}
242262

263+
let mappedActiveEventTypeIds = activeEventTypeIds;
264+
265+
if (RETELL_AI_TEST_MODE && RETELL_AI_TEST_EVENT_TYPE_MAP) {
266+
mappedActiveEventTypeIds = activeEventTypeIds.map((id) => {
267+
const mappedId = RETELL_AI_TEST_EVENT_TYPE_MAP[String(id)];
268+
return mappedId ? Number(mappedId) : id;
269+
});
270+
}
271+
243272
try {
244273
const agent = await this.getAgent(agentId);
245274
const llmId = getLlmId(agent);
@@ -269,7 +298,7 @@ export class AgentService {
269298
if (!eventTypeIdMatch) return false;
270299

271300
const eventTypeId = parseInt(eventTypeIdMatch[1]);
272-
return !activeEventTypeIds.includes(eventTypeId);
301+
return !mappedActiveEventTypeIds.includes(eventTypeId);
273302
});
274303

275304
if (toolsToRemove.length > 0) {
@@ -303,6 +332,7 @@ export class AgentService {
303332
this.logger.error("Failed to cleanup unused tools for agent", {
304333
agentId,
305334
activeEventTypeIds,
335+
mappedActiveEventTypeIds: RETELL_AI_TEST_MODE ? mappedActiveEventTypeIds : undefined,
306336
error,
307337
});
308338
throw new HttpError({

packages/lib/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,15 @@ export const CAL_AI_PHONE_NUMBER_MONTHLY_PRICE = (() => {
240240
const parsed = _rawCalAiPrice && _rawCalAiPrice.trim() !== "" ? Number(_rawCalAiPrice) : NaN;
241241
return Number.isFinite(parsed) ? parsed : 5;
242242
})();
243+
244+
// Retell AI test mode configuration
245+
export const RETELL_AI_TEST_MODE = process.env.RETELL_AI_TEST_MODE === "true";
246+
export const RETELL_AI_TEST_EVENT_TYPE_MAP = (() => {
247+
if (!process.env.RETELL_AI_TEST_EVENT_TYPE_MAP) return null;
248+
try {
249+
return JSON.parse(process.env.RETELL_AI_TEST_EVENT_TYPE_MAP);
250+
} catch (e) {
251+
console.warn("Failed to parse RETELL_AI_TEST_EVENT_TYPE_MAP", e);
252+
return null;
253+
}
254+
})();

turbo.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@
149149
"RENDER_EXTERNAL_URL",
150150
"RESERVED_SUBDOMAINS",
151151
"RETELL_AI_KEY",
152+
"RETELL_AI_TEST_MODE",
153+
"RETELL_AI_TEST_EVENT_TYPE_MAP",
154+
"RETELL_AI_TEST_CAL_API_KEY",
152155
"SALESFORCE_CONSUMER_KEY",
153156
"SALESFORCE_CONSUMER_SECRET",
154157
"SAML_ADMINS",

0 commit comments

Comments
 (0)