Gemini 및 Vertex AI로 Gmail 메시지 분석 및 라벨 지정

이 솔루션은 Vertex AI와 Gemini를 사용하여 Gmail 메일을 분석하고 감정에 따라 라벨을 지정합니다.

코딩 수준: 중급
기간: 30분
프로젝트 유형: Google Workspace 부가기능

  • 사이드바에서 Gmail을 확장하는 Google Workspace 부가기능
    그림 1: 감정 분석 부가기능은 Gmail에 사이드바를 표시하며, 사용자는 Gemini에 감정을 기반으로 메시지를 분석하고 라벨을 적용하도록 요청할 수 있습니다.
  • 중립적인 감정을 가진 Gmail 메일
    그림 2: 부가기능이 Gmail 메일에 중립적인 어조 😐 라벨을 지정합니다.
  • 기쁜 감정을 담은 Gmail 메일
    그림 3: 부가기능이 Gmail 메시지에 HAPPY TONE 😊 라벨을 지정합니다.
  • 불쾌한 감정이 담긴 Gmail 메일
    그림 4: 부가기능이 Gmail 메일에 불쾌한 어조 😡 라벨을 지정합니다.

목표

  • 솔루션의 기능을 이해합니다.
  • 솔루션 내에서 Google 서비스가 하는 역할을 이해합니다.
  • 환경을 설정합니다.
  • Google Apps Script 프로젝트를 설정합니다.
  • 스크립트를 실행합니다.

이 솔루션 정보

감정 분석 Google Workspace 부가기능 스크린샷

이 솔루션은 Gmail 메일의 감정을 기반으로 라벨을 적용하는 Google Workspace 부가기능입니다. 메시지 콘텐츠를 분석하기 위해 부가기능은 Vertex AI를 사용하여 Gemini 2.5 Flash 모델에 프롬프트를 표시하고 다음 감정 중 하나를 반환합니다.

  • 긍정적
  • 음성
  • 보통

Gemini의 대답을 통해 부가기능은 메일에 해당하는 Gmail 라벨을 적용합니다.

Vertex AI API에 대한 요청을 제한하기 위해 이 부가기능은 Gmail 사용자 받은편지함의 가장 최근 메시지 10개만 분석하고 라벨을 적용합니다. 할당량 및 한도에 대해 자세히 알아보려면 Vertex AI 문서를 참고하세요.

작동 방식

이 솔루션은 Google Apps Script로 빌드되며 다음 Google 서비스 및 제품을 사용합니다.

  • Vertex AI API: Gemini 2.5 Flash 모델에 Gmail 메시지의 콘텐츠를 분석하고 감정을 식별하도록 프롬프트를 표시합니다.
  • Apps Script 서비스:

    • Gmail 서비스: Gmail 메일을 가져와 라벨을 적용합니다. 선택적으로 부가기능 테스트를 위한 샘플 메시지를 만듭니다.
    • 카드 서비스 - Gmail의 사이드바로 표시되는 부가기능의 사용자 인터페이스를 만듭니다.
    • URL 가져오기 서비스: 감정 분석을 위해 Vertex AI API에 연결됩니다.
    • 스크립트 서비스: Vertex AI API를 호출하기 위해 getOAuthToken 메서드를 사용하여 부가기능의 OAuth 2.0 액세스 토큰을 가져옵니다.

기본 요건

환경 설정

이 섹션에서는 Google Cloud 콘솔 및 Apps Script에서 환경을 구성하고 설정하는 방법을 설명합니다.

Google Cloud 콘솔에서 클라우드 프로젝트 구성

이 섹션에서는 Cloud 프로젝트에서 Vertex AI API를 사용 설정하고 OAuth 동의 화면을 구성하는 방법을 보여줍니다.

Vertex AI API 사용 설정

  1. Google Cloud 콘솔에서 Google Cloud 프로젝트를 열고 Vertex AI API를 사용 설정합니다.

    API 사용 설정

  2. 올바른 Cloud 프로젝트에서 API를 사용 설정하고 있는지 확인한 후 다음을 클릭합니다.

  3. 올바른 API를 사용 설정했는지 확인한 후 사용 설정을 클릭합니다.

OAuth 동의 화면 구성

