(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; } }());
  • August 29, 2024
  • 17 min read

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

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

Have you ever wished for a personal assistant who could effortlessly manage your day-to-day tasks while helping you find relevant information online? Enter AI agents—intelligent software programs that use generative AI models to interact with users and autonomously perform tasks.

With Friendli Serverless Endpoints and Friendli Tools, you can build AI agents that integrate seamlessly with Google Calendar to optimize your productivity.

In this article, we’ll explore a real-world application of AI agents by learning how to code an AI agent that utilizes tools to connect with Google Calendar and conduct web searches. Stay tuned for part 2 of this blog, where we will build on this code to enhance the AI agent's UI/UX for a visual, interactive user experience on the web.

Figure 1: FriendliAI AI Agent Using Google Calendar- Easily manage your schedule by asking about, summarizing, or adding your events!

Highlights of this blog include…

Understanding AI Agents Through the 'Calendar AI Agent'

What is an AI agent in real-life examples?

AI agents are intelligent software programs that autonomously perform tasks using large language models (LLMs). These agents can manage activities like adding calendar events or conducting web searches, similar to virtual assistants like Siri. To demonstrate how these agents work in real life, we’ll walk you through a demo of a user-agent conversation, where the calendar AI agent is asked the following questions:

  1. Can you tell me my schedule for this week?
  2. Can you schedule a dinner date event for this Saturday at 6 pm?
  3. Can you search for the best seafood restaurants near the Hudson River that I can visit for my dinner date?

In Figure 2, the AI agent demonstrates its ability to handle a user query like, “Can you tell me my schedule for this week?” by utilizing tool calls to access the user's upcoming events. For example, the agent responds, “It looks like you have a lunch date scheduled for Thursday, August 29, from 1:00 PM to 2:00 PM.” This response is accurate, as confirmed by the user’s Google Calendar shown in Figure 1, which indeed lists this event in the last week of August.

Figure 2: FriendliAI AI Agent Responding to “Can you tell me my schedule for this week?” Using the 'authorizeCalendarAccess' and 'fetchCalendarEvents' Tool

What is function calling in LLMs?

Function calling in LLMs refers to the capability of the model to recognize when a user query requires a specific function call with what function arguments. For example, as shown in Figure 3, if a user asks, "Can you schedule a dinner date for this Saturday at 6 pm?", the LLM recognizes this as a request to add a new event to the Google Calendar. It further knows that it can complete these types of tasks using the createCalendarEvent function with ‘Dinner Date’ as the event title.

Figure 3: FriendliAI AI Agent Responding to “Can you schedule a dinner date event for this Saturday at 6 pm?” Using the ‘createCalendarEvent’ Tool

The AI agent powered by this LLM brain takes this information to call the createCalendarEvent function with the appropriate parameters. The result of the function call is a properly updated calendar, illustrated in Figure 4. Moreover, the result of the function call is informed back to the LLM brain which gives an adequate response.

Figure 4: Dinner Date Event Added to Google Calendar for Saturday, August 31, at 6:00 PM

This functionality also enables AI agents to perform complex tasks, such as searching for the best seafood restaurants near the Hudson River, as demonstrated in Figure 5, by interacting with external tools and APIs. Through function calling, AI agents extend their capabilities beyond simple conversation, allowing them to execute sophisticated tasks by leveraging these external resources.

Figure 5: FriendliAI AI Agent Responding to “Can you search for the best seafood restaurants near the Hudson River?” Using the ‘webSearch’ Tool

By understanding the concept of function calling in LLMs, you're already on the path to building powerful AI agents capable of executing complex tasks. If you're eager to apply this knowledge and create your own AI agent, our tutorial on building the calendar AI agent is the perfect place to start.

Function Calling AI Agents Tutorial Overview

List of Tools used for Web Search and Google Calendar Tools

Our AI agent uses the following four tools:

  • webSearch
  • authorizeCalendarAccess
  • fetchCalendarEvents
  • createCalendarEvent

webSearch Tool

  • Function description: This tool is designed for searching on DuckDuckGo for the desired query.
  • Function parameters: query: The query to search for.
  • Return value: The search results in a JSON format.
