У відділах продажу й сапорту email дуже швидко перетворюється на окремий фронт операційного навантаження. Листів багато, формулювання різні, частина звернень повторюється, а менеджер витрачає час не на рішення, а на одну й ту саму механіку: відкрити лист, зрозуміти контекст, написати чорнову відповідь, звірити тон, перевірити деталі. Стандартні фільтри Gmail корисні для маршрутизації, але вони не розуміють намір, пріоритет і зміст листа. Тут і починається зона, де LLM-агент справді сильніший за набір правил.

3D-ілюстрація: ноутбук з інтерфейсом пошти у стилі Google Workspace, AI-агент надсилає автовідповіді, стилізований зелений шестикутник Node
3D-ілюстрація: ноутбук з інтерфейсом пошти у стилі Google Workspace, AI-агент надсилає автовідповіді, стилізований зелений шестикутник Node

Але важлива межа така: ми не даємо ШІ відправляти листи наосліп. Надійніший сценарій — draft-first. Агент читає нечитані повідомлення, класифікує їх, підтягує контекст компанії та готує персоналізовану чернетку. Людина лише переглядає, править і натискає Send. Саме так автоматизація пошти дає виграш у часі без втрати контролю.

Як виглядає архітектура рішення

Базова схема тут складається з чотирьох вузлів: OAuth2-авторизація до Gmail API, фоновий процес на Node.js, LLM-шар для класифікації та генерації тексту, і Gmail Drafts як безпечна точка виходу. Gmail API справді працює через OAuth 2.0, а для server-side сценаріїв Google прямо рекомендує серверний потік авторизації, коли застосунок отримує токени для роботи від імені користувача, у тому числі коли той офлайн.

Для циклу обробки є два практичні режими. Перший — polling: він простіший для старту, коли ваш сервіс раз на N секунд або хвилин перевіряє is:unread. Другий — push через Gmail watch і Cloud Pub/Sub: він складніший у налаштуванні, зате знімає зайві опитування. Gmail API офіційно підтримує push-notifications через Pub/Sub, а watch треба поновлювати як мінімум раз на 7 днів.

Це хороший приклад того, як маленький агентний сервіс виростає з тієї ж логіки, що й мікросервіси на Node.js для Google Workspace: не розширення, не ручний плагін у браузері, а окремий керований контур, який бере на себе рутину, але лишає фінальне рішення людині.

Створили: доступ до Gmail API

Доступ до Gmail API через OAuth
Доступ до Gmail API через OAuth

Для швидкого старту вам потрібен Google Cloud проєкт, увімкнений Gmail API й OAuth client. У типовому Node.js quickstart Google пропонує створити OAuth client типу Desktop app, завантажити credentials.json і запускати локальну авторизацію через рекомендовані клієнтські бібліотеки. Quickstart прямо попереджає, що це спрощений підхід для тестового середовища, а в продакшні варто окремо продумати правильну модель автентифікації та авторизації.

На практиці поруч із credentials.json часто зберігають і локальний файл із уже виданим токеном — умовний token.json. Назва не магічна: важлива не вона, а сам факт, що після першої згоди користувача сервіс має куди безпечно покласти refresh/access token і не ганяти людину через consent screen щоразу.

Написали: модуль для читання нечитаних листів

Модуль читання нечитаних листів
Модуль читання нечитаних листів

Gmail API дає для цього метод users.messages.list. Він підтримує параметр q у тому ж форматі, що й пошук у самому Gmail, тобто ви можете запитати щось на кшталт is:unread category:primary newer_than:2d. Саме messages.list повертає ідентифікатори листів, а для повного вмісту далі вже викликається messages.get.

// gmailService.js
import { google } from "googleapis";

export async function buildGmailClient(auth) {
  return google.gmail({ version: "v1", auth });
}

export async function listUnreadMessages(gmail) {
  const res = await gmail.users.messages.list({
    userId: "me",
    q: "is:unread -category:social -category:promotions",
    maxResults: 10,
  });

  return res.data.messages || [];
}

