From ebf31348cedbfea33fadb49fe1354901ca4fe133 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 19 May 2026 08:13:06 +0000 Subject: [PATCH] feat: add OpenAI Agents SDK email triage example Complete email triage agent using OpenAI Agents SDK + AgentMail with: - Python and TypeScript implementations - function_tool integration for reply, escalate, skip, and draft - Full thread context for intelligent triage decisions - Label-based state tracking - Human-in-the-loop escalation flow This showcases AgentMail's two-way email capabilities with the fastest-growing agent framework. --- openai-agents-email-triage/README.md | 94 ++++++ .../python/.env.example | 8 + openai-agents-email-triage/python/agent.py | 292 ++++++++++++++++++ .../python/requirements.txt | 3 + .../typescript/.env.example | 8 + .../typescript/README.md | 5 + .../typescript/package.json | 18 ++ .../typescript/src/agent.ts | 256 +++++++++++++++ .../typescript/tsconfig.json | 13 + 9 files changed, 697 insertions(+) create mode 100644 openai-agents-email-triage/README.md create mode 100644 openai-agents-email-triage/python/.env.example create mode 100644 openai-agents-email-triage/python/agent.py create mode 100644 openai-agents-email-triage/python/requirements.txt create mode 100644 openai-agents-email-triage/typescript/.env.example create mode 100644 openai-agents-email-triage/typescript/README.md create mode 100644 openai-agents-email-triage/typescript/package.json create mode 100644 openai-agents-email-triage/typescript/src/agent.ts create mode 100644 openai-agents-email-triage/typescript/tsconfig.json diff --git a/openai-agents-email-triage/README.md b/openai-agents-email-triage/README.md new file mode 100644 index 0000000..996c934 --- /dev/null +++ b/openai-agents-email-triage/README.md @@ -0,0 +1,94 @@ +# Email Triage Agent — OpenAI Agents SDK + AgentMail + +An autonomous email triage agent built with the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) and [AgentMail](https://agentmail.to). + +The agent gets its own email inbox, reads incoming messages, classifies them, and either replies directly, escalates to a human, or skips spam — all autonomously. + +## What it does + +``` +Incoming email → AgentMail inbox → OpenAI Agent triages → Reply / Escalate / Skip +``` + +1. **Creates an inbox** — The agent provisions its own email address via AgentMail +2. **Polls for new mail** — Checks for unread messages every N seconds +3. **Reads full threads** — Fetches conversation history for context +4. **Triages with tools** — The OpenAI agent classifies and acts using function tools: + - `reply_to_email` — Confident answer → sends reply + labels the thread + - `escalate_to_human` — Complex/sensitive → forwards to team + sends holding reply + - `skip_message` — Spam or auto-reply → marks as skipped + - `create_draft` — Good reply but wants human review first + +## Why AgentMail + OpenAI Agents SDK? + +| Capability | What it enables | +|---|---| +| **Dedicated agent inboxes** | Each agent gets its own email address — no shared credentials | +| **Two-way email** | Read, reply, forward — not just send | +| **Thread management** | Full conversation context for every decision | +| **Labels** | Track state (category, escalated, auto-replied) on each message | +| **Drafts** | Create drafts for human review before sending | +| **OpenAI function tools** | Clean tool-use pattern — agent picks the right action | + +## Quick start + +```bash +cd python +pip install -r requirements.txt +cp .env.example .env +# Edit .env with your API keys +python agent.py +``` + +You'll see: +``` +📬 Email triage agent live at: triage-abc123@agentmail.to + Escalating to: team@yourcompany.com + Model: gpt-4o + Polling every 10s. Ctrl-C to stop. +``` + +Send an email to the printed address and watch it get triaged. + +## Configuration + +| Variable | Required | Description | +|---|---|---| +| `AGENTMAIL_API_KEY` | ✅ | Get one at [agentmail.to](https://agentmail.to) | +| `OPENAI_API_KEY` | ✅ | OpenAI API key | +| `ESCALATION_EMAIL` | ✅ | Where to forward escalated emails | +| `PRODUCT_NAME` | | Your product name (default: "Acme Corp") | +| `AGENT_NAME` | | Agent's sign-off name (default: "Alex") | +| `OPENAI_MODEL` | | Model to use (default: "gpt-4o") | +| `POLL_INTERVAL_SECONDS` | | Polling frequency (default: 10) | +| `INBOX_USERNAME` | | Custom inbox prefix (default: auto-generated) | + +## How it works + +The agent uses the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) `function_tool` decorator to expose AgentMail operations as callable tools. The SDK handles: + +- Tool schema generation from Python type hints +- Automatic tool calling and result parsing +- Conversation management + +AgentMail handles the email infrastructure: + +- Inbox provisioning (instant, API-driven) +- Message polling and thread reconstruction +- Sending replies and forwards +- Label-based state tracking +- Draft management for human-in-the-loop workflows + +## Extending this example + +- **Add webhooks**: Replace polling with [AgentMail webhooks](https://docs.agentmail.to/webhooks/webhooks-overview) for real-time processing +- **Multi-agent handoffs**: Use the Agents SDK handoff pattern to route emails to specialized agents +- **Knowledge base**: Add a RAG tool so the agent can search your docs before replying +- **Analytics**: Track triage metrics using labels and the AgentMail list API + +## More resources + +- [AgentMail docs](https://docs.agentmail.to) +- [AgentMail MCP server](https://github.com/agentmail-to/agentmail-mcp) +- [OpenAI Agents SDK docs](https://openai.github.io/openai-agents-python/) +- [AgentMail Python SDK](https://pypi.org/project/agentmail/) diff --git a/openai-agents-email-triage/python/.env.example b/openai-agents-email-triage/python/.env.example new file mode 100644 index 0000000..d895d3f --- /dev/null +++ b/openai-agents-email-triage/python/.env.example @@ -0,0 +1,8 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key +OPENAI_API_KEY=your_openai_api_key +ESCALATION_EMAIL=team@yourcompany.com +PRODUCT_NAME=Acme Corp +AGENT_NAME=Alex +OPENAI_MODEL=gpt-4o +POLL_INTERVAL_SECONDS=10 +# INBOX_USERNAME=support # optional: custom inbox prefix diff --git a/openai-agents-email-triage/python/agent.py b/openai-agents-email-triage/python/agent.py new file mode 100644 index 0000000..bf454e1 --- /dev/null +++ b/openai-agents-email-triage/python/agent.py @@ -0,0 +1,292 @@ +""" +Email Triage Agent — built with OpenAI Agents SDK + AgentMail. + +Gives an autonomous agent its own email inbox. The agent reads incoming +messages, classifies them, drafts replies, and either sends them directly +or escalates to a human depending on confidence. + +Run: + pip install -r requirements.txt + cp .env.example .env # fill in your keys + python agent.py +""" + +import json +import os +import time +from datetime import datetime, timezone +from email.utils import parseaddr +from pathlib import Path + +from agentmail import AgentMail +from agentmail.inboxes import CreateInboxRequest +from agents import Agent, Runner, function_tool +from dotenv import load_dotenv + +load_dotenv() + +# --- config ------------------------------------------------------------------- + +AGENTMAIL_API_KEY = os.environ["AGENTMAIL_API_KEY"] +OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] +ESCALATION_EMAIL = os.environ["ESCALATION_EMAIL"] +PRODUCT_NAME = os.getenv("PRODUCT_NAME", "Acme Corp") +AGENT_NAME = os.getenv("AGENT_NAME", "Alex") +MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") +POLL_INTERVAL = int(os.getenv("POLL_INTERVAL_SECONDS", "10")) +INBOX_USERNAME = os.getenv("INBOX_USERNAME") or None + +STATE_FILE = Path(".agent_state.json") + +CATEGORIES = ["billing", "bug_report", "feature_request", "question", "spam", "urgent"] + +# --- clients ------------------------------------------------------------------ + +mail = AgentMail(api_key=AGENTMAIL_API_KEY) + + +# --- state management --------------------------------------------------------- + + +def load_state() -> dict: + if STATE_FILE.exists(): + try: + return json.loads(STATE_FILE.read_text()) + except Exception: + pass + return {} + + +def save_state(state: dict) -> None: + STATE_FILE.write_text(json.dumps(state, indent=2)) + + +# --- helpers ------------------------------------------------------------------ + + +def sender_email(message) -> str: + sender = getattr(message, "from_", None) or getattr(message, "from", None) or "" + _, email = parseaddr(str(sender)) + return email.lower() + + +def get_or_create_inbox(): + state = load_state() + if state.get("inbox_id"): + try: + return mail.inboxes.get(state["inbox_id"]) + except Exception as e: + print(f"(stale inbox, creating new: {e})") + + inbox = mail.inboxes.create( + request=CreateInboxRequest( + username=INBOX_USERNAME, + display_name=f"{PRODUCT_NAME} Triage", + ) + ) + state["inbox_id"] = inbox.inbox_id + state["email"] = inbox.email + save_state(state) + return inbox + + +def build_thread_context(thread, our_email: str) -> str: + """Convert an AgentMail thread into a readable conversation transcript.""" + our_email = our_email.lower() + lines = [] + for m in thread.messages or []: + who = sender_email(m) + role = "Agent" if who == our_email else f"Customer ({who})" + body = (getattr(m, "extracted_text", None) or m.text or "").strip() + if body: + lines.append(f"[{role}]:\n{body}") + return "\n\n---\n\n".join(lines) if lines else "(empty thread)" + + +# --- tools (called by the OpenAI agent) -------------------------------------- + + +@function_tool +def reply_to_email( + inbox_id: str, message_id: str, text: str, category: str +) -> str: + """Send a reply to an email and label it. + + Args: + inbox_id: The inbox ID. + message_id: The message ID to reply to. + text: The reply body. + category: One of: billing, bug_report, feature_request, question, spam, urgent. + """ + mail.inboxes.messages.reply(inbox_id, message_id, text=text) + try: + mail.inboxes.messages.update( + inbox_id, message_id, + remove_labels=["unread"], + add_labels=[category, "auto-replied"], + ) + except Exception: + pass + return f"Reply sent and labeled as '{category}'." + + +@function_tool +def escalate_to_human( + inbox_id: str, message_id: str, reason: str, category: str +) -> str: + """Forward an email to the human team when the agent can't confidently respond. + + Args: + inbox_id: The inbox ID. + message_id: The message ID to escalate. + reason: A brief explanation of why this needs human attention. + category: One of: billing, bug_report, feature_request, question, spam, urgent. + """ + mail.inboxes.messages.forward( + inbox_id, message_id, + to=[ESCALATION_EMAIL], + text=f"[{category.upper()} — ESCALATION] {reason}", + ) + # Send a holding reply to the customer + mail.inboxes.messages.reply( + inbox_id, message_id, + text=( + "Thanks for reaching out. I've flagged this for our team and " + "someone will follow up with you shortly." + ), + ) + try: + mail.inboxes.messages.update( + inbox_id, message_id, + remove_labels=["unread"], + add_labels=[category, "escalated"], + ) + except Exception: + pass + return f"Escalated to human team. Category: {category}. Reason: {reason}" + + +@function_tool +def skip_message(inbox_id: str, message_id: str, reason: str) -> str: + """Skip a message (e.g., spam, auto-reply, or not actionable). + + Args: + inbox_id: The inbox ID. + message_id: The message to skip. + reason: Why this message is being skipped. + """ + try: + mail.inboxes.messages.update( + inbox_id, message_id, + remove_labels=["unread"], + add_labels=["skipped"], + ) + except Exception: + pass + return f"Message skipped: {reason}" + + +@function_tool +def create_draft( + inbox_id: str, to: str, subject: str, text: str +) -> str: + """Create a draft email for human review before sending. + + Args: + inbox_id: The inbox ID. + to: Recipient email address. + subject: Email subject line. + text: Draft body text. + """ + draft = mail.inboxes.drafts.create( + inbox_id, to=[to], subject=subject, text=text + ) + return f"Draft created (id: {draft.draft_id}). A human can review and send it." + + +# --- agent definition --------------------------------------------------------- + + +SYSTEM_PROMPT = f"""You are {AGENT_NAME}, an email triage agent for {PRODUCT_NAME}. + +Your job is to process incoming emails in the agent's inbox. For each email: + +1. Read the full conversation thread for context. +2. Classify the email into one of these categories: {', '.join(CATEGORIES)}. +3. Decide on an action: + - **reply_to_email**: You're confident you can answer. Write a helpful, concise reply. + - **escalate_to_human**: The question is complex, sensitive, or you're unsure. Forward it. + - **skip_message**: It's spam, an auto-reply, or a no-reply address. + - **create_draft**: You have a good reply but want a human to review it first. + +Guidelines: +- Be helpful, professional, and concise. +- Sign replies as "{AGENT_NAME}, {PRODUCT_NAME} Support". +- Never promise things you can't verify (refunds, SLAs, etc.) — escalate those. +- For billing and urgent issues, prefer escalation. +- For spam or auto-generated messages, skip them. +- When replying, reference specifics from the customer's message. +""" + +triage_agent = Agent( + name="EmailTriageAgent", + instructions=SYSTEM_PROMPT, + model=MODEL, + tools=[reply_to_email, escalate_to_human, skip_message, create_draft], +) + + +# --- main loop ---------------------------------------------------------------- + + +def process_message(message, inbox): + """Hand a single unread message to the agent for triage.""" + print(f" → fetching thread {message.thread_id}") + thread = mail.inboxes.threads.get(inbox.inbox_id, message.thread_id) + context = build_thread_context(thread, inbox.email) + + prompt = ( + f"New email in inbox {inbox.inbox_id}.\n" + f"Message ID: {message.message_id}\n" + f"From: {sender_email(message)}\n" + f"Subject: {message.subject or '(no subject)'}\n" + f"Date: {getattr(message, 'date', 'unknown')}\n\n" + f"--- Thread ---\n{context}\n\n" + f"Triage this email. Use exactly one tool." + ) + + result = Runner.run_sync(triage_agent, prompt) + print(f" ✓ agent output: {result.final_output[:120]}...") + + +def main(): + inbox = get_or_create_inbox() + print(f"\n📬 Email triage agent live at: {inbox.email}") + print(f" Escalating to: {ESCALATION_EMAIL}") + print(f" Model: {MODEL}") + print(f" Polling every {POLL_INTERVAL}s. Ctrl-C to stop.\n") + + seen: set[str] = set() + while True: + try: + resp = mail.inboxes.messages.list(inbox.inbox_id, labels=["unread"]) + new_msgs = [m for m in (resp.messages or []) if m.message_id not in seen] + for m in new_msgs: + seen.add(m.message_id) + if sender_email(m) == inbox.email.lower(): + continue + print( + f"\n📩 from {sender_email(m)}: " + f"{(m.subject or '(no subject)')[:60]}" + ) + try: + process_message(m, inbox) + except Exception as e: + print(f" ! error processing: {e}") + except Exception as e: + print(f"poll error: {e}") + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/openai-agents-email-triage/python/requirements.txt b/openai-agents-email-triage/python/requirements.txt new file mode 100644 index 0000000..c672dec --- /dev/null +++ b/openai-agents-email-triage/python/requirements.txt @@ -0,0 +1,3 @@ +agentmail +openai-agents +python-dotenv diff --git a/openai-agents-email-triage/typescript/.env.example b/openai-agents-email-triage/typescript/.env.example new file mode 100644 index 0000000..aedf2c2 --- /dev/null +++ b/openai-agents-email-triage/typescript/.env.example @@ -0,0 +1,8 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key +OPENAI_API_KEY=your_openai_api_key +ESCALATION_EMAIL=team@yourcompany.com +PRODUCT_NAME=Acme Corp +AGENT_NAME=Alex +OPENAI_MODEL=gpt-4o +POLL_INTERVAL_SECONDS=10 +# INBOX_USERNAME=support diff --git a/openai-agents-email-triage/typescript/README.md b/openai-agents-email-triage/typescript/README.md new file mode 100644 index 0000000..6fa6410 --- /dev/null +++ b/openai-agents-email-triage/typescript/README.md @@ -0,0 +1,5 @@ +/** + * Email Triage Agent — built with OpenAI Agents SDK (JS) + AgentMail. + * See ../README.md for details. + */ +export {}; diff --git a/openai-agents-email-triage/typescript/package.json b/openai-agents-email-triage/typescript/package.json new file mode 100644 index 0000000..207751c --- /dev/null +++ b/openai-agents-email-triage/typescript/package.json @@ -0,0 +1,18 @@ +{ + "name": "openai-agents-email-triage", + "version": "1.0.0", + "description": "Email triage agent built with OpenAI Agents SDK and AgentMail", + "scripts": { + "start": "tsx src/agent.ts" + }, + "dependencies": { + "@openai/agents": "^0.1.0", + "agentmail": "^1.0.0", + "dotenv": "^16.4.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.5.0" + } +} diff --git a/openai-agents-email-triage/typescript/src/agent.ts b/openai-agents-email-triage/typescript/src/agent.ts new file mode 100644 index 0000000..5a863d2 --- /dev/null +++ b/openai-agents-email-triage/typescript/src/agent.ts @@ -0,0 +1,256 @@ +/** + * Email Triage Agent — built with OpenAI Agents SDK (JS) + AgentMail. + * + * Gives an autonomous agent its own email inbox. The agent reads incoming + * messages, classifies them, drafts replies, and either sends them directly + * or escalates to a human depending on confidence. + * + * Run: + * npm install + * cp .env.example .env # fill in your keys + * npx tsx src/agent.ts + */ + +import { AgentMail } from "agentmail"; +import { Agent, run, tool } from "@openai/agents"; +import { z } from "zod"; +import "dotenv/config"; + +// --- config ------------------------------------------------------------------- + +const AGENTMAIL_API_KEY = process.env.AGENTMAIL_API_KEY!; +const OPENAI_API_KEY = process.env.OPENAI_API_KEY!; +const ESCALATION_EMAIL = process.env.ESCALATION_EMAIL!; +const PRODUCT_NAME = process.env.PRODUCT_NAME ?? "Acme Corp"; +const AGENT_NAME = process.env.AGENT_NAME ?? "Alex"; +const MODEL = process.env.OPENAI_MODEL ?? "gpt-4o"; +const POLL_INTERVAL = parseInt(process.env.POLL_INTERVAL_SECONDS ?? "10", 10); +const INBOX_USERNAME = process.env.INBOX_USERNAME ?? undefined; + +const CATEGORIES = [ + "billing", + "bug_report", + "feature_request", + "question", + "spam", + "urgent", +] as const; + +// --- clients ------------------------------------------------------------------ + +const mail = new AgentMail({ apiKey: AGENTMAIL_API_KEY }); + +// --- helpers ------------------------------------------------------------------ + +function senderEmail(message: any): string { + const from = message.from_ ?? message.from ?? ""; + const match = String(from).match(/<(.+?)>/); + return (match ? match[1] : String(from)).toLowerCase(); +} + +function buildThreadContext(thread: any, ourEmail: string): string { + const lines: string[] = []; + for (const m of thread.messages ?? []) { + const who = senderEmail(m); + const role = + who === ourEmail.toLowerCase() ? "Agent" : `Customer (${who})`; + const body = (m.extractedText ?? m.text ?? "").trim(); + if (body) lines.push(`[${role}]:\n${body}`); + } + return lines.length ? lines.join("\n\n---\n\n") : "(empty thread)"; +} + +// --- state ------------------------------------------------------------------- + +import { readFileSync, writeFileSync, existsSync } from "fs"; +const STATE_FILE = ".agent_state.json"; + +function loadState(): Record { + if (existsSync(STATE_FILE)) { + try { + return JSON.parse(readFileSync(STATE_FILE, "utf-8")); + } catch { + return {}; + } + } + return {}; +} + +function saveState(state: Record): void { + writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); +} + +// --- tools ------------------------------------------------------------------- + +const replyToEmail = tool({ + name: "reply_to_email", + description: + "Send a reply to an email and label it with a category.", + parameters: z.object({ + inboxId: z.string().describe("The inbox ID"), + messageId: z.string().describe("The message ID to reply to"), + text: z.string().describe("The reply body"), + category: z.enum(CATEGORIES).describe("Email category"), + }), + execute: async ({ inboxId, messageId, text, category }) => { + await mail.inboxes.messages.reply(inboxId, messageId, { text }); + try { + await mail.inboxes.messages.update(inboxId, messageId, { + removeLabels: ["unread"], + addLabels: [category, "auto-replied"], + }); + } catch {} + return `Reply sent and labeled as '${category}'.`; + }, +}); + +const escalateToHuman = tool({ + name: "escalate_to_human", + description: + "Forward an email to the human team when the agent can't confidently respond.", + parameters: z.object({ + inboxId: z.string().describe("The inbox ID"), + messageId: z.string().describe("The message ID to escalate"), + reason: z.string().describe("Why this needs human attention"), + category: z.enum(CATEGORIES).describe("Email category"), + }), + execute: async ({ inboxId, messageId, reason, category }) => { + await mail.inboxes.messages.forward(inboxId, messageId, { + to: [ESCALATION_EMAIL], + text: `[${category.toUpperCase()} — ESCALATION] ${reason}`, + }); + await mail.inboxes.messages.reply(inboxId, messageId, { + text: "Thanks for reaching out. I've flagged this for our team and someone will follow up with you shortly.", + }); + try { + await mail.inboxes.messages.update(inboxId, messageId, { + removeLabels: ["unread"], + addLabels: [category, "escalated"], + }); + } catch {} + return `Escalated. Category: ${category}. Reason: ${reason}`; + }, +}); + +const skipMessage = tool({ + name: "skip_message", + description: "Skip a message (spam, auto-reply, or not actionable).", + parameters: z.object({ + inboxId: z.string().describe("The inbox ID"), + messageId: z.string().describe("The message to skip"), + reason: z.string().describe("Why the message is being skipped"), + }), + execute: async ({ inboxId, messageId, reason }) => { + try { + await mail.inboxes.messages.update(inboxId, messageId, { + removeLabels: ["unread"], + addLabels: ["skipped"], + }); + } catch {} + return `Message skipped: ${reason}`; + }, +}); + +// --- agent ------------------------------------------------------------------- + +const triageAgent = new Agent({ + name: "EmailTriageAgent", + instructions: `You are ${AGENT_NAME}, an email triage agent for ${PRODUCT_NAME}. + +Your job is to process incoming emails. For each email: + +1. Read the full conversation thread for context. +2. Classify the email into one of: ${CATEGORIES.join(", ")}. +3. Decide on an action: + - reply_to_email: You're confident you can answer. + - escalate_to_human: Complex, sensitive, or unsure. + - skip_message: Spam, auto-reply, or no-reply address. + +Guidelines: +- Be helpful, professional, and concise. +- Sign replies as "${AGENT_NAME}, ${PRODUCT_NAME} Support". +- Never promise things you can't verify — escalate those. +- For billing and urgent issues, prefer escalation.`, + model: MODEL as any, + tools: [replyToEmail, escalateToHuman, skipMessage], +}); + +// --- inbox management -------------------------------------------------------- + +async function getOrCreateInbox() { + const state = loadState(); + if (state.inboxId) { + try { + return await mail.inboxes.get(state.inboxId); + } catch (e) { + console.log(`(stale inbox, creating new: ${e})`); + } + } + + const inbox = await mail.inboxes.create({ + username: INBOX_USERNAME, + displayName: `${PRODUCT_NAME} Triage`, + }); + state.inboxId = inbox.inboxId; + state.email = inbox.email; + saveState(state); + return inbox; +} + +// --- main loop --------------------------------------------------------------- + +async function processMessage(message: any, inbox: any) { + console.log(` → fetching thread ${message.threadId}`); + const thread = await mail.inboxes.threads.get(inbox.inboxId, message.threadId); + const context = buildThreadContext(thread, inbox.email); + + const prompt = [ + `New email in inbox ${inbox.inboxId}.`, + `Message ID: ${message.messageId}`, + `From: ${senderEmail(message)}`, + `Subject: ${message.subject ?? "(no subject)"}`, + `\n--- Thread ---\n${context}`, + `\nTriage this email. Use exactly one tool.`, + ].join("\n"); + + const result = await run(triageAgent, prompt); + console.log(` ✓ agent output: ${String(result.finalOutput).slice(0, 120)}...`); +} + +async function main() { + const inbox = await getOrCreateInbox(); + console.log(`\n📬 Email triage agent live at: ${inbox.email}`); + console.log(` Escalating to: ${ESCALATION_EMAIL}`); + console.log(` Model: ${MODEL}`); + console.log(` Polling every ${POLL_INTERVAL}s. Ctrl-C to stop.\n`); + + const seen = new Set(); + + while (true) { + try { + const resp = await mail.inboxes.messages.list(inbox.inboxId, { + labels: ["unread"], + }); + const newMsgs = (resp.messages ?? []).filter( + (m: any) => !seen.has(m.messageId) + ); + for (const m of newMsgs) { + seen.add(m.messageId); + if (senderEmail(m) === inbox.email.toLowerCase()) continue; + console.log( + `\n📩 from ${senderEmail(m)}: ${(m.subject ?? "(no subject)").slice(0, 60)}` + ); + try { + await processMessage(m, inbox); + } catch (e) { + console.error(` ! error processing: ${e}`); + } + } + } catch (e) { + console.error(`poll error: ${e}`); + } + await new Promise((r) => setTimeout(r, POLL_INTERVAL * 1000)); + } +} + +main().catch(console.error); diff --git a/openai-agents-email-triage/typescript/tsconfig.json b/openai-agents-email-triage/typescript/tsconfig.json new file mode 100644 index 0000000..1d6524b --- /dev/null +++ b/openai-agents-email-triage/typescript/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src"] +}