Flexible Labeling for Gmail using Gemini Pro API with Google Apps Script Part 2

Gists

Description

I have published “Flexible Labeling for Gmail using Gemini Pro API with Google Apps Script” on December 19, 2023. Today, I published “Categorization using Gemini Pro API with Google Apps Script”.

In this report, as part 2, I would like to introduce 2 sample scripts for flexible labeling for Gmail using the semantic search and the function calling of Gemini Pro API with Google Apps Script.

Usage

In order to test this script, please do the following flow.

1. Create an API key

Please access https://makersuite.google.com/app/apikey and create your API key. At that time, please enable Generative Language API at the API console. This API key is used for this sample script.

This official document can be also seen. Ref.

2. Create a Google Apps Script project

Please create a standalone Google Apps Script project. Of course, this script can be also used with the container-bound script.

And, please open the script editor of the Google Apps Script project.

In the following 2 scripts, if you run this script for the first time or prev is undefined, emails from your inbox in the past 1 hour are retrieved. After 2nd run, the labels are added to the new messages. This is the same with this post.

Flexible labeling for Gmail using semantic search

In this script, the labels are added to new messages using the semantic search. You can see the details of the semantic search using Google Apps Script at here.

Please set your API and labelObj to your situation.

const apiKey = "###"; // Please set your API key.

/**
 * ### Description
 * Calculate cosine similarity from 2 arrays.
 *
 * ### Sample script
 * ```
 * const array1 = [1, 2, 3, 4, 5];
 * const array2 = [3, 4, 5, 6, 7];
 * const res2 = UtlApp.cosineSimilarity(array1, array2); // false
 * ```
 *
 * @param {Array} array1 1-dimensional array.
 * @param {Array} array2 1-dimensional array.
 * @return {Number} Calculated result of cosine similarity.
 */
function cosineSimilarity_(array1, array2) {
  const dotProduct = array1.reduce((t, e, i) => (t += e * array2[i]), 0);
  const magnitudes = [array1, array2]
    .map((e) => Math.sqrt(e.reduce((t, f) => (t += f * f), 0)))
    .reduce((t, f) => (t *= f), 1);
  return dotProduct / magnitudes;
}

/**
 * ### Description
 * Request "Method: models.embedContent" of Gemini Pro API.
 *
 * @param {Object} requests Request body for Method: models.batchEmbedContents.
 * @return {Number} Embedding generated from the input texts.
 */
function getEmbedding_(requests) {
  const url = `https://generativelanguage.googleapis.com/v1/models/embedding-001:batchEmbedContents?key=${apiKey}`;
  const options = {
    payload: JSON.stringify({ requests }),
    contentType: "application/json",
  };
  const res = UrlFetchApp.fetch(url, options);
  return (obj = JSON.parse(res.getContentText()));
}

/**
 * ### Description
 * Get category from a search text and giving categories using the semantic search.
 *
 * @param {Array} searchTexts 1-dimensional array including the search texts.
 * @param {Array} categories 1-dimensional array including categories.
 * @return {String} Return a selected category related to the search text from the categories.
 */
function getCategory2_(searchTexts, categories) {
  const r1 = searchTexts.map((searchText) => ({
    model: "models/embedding-001",
    content: { parts: [{ text: searchText }] },
    taskType: "RETRIEVAL_QUERY",
  }));
  const r2 = categories.map(({ category, description }) => ({
    model: "models/embedding-001",
    content: { parts: [{ text: category }, { text: description }] },
    taskType: "RETRIEVAL_DOCUMENT",
    title: category,
  }));
  const obj = getEmbedding_([...r1, ...r2]);
  const embeddingsAr = obj.embeddings.map((e) => e.values);
  const embeddingsR1 = embeddingsAr.splice(0, r1.length);
  const embeddingsR2 = embeddingsAr;
  return embeddingsR1.map((e, i) => {
    const temp = embeddingsR2
      .map((ar, j) => ({
        cs: cosineSimilarity_(ar, e),
        searchText: searchTexts[i],
        category: categories[j].category,
      }))
      .sort((a, b) => (a.cs < b.cs ? 1 : -1));
    return { searchText: temp[0].searchText, category: temp[0].category };
  });
}

/**
 * ### Description
 * Get threads of Gmail.
 *
 * @return {Object} Return an object including threads and messages.
 */
function getNewMessages_() {
  const now = new Date();
  const nowTime = now.getTime();
  const p = PropertiesService.getScriptProperties();
  let prev = p.getProperty("prev");
  if (!prev) {
    prev = nowTime - 60 * 60 * 1000; // If you run this script for the first time or prev is undefined, emails from your inbox in the past 1 hour are retrieved.
  }
  const threads = GmailApp.getInboxThreads().filter(
    (t) => t.getLastMessageDate().getTime() > prev
  );
  if (threads.length == 0) return [];
  const res = threads.map((thread) => {
    const lastMessageDate = thread.getLastMessageDate().getTime();
    const lastMessage = thread
      .getMessages()
      .find((m) => m.getDate().getTime() == lastMessageDate);
    return { thread, text: lastMessage.getPlainBody() };
  });
  p.setProperty("prev", nowTime.toString());
  return res;
}

