Skip to content

Commit ba50268

Browse files
Udit-takkarAmit91848CarinaWolli
authored
feat: support auto create agent (calcom#23493)
* feat: support auto create agent * feat: add prompts * chore: add number to call * fix typo --------- Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com>
1 parent 4ca78f4 commit ba50268

7 files changed

Lines changed: 171 additions & 9 deletions

File tree

packages/features/calAIPhone/promptTemplates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const DEFAULT_PROMPT_VALUE = `## You are helping user set up a call with
3535
Avoid multiple questions in a single response.
3636
Get clarity: If the user only partially answers a question, or if the answer is unclear, keep asking to get clarity.
3737
Use a colloquial way of referring to the date (like Friday, Jan 14th, or Tuesday, Jan 12th, 2024 at 8am).
38-
If you are saying a time like 8:00 AM, just say 8 AM and emit the trailing zeros.
38+
If you are saying a time like 8:00 AM, just say 8 AM and omit the trailing zeros.
3939
4040
## Response Guideline
4141
Adapt and Guess: Try to understand transcripts that may contain transcription errors. Avoid mentioning \"transcription error\" in the response.
@@ -53,7 +53,7 @@ export const DEFAULT_PROMPT_VALUE = `## You are helping user set up a call with
5353
- 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.
5454
- if availability does not exist, ask user to select another time range for the appointment, repeat this step 3.
5555
5. Confirm the date and time selected by user: \"Just to confirm, you want to book the appointment at ...\".
56-
6. Once confirmed, call function book_appointment_{{eventTypeId}} to book the appointment.
56+
6. Once confirmed, you can use {{NUMBER_TO_CALL}} as phone number for creating booking and call function book_appointment_{{eventTypeId}} to book the appointment.
5757
- if booking returned booking detail, it means booking is successful, proceed to step 7.
5858
- if booking returned error message, let user know why the booking was not successful, and maybe start over with step 3.
5959
7. Inform the user booking is successful, and ask if user have any questions. Answer them if there are any.

packages/features/calAIPhone/providers/retellAI/services/CallService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export class CallService {
155155
EVENT_START_TIME_IN_ATTENDEE_TIMEZONE: "2:00 PM",
156156
EVENT_END_TIME_IN_ATTENDEE_TIMEZONE: "2:30 PM",
157157
eventTypeId: eventTypeId.toString(),
158+
NUMBER_TO_CALL: toNumber,
158159
},
159160
});
160161

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const styleGuardrails = `## Style Guardrails
2+
Be Concise: Respond succinctly, addressing one topic at most.
3+
Embrace Variety: Use diverse language and rephrasing to enhance clarity without repeating content.
4+
Be Conversational: Use everyday language, making the chat feel like talking to a friend.
5+
Be Proactive: Lead the conversation, often wrapping up with a question or next-step suggestion.
6+
Avoid multiple questions in a single response.
7+
Get clarity: If the user only partially answers a question, or if the answer is unclear, keep asking to get clarity.
8+
Use a colloquial way of referring to the date (like Friday, Jan 14th, or Tuesday, Jan 12th, 2024 at 8am).
9+
If you are saying a time like 8:00 AM, just say 8 AM and omit the trailing zeros.`;
10+
11+
const responseGuideline = `## Response Guideline
12+
Adapt and Guess: Try to understand transcripts that may contain transcription errors. Avoid mentioning \"transcription error\" in the response.
13+
Stay in Character: Keep conversations within your role'''s scope, guiding them back creatively without repeating.
14+
Ensure Fluid Dialogue: Respond in a role-appropriate, direct manner to maintain a smooth conversation flow.`;
15+
16+
const scheduleRule = ` ## Schedule Rule
17+
Current time is {{current_time}}. You only schedule time in current calendar year, you cannot schedule time that'''s in the past.`;
18+
19+
// Key are from components/sections/template/data/workflows.ts page in https://github.com/calcom/website
20+
export const calAIPhoneWorkflowTemplates = {
21+
// name: "Cal AI No-show Follow-up Call",
22+
// description: "Automatically call attendee when marked as no-show"
23+
"wf-10": {
24+
generalPrompt: `## You are calling an attendee who was marked as a no-show for their appointment. Your goal is to help them reschedule. Be understanding, friendly, and non-judgmental.
25+
26+
${styleGuardrails}
27+
28+
${responseGuideline}
29+
30+
${scheduleRule}
31+
32+
## Task Steps
33+
1. Start with a friendly greeting: "Hi {{ATTENDEE_NAME}}, this is a courtesy call from {{ORGANIZER_NAME}}. I noticed you weren't able to make your {{EVENT_NAME}} appointment on {{EVENT_DATE}} at {{EVENT_TIME}}."
34+
2. Express understanding: "I understand things come up. I'm calling to see if you'd like to reschedule for another time that works better for you."
35+
3. If they want to reschedule:
36+
3a. Ask "When would work best for you to reschedule?"
37+
3b. Call function check_availability_{{eventTypeId}} to check for availability in the user provided time range.
38+
- 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.
39+
- if availability does not exist, ask user to select another time range for the appointment, repeat this step 3a.
40+
5. If {{ATTENDEE_EMAIL}} is not unknown then Use name {{ATTENDEE_NAME}} and email {{ATTENDEE_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.
41+
6. Once confirmed, you can use {{NUMBER_TO_CALL}} as phone number for creating booking and call function book_appointment_{{eventTypeId}} to book the appointment.
42+
- if booking returned booking detail, it means booking is successful, proceed to step 7.
43+
- if booking returned error message, let user know why the booking was not successful, and maybe start over with step 3a.
44+
7. If they don't want to reschedule:
45+
- Thank them for their time and let them know they can always reach out if they change their mind.
46+
8. Before ending, ask if there's anything else you can help with.
47+
9. Thank them for their time and call function end_call to hang up.`,
48+
},
49+
50+
// name: "Cal AI 1-hour Meeting Reminder",
51+
// description: "Remind attendee 1 hour before the meeting"
52+
"wf-11": {
53+
generalPrompt: `## You are calling to remind an attendee about their upcoming appointment in 1 hour. Be friendly, helpful, and concise.
54+
55+
${styleGuardrails}
56+
57+
${responseGuideline}
58+
59+
${scheduleRule}
60+
61+
## Task Steps
62+
1. Start with a friendly greeting: "Hi {{ATTENDEE_NAME}}, this is a quick reminder call from {{ORGANIZER_NAME}} about your upcoming {{EVENT_NAME}} appointment."
63+
2. Provide the meeting details: "Your appointment is scheduled for today at {{EVENT_TIME}} {{TIMEZONE}}. That's in about an hour."
64+
3. Ask if they'll be able to make it: "Will you be able to join us?"
65+
4. If they confirm attendance:
66+
- Thank them and remind them of any preparation needed.
67+
- Say "Great! We'll see you at {{EVENT_TIME}}."
68+
5. If they need to reschedule or cancel:
69+
- Express understanding: "No problem, these things happen."
70+
- Ask: "Would you like to reschedule now, or would you prefer to contact us later?"
71+
- If they want to reschedule now:
72+
5a. If {{ATTENDEE_EMAIL}} is not unknown: Use name {{ATTENDEE_NAME}} and email {{ATTENDEE_EMAIL}} for creating booking
73+
5b. If {{ATTENDEE_EMAIL}} is unknown: Ask for user name and email and confirm by reading it back to user
74+
5c. Ask user for "When would you want to reschedule?"
75+
5d. Call function check_availability_{{eventTypeId}} to check for availability in the user provided time range.
76+
5e. 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.
77+
5f. If availability does not exist, ask user to select another time range for the appointment (repeat step 5c).
78+
5g. Confirm the date and time selected by user: "Just to confirm, you want to book the appointment at ..."
79+
5h. Once confirmed, you can use {{NUMBER_TO_CALL}} as phone number for creating booking and call function book_appointment_{{eventTypeId}} to book the appointment.
80+
5i. If booking returned booking detail, it means booking is successful, proceed to step 7.
81+
5j. If booking returned error message, let user know why the booking was not successful, and maybe start over (return to step 5c).
82+
- If they prefer to reschedule later: "No problem. You can reschedule anytime through the link in your confirmation email or by contacting us."
83+
6. If they have questions about the meeting:
84+
- Answer based on available information ({{ADDITIONAL_NOTES}}, {{LOCATION}}, etc.).
85+
- Common questions to handle:
86+
- Duration: Use {{EVENT_END_TIME}} to calculate and state duration
87+
- Location details: Provide {{LOCATION}} information
88+
- What to prepare: Check {{ADDITIONAL_NOTES}} for any preparation instructions
89+
- Who they're meeting: {{ORGANIZER_NAME}} is the person they'll be meeting
90+
7. End with: "Thanks for your time. Have a great day!" and call function end_call to hang up.`,
91+
},
92+
};