javascript
webSearch: tool({
  description: `This tool is designed for searching on DuckDuckGo for the desired query.\n`,
  parameters: z.object({
    query: z.string().describe('The query to search for.'),
  }),
  execute: async ({ query }) => {
    const searchResult = await search(query).then((res) => {
      return res;
    });
    if (searchResult.noResults) {
      return {
        message: `No results found for "${query}".`,
      };
    }
    const result = {
      message: `Here are the search results for "${query}"`,
      data: searchResult.results.map((result) => ({
        title: result.title,
        url: result.url,
        description: result.description.replace(/<\/?[^>]+(>|$)/g, ''),
      })),
    };
    return JSON.stringify(result);
  },
});

authorizeCalendarAccess Tool

  • Function description: This tool grants access to the user's calendar.
  • Function parameters: None.
  • Return value: “Successfully authorized access to your calendar.”
javascript
authorizeCalendarAccess: tool({
  description: `Grants the assistant access to the user's calendar.
    - Allows the assistant to view and manage calendar events.
    - Access expires at the end of the current session.
    - Used only when necessary to protect user privacy.`,
  parameters: z.object({}),
  execute: async () => {
    isAuthorized = true;
    const auth = await authorize();
    calendar = google.calendar({ version: 'v3', auth });
    return 'Successfully authorized access to your calendar.';
  },
});

fetchCalendarEvents Tool

  • Function description: This tool retrieves calendar events within a specified date range.
  • Function parameters: startDate: Start date of the search range (format: yyyy-MM-dd). endDate: End date of the search range (format: yyyy-MM-dd).
  • Return value: The searched events in a JSON format including each event’s summary, start date, and end date.
javascript
fetchCalendarEvnts: tool({
  description: `Retrieves calendar events within a specified date range.
    - Searches for all events between the start and end dates.
    - Displays the title, date, and time of each event.
    - Requires prior calendar access authorization.`,
  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 payload = {
      calendarId: 'primary',
      summary: 'Calendar Events',
      timeMin: new Date(`${startDate} 00:00`).toISOString(),
      timeMax: new Date(`${endDate} 23:59`).toISOString(),
      singleEvents: true,
      orderBy: 'startTime',
    };
    try {
      const calendarRes = await calendar.events.list(payload);
      const events = calendarRes.data.items
        ?.map((item) => {
          const { summary, start, end } = item;
          if (start.dateTime && end.dateTime) {
            return {
              summary,
              start: start,
              end: end,
              allDay: false,
            };
          } else {
            const startDate = new Date(start.date);
            const endDate = new Date(end.date);
            endDate.setSeconds(endDate.getSeconds() - 1);
            return {
              summary,
              start: format(startDate, 'yyyy-MM-dd'),
              end: format(endDate, 'yyyy-MM-dd'),
              allDay: true,
            };
          }
        })
        .filter((event) => event !== null);
      return JSON.stringify(events);
    } catch (error) {
      return JSON.stringify({
        message: 'Error fetching calendar events. Maybe you need to authorize the assistant to access your calendar.',
      });
    }
  },
});

createCalendarEvent Tool

  • Function description: This tool creates a new calendar event.
  • Function parameters: summary: Title of the event to be added. startTime: Date and time of the event, format should be 'yyyy-MM-dd HH:mm'. endTime: Date and time of the event, format should be 'yyyy-MM-dd HH:mm'.
  • Return value: The data of the newly added calendar event in a JSON format.
javascript
createCalendarEvent: tool({
  description: `Creates a new calendar event.
    - Adds a new event on the specified date and time.
    - Allows input of event title and optional description.
    - Requires prior calendar access authorization.`,
  parameters: z.object({
    summary: z.string().default('New Event').describe('Title of the event to be added'),
    startTime: z.string().default(format(new Date(), 'yyyy-MM-dd HH:mm')).describe("Date and time of the event, format should be 'yyyy-MM-dd HH:mm'"),
    endTime: z.string().default(format(new Date(), 'yyyy-MM-dd HH:mm')).describe("Date and time of the event, format should be 'yyyy-MM-dd HH:mm'"),
  }),
  execute: async ({ summary, startTime, endTime }) => {
    const payload = {
      calendarId: 'primary',
      requestBody: {
        summary: summary,
        start: {
          dateTime: new Date(startTime).toISOString(),
        },
        end: {
          dateTime: new Date(endTime).toISOString(),
        },
      },
    };
    try {
      const calendarRes = await calendar.events.insert(payload);
      return JSON.stringify(calendarRes.data);
    } catch (error) {
      return JSON.stringify({
        message: 'Error creating calendar event, Maybe you need to authorize the assistant to access your calendar.',
      });
    }
  },
});

