From 35d2fdfa3200e914aa075c206fc376388b53324f Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 4 Sep 2025 14:49:30 +0530 Subject: [PATCH] chore: local testing retell --- .env.example | 5 ++ .../retellAI/services/AgentService.ts | 56 ++++++++++++++----- packages/lib/constants.ts | 12 ++++ turbo.json | 3 + 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index ca21dccf75d266..64ef21e4ac9b71 100644 --- a/.env.example +++ b/.env.example @@ -386,6 +386,11 @@ UNKEY_ROOT_KEY= # Used for Cal.ai Voice AI Agents # https://retellai.com RETELL_AI_KEY= +# Local testing overrides for Retell AI +RETELL_AI_TEST_MODE=false +# JSON mapping of local to production event type IDs (e.g. {"50": 747280} without any string) +RETELL_AI_TEST_EVENT_TYPE_MAP= +RETELL_AI_TEST_CAL_API_KEY= # Used for buying phone number for cal ai voice agent STRIPE_PHONE_NUMBER_MONTHLY_PRICE_ID= diff --git a/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts b/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts index 283477d20aeb7a..8c39dbdbaaf1d2 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from "uuid"; +import { RETELL_AI_TEST_MODE, RETELL_AI_TEST_EVENT_TYPE_MAP } from "@calcom/lib/constants"; import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; @@ -81,6 +82,13 @@ export class AgentService { }); } + let eventTypeId = data.eventTypeId; + + if (RETELL_AI_TEST_MODE && RETELL_AI_TEST_EVENT_TYPE_MAP) { + const mappedId = RETELL_AI_TEST_EVENT_TYPE_MAP[String(data.eventTypeId)]; + eventTypeId = mappedId ? Number(mappedId) : data.eventTypeId; + } + try { const agent = await this.getAgent(agentId); const llmId = getLlmId(agent); @@ -99,8 +107,8 @@ export class AgentService { const existing = llmDetails?.general_tools ?? []; - const hasCheck = existing.some((t) => t.name === `check_availability_${data.eventTypeId}`); - const hasBook = existing.some((t) => t.name === `book_appointment_${data.eventTypeId}`); + const hasCheck = existing.some((t) => t.name === `check_availability_${eventTypeId}`); + const hasBook = existing.some((t) => t.name === `book_appointment_${eventTypeId}`); // If both already exist and end_call also exists, nothing to do const hasEndCallAlready = existing.some((t) => t.type === "end_call"); if (hasCheck && hasBook && hasEndCallAlready) { @@ -113,27 +121,29 @@ export class AgentService { )?.cal_api_key; const apiKey = - reusableKey ?? - (await this.createApiKey({ - userId: data.userId, - teamId: data.teamId || undefined, - })); + RETELL_AI_TEST_MODE && process.env.RETELL_AI_TEST_CAL_API_KEY + ? process.env.RETELL_AI_TEST_CAL_API_KEY + : reusableKey ?? + (await this.createApiKey({ + userId: data.userId, + teamId: data.teamId || undefined, + })); const newEventTools: NonNullable> = []; if (!hasCheck) { newEventTools.push({ - name: `check_availability_${data.eventTypeId}`, + name: `check_availability_${eventTypeId}`, type: "check_availability_cal", - event_type_id: data.eventTypeId, + event_type_id: eventTypeId, cal_api_key: apiKey, timezone: data.timeZone, }); } if (!hasBook) { newEventTools.push({ - name: `book_appointment_${data.eventTypeId}`, + name: `book_appointment_${eventTypeId}`, type: "book_appointment_cal", - event_type_id: data.eventTypeId, + event_type_id: eventTypeId, cal_api_key: apiKey, timezone: data.timeZone, }); @@ -180,6 +190,15 @@ export class AgentService { return; } + let mappedEventTypeIds = eventTypeIds; + + if (RETELL_AI_TEST_MODE && RETELL_AI_TEST_EVENT_TYPE_MAP) { + mappedEventTypeIds = eventTypeIds.map((id) => { + const mappedId = RETELL_AI_TEST_EVENT_TYPE_MAP[String(id)]; + return mappedId ? Number(mappedId) : id; + }); + } + try { const agent = await this.getAgent(agentId); const llmId = getLlmId(agent); @@ -199,7 +218,7 @@ export class AgentService { const existing = llmDetails?.general_tools ?? []; - const toolNamesToRemove = eventTypeIds.flatMap((eventTypeId) => [ + const toolNamesToRemove = mappedEventTypeIds.flatMap((eventTypeId) => [ `check_availability_${eventTypeId}`, `book_appointment_${eventTypeId}`, ]); @@ -212,6 +231,7 @@ export class AgentService { agentId, llmId, removedEventTypes: eventTypeIds, + mappedEventTypes: RETELL_AI_TEST_MODE ? mappedEventTypeIds : undefined, toolsRemoved: existing.length - filteredTools.length, }); } @@ -240,6 +260,15 @@ export class AgentService { }); } + let mappedActiveEventTypeIds = activeEventTypeIds; + + if (RETELL_AI_TEST_MODE && RETELL_AI_TEST_EVENT_TYPE_MAP) { + mappedActiveEventTypeIds = activeEventTypeIds.map((id) => { + const mappedId = RETELL_AI_TEST_EVENT_TYPE_MAP[String(id)]; + return mappedId ? Number(mappedId) : id; + }); + } + try { const agent = await this.getAgent(agentId); const llmId = getLlmId(agent); @@ -269,7 +298,7 @@ export class AgentService { if (!eventTypeIdMatch) return false; const eventTypeId = parseInt(eventTypeIdMatch[1]); - return !activeEventTypeIds.includes(eventTypeId); + return !mappedActiveEventTypeIds.includes(eventTypeId); }); if (toolsToRemove.length > 0) { @@ -303,6 +332,7 @@ export class AgentService { this.logger.error("Failed to cleanup unused tools for agent", { agentId, activeEventTypeIds, + mappedActiveEventTypeIds: RETELL_AI_TEST_MODE ? mappedActiveEventTypeIds : undefined, error, }); throw new HttpError({ diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index a414ace8516f24..9c5dfa49015872 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -240,3 +240,15 @@ export const CAL_AI_PHONE_NUMBER_MONTHLY_PRICE = (() => { const parsed = _rawCalAiPrice && _rawCalAiPrice.trim() !== "" ? Number(_rawCalAiPrice) : NaN; return Number.isFinite(parsed) ? parsed : 5; })(); + +// Retell AI test mode configuration +export const RETELL_AI_TEST_MODE = process.env.RETELL_AI_TEST_MODE === "true"; +export const RETELL_AI_TEST_EVENT_TYPE_MAP = (() => { + if (!process.env.RETELL_AI_TEST_EVENT_TYPE_MAP) return null; + try { + return JSON.parse(process.env.RETELL_AI_TEST_EVENT_TYPE_MAP); + } catch (e) { + console.warn("Failed to parse RETELL_AI_TEST_EVENT_TYPE_MAP", e); + return null; + } +})(); diff --git a/turbo.json b/turbo.json index 6de1629c0a6424..b43058599c2288 100644 --- a/turbo.json +++ b/turbo.json @@ -149,6 +149,9 @@ "RENDER_EXTERNAL_URL", "RESERVED_SUBDOMAINS", "RETELL_AI_KEY", + "RETELL_AI_TEST_MODE", + "RETELL_AI_TEST_EVENT_TYPE_MAP", + "RETELL_AI_TEST_CAL_API_KEY", "SALESFORCE_CONSUMER_KEY", "SALESFORCE_CONSUMER_SECRET", "SAML_ADMINS",