packages/features/ee/workflows/components/WorkflowStepContainer.tsx

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { WorkflowStep } from "@prisma/client";
22
import { type TFunction } from "i18next";
3-
import { useParams } from "next/navigation";
3+
import { useParams, useRouter, useSearchParams } from "next/navigation";
44
import type { Dispatch, SetStateAction } from "react";
55
import { useEffect, useRef, useState } from "react";
66
import type { UseFormReturn } from "react-hook-form";
@@ -128,8 +128,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
128128
const { t, i18n } = useLocale();
129129
const utils = trpc.useUtils();
130130
const params = useParams();
131+
const router = useRouter();
132+
const searchParams = useSearchParams();
131133

132-
const { step, form, reload, setReload, teamId } = props;
134+
const { step, form, reload, setReload, teamId, onSaveWorkflow } = props;
133135
const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
134136
{ teamId },
135137
{ enabled: !!teamId }
@@ -158,7 +160,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
158160
onSuccess: async (data) => {
159161
showToast(t("agent_created_successfully"), "success");
160162

161-
// Update the step's agentId in the form state
162163
if (step) {
163164
const stepIndex = step.stepNumber - 1;
164165
form.setValue(`steps.${stepIndex}.agentId`, data.id);
@@ -239,6 +240,66 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
239240
);
240241

241242
const [timeSectionText, setTimeSectionText] = useState(getTimeSectionText(form.getValues("trigger"), t));
243+
const [autoAgentCreationAttempted, setAutoAgentCreationAttempted] = useState(false);
244+
245+
useEffect(() => {
246+
const autoCreateAgent = searchParams?.get("autoCreateAgent");
247+
const templateWorkflowId = searchParams?.get("templateWorkflowId");
248+
249+
if (
250+
autoCreateAgent === "true" &&
251+
!autoAgentCreationAttempted &&
252+
templateWorkflowId &&
253+
step &&
254+
step.action === WorkflowActions.CAL_AI_PHONE_CALL &&
255+
!stepAgentId &&
256+
step.id &&
257+
onSaveWorkflow
258+
) {
259+
setAutoAgentCreationAttempted(true);
260+
261+
const createAgent = async () => {
262+
try {
263+
await onSaveWorkflow?.();
264+
265+
const updatedSteps = form.getValues("steps");
266+
const currentStepIndex = step.stepNumber - 1;
267+
const updatedStep = updatedSteps[currentStepIndex];
268+
269+
if (updatedStep?.id) {
270+
createAgentMutation.mutate({
271+
teamId,
272+
workflowStepId: updatedStep.id,
273+
templateWorkflowId,
274+
});
275+
276+
const url = new URL(window.location.href);
277+
url.searchParams.delete("autoCreateAgent");
278+
url.searchParams.delete("templateWorkflowId");
279+
router.replace(url.pathname + url.search);
280+
} else {
281+
showToast(t("failed_to_get_workflow_step_id"), "error");
282+
}
283+
} catch (error) {
284+
console.error("Failed to auto-create agent:", error);
285+
showToast(t("failed_to_create_agent"), "error");
286+
}
287+
};
288+
289+
createAgent();
290+
}
291+
}, [
292+
searchParams,
293+
autoAgentCreationAttempted,
294+
step,
295+
stepAgentId,
296+
teamId,
297+
onSaveWorkflow,
298+
createAgentMutation,
299+
form,
300+
t,
301+
router,
302+
]);
242303

243304
const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
244305
const triggerOptions = getWorkflowTriggerOptions(t);
@@ -681,8 +742,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
681742
color="secondary"
682743
onClick={async () => {
683744
// save the workflow first to get the step id
684-
if (props.onSaveWorkflow) {
685-
await props.onSaveWorkflow();
745+
if (onSaveWorkflow) {
746+
await onSaveWorkflow();
686747

687748
// After saving, get the updated step ID from the form
688749
const updatedSteps = form.getValues("steps");

packages/features/tasker/tasks/executeAIPhoneCall.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export async function executeAIPhoneCall(payload: string) {
183183
ATTENDEE_FIRST_NAME: attendeeFirstName,
184184
ATTENDEE_LAST_NAME: attendeeLastName,
185185
ATTENDEE_EMAIL: attendee?.email || "",
186+
NUMBER_TO_CALL: numberToCall,
186187
ATTENDEE_TIMEZONE: attendee?.timeZone || "",
187188
ADDITIONAL_NOTES: booking.description || "",
188189
EVENT_START_TIME_IN_ATTENDEE_TIMEZONE: dayjs(booking.startTime)

packages/trpc/server/routers/viewer/aiVoiceAgent/create.handler.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
22
import type { RetellLLMGeneralTools } from "@calcom/features/calAIPhone/providers/retellAI/types";
3+
import { calAIPhoneWorkflowTemplates } from "@calcom/features/calAIPhone/workflowTemplates";
34

45
import type { TrpcSessionUser } from "../../../types";
56
import type { TCreateInputSchema } from "./create.schema";
@@ -12,16 +13,21 @@ type CreateHandlerOptions = {
1213
};
1314

1415
export const createHandler = async ({ ctx, input }: CreateHandlerOptions) => {
15-
const { teamId, name, workflowStepId, ...retellConfig } = input;
16+
const { teamId, name, workflowStepId, templateWorkflowId, ...retellConfig } = input;
1617

1718
const aiService = createDefaultAIPhoneServiceProvider();
1819

20+
const generalPrompt = templateWorkflowId
21+
? calAIPhoneWorkflowTemplates?.[templateWorkflowId as keyof typeof calAIPhoneWorkflowTemplates]
22+
?.generalPrompt
23+
: undefined;
24+
1925
return await aiService.createAgent({
2026
name,
2127
userId: ctx.user.id,
2228
teamId,
2329
workflowStepId,
24-
generalPrompt: retellConfig.generalPrompt,
30+
generalPrompt: generalPrompt ?? retellConfig.generalPrompt,
2531
beginMessage: retellConfig.beginMessage,
2632
generalTools: retellConfig.generalTools as RetellLLMGeneralTools,
2733
userTimeZone: ctx.user.timeZone,

packages/trpc/server/routers/viewer/aiVoiceAgent/create.schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const ZCreateInputSchema = z.object({
44
name: z.string().optional(),
55
teamId: z.number().optional(),
66
workflowStepId: z.number().optional(),
7+
templateWorkflowId: z.string().optional(),
78
generalPrompt: z.string().optional(),
89
beginMessage: z.string().optional(),
910
generalTools: z

0 commit comments

Comments
 (0)