Code Breakdown

Reading this section before diving into the full code is helpful because it provides a clear understanding of the main loop that contains the core source code. Learn the overall structure and functionality of the program before reading the detailed code in the Run the Full AI Agent Code section.

Figure 6: Main While Loop in the AI Agent Code

Here's a breakdown of how the main while loop operates:

  1. User Input Collection (line 41):
  • The loop begins by waiting for input from the user through the terminal.question("You: ") method. This input serves as the prompt or query that the AI agent will respond to.
  1. Text Generation (lines 43-49):
  • Once the input is received, the code calls the generateText function to generate a response.
  • The generateText function uses a model, specifically meta-llama-3.1-8b-instruct, with specific parameters like temperature, frequencyPenalty, and maxToolRoundtrips. These parameters control the creativity of the response, the likelihood of repeating phrases, and the maximum number of automatic roundtrips for tool calls, respectively.
  • The prompt key in the function call is set to the user's input, driving the model's response generation.
  1. System Prompting (lines 51-78):
  • Before generating a response, the AI model is guided by this system prompt, which instructs its behavior.
  1. Tool Definitions (lines 80-243):
  • A list of tools that are accessible and can be called by the model.
  • The previously explained webSearch, authorizeCalendarAccess, fetchCalendarEvents, and createCalendarEvent are included as the tools.
  1. Response Output (line 245):
  • After generating the response using the generateText function and possibly invoking some of the tools, the loop ends by printing the generated response with PrintRoundtrip(roundtrips);.

Setup Instructions

Now that we are done with the theoretical part, let’s get our hands on the actual stuff. Follow these steps to set up your AI agent Node.js project:

  1. Prepare a Friendli Personal Access Token

A Friendli Personal Access Token is needed to authorize your account when sending inference requests through Friendli Serverless Endpoints.

  1. Set up Google Cloud Platform Credentials for the Google Calendar API

A credentials.json file needs to be uploaded to the project directory to use the Google Calendar API.

  • Create a project in the Google Cloud Console.
  • Follow the steps in the Google Calendar API Python Quickstart to enable the Google Calendar API and download the credentials.json file.
  • Rename the credentials-xxxxx.json file to credentials.json and upload it to the current working directory.
  1. Initialize the Node.js Project
npm init
# Creates package.json file
# Add "type": "module" to the package.json file

By default, Node.js treats JavaScript files as CommonJS modules. To use the modern JavaScript module system (using ‘import’ and ‘export’ statements), you need to add a "type": "module" line to the package.json file.

  1. Install the Dependencies
npm install zod ai @ai-sdk/openai googleapis @google-cloud/local-auth duck-duck-scrape chalk date-fns
# Creates package-lock.json file

The Final Example package.json File:

{
  "name": "agent_demo",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "type": "module",
  "dependencies": {
    "@ai-sdk/openai": "^0.0.54",
    "@google-cloud/local-auth": "^3.0.1",
    "ai": "^3.3.19",
    "chalk": "^5.3.0",
    "date-fns": "^3.6.0",
    "duck-duck-scrape": "^2.2.5",
    "googleapis": "^143.0.0",
    "zod": "^3.23.8"
  }
}
  1. Configure Environment Variables

Export your Friendli Personal Access Token (PAT) as the FRIENDLI_TOKEN environment variable. In your actual environment, make sure to fill in your own PAT as the environment variable.

export FRIENDLI_TOKEN="{YOUR PERSONAL ACCESS TOKEN e.g. flp_XXX}"

Run the Full AI Agent Code

Toggle the collapse button below and paste the following code to your main.js file, to run the code using the command:

node main.js
View the full 'main.js' code:
javascript
import path from 'path';
import fs from 'fs/promises';
import process from 'process';
import * as readline from 'node:readline/promises';

// vercel ai sdk dependencies
import { z } from 'zod';
import { generateText, tool } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';

// for calendar features
import { google } from 'googleapis';
import { authenticate } from '@google-cloud/local-auth';

// for web search tool
import { search } from 'duck-duck-scrape';

// utility dependencies
import chalk from 'chalk';
import { format } from 'date-fns';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const friendliai = createOpenAI({
  apiKey: process.env.FRIENDLI_TOKEN,
  baseURL: 'https://inference.friendli.ai/v1',
});

