Building an AI Agent for Google Calendar—Part 2: Designing the Interface
Welcome to part 2 of FriendliAI’s blog series on “Building an AI Agent for Google Calendar”. In part 1, we discussed the basic concepts of AI agents, the tools that we used to interact with Google Calendar, and how function calls work with Large Language Models (LLMs). Now, we will dig in deeper into improving the user interface (UI) and the user experience (UX) of the AI agent. By the end of this tutorial, you will be able to build an interactive AI agent that seamlessly integrates with Google Calendar with a user-friendly interface.
Improving the User Interface with a better Chat UI
To make the AI agent more intuitive and appealing to users, let’s design our chat interface. This UI will allow users to ask questions in real-time and receive responses from the AI agent. We will use Next.js to create a dynamic & responsive chat interface, enabling smooth interaction with the AI model. This post is composed of 6 separate steps as follows:
Before starting, make sure you have access to your FRIENDLI_TOKEN from the Friendli Suite. Now, let’s install the necessary dependencies. Keep in mind that you’ll have to replace the {your_friendli_token_here} part below with your own access token. Run the following command in the terminal:
Then, let’s create a app/api/chat/route.ts file to make the useChat hook work:
app/api/chat/route.ts
import { convertToCoreMessages, streamText } from "ai";import { friendli } from "@friendliai/ai-provider";export async function POST(req: Request) { const { messages, datetime } = await req.json(); if (!messages) return new Response("Messages is required", { status: 400 }); const result = await streamText({ model: friendli("meta-llama-3.1-70b-instruct"), system: `Today is ${datetime}. You are a helpful calendar assistant.`, messages: convertToCoreMessages(messages), abortSignal: req.signal, }); return result.toDataStreamResponse();}
In the image above, you can see our very basic chat UI.
Step 2. Understanding Tools Usage Patterns in Vercel AI SDK
Let's examine how tools can be defined within the Vercel AI SDK.
app/api/chat/route.ts
import { convertToCoreMessages, streamText } from "ai";import { friendli } from "@friendliai/ai-provider";import { z } from "zod";// ... omit redundant code const result = await streamText({ model: friendli("meta-llama-3.1-70b-instruct"), system: `Today is ${datetime}. You are a helpful calendar assistant.`, messages: convertToCoreMessages(messages), abortSignal: req.signal, tools: { fetchCalendarEvents: { description: `Retrieves calendar events within a specified date range.`, parameters: z.object({ startDate: z .string() .describe("Start date of the search range (format: yyyy-MM-dd)"), endDate: z .string() .describe("End date of the search range (format: yyyy-MM-dd)"), }), }, }, });// ... omit redundant code
In the image above, you can observe that the LLM can call the tool as needed, by declaring a tool with a description and parameters. But we haven't implemented the part of the code that actually runs the tool yet, so it stops at the call step before the execution.
The Vercel AI SDK supports three types of execution methods for the tool execution:
Automatically executed server-side tools
Automatically executed client-side tools
Tools that require user interaction, via confirmation dialogs
In this blog post, we will explore the most commonly used server-side automatic execution tools and tools that require user interaction.
2-1. Automatically executed server-side tools
It does not require any actions from the users for fetching calendar events, hence there's no need to execute any code on the client side. Therefore, the fetchCalendarEvents tool corresponds to the first type, ‘automatically executed server-side tools'.
This can be implemented as:
app/api/chat/route.ts
// ... omit redundant codemaxSteps: 4,tools: { fetchCalendarEvents: { description: `Retrieves calendar events within a specified date range.`, parameters: z.object({ startDate: z .string() .describe("Start date of the search range (format: yyyy-MM-dd)"), endDate: z .string() .describe("End date of the search range (format: yyyy-MM-dd)"), }), execute: async ({ startDate, endDate }) => { const mockEvents = [ { title: "Meeting with John", start: `${startDate}T10:00:00`, end: `${startDate}T11:00:00`, }, { title: "Lunch with Jane", start: `${startDate}T12:00:00`, end: `${startDate}T13:00:00`, }, ]; return { message: `Fetching calendar events from ${startDate} to ${endDate}`, events: mockEvents, }; }, },},// ... omit redundant code
For tool calling, it is essential to set maxSteps to a value greater than 1. In this case, we have added the execution within the function declaration to enable automatic execution on the server, and set maxSteps to 4 to allow the tool execution results to be sent back to the inference server for further processing.
You can see that the FetchCalendarEvents tool was called upon a user request to be executed automatically, then returned back to LLM to infer the final response.
2-2. Tools that require user interaction
Finally, let's look at the tools that are executed on the client side and require user interaction—for example, requiring confirmation dialogs or modals.
app/api/chat/route.ts
// ... omit redundant codecreateCalendarEvent: { description: `Creates a new calendar event.`, parameters: z.object({ summary: z .string() .default("New Event") .describe("Title of the event to be added"), startTime: z .string() .describe( "Date and time of the event, format should be 'yyyy-MM-dd HH:mm'" ), endTime: z .string() .describe( "Date and time of the event, format should be 'yyyy-MM-dd HH:mm'" ), }),},// ... omit redundant code
To achieve this, we have added the schema for the createCalendarEvent tool, and we need to modify the code on the client side. Unlike the automatically-executed case on the client, we don't use the onToolCall callback, but implement it as follows:
The tool invocation values are used to express on the UI that a tool is being called. When the user explicitly performs a specific action, the addToolResult() function is called to return the result of the function call. In the actual implementation, we plan to allow users to modify the event title and the date suggested by the LLM before actually adding it to the calendar. However, for testing purposes, let’s simply use a mock function for now that returns a response (i.e., "Event created!") when the button is pressed.
Step 3. Setting up Auth.js
Next, let’s make these tools work in practice using Auth.js and the Google Calendar API. We will implement the Google login functionality using Auth.js and integrate it with Google Calendar.
Setting up environment variables
First, you need to create a project in the Google Cloud Console and activate the necessary APIs. You can refer to this link for the setup instructions. Then, generate an OAuth 2.0 client ID to obtain the required authentication information. Based on this information, set up the following environment variables in the .env.local file:
We have completed the basic setup for Google login and Google Calendar API integration. In the next step, we will implement the functionality to actually create and retrieve calendar events based on these settings. This will allow our AI agent to directly interact with the user's Google Calendar.
4.1 Implementing the getCalendarInstance function
To use the Google Calendar API, we first need to implement a getCalendarInstance function that creates a calendar instance. This function is added to the lib/calendarApi.ts file:
lib/calendarApi.ts
"use server";import { calendar_v3, google } from "googleapis";import { OAuth2Client } from "google-auth-library";import { auth, EnrichedSession } from "@/auth";async function getGoogleCalendar(): Promise<calendar_v3.Calendar> { const session = (await auth()) as EnrichedSession; if (!session) throw new Error("Authentication session not found"); const oauth2Client = new OAuth2Client({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET, }); oauth2Client.setCredentials({ access_token: session.accessToken, }); return google.calendar({ version: "v3", auth: oauth2Client });}
The getCalendarInstance function returns a Google Calendar API instance by getting an access token from the user's session. This getCalendarInstance function will be used to implement other calendar functions
4.2 Implementing the createCalendarEvent tool
Let's start by implementing the createCalendarEvent function, which will create a new calendar event. This function will be added to the lib/calendarApi.ts file:
This function takes the title (summary), start time (startTime), and end time (endTime) of the event as parameters and creates a new event through the Google Calendar API.
4.3 Implementing the fetchCalendarEvents tool
Next, let’s implement the fetchCalendarEvents function, which fetches calendar events for a specific period of time. This function is also added to the lib/calendarApi.ts file:
lib/calendarApi.ts
// ... omit redundant codeinterface CalendarEvent { summary: string; start: string; end?: string; allDay: boolean;}export async function fetchCalendarEvents( startDate: string, endDate: string): Promise<CalendarEvent[]> { try { const calendar = await getGoogleCalendar(); const response = await calendar.events.list({ calendarId: "primary", timeMin: new Date(`${startDate} 00:00:00`).toISOString(), timeMax: new Date(`${endDate} 23:59:59`).toISOString(), timeZone: "UTC", singleEvents: true, orderBy: "startTime", }); return response.data.items?.map( ({ summary, start, end }: calendar_v3.Schema$Event) => { if (start?.dateTime && end?.dateTime) { return { summary, start: new Date(start.dateTime).toLocaleString(), end: new Date(end.dateTime).toLocaleString(), allDay: false, }; } return { summary, start: new Date(start?.date as string).toLocaleDateString(), allDay: true, }; } ) as CalendarEvent[]; } catch (error) { console.error("Error fetching calendar events:", error); throw new Error("Failed to fetch calendar events"); }}
This function takes a start date (startDate) and an end date (endDate) as parameters and retrieves all calendar events for that period.
4.3 Connecting Tools to the AI Agent
Now, we need to connect the tools we implemented to the AI agent. Let’s modify the api/chat/route.ts file as follows:
Now, our AI agent is fully integrated with Google Calendar and can create and retrieve events. Users can manage their schedules through a natural conversation with the AI agent, and it can accurately understand and execute user requests. These features greatly enhance the user experience and make schedule management more efficient.
Step 5. Enhancing Functionality with Friendli Built-in Tools
Friendli Built-in Tools make adding new features easier by providing readily-made, optimized tools that can be integrated quickly into AI agents. Unlike traditional methods, where developers build features from scratch, FriendliAI offers a library of pre-configured tools that reduce the development time. These tools seamlessly integrate and handle complex tasks like data retrieval or API interactions, simplifying feature expansion.
Let’s take a look at the following example:
api/chat/route.ts
// ... omit redundant code const result = await streamText({ model: friendli("meta-llama-3.1-70b-instruct", { tools: [ { type: "web:search", }, ], // To prevent too many events from being added at once parallelToolCalls: false, }), system: `Today is ${datetime}. You are a helpful calendar assistant.`, messages: convertToCoreMessages(messages), abortSignal: req.signal, maxSteps: 4,// ... omit redundant code
The code above can be used to easily add the web:search functionality to your AI agent. Now that we have both capabilities for the calendar and web searching, you can retrieve real-time information from the internet and utilize it to create schedules as follows:
prompt
Find Real Madrid match schedules and add it to my calendar.
Step 6. The Full Code
Now as the functionality of our AI agent is complete, let’s enhance the user interface as our final step. A visually appealing and easy-to-use UI can further enhance the user experience. We will use Tailwind CSS to style the chat interface and calendar event display. Now let's take a look at the full code with styling applied.
import { convertToCoreMessages, streamText } from "ai";import { friendli } from "@friendliai/ai-provider";import { auth } from "@/auth";import { z } from "zod";import { fetchCalendarEvents } from "@/lib/calendarApi";export async function POST(req: Request) { const { messages, datetime } = await req.json(); if (!messages) return new Response("Messages is required", { status: 400 }); if (!(await auth().then((session) => session?.user))) { return new Response("Unauthorized", { status: 401 }); } const result = await streamText({ model: friendli("meta-llama-3.1-70b-instruct", { tools: [ { type: "web:search", }, ], parallelToolCalls: false, //To prevent too many events from being added at once }), system: `Today is ${datetime}. You are a helpful calendar assistant.`, messages: convertToCoreMessages(messages), abortSignal: req.signal, maxSteps: 4, tools: { createCalendarEvent: { description: `Creates a new calendar event.`, parameters: z.object({ summary: z.string().describe("Title of the event to be added"), startTime: z .string() .describe( "Date and time of the event, format should be 'yyyy-MM-dd HH:mm'" ), endTime: z .string() .describe( "Date and time of the event, format should be 'yyyy-MM-dd HH:mm'" ), }), }, fetchCalendarEvents: { description: `Retrieves calendar events within a specified date range.`, parameters: z.object({ startDate: z .string() .describe("Start date of the search range (format: yyyy-MM-dd)"), endDate: z .string() .describe("End date of the search range (format: yyyy-MM-dd)"), }), execute: async ({ startDate, endDate }) => { try { const events = await fetchCalendarEvents(startDate, endDate); return JSON.stringify({ message: "Calendar events fetched successfully.", events, }); } catch { return JSON.stringify({ message: "Error fetching calendar events.", }); } }, }, }, }); return result.toDataStreamResponse();}
lib/calendarApi.ts
"use server";import { calendar_v3, google } from "googleapis";import { OAuth2Client } from "google-auth-library";import { auth, EnrichedSession } from "@/auth";async function getGoogleCalendar(): Promise<calendar_v3.Calendar> { const session = (await auth()) as EnrichedSession; if (!session) throw new Error("Authentication session not found"); const oauth2Client = new OAuth2Client({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET, }); oauth2Client.setCredentials({ access_token: session.accessToken, }); return google.calendar({ version: "v3", auth: oauth2Client });}interface CreateEventParams { summary: string; startTime: string; endTime: string;}export async function createCalendarEvent({ summary, startTime, endTime,}: CreateEventParams): Promise<calendar_v3.Schema$Event> { try { const calendar = await getGoogleCalendar(); const response = await calendar.events.insert({ calendarId: "primary", requestBody: { summary: `[AI] ${summary}`, start: { dateTime: new Date(startTime).toISOString(), timeZone: "UTC" }, end: { dateTime: new Date(endTime).toISOString(), timeZone: "UTC" }, }, }); return response.data; } catch (error) { console.error("Error creating calendar event:", error); throw new Error("Failed to create calendar event"); }}interface CalendarEvent { summary: string; start: string; end?: string; allDay: boolean;}export async function fetchCalendarEvents( startDate: string, endDate: string): Promise<CalendarEvent[]> { try { const calendar = await getGoogleCalendar(); const response = await calendar.events.list({ calendarId: "primary", timeMin: new Date(`${startDate} 00:00:00`).toISOString(), timeMax: new Date(`${endDate} 23:59:59`).toISOString(), timeZone: "UTC", singleEvents: true, orderBy: "startTime", }); return response.data.items?.map( ({ summary, start, end }: calendar_v3.Schema$Event) => { if (start?.dateTime && end?.dateTime) { return { summary, start: new Date(start.dateTime).toLocaleString(), end: new Date(end.dateTime).toLocaleString(), allDay: false, }; } return { summary, start: new Date(start?.date as string).toLocaleDateString(), allDay: true, }; } ) as CalendarEvent[]; } catch (error) { console.error("Error fetching calendar events:", error); throw new Error("Failed to fetch calendar events"); }}
Congratulations! You have successfully made a fully-functioning calendar manager AI agent with proper UI/UX. In our example, we have handled a case for managing Google Calendar events through a chat interaction-based AI agent. Likewise, this can be applied to a variety of cases where AI could help users perform various tasks and organize their materials more easily.
FriendliAI is a GPU-inference platform that lets you deploy, scale, and monitor large language and multimodal models in production, without owning or managing GPU infrastructure. We offer three things for your AI models: Unmatched speed, cost efficiency, and operational simplicity. Find out which product is the best fit for you in here.
How does FriendliAI help my business?
Our Friendli Inference allows you to squeeze more tokens-per-second out of every GPU. Because you need fewer GPUs to serve the same load, the true metric—tokens per dollar—comes out higher even if the hourly GPU rate looks similar on paper. View pricing
Which models and modalities are supported?
Over 380,000 text, vision, audio, and multi-modal models are deployable out of the box. You can also upload custom models or LoRA adapters. Explore models
Can I deploy models from Hugging Face directly?
Yes. A one-click deploy by selecting “Friendli Endpoints” on the Hugging Face Hub will take you to our model deployment page. The page provides an easy-to-use interface for setting up Friendli Dedicated Endpoints, a managed service for generative AI inference. Learn more about our Hugging Face partnership
Still have questions?
If you want a customized solution for that key issue that is slowing your growth, contact@friendli.ai or click Talk to an engineer — our engineers (not a bot) will reply within one business day.