Skip to navigation

Агент умной разметки писем

Обзор

Соберите агента классификации почты: ИИ анализирует письма и проставляет метки. Средний уровень: метки Agent Inbox и OpenAI GPT-4o-mini для автоматизации inbox.

Что вы получите

В конце руководства будет агент, который:

  1. Принимает письма в выделенный inbox Agent Inbox
  2. Анализирует каждое письмо ИИ по 4 измерениям:
    • Sentiment (тональность): positive, neutral, negative
    • Category (категория): question, complaint, feature-request, bug-report, praise
    • Priority (приоритет): urgent, high, normal, low
    • Department (отдел): sales, support, billing, technical
  3. Автоматически ставит метки для фильтрации
  4. Обрабатывает сбои — повторы и валидация ответа модели

Пример прихода письма:

Письмо: "Your product crashed! I need help ASAP!"
↓
Анализ ИИ
↓
Метки:
• negative
• complaint
• urgent
• support

Требования

Нужно:

Настройка проекта

Шаг 1: каталог

Создайте папку агента:

mkdir smart-labeling-agent
cd smart-labeling-agent

Шаг 2: код agent.py

"""
Smart Email Labeling Agent
An AI-powered email classification agent that automatically analyzes incoming
emails across multiple dimensions and applies appropriate labels.
"""
import os
import json
import time
from dotenv import load_dotenv
load_dotenv()
from flask import Flask, request, Response
import ngrok
from agentinbox import Agentinbox
from openai import OpenAI
# Configuration
PORT = int(os.getenv("PORT", "8080"))
INBOX_USERNAME = os.getenv("INBOX_USERNAME", "smart-labels")
WEBHOOK_DOMAIN = os.getenv("WEBHOOK_DOMAIN")
# Initialize
app = Flask(__name__)
client = Agentinbox()
openai_client = OpenAI()
def setup_agentinbox():
"""Create inbox and webhook with idempotency."""
# Create inbox
try:
inbox = client.inboxes.create(
username=INBOX_USERNAME,
client_id=f"{INBOX_USERNAME}-inbox"
)
except Exception as e:
if "already exists" in str(e).lower():
inbox_id = f"{INBOX_USERNAME}@agentinbox.space"
class SimpleInbox:
def __init__(self, inbox_id):
self.inbox_id = inbox_id
inbox = SimpleInbox(inbox_id)
else:
raise
# Start ngrok
listener = ngrok.forward(PORT, domain=WEBHOOK_DOMAIN, authtoken_from_env=True)
# Create webhook
try:
client.webhooks.create(
url=f"{listener.url()}/webhook/inbound",
event_types=["message.received"],
client_id=f"{INBOX_USERNAME}-webhook"
)
except Exception as e:
if "already exists" not in str(e).lower():
raise
print(f"Ready: {inbox.inbox_id}\n")
return inbox, listener
def analyze_email(subject, content):
"""Use AI to classify email across multiple dimensions with retry logic."""
valid_values = {
"sentiment": {"positive", "neutral", "negative"},
"category": {"question", "complaint", "feature-request", "bug-report", "praise"},
"priority": {"urgent", "high", "normal", "low"},
"department": {"sales", "support", "billing", "technical"}
}
for attempt in range(1, 4):
try:
if attempt > 1:
time.sleep(1)
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are an expert email classifier. Analyze emails and return structured classifications."
},
{
"role": "user",
"content": f"""Analyze this email across 4 dimensions:
Subject: {subject}
Content: {content}
Classify into:
1. sentiment: positive | neutral | negative
2. category: question | complaint | feature-request | bug-report | praise
3. priority: urgent | high | normal | low
4. department: sales | support | billing | technical
Consider:
- Sentiment: Overall tone and emotion
- Category: Primary intent of the email
- Priority: Urgency indicators (ASAP, urgent, immediately, deadline mentions, emergency)
- Department: Best team to handle this
Return ONLY valid JSON with these exact keys: sentiment, category, priority, department.
Example: {{"sentiment": "positive", "category": "question", "priority": "normal", "department": "sales"}}
"""
}
],
response_format={"type": "json_object"},
temperature=0.3
)
# Parse and validate
result = json.loads(response.choices[0].message.content)
required_keys = ["sentiment", "category", "priority", "department"]
missing_keys = [key for key in required_keys if key not in result]
if missing_keys:
raise ValueError(f"Missing keys: {missing_keys}")
invalid_values = []
for dimension, value in result.items():
if dimension in valid_values and value not in valid_values[dimension]:
invalid_values.append(f"{dimension}={value}")
if invalid_values:
raise ValueError(f"Invalid values: {', '.join(invalid_values)}")
return result
except Exception as e:
if attempt == 3:
raise Exception(f"AI classification failed: {e}")
def apply_labels(inbox_id, message_id, classifications):
"""Apply labels based on classification results."""
labels = [
f"{classifications['sentiment']}",
f"{classifications['category']}",
f"{classifications['priority']}",
f"{classifications['department']}"
]
# Try batch first
try:
client.inboxes.messages.update(
inbox_id=inbox_id,
message_id=message_id,
add_labels=labels
)
for label in labels:
print(f" ✓ {label}")
return
except Exception:
pass
# Try individually
successful = []
for label in labels:
try:
client.inboxes.messages.update(
inbox_id=inbox_id,
message_id=message_id,
add_labels=[label]
)
successful.append(label)
print(f" ✓ {label}")
except Exception:
print(f" ✗ {label}")
if not successful:
raise Exception("Failed to apply labels")
@app.route('/webhook/inbound', methods=['POST'])
def receive_webhook():
"""Webhook endpoint to receive incoming email notifications."""
try:
payload = request.json
event_type = payload.get('type') or payload.get('event_type')
# Ignore outgoing messages
if event_type == 'message.sent':
return Response(status=200)
message = payload.get('message', {})
message_id = message.get('message_id')
inbox_id = message.get('inbox_id')
from_field = message.get('from_', '') or message.get('from', '')
# Validate required fields
if not message_id or not inbox_id or not from_field:
return Response(status=200)
# Extract sender email
if '<' in from_field and '>' in from_field:
sender_email = from_field.split('<')[1].split('>')[0].strip()
else:
sender_email = from_field.strip()
subject = message.get('subject', '(no subject)')
email_body = message.get('text', '') or message.get('body', '') or message.get('html', '')
# Log
print(f"\n📧 {sender_email}: {subject}")
# Analyze
classifications = analyze_email(subject, email_body)
print(f" Sentiment: {classifications['sentiment']}")
print(f" Category: {classifications['category']}")
print(f" Priority: {classifications['priority']}")
print(f" Department: {classifications['department']}")
# Apply labels
apply_labels(inbox_id, message_id, classifications)
print("Done\n")
except Exception as e:
print(f"Error: {e}\n")
return Response(status=200)
if __name__ == '__main__':
print("SMART EMAIL LABELING AGENT\n")
inbox, listener = setup_agentinbox()
print("Waiting for emails...\n")
app.run(port=PORT)