// for google account authentication
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');

let calendar;
let isAuthorized = false;

while (1) {
  const input = await terminal.question('You: ');

  const { roundtrips } = await generateText({
    temperature: 0.2,
    frequencyPenalty: 0.5,
    maxToolRoundtrips: 4,
    prompt: input,

    model: friendliai('meta-llama-3.1-8b-instruct'),

    system:
      `You are an assistant that can help users with various tasks.\n
      You can use tools to assist users if needed, but using a tool is not neccessary.\n
      Please answer the user’s questions based on what you know.\n
      If you know the answer based on your knowledge, please do not use the 'webSearch' tool.\n
      Use the 'webSearch' tool only when you do not have accurate information to answer the question.\n
      When you use the 'webSearch' tool, do not inform users that you have used it.\n
      If the user requests help with users' calendar, you should assist them.\n
      If the user cancels, do not try again.\n` +
      `To assist with their calendar, please remember that today's date is ${new Date().toLocaleDateString('en-US', {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      })}.\n` +
      `Use 'createCalendarEvent' tool and 'fetchCalendarEvents' tool only when you assist with user's calendar
      Make sure that when using tools, only use one tool per turn.` +
      `createCalendarEvent and fetchCalendarEvents tools require authorization to access the user's calendar.
      CURRENT AUTHORIZATION STATUS: ${isAuthorized ? 'Authorized' : 'Unauthorized, Authentication is required prior to calling the calendar or schedule.'}
      IF AUTHORIZATION IS NEEDED, PLEASE USE 'authorizeCalendarAccess' TOOL TO AUTHORIZE THE ASSISTANT TO ACCESS THE USER'S CALENDAR.
      `,

    tools: {
      webSearch: tool({
        description: `This tool is designed for searching DuckDuckGo for the desired query.\n`,
        parameters: z.object({
          query: z.string().describe('The query to search for.'),
        }),
        execute: async ({ query }) => {
          const searchResult = await search(query).then((res) => {
            return res;
          });

          if (searchResult.noResults) {
            return {
              message: `No results found for "${query}".`,
            };
          }

          const result = {
            message: `Here are the search results for "${query}"`,
            data: searchResult.results.map((result) => ({
              title: result.title,
              url: result.url,
              description: result.description.replace(/<\/?[^>]+(>|$)/g, ''),
            })),
          };
          return JSON.stringify(result);
        },
      }),
      authorizeCalendarAccess: tool({
        description: `Grants the assistant access to the user's calendar.
          - Allows the assistant to view and manage calendar events.
          - Access expires at the end of the current session.
          - Used only when necessary to protect user privacy.`,
        parameters: z.object({}),
        execute: async () => {
          isAuthorized = true;

          const auth = await authorize();
          calendar = google.calendar({ version: 'v3', auth });

          return 'Successfully authorized access to your calendar.';
        },
      }),
      fetchCalendarEvents: tool({
        description: `Retrieves calendar events within a specified date range.
          - Searches for all events between the start and end dates.
          - Displays the title, date, and time of each event.
          - Requires prior calendar access authorization.`,
        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 payload = {
            calendarId: 'primary',
            summary: 'Calendar Events',
            timeMin: new Date(`${startDate} 00:00`).toISOString(),
            timeMax: new Date(`${endDate} 23:59`).toISOString(),
            singleEvents: true,
            orderBy: 'startTime',
          };

          try {
            const calendarRes = await calendar.events.list(payload);

            const events = calendarRes.data.items
              ?.map((item) => {
                const { summary, start, end } = item;

                if (start.dateTime && end.dateTime) {
                  return {
                    summary,
                    start: start,
                    end: end,
                    allDay: false,
                  };
                } else {
                  const startDate = new Date(start.date);
                  const endDate = new Date(end.date);
                  endDate.setSeconds(endDate.getSeconds() - 1);

                  return {
                    summary,
                    start: format(startDate, 'yyyy-MM-dd'),
                    end: format(endDate, 'yyyy-MM-dd'),
                    allDay: true,
                  };
                }
              })
              .filter((event) => event !== null);
            return JSON.stringify(events);
          } catch (error) {
            return JSON.stringify({
              message: 'Error fetching calendar events. Maybe you need to authorize the assistant to access your calendar.',
            });
          }
        },
      }),
      createCalendarEvent: tool({
        description: `Creates a new calendar event.
          - Adds a new event on the specified date and time.
          - Allows input of event title and optional description.
          - Requires prior calendar access authorization.`,
        parameters: z.object({
          summary: z.string().default('New Event').describe('Title of the event to be added'),
          startTime: z.string().default(format(new Date(), 'yyyy-MM-dd HH:mm')).describe("Date and time of the event, format should be 'yyyy-MM-dd HH:mm'"),
          endTime: z.string().default(format(new Date(), 'yyyy-MM-dd HH:mm')).describe("Date and time of the event, format should be 'yyyy-MM-dd HH:mm'"),
        }),
        execute: async ({ summary, startTime, endTime }) => {
          const payload = {
            calendarId: 'primary',
            requestBody: {
              summary: summary,
              start: {
                dateTime: new Date(startTime).toISOString(),
              },
              end: {
                dateTime: new Date(endTime).toISOString(),
              },
            },
          };

          try {
            const calendarRes = await calendar.events.insert(payload);
            return JSON.stringify(calendarRes.data);
          } catch (error) {
            return JSON.stringify({
              message: 'Error creating calendar event, Maybe you need to authorize the assistant to access your calendar.',
            });
          }
        },
      }),
    },
  });

  PrintRoundtrip(roundtrips);
}