export async function getMessage(gmail, id) {
  const res = await gmail.users.messages.get({
    userId: "me",
    id,
    format: "full",
  });

  return res.data;
}

export function extractHeader(message, name) {
  return (
    message.payload?.headers?.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value || ""
  );
}

export function extractPlainText(message) {
  const parts = message.payload?.parts || [];
  const textPart = parts.find((p) => p.mimeType === "text/plain");

  if (textPart?.body?.data) {
    return Buffer.from(textPart.body.data, "base64").toString("utf8");
  }

  if (message.payload?.body?.data) {
    return Buffer.from(message.payload.body.data, "base64").toString("utf8");
  }

  return "";
}

Тут логіка навмисно проста: спочатку беремо тільки unread, без “розважальних” категорій, потім дістаємо From, Subject, plain text і вже після цього передаємо матеріал в LLM-шар.

Промпт-інжиніринг: роль, межі й контекст компанії

Найчастіша помилка в таких сценаріях — дати моделі занадто багато свободи. Якщо ви просто скажете “відповідай як менеджер”, то отримаєте красивий текст, але не керований інструмент. Правильніше ставити роль, межі, бізнес-контекст і формат виходу. OpenAI у своїх актуальних гайдах рекомендує для нових проєктів Responses API, а якщо вам потрібен стабільний структурований вихід, Structured Outputs надійніші за звичайний JSON mode.

// aiService.js
import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const COMPANY_CONTEXT = `
Ти працюєш як менеджер служби підтримки B2B-компанії.
Тон: спокійний, професійний, без пафосу.
Не вигадуй політики, ціни, терміни або технічні деталі, яких немає у вхідному контексті.
Якщо даних недостатньо — чесно попроси уточнення.
Мета: підготувати чернетку відповіді, а не фінально відправити лист.
`;

export async function classifyAndDraftEmail({ subject, from, body }) {
  const response = await client.responses.create({
    model: "gpt-5.4-mini",
    input: [
      {
        role: "system",
        content: COMPANY_CONTEXT,
      },
      {
        role: "user",
        content: `
Проаналізуй лист і поверни JSON:
{
  "category": "support|sales|billing|spam|other",
  "priority": "low|medium|high",
  "needsHumanReview": true,
  "draftSubject": "...",
  "draftBody": "..."
}

From: ${from}
Subject: ${subject}
Body:
${body}
        `,
      },
    ],
    text: {
      format: {
        type: "json_schema",
        name: "email_triage",
        strict: true,
        schema: {
          type: "object",
          additionalProperties: false,
          properties: {
            category: { type: "string" },
            priority: { type: "string" },
            needsHumanReview: { type: "boolean" },
            draftSubject: { type: "string" },
            draftBody: { type: "string" },
          },
          required: ["category", "priority", "needsHumanReview", "draftSubject", "draftBody"],
        },
      },
    },
  });

  return JSON.parse(response.output_text);
}

Такий prompt уже не просто “пише листи”, а виконує NLP-задачу: класифікує вхідний намір, обмежує фантазію моделі й повертає придатний до обробки об’єкт. За тим самим принципом, до речі, працює і AI Lead Gen на Puppeteer + OpenAI API: там LLM теж не “магічно думає”, а отримує чітку роль, текстовий контекст і формалізований вихід.

Створюємо не відправку, а чернетку

І тут ключова інженерна ідея всієї системи: не messages.send, а users.drafts.create. Gmail API окремо підтримує створення чернеток через users.drafts.create, а успішний виклик повертає об’єкт Draft. Для цього методу потрібні відповідні OAuth scopes, зокрема gmail.compose або gmail.modify.