Шаг 3: requirements.txt

agentinbox
flask>=3.0.0
ngrok>=1.0.0
python-dotenv>=1.0.0
openai>=1.0.0

Шаг 4: установка зависимостей

pip install -r requirements.txt

Шаг 5: .env

# Agent Inbox Configuration
AGENTINBOX_API_KEY=your_agentinbox_api_key_here
# OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key_here
# Ngrok Configuration
NGROK_AUTHTOKEN=your_ngrok_authtoken_here
WEBHOOK_DOMAIN=your-domain.ngrok-free.app
# Agent Settings
INBOX_USERNAME=smart-labels
PORT=8080

Разбор кода

Схема

Письмо → Agent Inbox → Webhook → ngrok → Flask → ИИ → Метки

1. Инициализация

# Load environment variables first
load_dotenv()
# Initialize three clients
app = Flask(__name__) # Web server for webhooks
client = Agentinbox() # Agent Inbox SDK
openai_client = OpenAI() # OpenAI for AI classification

2. Инфраструктура

setup_agentinbox() создаёт inbox и вебхук:

# Create inbox with idempotency
inbox = client.inboxes.create(
username=INBOX_USERNAME,
client_id=f"{INBOX_USERNAME}-inbox" # Prevents duplicates
)
# Start ngrok tunnel (localhost → public URL)
listener = ngrok.forward(PORT, domain=WEBHOOK_DOMAIN)
# Register webhook with Agent Inbox
client.webhooks.create(
url=f"{listener.url()}/webhook/inbound",
event_types=["message.received"],
client_id=f"{INBOX_USERNAME}-webhook"
)

Идемпотентность client_id: перезапуск агента не создаёт дубликаты — вернётся существующий ресурс.

3. Анализ писем ИИ

Ядро агента — analyze_email():

def analyze_email(subject, content):
# Define valid classification values
valid_values = {
"sentiment": {"positive", "neutral", "negative"},
"category": {"question", "complaint", "feature-request", ...},
"priority": {"urgent", "high", "normal", "low"},
"department": {"sales", "support", "billing", "technical"}
}
# Retry up to 3 times
for attempt in range(1, 4):
try:
# Call OpenAI API
response = openai_client.chat.completions.create(...)
# Parse JSON response
result = json.loads(response.choices[0].message.content)
# Validate keys and values
# ... validation logic ...
return result
except Exception as e:
if attempt == 3:
raise

Особенности:

  1. Структурированный промпт — четыре измерения классификации
  2. JSON mode — валидный JSON от OpenAI
  3. Повторы — до 3 попыток при ошибках
  4. Строгая валидация — значения только из допустимых множеств
  5. Низкая temperature (0.3) — стабильные метки

Пример промпта:

