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:
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 }),
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AIPhoneCallConfiguration" ALTER COLUMN "guestCompany" DROP NOT NULL,
|
||||
ALTER COLUMN "guestEmail" DROP NOT NULL;
|
||||
@@ -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?
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user