Google Workspace 부가기능에는 동의 화면 구성이 필요합니다. 부가기능의 OAuth 동의 화면을 구성하면 Google에서 사용자에게 표시하는 내용이 정의됩니다.

  1. Google Cloud 콘솔에서 메뉴 > > 브랜딩으로 이동합니다.

    브랜딩으로 이동

  2. 을 이미 구성한 경우 브랜딩, 잠재고객, 데이터 액세스에서 다음 OAuth 동의 화면 설정을 구성할 수 있습니다. 아직 구성되지 않음이라는 메시지가 표시되면 시작하기를 클릭합니다.
    1. 앱 정보앱 이름에 앱 이름을 입력합니다.
    2. 사용자 지원 이메일에서 사용자가 동의에 대해 문의할 수 있는 지원 이메일 주소를 선택합니다.
    3. 다음을 클릭합니다.
    4. 시청자층에서 내부를 선택합니다.
    5. 다음을 클릭합니다.
    6. 연락처 정보에서 프로젝트 변경사항에 대한 알림을 받을 수 있는 이메일 주소를 입력합니다.
    7. 다음을 클릭합니다.
    8. 완료에서 Google API 서비스 사용자 데이터 정책을 검토하고 동의하는 경우 Google API 서비스: 사용자 데이터 정책에 동의합니다를 선택합니다.
    9. 계속을 클릭합니다.
    10. 만들기를 클릭합니다.
  3. 지금은 범위를 추가하지 않아도 됩니다. 향후 Google Workspace 조직 외부에서 사용할 앱을 만들 때는 사용자 유형외부로 변경해야 합니다. 그런 다음 앱에 필요한 승인 범위를 추가합니다. 자세한 내용은 OAuth 동의 구성 가이드를 참고하세요.

Apps Script 프로젝트 만들기 및 설정

부가기능용 Apps Script 프로젝트를 만들고 설정하려면 다음 단계를 완료하세요.

  1. 다음 버튼을 클릭하여 Gmail Sentiment Analysis with Gemini and Vertex AI(Gmail의 감정 분석(Gemini 및 Vertex AI 사용)) Apps Script 프로젝트를 엽니다.
    Apps Script 프로젝트 열기

  2. 개요 를 클릭합니다.

  3. 개요 페이지에서 사본 만들기 사본 만들기 아이콘를 클릭합니다.

  4. Cloud 프로젝트 번호를 가져옵니다.

    1. Google Cloud 콘솔에서 메뉴 > IAM 및 관리자 > 설정으로 이동합니다.

      IAM & 관리자 설정으로 이동

    2. 프로젝트 번호 필드에서 값을 복사합니다.
  5. Cloud 프로젝트를 Apps Script 프로젝트와 연결합니다.

    1. 복사된 Apps Script 프로젝트에서 프로젝트 설정 프로젝트 설정 아이콘을 클릭합니다.
    2. Google Cloud Platform(GCP) 프로젝트에서 프로젝트 변경을 클릭합니다.
    3. GCP 프로젝트 번호에 Cloud 프로젝트 번호를 붙여넣습니다.
    4. 프로젝트 설정을 클릭합니다.

부가기능 테스트

부가기능을 사용해 보려면 테스트 배포를 설치한 다음 Gmail에서 부가기능을 엽니다.

  1. Apps Script 테스트 배포를 만들고 설치합니다.
    1. 복사된 Apps Script 프로젝트에서 편집기 를 클릭합니다.
    2. Code.gs 파일을 열고 실행을 클릭합니다. 메시지가 표시되면 스크립트를 승인합니다.
    3. 배포 > 배포 테스트를 클릭합니다.
    4. 설치 > 완료를 클릭합니다.
  2. Gmail을 엽니다.

    Gmail로 이동

  3. 오른쪽 사이드바에서 부가기능 감정 분석을 엽니다.

  4. 메시지가 표시되면 부가기능을 승인합니다.

  5. 선택사항: 부가기능으로 테스트할 메시지를 만들려면 샘플 이메일 생성을 클릭합니다. 받은편지함에 메시지 3개가 표시됩니다. 표시되지 않으면 페이지를 새로고침하세요.

  6. 라벨을 추가하려면 이메일 분석을 클릭합니다.

이 부가기능은 받은편지함의 최신 메시지 10개를 검토한 다음 메시지 콘텐츠에 따라 다음 라벨 중 하나를 적용합니다.

  • 밝은 어조 😊
  • 중립적인 어조 😐
  • 화난 어조 😡

코드 검토

이 솔루션의 Apps Script 코드를 검토합니다.

소스 코드 보기

Code.gs

gmail-sentiment-analysis/Code.gs
/*
Copyright 2024 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Triggered when the add-on is opened from the Gmail homepage.
 *
 * @param {Object} e - The event object.
 * @returns {Card} - The homepage card.
 */