// Please run this function.
function labeling2() {
  // Please set the label name on Gmail and the description of the label.
  const labelObj = [
    {
      label: "academic",
      description:
        "Related to university, laboratory, research, education, and etc.",
    },
    {
      label: "commission",
      description:
        "Related to a comission, a request, a job offer, orders, and etc.",
    },
    {
      label: "advertisement",
      description: "Related to advertisement, new product, and etc.",
    },
    { label: "INBOX", description: "Others" },
  ];

  const ar = getNewMessages_();
  if (ar.length == 0) return;
  const searchTexts = ar.map(({ text }) => text);
  const categories = labelObj.map(({ label, description }) => ({
    category: label,
    description,
  }));
  const res = getCategory2_(searchTexts, categories);
  const rr = res.map((e, i) => ({
    category: e.category,
    thread: ar[i].thread,
  }));
  rr.forEach(({ thread, category }) => {
    if (category && category != "INBOX") {
      console.log(
        `Mail subject: ${thread
          .getMessages()
          .pop()
          .getSubject()}, labeled to "${category}"`
      );
      thread.moveToArchive();
      thread.addLabel(GmailApp.getUserLabelByName(category));
    }
  });
}

Flexible labeling for Gmail using function calling

In this script, the labels are added to new messages using the function calling of Gemini Pro API. Ref

Please set your API and labelObj to your situation.

const apiKey = "###"; // Please set your API key.

/**
 * ### Description
 * Get a category from a search text and give categories using the function calling of "Method: models.generateContent".
 *
 * @param {String} searchText Search text.
 * @param {Array} categories 1-dimensional array including categories.
 * @return {String} Return a selected category related to the search text from the categories.
 */
function getCategory1_(searchText, categories) {
  const function_declarations = categories.map(({ category, description }) => ({
    name: category,
    description: description,
  }));
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`;
  const q = `Read the following text and select the related category.`;
  const payload = {
    contents: [{ parts: [{ text: q }, { text: searchText }] }],
    tools: [{ function_declarations }],
  };
  const options = {
    payload: JSON.stringify(payload),
    contentType: "application/json",
  };
  const res = UrlFetchApp.fetch(url, options);
  const obj = JSON.parse(res.getContentText());
  if (
    obj.candidates.length > 0 &&
    obj.candidates[0].content.parts.length > 0 &&
    obj.candidates[0].content.parts[0].functionCall
  ) {
    if (
      !categories.some(
        ({ category }) =>
          category == obj.candidates[0].content.parts[0].functionCall.name
      )
    ) {
      return "other";
    }
    return obj.candidates[0].content.parts[0].functionCall.name;
  }
  return "No response.";
}

/**
 * ### Description
 * Get threads of Gmail.
 *
 * @return {Object} Return an object including threads and messages.
 */
function getNewMessages_() {
  const now = new Date();
  const nowTime = now.getTime();
  const p = PropertiesService.getScriptProperties();
  let prev = p.getProperty("prev");
  if (!prev) {
    prev = nowTime - 60 * 60 * 1000; // If you run this script for the first time or prev is undefined, emails from your inbox in the past 1 hour are retrieved.
  }
  const threads = GmailApp.getInboxThreads().filter(
    (t) => t.getLastMessageDate().getTime() > prev
  );
  if (threads.length == 0) return [];
  const res = threads.map((thread) => {
    const lastMessageDate = thread.getLastMessageDate().getTime();
    const lastMessage = thread
      .getMessages()
      .find((m) => m.getDate().getTime() == lastMessageDate);
    return { thread, text: lastMessage.getPlainBody() };
  });
  p.setProperty("prev", nowTime.toString());
  return res;
}

// Please run this function.
function labeling1() {
  // Please set the label name on Gmail and the description of the label.
  const labelObj = [
    {
      label: "academic",
      description:
        "Related to university, laboratory, research, education, and etc.",
    },
    {
      label: "commission",
      description:
        "Related to a comission, a request, a job offer, orders, and etc.",
    },
    {
      label: "advertisement",
      description: "Related to advertisement, new product, and etc.",
    },
    { label: "INBOX", description: "Others" },
  ];

  const ar = getNewMessages_();
  if (ar.length == 0) return;
  const categories = labelObj.map(({ label, description }) => ({
    category: label,
    description,
  }));
  const searchTexts = ar.map(({ text }) => text);
  const res = searchTexts.map((searchText, i) => ({
    category: getCategory1_(searchText, categories),
    thread: ar[i],
  }));
  res.forEach(({ thread: { thread }, category }) => {
    if (category && category != "other") {
      console.log(
        `Mail subject: ${thread
          .getMessages()
          .pop()
          .getSubject()}, labeled to "${category}"`
      );
      thread.moveToArchive();
      thread.addLabel(GmailApp.getUserLabelByName(category));
    }
  });
}

Note

  • In the case of “Flexible labeling for Gmail using the semantic search”, the batchRequest can be used. So, the process cost of this is much lower than that of “Flexible labeling for Gmail using function calling”. I believe that this situation will be resolved by the future update.
  • If in the case that the correct categorization was small, modifying the description might be resolved.

 Share!