// Helper function to print the roundtrips
function PrintRoundtrip(roundtrips) {
  roundtrips.forEach((roundtrip, idx) => {
    console.log(chalk.bold.blue(`\n===== ROUNDTRIP: ${idx + 1} =====`));

    if (roundtrip.toolCalls && roundtrip.toolCalls.length > 0) {
      console.log(chalk.yellow('\nTool Calls:'));
      roundtrip.toolCalls.forEach((toolCall, toolIdx) => {
        const matchingResult = roundtrip.toolResults.find((result) => result.toolCallId === toolCall.toolCallId);

        console.log(chalk.yellow(`  ${toolIdx + 1}. ${formatToolCallAndResult(toolCall, matchingResult)}`));
      });
    } else {
      console.log(chalk.yellow('\nNo Tool Calls'));
    }

    if (roundtrip.text) {
      console.log(chalk.magenta(`\nAI Response: ${roundtrip.text}`));
    } else {
      console.log(chalk.magenta('\nNo AI Response'));
    }
  });
}

function formatToolCallAndResult(toolCall, toolResult) {
  const formattedArgs =
    toolCall.args && Object.keys(toolCall.args).length > 0
      ? `Arguments:\n${Object.entries(toolCall.args)
          .map(([key, value]) => `${key}: ${value}`)
          .join('\n')}`
      : 'No arguments';

  const formattedResult = toolResult ? `Result: ${JSON.stringify(toolResult.result).length > 50 ? JSON.stringify(toolResult.result).substring(0, 50) + '...' : JSON.stringify(toolResult.result)}` : 'No corresponding result';

  return `ID: ${toolCall.toolCallId}\nName: ${toolCall.toolName}\n${formattedArgs}\n${formattedResult}`;
}

async function loadSavedCredentialsIfExist() {
  try {
    const content = await fs.readFile(TOKEN_PATH);
    const credentials = JSON.parse(content);
    return google.auth.fromJSON(credentials);
  } catch (err) {
    return null;
  }
}

async function saveCredentials(client) {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content);
  const key = keys.installed || keys.web;
  const payload = JSON.stringify({
    type: 'authorized_user',
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
  });
  await fs.writeFile(TOKEN_PATH, payload);
}

async function authorize() {
  let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }
  client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });
  if (client.credentials) {
    await saveCredentials(client);
  }
  return client;
}

Final Thoughts

In this tutorial, we have been able to develop a simple AI agent for Google Calendar using the Vercel AI SDK and the Friendli Serverless Endpoints. The project can be easily deployed and operated within minutes with help from Vercel and FriendliAI. Furthermore, if you wish to use custom or fine-tuned generative AI models for your applications, you can also build your application using Friendli Dedicated Endpoints, which lets users upload their own models on the GPUs of their choice.

Congratulations on completing this tutorial. We hope that you found it informative and useful. We will soon come back with part 2 of this blog, which handles the UI and the visual parts of the application.

Happy coding!


Written by

FriendliAI logo

FriendliAI Tech & Research


Share