diff --git a/apps/web/components/eventtype/AIEventController.tsx b/apps/web/components/eventtype/AIEventController.tsx index 36451d101f..783161abb3 100644 --- a/apps/web/components/eventtype/AIEventController.tsx +++ b/apps/web/components/eventtype/AIEventController.tsx @@ -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, }); diff --git a/apps/web/modules/event-types/views/event-types-single-view.tsx b/apps/web/modules/event-types/views/event-types-single-view.tsx index 5b76128dcc..19e41f96e7 100644 --- a/apps/web/modules/event-types/views/event-types-single-view.tsx +++ b/apps/web/modules/event-types/views/event-types-single-view.tsx @@ -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(() => diff --git a/apps/web/pages/api/get-inbound-dynamic-variables.ts b/apps/web/pages/api/get-inbound-dynamic-variables.ts new file mode 100644 index 0000000000..34676713b2 --- /dev/null +++ b/apps/web/pages/api/get-inbound-dynamic-variables.ts @@ -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 }), +}); diff --git a/packages/lib/retellAIFetcher.ts b/packages/lib/retellAIFetcher.ts new file mode 100644 index 0000000000..7cb9f654c5 --- /dev/null +++ b/packages/lib/retellAIFetcher.ts @@ -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); +}; diff --git a/packages/prisma/migrations/20240405142908_make_guest_company_and_email_optional/migration.sql b/packages/prisma/migrations/20240405142908_make_guest_company_and_email_optional/migration.sql new file mode 100644 index 0000000000..400b0d257b --- /dev/null +++ b/packages/prisma/migrations/20240405142908_make_guest_company_and_email_optional/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "AIPhoneCallConfiguration" ALTER COLUMN "guestCompany" DROP NOT NULL, +ALTER COLUMN "guestEmail" DROP NOT NULL; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index fd6e8f8cb9..a12599c21c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -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? diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index d6b7125589..b3cecc4324 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -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; diff --git a/packages/trpc/server/routers/viewer/organizations/createPhoneCall.handler.ts b/packages/trpc/server/routers/viewer/organizations/createPhoneCall.handler.ts index 16dd03f7e3..a4ed536f0b 100644 --- a/packages/trpc/server/routers/viewer/organizations/createPhoneCall.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/createPhoneCall.handler.ts @@ -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);