Проанализируйте это письмо по 4 измерениям:
Тема: Продукт постоянно падает!
Текст: Ваш софт ужасен. Исправьте немедленно!
Классифицируйте:
1. sentiment: positive | neutral | negative
2. category: question | complaint | feature-request | bug-report | praise
3. priority: urgent | high | normal | low
4. department: sales | support | billing | technical
Верните ТОЛЬКО валидный JSON...

Ответ модели:

{
"sentiment": "negative",
"category": "complaint",
"priority": "urgent",
"department": "support"
}

4. Простановка меток

apply_labels() — двухуровневая стратегия:

# Create labels from classification values
labels = [
"negative",
"complaint",
"urgent",
"support"
]
# Try batch application first (most efficient)
try:
client.inboxes.messages.update(
inbox_id=inbox_id,
message_id=message_id,
add_labels=labels # Apply all at once
)
return
except Exception:
pass # If batch fails, try individually
# Apply labels one by one
for label in labels:
try:
client.inboxes.messages.update(
inbox_id=inbox_id,
message_id=message_id,
add_labels=[label] # One at a time
)
except Exception:
pass # Log failure but continue

Зачем два уровня:

  1. Сначала пакет — один вызов API на все метки
  2. Потом по одной — если пакет не прошёл, сохраняем хотя бы часть
  3. Устойчивость — сбой одной метки не роняет всё

5. Вебхук

receive_webhook() связывает шаги:

@app.route('/webhook/inbound', methods=['POST'])
def receive_webhook():
payload = request.json
# Ignore outgoing messages (prevents infinite loops)
if event_type == 'message.sent':
return Response(status=200)
# Extract email data
message_id = message.get('message_id')
inbox_id = message.get('inbox_id')
sender_email = extract_email(from_field)
subject = message.get('subject')
email_body = message.get('text')
# Classify with AI
classifications = analyze_email(subject, email_body)
# Apply labels
apply_labels(inbox_id, message_id, classifications)
# Always return 200 (tells Agent Inbox we received it)
return Response(status=200)

Почему всегда 200?

Даже при ошибке классификации или меток отвечаем 200 — вебхук принят. Коды 4xx/5xx вызовут ретраи доставки, что не лечит ошибки приложения.

Запуск

python agent.py

Ожидаемый вывод:

SMART EMAIL LABELING AGENT
Ready: smart-labels@agentinbox.space
Waiting for emails...
* Running on http://127.0.0.1:8080

Готово. Агент классифицирует входящие письма. Терминал не закрывайте.

Проверка

Пример 1: срочная жалоба

Отправьте письмо:

To: smart-labels@agentinbox.space
Subject: Product crashed - need immediate help!
Body: Your product is TERRIBLE! It crashed 3 times today and I lost all my work.
I need this fixed IMMEDIATELY or I want a full refund!

Консоль:

you@example.com: Product crashed - need immediate help!
Sentiment: negative
Category: complaint
Priority: urgent
Department: support
✓ negative
✓ complaint
✓ urgent
✓ support
Done

Почему так:

  • Sentiment: negative («terrible», «lost work»)
  • Category: complaint
  • Priority: urgent («IMMEDIATELY», «crashed 3 times»)
  • Department: support (проблема продукта)

Пример 2: запрос фичи

Письмо:

To: smart-labels@agentinbox.space
Subject: Dark mode would be amazing!
Body: Hi! I absolutely love your product. I use it every day.
One feature that would make it even better is dark mode support.
Keep up the great work!

Консоль:

you@example.com: Dark mode would be amazing!
Sentiment: positive
Category: feature-request
Priority: normal
Department: technical
✓ positive
✓ feature-request
✓ normal
✓ technical
Done

Почему так:

  • Sentiment: positive («love», «amazing», «great work»)
  • Category: feature-request
  • Priority: normal
  • Department: technical

Дальше

В консоли и inbox

Фильтруйте письма по меткам в Agent Inbox:

Test image

По тональности: negative / positive / neutral и т.д.

Комбинации: urgent + negative — критичные кейсы; sales + high — горячие лиды; technical + bug-report — бэклог инженеров.

Совет: через API Agent Inbox можно выбирать письма по меткам и строить свои процессы, дашборды и аналитику.

Автоматизация на основе меток

1. Уведомления о приоритете

При метках urgent + negative — сразу Slack в канал саппорта с email отправителя и темой.

2. Эскалация по тональности

Собирайте negative, уведомляйте customer success при ухудшении тренда; при 3+ негативных письмах от одного клиента за неделю — персональный контакт от руководства.

3. Маршрутизация по отделу

Правила по меткам отдела: sales → sales@…; billing → тикет в биллинге; technical → #engineering; support → очередь с SLA.

4. Умный автоответ

Ответы по классу: question, bug-report, complaint, feature-request — разные шаблоны вместо одного общего.

5. Аналитика

Дашборд по меткам: тренды тональности, объём по отделам, время ответа по приоритетам, частые категории запросов.


Вы собрали систему классификации почты на ИИ; метки Agent Inbox позволяют строить сложную автоматизацию inbox и аналитику.