Skip to content

Commit 1ce7df3

Browse files
authored
feat: retell AI webhook url (#14430)
* feat: retell AI webhook url * fix: type error * chore * chore: make company and email optional * chore: remove logs * chore: type error * chore: type error * fix: type err * fix: pass guest email * fix: type err * chore: change date format * chore: move advanced format
1 parent e3b7264 commit 1ce7df3

File tree

8 files changed

+150
-31
lines changed

8 files changed

+150
-31
lines changed

apps/web/components/eventtype/AIEventController.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,15 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
126126
const values = formMethods.getValues("aiPhoneCallConfig");
127127

128128
const data = await AIPhoneSettingSchema.parseAsync({
129-
...values,
129+
generalPrompt: values.generalPrompt,
130+
beginMessage: values.beginMessage,
131+
enabled: values.enabled,
132+
guestName: values.guestName,
133+
guestEmail: values.guestEmail.trim().length ? values.guestEmail : undefined,
134+
guestCompany: values.guestCompany.trim().length ? values.guestCompany : undefined,
130135
eventTypeId: eventType.id,
136+
numberToCall: values.numberToCall,
137+
yourPhoneNumber: values.yourPhoneNumber,
131138
calApiKey,
132139
});
133140

apps/web/modules/event-types/views/event-types-single-view.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const DEFAULT_PROMPT_VALUE = `## You are helping user set up a call with the sup
5656
5757
## Task Steps
5858
1. I am here to learn more about your issue and help schedule an appointment with our support team.
59-
2. Use name {{name}} and email {{email}} for creating booking.
59+
2. If {{email}} is not unknown then Use name {{name}} and email {{email}} for creating booking else Ask for user name and email and Confirm the name and email with user by reading it back to user.
6060
3. Ask user for \"When would you want to meet with one of our representive\".
6161
4. Call function check_availability to check for availability in the user provided time range.
6262
- if availability exists, inform user about the availability range (do not repeat the detailed available slot) and ask user to choose from it. Make sure user chose a slot within detailed available slot.
@@ -68,7 +68,7 @@ const DEFAULT_PROMPT_VALUE = `## You are helping user set up a call with the sup
6868
7. Inform the user booking is successful, and ask if user have any questions. Answer them if there are any.
6969
8. After all questions answered, call function end_call to hang up.`;
7070

71-
const DEFAULT_BEGIN_MESSAGE = "Hi {{name}} from {{company}}. How are you doing?";
71+
const DEFAULT_BEGIN_MESSAGE = "Hi. How are you doing?";
7272

7373
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
7474
const EventSetupTab = dynamic(() =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import advancedFormat from "dayjs/plugin/advancedFormat";
2+
import type { NextApiRequest, NextApiResponse } from "next";
3+
import { z } from "zod";
4+
5+
import dayjs from "@calcom/dayjs";
6+
import { fetcher } from "@calcom/lib/retellAIFetcher";
7+
import { defaultHandler } from "@calcom/lib/server";
8+
import prisma from "@calcom/prisma";
9+
import { getRetellLLMSchema } from "@calcom/prisma/zod-utils";
10+
import type { TGetRetellLLMSchema } from "@calcom/prisma/zod-utils";
11+
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
12+
13+
dayjs.extend(advancedFormat);
14+
15+
const schema = z.object({
16+
llm_id: z.string(),
17+
from_number: z.string(),
18+
to_number: z.string(),
19+
});
20+
21+
const getEventTypeIdFromRetellLLM = (
22+
generalTools: TGetRetellLLMSchema["general_tools"]
23+
): number | undefined => {
24+
const generalTool = generalTools.find((tool) => !!tool.event_type_id && !!tool.timezone);
25+
26+
return generalTool?.event_type_id;
27+
};
28+
29+
async function handler(req: NextApiRequest, res: NextApiResponse) {
30+
const response = schema.safeParse(req.body);
31+
32+
if (!response.success) {
33+
return res.status(400).send({
34+
message: "Invalid Payload",
35+
});
36+
}
37+
38+
const body = response.data;
39+
40+
const retellLLM = await fetcher(`/get-retell-llm/${body.llm_id}`).then(getRetellLLMSchema.parse);
41+
42+
const eventTypeId = getEventTypeIdFromRetellLLM(retellLLM.general_tools);
43+
44+
if (!eventTypeId) return res.status(404).json({ message: "eventTypeId not found" });
45+
46+
const eventType = await prisma.eventType.findUnique({
47+
where: {
48+
id: eventTypeId,
49+
},
50+
select: {
51+
id: true,
52+
teamId: true,
53+
team: {
54+
select: {
55+
parent: {
56+
select: {
57+
slug: true,
58+
},
59+
},
60+
},
61+
},
62+
},
63+
});
64+
65+
if (!eventType) return res.status(404).json({ message: "eventType not found id" });
66+
67+
const now = dayjs();
68+
69+
const startTime = now.startOf("month").toISOString();
70+
const endTime = now.add(2, "month").endOf("month").toISOString();
71+
const orgSlug = eventType?.team?.parent?.slug ?? undefined;
72+
73+
const availableSlots = await getAvailableSlots({
74+
input: {
75+
startTime,
76+
endTime,
77+
eventTypeId,
78+
isTeamEvent: !!eventType?.teamId,
79+
orgSlug,
80+
},
81+
});
82+
83+
const firstAvailableDate = Object.keys(availableSlots.slots)[0];
84+
const firstSlot = availableSlots?.slots?.[firstAvailableDate]?.[0]?.time;
85+
86+
return res.status(200).json({
87+
next_available: firstSlot ? dayjs.utc(firstSlot).format("dddd [the] Do [at] h:mma [GMT]") : undefined,
88+
});
89+
}
90+
91+
export default defaultHandler({
92+
POST: Promise.resolve({ default: handler }),
93+
});

packages/lib/retellAIFetcher.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { handleErrorsJson } from "@calcom/lib/errors";
2+
3+
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
4+
return fetch(`https://api.retellai.com${endpoint}`, {
5+
method: "GET",
6+
headers: {
7+
Authorization: `Bearer ${process.env.RETELL_AI_KEY}`,
8+
"Content-Type": "application/json",
9+
...init?.headers,
10+
},
11+
...init,
12+
}).then(handleErrorsJson);
13+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "AIPhoneCallConfiguration" ALTER COLUMN "guestCompany" DROP NOT NULL,
3+
ALTER COLUMN "guestEmail" DROP NOT NULL;