// gmailService.js
export async function createDraft(gmail, { to, subject, body }) {
  const message = [
    `To: ${to}`,
    "Content-Type: text/plain; charset=utf-8",
    "MIME-Version: 1.0",
    `Subject: ${subject}`,
    "",
    body,
  ].join("
");

  const encodedMessage = Buffer.from(message)
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");

  const res = await gmail.users.drafts.create({
    userId: "me",
    requestBody: {
      message: {
        raw: encodedMessage,
      },
    },
  });

  return res.data;
}

Саме цей підхід і дає Human in the loop. Агент уже зробив 90% рутинної роботи: прочитав, зрозумів, оформив думку, витримав тон. Але фінальна відповідальність лишається за людиною.

app.js: головний цикл

Окремий модуль під Gmail, окремий під LLM, окремий головний цикл — це не “архітектура заради архітектури”, а нормальний спосіб не перетворити сервіс на один великий файл зі змішаною логікою.

// app.js
import { authenticate } from "@google-cloud/local-auth";
import { listUnreadMessages, getMessage, extractHeader, extractPlainText, buildGmailClient, createDraft } from "./gmailService.js";
import { classifyAndDraftEmail } from "./aiService.js";

const SCOPES = [
  "https://www.googleapis.com/auth/gmail.modify",
  "https://www.googleapis.com/auth/gmail.compose",
];

async function main() {
  const auth = await authenticate({
    scopes: SCOPES,
    keyfilePath: "./credentials.json",
  });

  const gmail = await buildGmailClient(auth);
  const unread = await listUnreadMessages(gmail);

  for (const item of unread) {
    const message = await getMessage(gmail, item.id);
    const from = extractHeader(message, "From");
    const subject = extractHeader(message, "Subject");
    const body = extractPlainText(message);

    const ai = await classifyAndDraftEmail({ from, subject, body });

    if (ai.category === "spam") continue;

    await createDraft(gmail, {
      to: from,
      subject: ai.draftSubject || `Re: ${subject}`,
      body: ai.draftBody,
    });

    console.log(`Draft created for: ${subject}`);
  }
}

main().catch(console.error);

Для старту цього достатньо. Потім ви вже додаєте дедуплікацію, збереження history ID, логування, label-based routing, knowledge base компанії й окрему чергу обробки. А якщо поруч у вас уже є контури на кшталт інтеграції Google Sheets з Telegram та Slack, то наступним кроком агент може не тільки готувати draft, а й ескалювати лист у внутрішній канал, якщо запит високопріоритетний.

Що це дає бізнесу на практиці

Найбільший виграш тут не в “автоматичній відповіді”, а в скороченні часу на старт чернетки. Менеджер більше не пише кожен лист з нуля. Він редагує вже осмислений варіант. У сапорті це зменшує когнітивну втому, у продажах — пришвидшує першу реакцію, а для операційного керівника це означає більш передбачуваний SLA без найму ще однієї людини лише на шаблонні відповіді.

Такий агент особливо корисний там, де листи схожі за структурою, але не ідентичні за змістом: уточнення по послугах, перші B2B-запити, типові питання щодо документів, повторювані customer-success сценарії. Саме в таких місцях LLM дає не “красивий текст”, а реальний time saving.

Data Privacy і межа, де вже потрібен повноцінний сервіс

Приватність даних та сучасні сервіси
Приватність даних та сучасні сервіси

Уся цінність цього підходу псується, якщо ви віддаєте корпоративну пошту випадковому browser-extension або SaaS-посереднику, який сидить між Gmail і моделлю. Саме тому власний мікросервіс тут часто безпечніший: ви контролюєте scopes, логи, retention, prompt, fallback-логіку і те, які саме дані взагалі йдуть у модель.

Якщо обсяг невеликий, а сценарій обмежується “прочитав → класифікував → створив draft”, цього рішення достатньо надовго. Але якщо у вас кілька скриньок, SLA, черги, ескалації, knowledge retrieval, audit trail і багатоканальні інтеграції — тоді Gmail-агент уже варто вбудовувати в ширший контур на Node.js, а не тримати як ізольований скрипт. І це нормальний момент для переходу від “розумної автоматизації пошти” до повноцінної мікросервісної архітектури.