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
This commit is contained in:
Udit Takkar authored and GitHub committed 2024-04-05 22:44:57 +05:30
1 parent e3b7264208
commit 1ce7df324d
8 files changed
+150 -31

No files matched your search

@@ -126,8 +126,15 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
const values = formMethods.getValues("aiPhoneCallConfig");
const data = await AIPhoneSettingSchema.parseAsync({
...values,
generalPrompt: values.generalPrompt,
beginMessage: values.beginMessage,
enabled: values.enabled,
guestName: values.guestName,
guestEmail: values.guestEmail.trim().length ? values.guestEmail : undefined,
guestCompany: values.guestCompany.trim().length ? values.guestCompany : undefined,
eventTypeId: eventType.id,
numberToCall: values.numberToCall,
yourPhoneNumber: values.yourPhoneNumber,
calApiKey,
});
@@ -56,7 +56,7 @@ const DEFAULT_PROMPT_VALUE = `## You are helping user set up a call with the sup
## Task Steps
1. I am here to learn more about your issue and help schedule an appointment with our support team.
2. Use name {{name}} and email {{email}} for creating booking.
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.
3. Ask user for \"When would you want to meet with one of our representive\".
4. Call function check_availability to check for availability in the user provided time range.
- 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
7. Inform the user booking is successful, and ask if user have any questions. Answer them if there are any.
8. After all questions answered, call function end_call to hang up.`;
const DEFAULT_BEGIN_MESSAGE = "Hi {{name}} from {{company}}. How are you doing?";
const DEFAULT_BEGIN_MESSAGE = "Hi. How are you doing?";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
const EventSetupTab = dynamic(() =>
@@ -0,0 +1,93 @@
import advancedFormat from "dayjs/plugin/advancedFormat";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { fetcher } from "@calcom/lib/retellAIFetcher";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { getRetellLLMSchema } from "@calcom/prisma/zod-utils";
import type { TGetRetellLLMSchema } from "@calcom/prisma/zod-utils";
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
dayjs.extend(advancedFormat);
const schema = z.object({
llm_id: z.string(),
from_number: z.string(),
to_number: z.string(),
});
const getEventTypeIdFromRetellLLM = (
generalTools: TGetRetellLLMSchema["general_tools"]
): number | undefined => {
const generalTool = generalTools.find((tool) => !!tool.event_type_id && !!tool.timezone);
return generalTool?.event_type_id;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
const response = schema.safeParse(req.body);
if (!response.success) {
return res.status(400).send({
message: "Invalid Payload",
});
}
const body = response.data;
const retellLLM = await fetcher(`/get-retell-llm/${body.llm_id}`).then(getRetellLLMSchema.parse);
const eventTypeId = getEventTypeIdFromRetellLLM(retellLLM.general_tools);
if (!eventTypeId) return res.status(404).json({ message: "eventTypeId not found" });
const eventType = await prisma.eventType.findUnique({
where: {
id: eventTypeId,
},
select: {
id: true,
teamId: true,
team: {
select: {
parent: {
select: {
slug: true,
},
},
},
},
},
});
if (!eventType) return res.status(404).json({ message: "eventType not found id" });
const now = dayjs();
const startTime = now.startOf("month").toISOString();
const endTime = now.add(2, "month").endOf("month").toISOString();
const orgSlug = eventType?.team?.parent?.slug ?? undefined;
const availableSlots = await getAvailableSlots({
input: {
startTime,
endTime,
eventTypeId,
isTeamEvent: !!eventType?.teamId,
orgSlug,
},
});
const firstAvailableDate = Object.keys(availableSlots.slots)[0];
const firstSlot = availableSlots?.slots?.[firstAvailableDate]?.[0]?.time;
return res.status(200).json({
next_available: firstSlot ? dayjs.utc(firstSlot).format("dddd [the] Do [at] h:mma [GMT]") : undefined,
});
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
+13
View File
@@ -0,0 +1,13 @@
import { handleErrorsJson } from "@calcom/lib/errors";
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
return fetch(`https://api.retellai.com${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.RETELL_AI_KEY}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
}).then(handleErrorsJson);
};
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "AIPhoneCallConfiguration" ALTER COLUMN "guestCompany" DROP NOT NULL,
ALTER COLUMN "guestEmail" DROP NOT NULL;
+2 -2
View File
@@ -923,8 +923,8 @@ model AIPhoneCallConfiguration {
yourPhoneNumber String
numberToCall String
guestName String
guestEmail String
guestCompany String
guestEmail String?
guestCompany String?
enabled Boolean @default(false)
beginMessage String?
llmId String?
+24 -6
View File
@@ -702,12 +702,8 @@ export const AIPhoneSettingSchema = z.object({
guestName: z.string().trim().min(1, {
message: "Please enter Guest Name",
}),
guestEmail: z.string().trim().email().min(1, {
message: "Please enter Guest Email",
}),
guestCompany: z.string().trim().min(1, {
message: "Please enter Guest Company",
}),
guestEmail: z.string().email().optional(),
guestCompany: z.string().optional(),
generalPrompt: z.string().trim().min(1, {
message: "Please enter prompt",
}),
@@ -717,3 +713,25 @@ export const AIPhoneSettingSchema = z.object({
message: "Please enter CAL API Key",
}),
});
export const getRetellLLMSchema = z
.object({
general_prompt: z.string(),
begin_message: z.string().nullable(),
llm_id: z.string(),
llm_websocket_url: z.string(),
general_tools: z.array(
z
.object({
name: z.string(),
type: z.string(),
cal_api_key: z.string().optional(),
event_type_id: z.number().optional(),
timezone: z.string().optional(),
})
.passthrough()
),
})
.passthrough();
export type TGetRetellLLMSchema = z.infer<typeof getRetellLLMSchema>;
@@ -1,9 +1,11 @@
import { z } from "zod";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { handleErrorsJson } from "@calcom/lib/errors";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { fetcher } from "@calcom/lib/retellAIFetcher";
import type { PrismaClient } from "@calcom/prisma";
import { getRetellLLMSchema } from "@calcom/prisma/zod-utils";
import type { AIPhoneSettingSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -22,13 +24,6 @@ const createRetellLLMSchema = z
})
.passthrough();
const getRetellLLMSchema = z
.object({
general_prompt: z.string(),
begin_message: z.string().nullable(),
})
.passthrough();
const createPhoneSchema = z
.object({
call_id: z.string(),
@@ -42,18 +37,6 @@ const getPhoneNumberSchema = z
})
.passthrough();
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
return fetch(`https://api.retellai.com${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.RETELL_AI_KEY}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
}).then(handleErrorsJson);
};
const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
@@ -107,6 +90,7 @@ const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
body: JSON.stringify({
general_prompt: generalPrompt,
begin_message: beginMessage,
inbound_dynamic_variables_webhook_url: `${WEBAPP_URL}/api/get-inbound-dynamic-variables`,
general_tools: [
{
type: "end_call",
@@ -152,6 +136,7 @@ const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
body: JSON.stringify({
general_prompt: generalPrompt,
begin_message: beginMessage,
inbound_dynamic_variables_webhook_url: `${WEBAPP_URL}/api/get-inbound-dynamic-variables`,
}),
}).then(getRetellLLMSchema.parse);