- October 15, 2024
- 7 min read
Building an AI Agent for Google Calendar - Part 2/2
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:
- Building a Basic Chat UI
- Understanding Tools Usage Patterns in Vercel AI SDK
- Setting up Auth.js
- Integrating the Google Calendar
- Enhancing Functionality with Friendli Built-in Tools
- The Full Code
Step 1. Building a Basic Chat UI
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:
shell# nextjs project setup pnpx create-next-app@latest calendar-chatbot cd calendar-chatbot #set friendli token echo "FRIENDLI_TOKEN={your_friendli_token_here}" >> .env.local # install dependency pnpm i @friendliai/ai-provider zod ai pnpm i google-auth-library googleapis next-auth@beta
Next, let's create a basic chat UI by modifying the app/page.tsx
file as follows:
app/page.tsx"use client"; import { useChat } from "ai/react"; export default function Chat() { const { messages, input, handleInputChange, handleSubmit, isLoading, error } = useChat({ body: { datetime: new Date().toLocaleString(), }, }); const isWaiting = isLoading && messages[messages.length - 1]?.role !== "assistant"; return ( <> {messages.map((message) => ( <div key={message.id} className="w-2/5"> {`${message.role}: ${message.content}`}
Then, let’s create a app/api/chat/route.ts
file to make the useChat
hook work:
app/api/chat/route.tsimport { 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.tsimport { 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()
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 code maxSteps: 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`,
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 code createCalendarEvent: { 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:
app/page.tsx// ... omit redundant code const { messages, input, handleInputChange, handleSubmit, isLoading, error, addToolResult, } = useChat({ // ... omit redundant code {`${message.role}: ${message.content}`} {message.toolInvocations?.map((toolInvocation) => { const toolcall = `${toolInvocation.toolName}: ${JSON.stringify( toolInvocation.args )} -> ${toolInvocation.state}`;
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:
.env.localFRIENDLI_TOKEN={your_friendli_token_here} AUTH_GOOGLE_ID=xxxxxxxxx.apps.googleusercontent.com AUTH_GOOGLE_SECRET=GOCSPX-xxxxxxxxxxxx AUTH_SECRET=your-nextauth-secret-(Random-value)
These environment variables will be used when integrating Auth.js and Google Calendar API.
Adding the code
app/api/auth/[...nextauth]/route.tsimport { handlers } from "@/auth"; export const { GET, POST } = handlers;
middleware.tsexport { auth as middleware } from "@/auth";
auth.ts// Warning, not a good implementation in terms of reliability. // Don't use it in production. import Google from "next-auth/providers/google"; import NextAuth, { type Session } from "next-auth"; export interface EnrichedSession extends Session { accessToken: string; } export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ Google({ authorization: { params: { scope: "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/userinfo.email", }, }, }),
api/chat/route.ts// ... omit redundant code 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({ // ... omit redundant code
layout.tsximport { auth, signIn } from "@/auth"; import { SessionProvider } from "next-auth/react"; import "./globals.css"; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const session = await auth(); return ( <html lang="en"> <body className="flex flex-col space-y-2 items-center justify-center h-screen"> <SessionProvider refetchOnWindowFocus> {session?.user ? ( children ) : ( <form action={async () => {
Step 4. Integrating the Google Calendar
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:
lib/calendarApi.ts// ... omit redundant code 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" },
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 code 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",
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:
api/chat/route.tsimport { fetchCalendarEvents } from "@/lib/calendarApi"; // ... omit redundant code maxSteps: 4, tools: { fetchCalendarEvents: { // ... omit redundant code execute: async ({ startDate, endDate }) => { try { const events = await fetchCalendarEvents(startDate, endDate); return JSON.stringify({ message: "Calendar events fetched successfully.", events, }); } catch (error) { return JSON.stringify({ message: "Error fetching calendar events.",
page.tsximport { createCalendarEvent } from "@/lib/calendarApi"; // ... omit redundant code switch (toolInvocation.toolName) { case "createCalendarEvent": return ( <div key={toolInvocation.toolCallId} className="mt-2 p-2 bg-secondary/20 rounded" > <p className="text-sm text-muted-foreground mb-2"> {JSON.stringify(toolInvocation.args)} </p> <button className="bg-primary text-white px-3 py-1 rounded" onClick={() => { createCalendarEvent({ summary: toolInvocation.args.summary, startTime: toolInvocation.args.startTime,
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 Build-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:
promptFind 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.
page.tsx"use client"; import React from "react"; import { useChat } from "ai/react"; import { createCalendarEvent } from "@/lib/calendarApi"; import { signOut } from "next-auth/react"; export default function Page() { const { messages, input, handleInputChange, handleSubmit, isLoading, error, addToolResult, } = useChat({ body: { datetime: new Date().toLocaleString(), },
api/chat/route.tsimport { 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",
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, });
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.
You can check out the working demo on this link: https://calendar-agent.friendli.ai/
Behind the curtains, Friendli Engine powers the AI agent with the AI capabilities, through its cost-effective throughput-oriented AI execution.
Ready to unleash the power of your LLM? Experience Friendli Engine's performance! We offer three options to suit your preferences:
- Friendli Dedicated Endpoints: Run any custom generative AI models on dedicated GPUs in autopilot.
- Friendli Serverless Endpoints: No setup required, simply call our APIs and let us handle the rest.
- Friendli Container: Deploy the engine on your own infrastructure for ultimate control.
Visit https://suite.friendli.ai to begin your journey into the world of high-performance LLM serving with the Friendli Engine!
Written by
FriendliAI Tech & Research
Share