(function() { var utmInheritingDomain = "appstore.com", utmRegExp = /(&|\?)utm_[A-Za-z]+=[A-Za-z0-9]+/gi, links = document.getElementsByTagName("a"), utms = [ "utm_medium={{URL - utm_medium}}", "utm_source={{URL - utm_source}}", "utm_campaign={{URL - utm_campaign}}" ]; for (var index = 0; index < links.length; index += 1) { var tempLink = links[index].href, tempParts; if (tempLink.indexOf(utmInheritingDomain) > 0) { tempLink = tempLink.replace(utmRegExp, ""); tempParts = tempLink.split("#"); if (tempParts[0].indexOf("?") < 0 ) { tempParts[0] += "?" + utms.join("&"); } else { tempParts[0] += "&" + utms.join("&"); } tempLink = tempParts.join("#"); } links[index].href = tempLink; } }());
  • October 15, 2024
  • 17 min read

Building an AI Agent for Google Calendar - Part 2/2

Building an AI Agent for Google Calendar - Part 2/2 thumbnail

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:

  1. Building a Basic Chat UI
  2. Understanding Tools Usage Patterns in Vercel AI SDK
  3. Setting up Auth.js
  4. Integrating the Google Calendar
  5. Enhancing Functionality with Friendli Built-in Tools
  6. 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.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()

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:

  1. Automatically executed server-side tools
  2. Automatically executed client-side tools
  3. 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.local
FRIENDLI_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.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
middleware.ts
export { 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.tsx
import { 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.ts
import { 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.tsx
import { 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:

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.

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.ts
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",
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:

Visit https://suite.friendli.ai to begin your journey into the world of high-performance LLM serving with the Friendli Engine!


Written by

FriendliAI logo

FriendliAI Tech & Research


Share