function onHomepageTrigger(e) {
  return buildHomepageCard();
}

Cards.gs

gmail-sentiment-analysis/Cards.gs
/*
Copyright 2024-2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Builds the main card displayed on the Gmail homepage.
 *
 * @returns {Card} - The homepage card.
 */
function buildHomepageCard() {
  // Create a new card builder
  const cardBuilder = CardService.newCardBuilder();

  // Create a card header
  const cardHeader = CardService.newCardHeader();
  cardHeader.setImageUrl('https://fonts.gstatic.com/s/i/googlematerialicons/mail/v6/black-24dp/1x/gm_mail_black_24dp.png');
  cardHeader.setImageStyle(CardService.ImageStyle.CIRCLE);
  cardHeader.setTitle("Analyze your Gmail");

  // Add the header to the card
  cardBuilder.setHeader(cardHeader);

  // Create a card section
  const cardSection = CardService.newCardSection();

  // Create buttons for generating sample emails and analyzing sentiment
  const buttonSet = CardService.newButtonSet();

  // Create "Generate sample emails" button
  const generateButton = createFilledButton('Generate sample emails', 'generateSampleEmails', '#34A853');
  buttonSet.addButton(generateButton);

  // Create "Analyze emails" button
  const analyzeButton = createFilledButton('Analyze emails', 'analyzeSentiment', '#FF0000');
  buttonSet.addButton(analyzeButton);

  // Add the button set to the section
  cardSection.addWidget(buttonSet);

  // Add the section to the card
  cardBuilder.addSection(cardSection);

  // Build and return the card
  return cardBuilder.build();
}

/**
 * Creates a filled text button with the specified text, function, and color.
 *
 * @param {string} text - The text to display on the button.
 * @param {string} functionName - The name of the function to call when the button is clicked.
 * @param {string} color - The background color of the button.
 * @returns {TextButton} - The created text button.
 */
function createFilledButton(text, functionName, color) {
  // Create a new text button
  const textButton = CardService.newTextButton();

  // Set the button text
  textButton.setText(text);

  // Set the action to perform when the button is clicked
  const action = CardService.newAction();
  action.setFunctionName(functionName);
  textButton.setOnClickAction(action);

  // Set the button style to filled
  textButton.setTextButtonStyle(CardService.TextButtonStyle.FILLED);

  // Set the background color
  textButton.setBackgroundColor(color);

  return textButton;
}

/**
 * Creates a notification response with the specified text.
 *
 * @param {string} notificationText - The text to display in the notification.
 * @returns {ActionResponse} - The created action response.
 */
function buildNotificationResponse(notificationText) {
  // Create a new notification
  const notification = CardService.newNotification();
  notification.setText(notificationText);

  // Create a new action response builder
  const actionResponseBuilder = CardService.newActionResponseBuilder();

  // Set the notification for the action response
  actionResponseBuilder.setNotification(notification);

  // Build and return the action response
  return actionResponseBuilder.build();
}

Gmail.gs

gmail-sentiment-analysis/Gmail.gs
/*
Copyright 2024-2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Analyzes the sentiment of the first 10 threads in the inbox
 * and labels them accordingly.
 *
 * @returns {ActionResponse} - A notification confirming completion.
 */
function analyzeSentiment() {
  // Analyze and label emails
  analyzeAndLabelEmailSentiment();

  // Return a notification
  return buildNotificationResponse("Successfully completed sentiment analysis");
}

/**
 * Analyzes the sentiment of emails and applies appropriate labels.
 */
function analyzeAndLabelEmailSentiment() {
  // Define label names
  const labelNames = ["HAPPY TONE 😊", "NEUTRAL TONE 😐", "UPSET TONE 😡"];

  // Get or create labels for each sentiment
  const positiveLabel = GmailApp.getUserLabelByName(labelNames[0]) || GmailApp.createLabel(labelNames[0]);
  const neutralLabel = GmailApp.getUserLabelByName(labelNames[1]) || GmailApp.createLabel(labelNames[1]);
  const negativeLabel = GmailApp.getUserLabelByName(labelNames[2]) || GmailApp.createLabel(labelNames[2]);

  // Get the first 10 threads in the inbox
  const threads = GmailApp.getInboxThreads(0, 10);

  // Iterate through each thread
  for (const thread of threads) {
    // Iterate through each message in the thread
    const messages = thread.getMessages();
    for (const message of messages) {
      // Get the plain text body of the message
      const emailBody = message.getPlainBody();

      // Analyze the sentiment of the email body
      const sentiment = processSentiment(emailBody);

      // Apply the appropriate label based on the sentiment
      if (sentiment === 'positive') {
        thread.addLabel(positiveLabel);
      } else if (sentiment === 'neutral') {
        thread.addLabel(neutralLabel);
      } else if (sentiment === 'negative') {
        thread.addLabel(negativeLabel);
      }
    }
  }
}