packages/prisma/schema.prisma

+2-2
Original file line numberDiff line numberDiff line change
@@ -923,8 +923,8 @@ model AIPhoneCallConfiguration {
923923
yourPhoneNumber String
924924
numberToCall String
925925
guestName String
926-
guestEmail String
927-
guestCompany String
926+
guestEmail String?
927+
guestCompany String?
928928
enabled Boolean @default(false)
929929
beginMessage String?
930930
llmId String?

packages/prisma/zod-utils.ts

+24-6
Original file line numberDiff line numberDiff line change
@@ -702,12 +702,8 @@ export const AIPhoneSettingSchema = z.object({
702702
guestName: z.string().trim().min(1, {
703703
message: "Please enter Guest Name",
704704
}),
705-
guestEmail: z.string().trim().email().min(1, {
706-
message: "Please enter Guest Email",
707-
}),
708-
guestCompany: z.string().trim().min(1, {
709-
message: "Please enter Guest Company",
710-
}),
705+
guestEmail: z.string().email().optional(),
706+
guestCompany: z.string().optional(),
711707
generalPrompt: z.string().trim().min(1, {
712708
message: "Please enter prompt",
713709
}),
@@ -717,3 +713,25 @@ export const AIPhoneSettingSchema = z.object({
717713
message: "Please enter CAL API Key",
718714
}),
719715
});
716+
717+
export const getRetellLLMSchema = z
718+
.object({
719+
general_prompt: z.string(),
720+
begin_message: z.string().nullable(),
721+
llm_id: z.string(),
722+
llm_websocket_url: z.string(),
723+
general_tools: z.array(
724+
z
725+
.object({
726+
name: z.string(),
727+
type: z.string(),
728+
cal_api_key: z.string().optional(),
729+
event_type_id: z.number().optional(),
730+
timezone: z.string().optional(),
731+
})
732+
.passthrough()
733+
),
734+
})
735+
.passthrough();
736+
737+
export type TGetRetellLLMSchema = z.infer<typeof getRetellLLMSchema>;

packages/trpc/server/routers/viewer/organizations/createPhoneCall.handler.ts

+5-20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { z } from "zod";
22

33
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
4-
import { handleErrorsJson } from "@calcom/lib/errors";
4+
import { WEBAPP_URL } from "@calcom/lib/constants";
55
import logger from "@calcom/lib/logger";
6+
import { fetcher } from "@calcom/lib/retellAIFetcher";
67
import type { PrismaClient } from "@calcom/prisma";
8+
import { getRetellLLMSchema } from "@calcom/prisma/zod-utils";
79
import type { AIPhoneSettingSchema } from "@calcom/prisma/zod-utils";
810
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
911

@@ -22,13 +24,6 @@ const createRetellLLMSchema = z
2224
})
2325
.passthrough();
2426

25-
const getRetellLLMSchema = z
26-
.object({
27-
general_prompt: z.string(),
28-
begin_message: z.string().nullable(),
29-
})
30-
.passthrough();
31-
3227
const createPhoneSchema = z
3328
.object({
3429
call_id: z.string(),
@@ -42,18 +37,6 @@ const getPhoneNumberSchema = z
4237
})
4338
.passthrough();
4439

45-
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
46-
return fetch(`https://api.retellai.com${endpoint}`, {
47-
method: "GET",
48-
headers: {
49-
Authorization: `Bearer ${process.env.RETELL_AI_KEY}`,
50-
"Content-Type": "application/json",
51-
...init?.headers,
52-
},
53-
...init,
54-
}).then(handleErrorsJson);
55-
};
56-
5740
const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
5841
await checkRateLimitAndThrowError({
5942
rateLimitingType: "core",
@@ -107,6 +90,7 @@ const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
10790
body: JSON.stringify({
10891
general_prompt: generalPrompt,
10992
begin_message: beginMessage,
93+
inbound_dynamic_variables_webhook_url: `${WEBAPP_URL}/api/get-inbound-dynamic-variables`,
11094
general_tools: [
11195
{
11296
type: "end_call",
@@ -152,6 +136,7 @@ const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
152136
body: JSON.stringify({
153137
general_prompt: generalPrompt,
154138
begin_message: beginMessage,
139+
inbound_dynamic_variables_webhook_url: `${WEBAPP_URL}/api/get-inbound-dynamic-variables`,
155140
}),
156141
}).then(getRetellLLMSchema.parse);
157142

0 commit comments

Comments
 (0)