/**
 * Generates sample emails for testing the sentiment analysis.
 *
 * @returns {ActionResponse} - A notification confirming email generation.
 */
function generateSampleEmails() {
  // Get the current user's email address
  const userEmail = Session.getActiveUser().getEmail();

  // Define sample emails
  const sampleEmails = [
    {
      subject: 'Thank you for amazing service!',
      body: 'Hi, I really enjoyed working with you. Thank you again!',
      name: 'Customer A'
    },
    {
      subject: 'Request for information',
      body: 'Hello, I need more information on your recent product launch. Thank you.',
      name: 'Customer B'
    },
    {
      subject: 'Complaint!',
      body: '',
      htmlBody: `<p>Hello, You are late in delivery, again.</p>
<p>Please contact me ASAP before I cancel our subscription.</p>`,
      name: 'Customer C'
    }
  ];

  // Send each sample email
  for (const email of sampleEmails) {
    GmailApp.sendEmail(userEmail, email.subject, email.body, {
      name: email.name,
      htmlBody: email.htmlBody
    });
  }

  // Return a notification
  return buildNotificationResponse("Successfully generated sample emails");
}

Vertex.gs

gmail-sentiment-analysis/Vertex.gs
/*
Copyright 2024-2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Replace with your project ID
const PROJECT_ID = '[ADD YOUR GCP PROJECT ID HERE]';

// Location for your Vertex AI model
const VERTEX_AI_LOCATION = 'us-central1';

// Model ID to use for sentiment analysis
const MODEL_ID = 'gemini-2.5-flash';

/**
 * Sends the email text to Vertex AI for sentiment analysis.
 *
 * @param {string} emailText - The text of the email to analyze.
 * @returns {string} - The sentiment of the email ('positive', 'negative', or 'neutral').
 */
function processSentiment(emailText) {
  // Construct the API endpoint URL
  const apiUrl = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;

  // Prepare the request payload
  const payload = {
    contents: [
      {
        role: "user",
        parts: [
          {
            text: `Analyze the sentiment of the following message: ${emailText}`
          }
        ]
      }
    ],
    generationConfig: {
      temperature: 0.9,
      maxOutputTokens: 1024,
      responseMimeType: "application/json",
      // Expected response format for simpler parsing.
      responseSchema: {
        type: "object",
        properties: {
          response: {
            type: "string",
            enum: ["positive", "negative", "neutral"]
          }
        }
      }
    }
  };

  // Prepare the request options
  const options = {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    },
    contentType: 'application/json',
    muteHttpExceptions: true, // Set to true to inspect the error response
    payload: JSON.stringify(payload)
  };

  // Make the API request
  const response = UrlFetchApp.fetch(apiUrl, options);

  // Parse the response. There are two levels of JSON responses to parse.
  const parsedResponse = JSON.parse(response.getContentText());
  const sentimentResponse = JSON.parse(parsedResponse.candidates[0].content.parts[0].text).response;

  // Return the sentiment
  return sentimentResponse;
}

appsscript.json

gmail-sentiment-analysis/appsscript.json
{
  "timeZone": "America/Toronto",
  "oauthScopes": [
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/gmail.labels",
    "https://www.googleapis.com/auth/gmail.modify",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/userinfo.email"
  ],
  "addOns": {
    "common": {
      "name": "Sentiment Analysis",
      "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/sentiment_extremely_dissatisfied/v6/black-24dp/1x/gm_sentiment_extremely_dissatisfied_black_24dp.png"
    },
    "gmail": {
      "homepageTrigger": {
        "runFunction": "onHomepageTrigger",
        "enabled": true
      }
    }
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

삭제

이 튜토리얼에서 사용한 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 Cloud 프로젝트를 삭제하는 것이 좋습니다.

  1. Google Cloud 콘솔에서 리소스 관리 페이지로 이동합니다. 메뉴 > IAM 및 관리자 > 리소스 관리를 클릭합니다.

    Resource Manager로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제 를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 다음 종료를 클릭하여 프로젝트를 삭제합니다.

다음 단계