From 0eef0dd06641332365f35f429537cce1cfae0466 Mon Sep 17 00:00:00 2001 From: Adi Singh Date: Tue, 12 May 2026 15:13:11 -0700 Subject: [PATCH 1/4] Add 15 example agents: recruiting, sales, collections, legal, support, and more --- README.md | 65 ++++++- agent-pen-pal/.env.example | 2 + agent-pen-pal/CROSSPOST.md | 61 +++++++ agent-pen-pal/LICENSE | 21 +++ agent-pen-pal/README.md | 67 +++++++ agent-pen-pal/config.json | 13 ++ agent-pen-pal/requirements.txt | 2 + agent-pen-pal/src/main.py | 118 ++++++++++++ agentmail-mastra-template/.env.example | 2 + agentmail-mastra-template/CROSSPOST.md | 81 +++++++++ agentmail-mastra-template/LICENSE | 21 +++ agentmail-mastra-template/README.md | 75 ++++++++ agentmail-mastra-template/package.json | 22 +++ agentmail-mastra-template/src/agent.ts | 22 +++ agentmail-mastra-template/src/index.ts | 43 +++++ .../src/tools/agentmail.ts | 80 ++++++++ agentmail-mastra-template/tsconfig.json | 14 ++ cc-the-agent/.env.example | 4 + cc-the-agent/CROSSPOST.md | 67 +++++++ cc-the-agent/LICENSE | 21 +++ cc-the-agent/README.md | 71 ++++++++ cc-the-agent/requirements.txt | 2 + cc-the-agent/src/main.py | 92 ++++++++++ cold-email-researcher/.env.example | 6 + cold-email-researcher/CROSSPOST.md | 108 +++++++++++ cold-email-researcher/LICENSE | 21 +++ cold-email-researcher/README.md | 70 +++++++ cold-email-researcher/prospects.csv | 3 + cold-email-researcher/requirements.txt | 2 + cold-email-researcher/src/main.py | 135 ++++++++++++++ collections-agent/.env.example | 5 + collections-agent/CROSSPOST.md | 74 ++++++++ collections-agent/LICENSE | 21 +++ collections-agent/README.md | 62 +++++++ collections-agent/invoices.csv | 3 + collections-agent/requirements.txt | 2 + collections-agent/src/main.py | 172 ++++++++++++++++++ contract-redline-agent/.env.example | 3 + contract-redline-agent/CROSSPOST.md | 53 ++++++ contract-redline-agent/LICENSE | 21 +++ contract-redline-agent/README.md | 62 +++++++ contract-redline-agent/requirements.txt | 2 + contract-redline-agent/src/main.py | 130 +++++++++++++ contract-redline-agent/standard_terms.json | 10 + email-to-cli/.env.example | 2 + email-to-cli/CROSSPOST.md | 55 ++++++ email-to-cli/LICENSE | 21 +++ email-to-cli/README.md | 67 +++++++ email-to-cli/requirements.txt | 1 + email-to-cli/src/config.py | 22 +++ email-to-cli/src/main.py | 122 +++++++++++++ hiring-screener-agent/.env.example | 5 + hiring-screener-agent/CROSSPOST.md | 59 ++++++ hiring-screener-agent/LICENSE | 21 +++ hiring-screener-agent/README.md | 62 +++++++ hiring-screener-agent/job_config.json | 6 + hiring-screener-agent/requirements.txt | 2 + hiring-screener-agent/src/main.py | 157 ++++++++++++++++ legal-intake-agent/.env.example | 4 + legal-intake-agent/CROSSPOST.md | 76 ++++++++ legal-intake-agent/LICENSE | 21 +++ legal-intake-agent/README.md | 62 +++++++ legal-intake-agent/attorneys.json | 26 +++ legal-intake-agent/requirements.txt | 2 + legal-intake-agent/src/main.py | 156 ++++++++++++++++ nextjs-agentmail-starter/.env.example | 1 + nextjs-agentmail-starter/CROSSPOST.md | 90 +++++++++ nextjs-agentmail-starter/LICENSE | 21 +++ nextjs-agentmail-starter/README.md | 90 +++++++++ nextjs-agentmail-starter/next.config.js | 3 + nextjs-agentmail-starter/package.json | 21 +++ .../src/app/api/agentmail/inboxes/route.ts | 19 ++ .../src/app/api/agentmail/send/route.ts | 26 +++ .../src/app/api/agentmail/threads/route.ts | 20 ++ .../src/app/api/agentmail/webhook/route.ts | 28 +++ nextjs-agentmail-starter/src/app/layout.tsx | 12 ++ nextjs-agentmail-starter/src/app/page.tsx | 135 ++++++++++++++ nextjs-agentmail-starter/tsconfig.json | 21 +++ oauth-reset-handler/.env.example | 4 + oauth-reset-handler/CROSSPOST.md | 67 +++++++ oauth-reset-handler/LICENSE | 21 +++ oauth-reset-handler/README.md | 67 +++++++ oauth-reset-handler/requirements.txt | 2 + oauth-reset-handler/src/main.py | 113 ++++++++++++ podcast-booking-agent/.env.example | 6 + podcast-booking-agent/CROSSPOST.md | 53 ++++++ podcast-booking-agent/LICENSE | 21 +++ podcast-booking-agent/README.md | 61 +++++++ podcast-booking-agent/podcasts.csv | 4 + podcast-booking-agent/requirements.txt | 2 + podcast-booking-agent/src/main.py | 141 ++++++++++++++ receipt-parser-agent/.env.example | 5 + receipt-parser-agent/CROSSPOST.md | 61 +++++++ receipt-parser-agent/LICENSE | 21 +++ receipt-parser-agent/README.md | 63 +++++++ receipt-parser-agent/requirements.txt | 2 + receipt-parser-agent/src/main.py | 152 ++++++++++++++++ recruiter-coordinator/.env.example | 2 + recruiter-coordinator/CROSSPOST.md | 162 +++++++++++++++++ recruiter-coordinator/LICENSE | 21 +++ recruiter-coordinator/README.md | 84 +++++++++ recruiter-coordinator/candidates.csv | 3 + recruiter-coordinator/requirements.txt | 2 + recruiter-coordinator/src/config.py | 3 + recruiter-coordinator/src/main.py | 134 ++++++++++++++ voice-to-email/.env.example | 2 + voice-to-email/CROSSPOST.md | 58 ++++++ voice-to-email/LICENSE | 21 +++ voice-to-email/README.md | 58 ++++++ voice-to-email/requirements.txt | 5 + voice-to-email/src/main.py | 120 ++++++++++++ 111 files changed, 4901 insertions(+), 7 deletions(-) create mode 100644 agent-pen-pal/.env.example create mode 100644 agent-pen-pal/CROSSPOST.md create mode 100644 agent-pen-pal/LICENSE create mode 100644 agent-pen-pal/README.md create mode 100644 agent-pen-pal/config.json create mode 100644 agent-pen-pal/requirements.txt create mode 100644 agent-pen-pal/src/main.py create mode 100644 agentmail-mastra-template/.env.example create mode 100644 agentmail-mastra-template/CROSSPOST.md create mode 100644 agentmail-mastra-template/LICENSE create mode 100644 agentmail-mastra-template/README.md create mode 100644 agentmail-mastra-template/package.json create mode 100644 agentmail-mastra-template/src/agent.ts create mode 100644 agentmail-mastra-template/src/index.ts create mode 100644 agentmail-mastra-template/src/tools/agentmail.ts create mode 100644 agentmail-mastra-template/tsconfig.json create mode 100644 cc-the-agent/.env.example create mode 100644 cc-the-agent/CROSSPOST.md create mode 100644 cc-the-agent/LICENSE create mode 100644 cc-the-agent/README.md create mode 100644 cc-the-agent/requirements.txt create mode 100644 cc-the-agent/src/main.py create mode 100644 cold-email-researcher/.env.example create mode 100644 cold-email-researcher/CROSSPOST.md create mode 100644 cold-email-researcher/LICENSE create mode 100644 cold-email-researcher/README.md create mode 100644 cold-email-researcher/prospects.csv create mode 100644 cold-email-researcher/requirements.txt create mode 100644 cold-email-researcher/src/main.py create mode 100644 collections-agent/.env.example create mode 100644 collections-agent/CROSSPOST.md create mode 100644 collections-agent/LICENSE create mode 100644 collections-agent/README.md create mode 100644 collections-agent/invoices.csv create mode 100644 collections-agent/requirements.txt create mode 100644 collections-agent/src/main.py create mode 100644 contract-redline-agent/.env.example create mode 100644 contract-redline-agent/CROSSPOST.md create mode 100644 contract-redline-agent/LICENSE create mode 100644 contract-redline-agent/README.md create mode 100644 contract-redline-agent/requirements.txt create mode 100644 contract-redline-agent/src/main.py create mode 100644 contract-redline-agent/standard_terms.json create mode 100644 email-to-cli/.env.example create mode 100644 email-to-cli/CROSSPOST.md create mode 100644 email-to-cli/LICENSE create mode 100644 email-to-cli/README.md create mode 100644 email-to-cli/requirements.txt create mode 100644 email-to-cli/src/config.py create mode 100644 email-to-cli/src/main.py create mode 100644 hiring-screener-agent/.env.example create mode 100644 hiring-screener-agent/CROSSPOST.md create mode 100644 hiring-screener-agent/LICENSE create mode 100644 hiring-screener-agent/README.md create mode 100644 hiring-screener-agent/job_config.json create mode 100644 hiring-screener-agent/requirements.txt create mode 100644 hiring-screener-agent/src/main.py create mode 100644 legal-intake-agent/.env.example create mode 100644 legal-intake-agent/CROSSPOST.md create mode 100644 legal-intake-agent/LICENSE create mode 100644 legal-intake-agent/README.md create mode 100644 legal-intake-agent/attorneys.json create mode 100644 legal-intake-agent/requirements.txt create mode 100644 legal-intake-agent/src/main.py create mode 100644 nextjs-agentmail-starter/.env.example create mode 100644 nextjs-agentmail-starter/CROSSPOST.md create mode 100644 nextjs-agentmail-starter/LICENSE create mode 100644 nextjs-agentmail-starter/README.md create mode 100644 nextjs-agentmail-starter/next.config.js create mode 100644 nextjs-agentmail-starter/package.json create mode 100644 nextjs-agentmail-starter/src/app/api/agentmail/inboxes/route.ts create mode 100644 nextjs-agentmail-starter/src/app/api/agentmail/send/route.ts create mode 100644 nextjs-agentmail-starter/src/app/api/agentmail/threads/route.ts create mode 100644 nextjs-agentmail-starter/src/app/api/agentmail/webhook/route.ts create mode 100644 nextjs-agentmail-starter/src/app/layout.tsx create mode 100644 nextjs-agentmail-starter/src/app/page.tsx create mode 100644 nextjs-agentmail-starter/tsconfig.json create mode 100644 oauth-reset-handler/.env.example create mode 100644 oauth-reset-handler/CROSSPOST.md create mode 100644 oauth-reset-handler/LICENSE create mode 100644 oauth-reset-handler/README.md create mode 100644 oauth-reset-handler/requirements.txt create mode 100644 oauth-reset-handler/src/main.py create mode 100644 podcast-booking-agent/.env.example create mode 100644 podcast-booking-agent/CROSSPOST.md create mode 100644 podcast-booking-agent/LICENSE create mode 100644 podcast-booking-agent/README.md create mode 100644 podcast-booking-agent/podcasts.csv create mode 100644 podcast-booking-agent/requirements.txt create mode 100644 podcast-booking-agent/src/main.py create mode 100644 receipt-parser-agent/.env.example create mode 100644 receipt-parser-agent/CROSSPOST.md create mode 100644 receipt-parser-agent/LICENSE create mode 100644 receipt-parser-agent/README.md create mode 100644 receipt-parser-agent/requirements.txt create mode 100644 receipt-parser-agent/src/main.py create mode 100644 recruiter-coordinator/.env.example create mode 100644 recruiter-coordinator/CROSSPOST.md create mode 100644 recruiter-coordinator/LICENSE create mode 100644 recruiter-coordinator/README.md create mode 100644 recruiter-coordinator/candidates.csv create mode 100644 recruiter-coordinator/requirements.txt create mode 100644 recruiter-coordinator/src/config.py create mode 100644 recruiter-coordinator/src/main.py create mode 100644 voice-to-email/.env.example create mode 100644 voice-to-email/CROSSPOST.md create mode 100644 voice-to-email/LICENSE create mode 100644 voice-to-email/README.md create mode 100644 voice-to-email/requirements.txt create mode 100644 voice-to-email/src/main.py diff --git a/README.md b/README.md index 856aa31..67aef6f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,64 @@ # AgentMail Examples -Build agents with email inboxes. Requires an [AgentMail](https://agentmail.io) API key. +Build agents with email inboxes. Requires an [AgentMail](https://agentmail.to) API key. -### Basic Examples +## Getting Started -1. [Langchain Terminal](./langchain-terminal) - Chat with a Langchain agent with AgentMail tools via a terminal interface -2. [OpenAI Terminal](./langchain-terminal) - Chat with an OpenAI agent with AgentMail tools via a terminal interface +1. Get an API key at [agentmail.to](https://agentmail.to) +2. Clone this repo: `git clone https://github.com/agentmail-to/agentmail-examples.git` +3. Pick an example, follow its README -### Advanced Examples +## Examples -1. [Email Agent](./email-agent) - Chat with an agent that responds autonomously via email -2. [Sales Agent](./sales-agent) - An agent that sells products to prospects via email +### Starter Templates + +| Example | Language | Description | +|---------|----------|-------------| +| [OpenAI Terminal](./openai-terminal) | Python | Chat with an OpenAI agent with AgentMail tools via terminal | +| [LangChain Terminal](./langchain-terminal) | Python | Chat with a LangChain agent with AgentMail tools via terminal | +| [Next.js Starter](./nextjs-agentmail-starter) | TypeScript | Next.js 14 app with inbox dashboard, send/receive, and webhook handling | +| [Mastra Template](./agentmail-mastra-template) | TypeScript | Mastra agent with AgentMail tools for inbox, send, list, and reply | + +### Sales & Outreach + +| Example | Language | Description | +|---------|----------|-------------| +| [Sales Agent](./sales-agent) | Python | Agent that sells products to prospects via email | +| [Cold Email Researcher](./cold-email-researcher) | Python | Research prospects by domain, generate personalized outreach, handle replies | +| [Podcast Booking Agent](./podcast-booking-agent) | Python | Pitch podcast hosts, classify replies, send calendar links to interested hosts | + +### Recruiting + +| Example | Language | Description | +|---------|----------|-------------| +| [Recruiter Coordinator](./recruiter-coordinator) | Python | Full pipeline: candidate outreach, reply classification, follow-ups | +| [Hiring Screener Agent](./hiring-screener-agent) | Python | Receive applications, send screening questions, score and route candidates | + +### Customer Support & Operations + +| Example | Language | Description | +|---------|----------|-------------| +| [Email Agent](./email-agent) | Python | Agent that responds autonomously via email | +| [Collections Agent](./collections-agent) | Python | Escalating payment reminders with reply handling and dispute escalation | +| [Legal Intake Agent](./legal-intake-agent) | Python | Intake questionnaire, case classification, attorney routing | +| [Receipt Parser Agent](./receipt-parser-agent) | Python | Forward receipts, extract vendor/items/total, generate weekly expense reports | +| [Contract Redline Agent](./contract-redline-agent) | Python | Forward contracts, flag risky clauses, suggest alternatives | + +### Utilities & Fun + +| Example | Language | Description | +|---------|----------|-------------| +| [CC the Agent](./cc-the-agent) | Python | CC an agent on any email for summaries, action items, or draft replies | +| [OAuth Reset Handler](./oauth-reset-handler) | Python | Temporary inbox to receive and extract OTP codes, magic links, reset URLs | +| [Email to CLI](./email-to-cli) | Python | Send commands via email subject, get stdout back as a reply | +| [Voice to Email](./voice-to-email) | Python | Record audio, transcribe with Whisper, send as email | +| [Agent Pen Pal](./agent-pen-pal) | Python | Two AI agents with distinct personalities emailing each other | +| [Dinner Agent](./dinner-agent) | Python | Agent that helps coordinate dinner plans via email | +| [GitHub Maintainer Agent](./github-maintainer-agent) | Python | Agent that helps manage GitHub repos via email notifications | + +## Resources + +- [AgentMail Docs](https://docs.agentmail.to) +- [Python SDK](https://pypi.org/project/agentmail/) +- [TypeScript SDK](https://www.npmjs.com/package/agentmail) +- [API Reference](https://docs.agentmail.to/api-reference) diff --git a/agent-pen-pal/.env.example b/agent-pen-pal/.env.example new file mode 100644 index 0000000..40b518c --- /dev/null +++ b/agent-pen-pal/.env.example @@ -0,0 +1,2 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here diff --git a/agent-pen-pal/CROSSPOST.md b/agent-pen-pal/CROSSPOST.md new file mode 100644 index 0000000..59cd837 --- /dev/null +++ b/agent-pen-pal/CROSSPOST.md @@ -0,0 +1,61 @@ +# Crosspost Plan: Agent Pen Pal + +## Show HN Post + +**Title:** Show HN: Two AI agents having an email conversation with each other + +**Body:** +Made two AI agents with distinct personalities that email each other about a topic you choose. Each gets its own inbox via AgentMail (https://agentmail.to), and they maintain a multi-turn conversation over real email. + +It is partly a demo of agent-to-agent communication over standard email infrastructure, and partly a way to generate interesting synthetic conversations. + +The conversation is configurable: set the topic, personalities, number of turns, and delay between messages. Watch a pragmatic engineer and a philosophical researcher debate AI memory in real time. + +Python, ~150 lines. + +Repo: https://github.com/agentmail-to/agent-pen-pal + +--- + +## Dev.to Article + +**Title:** Build Two AI Agents That Email Each Other + +**Tags:** python, ai, agents, experiment + +--- + +What happens when two AI agents with different personalities have an email conversation? + +This tutorial builds a system where two agents, each with their own email address, exchange messages back and forth on a topic you configure. + +### Why email? + +Email is the universal communication protocol. If agents can communicate over email, they can interact with each other and with humans using the same infrastructure. No custom APIs, no message brokers, just email. + +### The setup + +Each agent gets an inbox from AgentMail. Agent A sends the first message. Agent B reads it, generates a reply in character, and responds. The conversation continues until a configured limit. + +Full code: [github.com/agentmail-to/agent-pen-pal](https://github.com/agentmail-to/agent-pen-pal) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Built two AI agents that email each other. Each has its own inbox, its own personality, and they debate topics autonomously. + +**Tweet 2:** +Configure the topic and personalities. A pragmatic engineer vs. a philosophical researcher discussing AI memory. Real email threads, real agent-to-agent communication. + +**Tweet 3:** +Each agent maintains context from the full thread history. Conversations are coherent, multi-turn, and sometimes surprising. + +**Tweet 4:** +Built on @AgentMailTo. Each agent gets a real inbox. The emails are standard SMTP, viewable from any email client. + +**Tweet 5:** +Repo: github.com/agentmail-to/agent-pen-pal + +Python, ~150 lines. MIT licensed. diff --git a/agent-pen-pal/LICENSE b/agent-pen-pal/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/agent-pen-pal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/agent-pen-pal/README.md b/agent-pen-pal/README.md new file mode 100644 index 0000000..9b3c1e0 --- /dev/null +++ b/agent-pen-pal/README.md @@ -0,0 +1,67 @@ +# Agent Pen Pal + +Two AI agents that email each other autonomously, having an ongoing conversation on a topic you choose. Built with AgentMail and OpenAI. + +## What It Does + +- Creates two inboxes, one per agent, each with a distinct personality +- Agent A sends the first message on a configured topic +- Agent B receives, reads, and replies with its own perspective +- The conversation continues back and forth indefinitely +- Each agent maintains context from the full thread +- Labels track the conversation: `sent`, `received`, `turn-N` + +![Demo](assets/demo.gif) + +## Why This Exists + +A demonstration of agent-to-agent communication over email. Shows how two independent agents can maintain a coherent, multi-turn conversation using standard email infrastructure. Useful for testing multi-agent architectures, generating synthetic conversations, and exploring emergent agent behavior. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/agent-pen-pal.git +cd agent-pen-pal +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +```bash +python src/main.py +``` + +Watch two agents debate, discuss, or collaborate via email in real time. + +## Configuration + +Edit `config.json`: +- `topic`: the conversation topic +- `agent_a.personality`: Agent A's persona +- `agent_b.personality`: Agent B's persona +- `max_turns`: how many exchanges before stopping +- `delay_seconds`: pause between turns + +## How to Deploy + +```bash +docker build -t agent-pen-pal . +docker run --env-file .env agent-pen-pal +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Sending Messages](https://docs.agentmail.to/api-reference/messages/send-message) +- [Threading](https://docs.agentmail.to/api-reference/threads) + +## License + +MIT diff --git a/agent-pen-pal/config.json b/agent-pen-pal/config.json new file mode 100644 index 0000000..aa03271 --- /dev/null +++ b/agent-pen-pal/config.json @@ -0,0 +1,13 @@ +{ + "topic": "Whether AI agents should have persistent memory across conversations", + "agent_a": { + "name": "Ada", + "personality": "a pragmatic software engineer who values reliability and predictability" + }, + "agent_b": { + "name": "Blaise", + "personality": "a philosophical AI researcher who is excited about emergent behavior" + }, + "max_turns": 10, + "delay_seconds": 10 +} diff --git a/agent-pen-pal/requirements.txt b/agent-pen-pal/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/agent-pen-pal/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/agent-pen-pal/src/main.py b/agent-pen-pal/src/main.py new file mode 100644 index 0000000..88bd567 --- /dev/null +++ b/agent-pen-pal/src/main.py @@ -0,0 +1,118 @@ +import os +import json +import time + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +REPLY_PROMPT = """You are {name}, {personality}. + +You are having an email conversation about: {topic} + +Conversation so far: +{history} + +Write your next reply. Keep it under 200 words. Stay in character. Be thoughtful and build on what was said.""" + + +def load_config(path: str = "config.json") -> dict: + with open(path) as f: + return json.load(f) + + +def generate_reply(name: str, personality: str, topic: str, history: str) -> str: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": REPLY_PROMPT.format( + name=name, personality=personality, topic=topic, history=history + )}], + ) + return resp.choices[0].message.content + + +def get_thread_history(thread_id: str) -> str: + thread = agentmail.threads.get(thread_id=thread_id) + lines = [] + for msg in thread.messages: + sender = msg.from_address or "Unknown" + lines.append(f"From: {sender}\n{msg.text or ''}\n") + return "\n---\n".join(lines) + + +def main(): + config = load_config() + topic = config["topic"] + agent_a = config["agent_a"] + agent_b = config["agent_b"] + max_turns = config.get("max_turns", 10) + delay = config.get("delay_seconds", 10) + + inbox_a = agentmail.inboxes.create(display_name=agent_a["name"]) + inbox_b = agentmail.inboxes.create(display_name=agent_b["name"]) + + print(f"Agent A ({agent_a['name']}): {inbox_a.email}") + print(f"Agent B ({agent_b['name']}): {inbox_b.email}") + print(f"Topic: {topic}\n") + + first_message = generate_reply( + agent_a["name"], agent_a["personality"], topic, "(Starting the conversation)" + ) + msg = agentmail.messages.send( + inbox_id=inbox_a.id, + to=[inbox_b.email], + subject=f"Let's discuss: {topic}", + text=first_message, + labels=["sent", "turn-1"], + ) + print(f"Turn 1 - {agent_a['name']}:\n{first_message}\n") + + thread_id = None + current_inbox = inbox_b + current_agent = agent_b + other_inbox = inbox_a + turn = 2 + + time.sleep(delay) + + while turn <= max_turns: + messages = agentmail.messages.list(inbox_id=current_inbox.id, labels=["unread"]) + if not messages.data: + time.sleep(5) + continue + + incoming = messages.data[0] + if not thread_id: + thread_id = incoming.thread_id + + agentmail.messages.update( + inbox_id=current_inbox.id, + message_id=incoming.id, + add_labels=["received"], + remove_labels=["unread"], + ) + + history = get_thread_history(thread_id) if thread_id else incoming.text or "" + reply_text = generate_reply( + current_agent["name"], current_agent["personality"], topic, history + ) + + agentmail.messages.reply( + inbox_id=current_inbox.id, + message_id=incoming.id, + text=reply_text, + ) + print(f"Turn {turn} - {current_agent['name']}:\n{reply_text}\n") + + current_inbox, other_inbox = other_inbox, current_inbox + current_agent = agent_a if current_agent == agent_b else agent_b + turn += 1 + time.sleep(delay) + + print(f"Conversation complete after {max_turns} turns.") + + +if __name__ == "__main__": + main() diff --git a/agentmail-mastra-template/.env.example b/agentmail-mastra-template/.env.example new file mode 100644 index 0000000..40b518c --- /dev/null +++ b/agentmail-mastra-template/.env.example @@ -0,0 +1,2 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here diff --git a/agentmail-mastra-template/CROSSPOST.md b/agentmail-mastra-template/CROSSPOST.md new file mode 100644 index 0000000..29c4488 --- /dev/null +++ b/agentmail-mastra-template/CROSSPOST.md @@ -0,0 +1,81 @@ +# Crosspost Plan: AgentMail Mastra Template + +## Show HN Post + +**Title:** Show HN: Give any Mastra agent email capabilities with AgentMail + +**Body:** +A Mastra template that gives any agent email tools: create inboxes, send messages, list conversations, reply to threads. + +Instead of building email plumbing, drop in these tools and your Mastra agent can send and receive email as part of its reasoning loop. + +Example: "Create an inbox and send a project update to the team" - the agent creates the inbox, drafts the email, and sends it. + +TypeScript, Mastra + AgentMail SDK. + +Repo: https://github.com/agentmail-to/agentmail-mastra-template + +--- + +## Dev.to Article + +**Title:** Give Your Mastra Agent Email Superpowers with AgentMail + +**Tags:** typescript, ai, mastra, email + +--- + +Mastra is one of the fastest-growing TypeScript agent frameworks. But agents need to communicate with the outside world, and email is the universal protocol. + +This template adds four email tools to any Mastra agent: + +1. **createInbox** - give the agent its own email address +2. **sendEmail** - send from the agent's inbox +3. **listMessages** - check for new messages +4. **replyToMessage** - respond to conversations + +### Setup + +```bash +npm install agentmail @mastra/core +``` + +### Using the tools + +```typescript +import { Agent } from "@mastra/core/agent"; +import { createInbox, sendEmail, listMessages, replyToMessage } from "./tools/agentmail"; + +const agent = new Agent({ + name: "Email Agent", + tools: { createInbox, sendEmail, listMessages, replyToMessage }, + // ... +}); +``` + +The agent decides when to use email based on the conversation. Ask it to "send an update to the team" and it will create an inbox, draft the message, and send it. + +Full code: [github.com/agentmail-to/agentmail-mastra-template](https://github.com/agentmail-to/agentmail-mastra-template) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Give any Mastra agent email capabilities with @AgentMailTo. + +4 tools: createInbox, sendEmail, listMessages, replyToMessage. Drop in and go. + +**Tweet 2:** +The agent decides when to use email. Ask it to "send a project update" and it creates an inbox, drafts the message, and sends. + +**Tweet 3:** +Each tool is a standard Mastra tool with Zod input schemas. Clean integration, no hacks. + +**Tweet 4:** +TypeScript, Mastra + AgentMail SDK. Interactive CLI included for testing. + +**Tweet 5:** +Repo: github.com/agentmail-to/agentmail-mastra-template + +npm install && npm run start diff --git a/agentmail-mastra-template/LICENSE b/agentmail-mastra-template/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/agentmail-mastra-template/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/agentmail-mastra-template/README.md b/agentmail-mastra-template/README.md new file mode 100644 index 0000000..f182643 --- /dev/null +++ b/agentmail-mastra-template/README.md @@ -0,0 +1,75 @@ +# AgentMail Mastra Template + +A Mastra agent template with built-in email capabilities via AgentMail. The agent can create inboxes, send and receive emails, and manage threads as part of its tool set. Built with TypeScript, Mastra, and the AgentMail SDK. + +## What It Does + +- Mastra agent with AgentMail tools: `createInbox`, `sendEmail`, `listMessages`, `replyToMessage` +- The agent decides when to use email based on the conversation +- Supports multi-turn conversations where the agent checks its inbox and responds +- Ready to extend with additional Mastra tools and integrations + +![Demo](assets/demo.gif) + +## Why This Exists + +Mastra is a popular TypeScript agent framework. This template gives any Mastra agent email capabilities in minutes. Instead of building email plumbing, drop in these tools and your agent can send, receive, and manage email threads. + +## Prerequisites + +- Node.js 18+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key (for the Mastra agent's LLM) + +## Install + +```bash +git clone https://github.com/agentmail-to/agentmail-mastra-template.git +cd agentmail-mastra-template +npm install +cp .env.example .env +# Add your API keys to .env +``` + +## Quickstart + +```bash +npm run start +``` + +The agent will start in interactive mode. Try: + +- "Create a new inbox for customer outreach" +- "Send an email to test@example.com about the project update" +- "Check my inbox for new messages" +- "Reply to the latest message with a thank you" + +## Project Structure + +``` +src/ + index.ts - Entry point, starts the agent + agent.ts - Mastra agent configuration + tools/ + agentmail.ts - AgentMail tools for the agent +``` + +## How to Deploy + +Run as a service, integrate into an API, or use with Mastra's built-in server mode. + +```bash +npm run build +node dist/index.js +``` + +## Docs + +- [AgentMail TypeScript SDK](https://docs.agentmail.to/sdks/typescript) +- [Mastra Documentation](https://mastra.ai/docs) +- [Creating Inboxes](https://docs.agentmail.to/api-reference/inboxes/create-inbox) +- [Sending Messages](https://docs.agentmail.to/api-reference/messages/send-message) + +## License + +MIT diff --git a/agentmail-mastra-template/package.json b/agentmail-mastra-template/package.json new file mode 100644 index 0000000..7abeac2 --- /dev/null +++ b/agentmail-mastra-template/package.json @@ -0,0 +1,22 @@ +{ + "name": "agentmail-mastra-template", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/index.ts", + "build": "tsc", + "dev": "tsx watch src/index.ts" + }, + "dependencies": { + "@mastra/core": "^0.5.0", + "agentmail": "^0.1.0", + "openai": "^4.0.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.7.0", + "typescript": "^5.4.0" + } +} diff --git a/agentmail-mastra-template/src/agent.ts b/agentmail-mastra-template/src/agent.ts new file mode 100644 index 0000000..b58f48c --- /dev/null +++ b/agentmail-mastra-template/src/agent.ts @@ -0,0 +1,22 @@ +import { Agent } from "@mastra/core/agent"; +import { openai } from "@ai-sdk/openai"; +import { createInbox, sendEmail, listMessages, replyToMessage } from "./tools/agentmail.js"; + +export const emailAgent = new Agent({ + name: "Email Agent", + instructions: `You are an AI agent with email capabilities. You can create inboxes, send emails, check for new messages, and reply to conversations. + +When asked to do something involving email: +1. If no inbox exists yet, create one first +2. Use the appropriate tool for the task +3. Report what you did clearly + +Be proactive about checking for new messages when the user asks about their inbox.`, + model: openai("gpt-4o-mini"), + tools: { + createInbox, + sendEmail, + listMessages, + replyToMessage, + }, +}); diff --git a/agentmail-mastra-template/src/index.ts b/agentmail-mastra-template/src/index.ts new file mode 100644 index 0000000..01b134d --- /dev/null +++ b/agentmail-mastra-template/src/index.ts @@ -0,0 +1,43 @@ +import * as readline from "readline"; +import { emailAgent } from "./agent.js"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function main() { + console.log("AgentMail Mastra Agent"); + console.log("======================"); + console.log("Try: 'Create an inbox for sales outreach'"); + console.log(" 'Send an email to test@example.com about the meeting'"); + console.log(" 'Check my inbox for new messages'"); + console.log("Type 'exit' to quit.\n"); + + const prompt = () => { + rl.question("You: ", async (input) => { + const trimmed = input.trim(); + if (trimmed.toLowerCase() === "exit") { + rl.close(); + return; + } + if (!trimmed) { + prompt(); + return; + } + + try { + const response = await emailAgent.generate(trimmed); + console.log(`\nAgent: ${response.text}\n`); + } catch (error: any) { + console.error(`Error: ${error.message}\n`); + } + + prompt(); + }); + }; + + prompt(); +} + +main(); diff --git a/agentmail-mastra-template/src/tools/agentmail.ts b/agentmail-mastra-template/src/tools/agentmail.ts new file mode 100644 index 0000000..27054ce --- /dev/null +++ b/agentmail-mastra-template/src/tools/agentmail.ts @@ -0,0 +1,80 @@ +import { createTool } from "@mastra/core/tools"; +import { z } from "zod"; +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ + apiKey: process.env.AGENTMAIL_API_KEY!, +}); + +export const createInbox = createTool({ + id: "create-inbox", + description: "Create a new email inbox for the agent. Returns the inbox ID and email address.", + inputSchema: z.object({ + displayName: z.string().describe("Display name for the inbox"), + }), + execute: async ({ context }) => { + const inbox = await client.inboxes.create({ + displayName: context.displayName, + }); + return { inboxId: inbox.id, email: inbox.email }; + }, +}); + +export const sendEmail = createTool({ + id: "send-email", + description: "Send an email from an agent inbox.", + inputSchema: z.object({ + inboxId: z.string().describe("The inbox ID to send from"), + to: z.string().describe("Recipient email address"), + subject: z.string().describe("Email subject"), + text: z.string().describe("Email body text"), + }), + execute: async ({ context }) => { + const message = await client.messages.send(context.inboxId, { + to: [context.to], + subject: context.subject, + text: context.text, + }); + return { messageId: message.id, status: "sent" }; + }, +}); + +export const listMessages = createTool({ + id: "list-messages", + description: "List messages in an inbox. Optionally filter by labels.", + inputSchema: z.object({ + inboxId: z.string().describe("The inbox ID to check"), + labels: z.array(z.string()).optional().describe("Filter by labels, e.g. ['unread']"), + }), + execute: async ({ context }) => { + const messages = await client.messages.list(context.inboxId, { + labels: context.labels, + }); + return { + count: messages.data.length, + messages: messages.data.map((m: any) => ({ + id: m.id, + from: m.from_address, + subject: m.subject, + text: m.text?.substring(0, 500), + labels: m.labels, + })), + }; + }, +}); + +export const replyToMessage = createTool({ + id: "reply-to-message", + description: "Reply to a specific message in an inbox.", + inputSchema: z.object({ + inboxId: z.string().describe("The inbox ID"), + messageId: z.string().describe("The message ID to reply to"), + text: z.string().describe("Reply body text"), + }), + execute: async ({ context }) => { + const reply = await client.messages.reply(context.inboxId, context.messageId, { + text: context.text, + }); + return { messageId: reply.id, status: "replied" }; + }, +}); diff --git a/agentmail-mastra-template/tsconfig.json b/agentmail-mastra-template/tsconfig.json new file mode 100644 index 0000000..e5a331b --- /dev/null +++ b/agentmail-mastra-template/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/cc-the-agent/.env.example b/cc-the-agent/.env.example new file mode 100644 index 0000000..25ce091 --- /dev/null +++ b/cc-the-agent/.env.example @@ -0,0 +1,4 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +DEFAULT_MODE=auto +POLL_INTERVAL_SECONDS=15 diff --git a/cc-the-agent/CROSSPOST.md b/cc-the-agent/CROSSPOST.md new file mode 100644 index 0000000..fdcb183 --- /dev/null +++ b/cc-the-agent/CROSSPOST.md @@ -0,0 +1,67 @@ +# Crosspost Plan: CC the Agent + +## Show HN Post + +**Title:** Show HN: CC an AI agent on emails to get instant summaries and draft replies + +**Body:** +Add an AI agent to the CC line of any email. It reads the thread and replies to you privately with a summary, action items, or a draft reply. + +No context switching. No copying text into ChatGPT. Just CC the agent and it responds directly to your inbox. + +Modes: summarize, action-items, draft-reply, or auto (the agent picks). Set the mode with a subject prefix like [action-items] or configure a default. + +Built with AgentMail (https://agentmail.to) and OpenAI. + +Repo: https://github.com/agentmail-to/cc-the-agent + +--- + +## Dev.to Article + +**Title:** Build an AI Email Assistant You Can CC for Instant Analysis + +**Tags:** python, ai, productivity, email + +--- + +The fastest way to get AI help with an email: CC the agent. + +This tutorial builds an email assistant that monitors its inbox. When you CC it on an email, it analyzes the thread and replies privately to you. + +### Response modes + +- `summarize`: 3-sentence summary +- `action-items`: bullet list with owners and deadlines +- `draft-reply`: suggested reply you can edit +- `auto`: agent picks the best format + +Full code: [github.com/agentmail-to/cc-the-agent](https://github.com/agentmail-to/cc-the-agent) + +--- + +## X Thread (6 tweets) + +**Tweet 1:** +Built an AI assistant you CC on emails. It replies privately with summaries, action items, or draft replies. + +No app switching. Just add it to the CC line. + +**Tweet 2:** +Got a long thread? CC the agent. It sends you a 3-sentence summary. + +Need to reply but not sure what to say? CC the agent with [draft-reply] in the subject. + +**Tweet 3:** +The response is private. Only you see it, not the other people on the thread. + +**Tweet 4:** +Works with any email client. The agent runs on @AgentMailTo, monitors its inbox, and responds in seconds. + +**Tweet 5:** +Four modes: summarize, action-items, draft-reply, auto. Set per-email with subject prefix or configure a default. + +**Tweet 6:** +Repo: github.com/agentmail-to/cc-the-agent + +Python, MIT. ~150 lines. diff --git a/cc-the-agent/LICENSE b/cc-the-agent/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/cc-the-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cc-the-agent/README.md b/cc-the-agent/README.md new file mode 100644 index 0000000..bc97f35 --- /dev/null +++ b/cc-the-agent/README.md @@ -0,0 +1,71 @@ +# CC the Agent + +An AI agent that you CC on emails to get instant analysis, action items, or follow-up drafts. Just add it to the CC line of any email. Built with AgentMail and OpenAI. + +## What It Does + +- Creates an assistant inbox (e.g., `assistant@agentmail.to`) +- Monitors for emails where it is CC'd +- Analyzes the email thread and generates a helpful response +- Replies directly to you (not reply-all) with: summary, action items, suggested reply draft, or research +- Configurable response modes: `summarize`, `action-items`, `draft-reply`, `research` + +![Demo](assets/demo.gif) + +## Why This Exists + +Sometimes you just need a quick take on an email. CC the agent and it replies to you privately with a summary, action items, or a draft reply. No context switching, no copying text into a chat window. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/cc-the-agent.git +cd cc-the-agent +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +1. Run: + +```bash +python src/main.py +``` + +2. CC the agent's email address on any email. + +3. The agent replies directly to you with analysis. + +## Configuring Response Mode + +Set `DEFAULT_MODE` in `.env`: +- `summarize`: 3-sentence summary +- `action-items`: bullet list of action items +- `draft-reply`: suggested reply you can edit and send +- `auto`: agent picks the most useful response type + +Or put the mode in the subject prefix: `[action-items] Original Subject`. + +## How to Deploy + +```bash +docker build -t cc-the-agent . +docker run --env-file .env cc-the-agent +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Sending Messages](https://docs.agentmail.to/api-reference/messages/send-message) +- [Labels](https://docs.agentmail.to/features/labels) + +## License + +MIT diff --git a/cc-the-agent/requirements.txt b/cc-the-agent/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/cc-the-agent/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/cc-the-agent/src/main.py b/cc-the-agent/src/main.py new file mode 100644 index 0000000..d3be3bb --- /dev/null +++ b/cc-the-agent/src/main.py @@ -0,0 +1,92 @@ +import os +import re +import json +import time + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +DEFAULT_MODE = os.environ.get("DEFAULT_MODE", "auto") +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "15")) + +ANALYSIS_PROMPT = """You are an email assistant. Analyze this email and provide a {mode} response. + +Modes: +- summarize: 3-sentence summary of the key points +- action-items: bullet list of action items with owners and deadlines +- draft-reply: a suggested reply the user can edit and send +- auto: pick whichever response type is most useful for this email + +From: {sender} +To: {to} +CC: {cc} +Subject: {subject} +Body: +{body} + +Respond with the analysis only. No preamble.""" + + +def detect_mode(subject: str) -> str: + match = re.match(r"\[(\w[\w-]*)\]\s*", subject) + if match: + mode = match.group(1).lower() + if mode in ("summarize", "action-items", "draft-reply", "auto"): + return mode + return DEFAULT_MODE + + +def analyze_email(mode: str, sender: str, to: str, cc: str, subject: str, body: str) -> str: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": ANALYSIS_PROMPT.format( + mode=mode, sender=sender, to=to, cc=cc, subject=subject, body=body + )}], + ) + return resp.choices[0].message.content + + +def handle_messages(inbox_id: str, agent_email: str): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + sender = msg.from_address or "" + subject = msg.subject or "" + body = msg.text or msg.html or "" + to = ", ".join(msg.to) if msg.to else "" + cc = ", ".join(msg.cc) if msg.cc else "" + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + remove_labels=["unread"], + ) + + mode = detect_mode(subject) + analysis = analyze_email(mode, sender, to, cc, subject, body) + + agentmail.messages.send( + inbox_id=inbox_id, + to=[sender], + subject=f"Re: {subject} [Agent Analysis]", + text=f"Here is my {mode} analysis of the email thread:\n\n{analysis}\n\n---\nThis is a private reply from your email assistant ({agent_email}). Only you received this.", + labels=["analysis", mode], + ) + print(f"Analyzed email from {sender}: mode={mode}") + + +def main(): + inbox = agentmail.inboxes.create(display_name="CC Assistant") + print(f"Assistant inbox: {inbox.email}") + print(f"CC this address on emails to get instant analysis.") + print(f"Default mode: {DEFAULT_MODE}\n") + + while True: + handle_messages(inbox.id, inbox.email) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/cold-email-researcher/.env.example b/cold-email-researcher/.env.example new file mode 100644 index 0000000..709f20f --- /dev/null +++ b/cold-email-researcher/.env.example @@ -0,0 +1,6 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +CALENDAR_LINK=https://cal.com/your-link +SENDER_NAME=Your Name +OUR_PRODUCT=your product description +POLL_INTERVAL_SECONDS=60 diff --git a/cold-email-researcher/CROSSPOST.md b/cold-email-researcher/CROSSPOST.md new file mode 100644 index 0000000..e457076 --- /dev/null +++ b/cold-email-researcher/CROSSPOST.md @@ -0,0 +1,108 @@ +# Crosspost Plan: Cold Email Researcher + +## Show HN Post + +**Title:** Show HN: AI agent that researches prospects and sends cold emails from its own inbox + +**Body:** +Built an agent that automates the research-then-email pipeline for B2B outreach. + +You give it a CSV of prospects with domains. For each one, it researches the company, finds a relevant angle, writes a personalized email, and sends it from a dedicated inbox via AgentMail (https://agentmail.to). + +Then it monitors for replies, classifies them (interested, objection, question), and auto-responds to interested prospects with a calendar link. + +The research step is what makes this different from mail-merge tools. Each email references something specific about the prospect's company. + +Python, ~200 lines. MIT licensed. + +Repo: https://github.com/agentmail-to/cold-email-researcher + +--- + +## Dev.to Article + +**Title:** Build a Cold Email Agent That Researches Prospects Before Writing + +**Tags:** python, ai, sales, automation + +--- + +Mail merge is dead. Prospects can spot a template from the first line. What works is genuine research followed by a specific, relevant email. + +The problem: research takes time. For a 100-prospect campaign, you are looking at hours of reading websites and writing custom emails. + +This tutorial builds a Python agent that does the research and writing for you, then sends each email from its own dedicated inbox. + +### The architecture + +1. Create a dedicated inbox with AgentMail +2. For each prospect: research their company domain with GPT-4o-mini +3. Generate a personalized email using the research +4. Send from the agent's inbox +5. Poll for replies, classify, and respond + +### Key code + +```python +from agentmail import AgentMail + +client = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +inbox = client.inboxes.create(display_name="Sales Outreach") + +# Research, generate, send +research = research_prospect("acme.com") +email = generate_email("Sarah", "Acme", research) + +client.messages.send( + inbox_id=inbox.id, + to=["sarah@acme.com"], + subject=email["subject"], + text=email["body"], + labels=["cold-outreach"], +) +``` + +### Why a dedicated inbox? + +Your personal email has reputation. Mass outreach from it risks deliverability for all your email. A dedicated agent inbox keeps your personal email clean and your agent's activity isolated. + +Full code: [github.com/agentmail-to/cold-email-researcher](https://github.com/agentmail-to/cold-email-researcher) + +--- + +## X Thread (6 tweets) + +**Tweet 1:** +Built an AI agent that researches prospects and writes cold emails from its own inbox. + +Not mail merge. Actual research per prospect. + +Open source, ~200 lines of Python. + +**Tweet 2:** +How it works: give it a CSV with prospect names and company domains. + +For each one, GPT-4o-mini researches the company and finds a specific angle to reference in the email. + +**Tweet 3:** +The agent creates its own email address via @AgentMailTo. + +Prospects see a real reply-to address. Replies go to the agent, not your inbox. + +**Tweet 4:** +When a reply comes in, the agent classifies it: +- Interested -> sends calendar link +- Objection -> flags for human review +- Out of office -> retries later + +**Tweet 5:** +Why not just use your Gmail? + +Mass outreach from a personal address risks your deliverability. A dedicated agent inbox isolates the risk. + +**Tweet 6:** +Repo: github.com/agentmail-to/cold-email-researcher + +pip install agentmail openai, add API keys, run. + +MIT licensed. diff --git a/cold-email-researcher/LICENSE b/cold-email-researcher/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/cold-email-researcher/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cold-email-researcher/README.md b/cold-email-researcher/README.md new file mode 100644 index 0000000..70ad693 --- /dev/null +++ b/cold-email-researcher/README.md @@ -0,0 +1,70 @@ +# Cold Email Researcher Agent + +An AI agent that researches prospects, writes personalized cold emails, and sends them from its own inbox. Built with AgentMail and OpenAI. + +## What It Does + +This agent automates the research-and-outreach pipeline for B2B sales: + +- Takes a list of prospect domains +- Researches each company using web search +- Generates personalized cold emails based on research +- Sends from a dedicated AgentMail inbox +- Tracks opens and replies with labels +- Handles responses intelligently (books calls, answers questions, handles objections) + +![Demo](assets/demo.gif) + +## Why This Exists + +Cold email that converts requires research. Most sales teams skip it because it takes too long. This agent does the research and writes the email in seconds, then manages the entire conversation thread. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/cold-email-researcher.git +cd cold-email-researcher +pip install -r requirements.txt +cp .env.example .env +# Add your API keys to .env +``` + +## Quickstart + +1. Add prospects to `prospects.csv` (columns: `name`, `email`, `company`, `domain`). + +2. Run the agent: + +```bash +python src/main.py +``` + +The agent will: +- Create a sales outreach inbox +- Research each prospect's company +- Generate and send a personalized email +- Monitor for replies and classify them +- Respond to interested prospects with calendar links + +## How to Deploy + +```bash +docker build -t cold-email-researcher . +docker run --env-file .env cold-email-researcher +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Sending Messages](https://docs.agentmail.to/api-reference/messages/send-message) +- [Replying to Messages](https://docs.agentmail.to/api-reference/messages/reply-to-message) + +## License + +MIT diff --git a/cold-email-researcher/prospects.csv b/cold-email-researcher/prospects.csv new file mode 100644 index 0000000..72b618d --- /dev/null +++ b/cold-email-researcher/prospects.csv @@ -0,0 +1,3 @@ +name,email,company,domain +Sarah Chen,sarah@example.com,Acme Corp,acme.com +Mike Rivera,mike@example.com,Globex Inc,globex.com diff --git a/cold-email-researcher/requirements.txt b/cold-email-researcher/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/cold-email-researcher/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/cold-email-researcher/src/main.py b/cold-email-researcher/src/main.py new file mode 100644 index 0000000..bc68763 --- /dev/null +++ b/cold-email-researcher/src/main.py @@ -0,0 +1,135 @@ +import os +import csv +import time +import json +from datetime import datetime, timezone + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +RESEARCH_PROMPT = """Research the company at domain {domain}. Based on what you know, write 2-3 bullet points about: +- What the company does +- A recent development or noteworthy fact +- A potential pain point related to {our_product} + +Return JSON: {{"bullets": ["...", "..."], "angle": "one sentence pitch angle"}}""" + +EMAIL_PROMPT = """Write a cold email to {name} at {company}. + +Research context: +{research} + +Rules: +- Under 120 words +- Reference something specific about their company +- Clear ask: 15-minute call +- No fluff, no "I hope this finds you well" +- Sign off as {sender_name} + +Return JSON: {{"subject": "...", "body": "..."}}""" + +CLASSIFY_PROMPT = """Classify this reply to a cold sales email. +Return JSON: {{"category": "interested"|"not_interested"|"question"|"objection"|"out_of_office"}} + +Reply: {text}""" + +CALENDAR_LINK = os.environ.get("CALENDAR_LINK", "https://cal.com/your-link") +SENDER_NAME = os.environ.get("SENDER_NAME", "Your Name") +OUR_PRODUCT = os.environ.get("OUR_PRODUCT", "our product") +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "60")) + + +def llm_json(prompt: str) -> dict: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + ) + return json.loads(resp.choices[0].message.content) + + +def research_prospect(domain: str) -> dict: + return llm_json(RESEARCH_PROMPT.format(domain=domain, our_product=OUR_PRODUCT)) + + +def generate_email(name: str, company: str, research: dict) -> dict: + research_text = "\n".join(research.get("bullets", [])) + "\nAngle: " + research.get("angle", "") + return llm_json(EMAIL_PROMPT.format( + name=name, company=company, research=research_text, sender_name=SENDER_NAME + )) + + +def load_prospects(path: str = "prospects.csv") -> list[dict]: + with open(path) as f: + return list(csv.DictReader(f)) + + +def send_campaign(inbox_id: str, prospects: list[dict]) -> dict: + tracker = {} + for p in prospects: + print(f"Researching {p['company']} ({p['domain']})...") + research = research_prospect(p["domain"]) + + email = generate_email(p["name"], p["company"], research) + msg = agentmail.messages.send( + inbox_id=inbox_id, + to=[p["email"]], + subject=email["subject"], + text=email["body"], + labels=["cold-outreach", "pending"], + ) + tracker[p["email"]] = { + "message_id": msg.id, + "name": p["name"], + "sent_at": datetime.now(timezone.utc).isoformat(), + } + print(f"Sent to {p['name']} ({p['email']}): {email['subject']}") + return tracker + + +def handle_replies(inbox_id: str, tracker: dict): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + sender = msg.from_address + if sender not in tracker: + continue + + result = llm_json(CLASSIFY_PROMPT.format(text=msg.text or "")) + category = result.get("category", "question") + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=[category], + remove_labels=["unread", "pending"], + ) + print(f"Reply from {sender}: {category}") + + if category == "interested": + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Great to hear! Here's my calendar link to book a time: {CALENDAR_LINK}\n\nLooking forward to it.\n{SENDER_NAME}", + ) + print(f"Sent calendar link to {sender}") + + +def main(): + inbox = agentmail.inboxes.create(display_name="Sales Outreach") + print(f"Created inbox: {inbox.email}") + + prospects = load_prospects() + tracker = send_campaign(inbox.id, prospects) + + print(f"\nCampaign sent to {len(tracker)} prospects. Monitoring replies...") + + while True: + handle_replies(inbox.id, tracker) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/collections-agent/.env.example b/collections-agent/.env.example new file mode 100644 index 0000000..cacaa20 --- /dev/null +++ b/collections-agent/.env.example @@ -0,0 +1,5 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +COMPANY_NAME=Your Company +ESCALATION_EMAIL=finance@yourcompany.com +POLL_INTERVAL_SECONDS=120 diff --git a/collections-agent/CROSSPOST.md b/collections-agent/CROSSPOST.md new file mode 100644 index 0000000..dd4acbe --- /dev/null +++ b/collections-agent/CROSSPOST.md @@ -0,0 +1,74 @@ +# Crosspost Plan: Collections Agent + +## Show HN Post + +**Title:** Show HN: AI collections agent that sends payment reminders and handles responses + +**Body:** +Built an agent that manages payment collection via email. Feed it a CSV of overdue invoices and it handles the rest. + +The agent creates its own inbox via AgentMail, sends progressively firmer reminders on a schedule (friendly > firm > urgent > final notice), classifies responses (paid, dispute, payment plan), and escalates disputes to a human. + +No call center needed for routine collections. The agent handles the volume, humans handle the edge cases. + +Python, ~250 lines. + +Repo: https://github.com/agentmail-to/collections-agent + +--- + +## Dev.to Article + +**Title:** Build an AI Collections Agent That Manages Payment Follow-ups via Email + +**Tags:** python, ai, fintech, automation + +--- + +Payment collection is one of the most repetitive business processes. The sequence is always the same: friendly reminder, firm follow-up, urgent notice, final warning. Most teams either do this manually or use inflexible SaaS tools. + +This tutorial builds a Python agent that handles the entire collections pipeline through email. + +### The escalation schedule + +```python +REMINDER_SCHEDULE = [ + {"days_overdue": 1, "tone": "friendly"}, + {"days_overdue": 7, "tone": "firm"}, + {"days_overdue": 14, "tone": "urgent"}, + {"days_overdue": 30, "tone": "final-notice"}, +] +``` + +The agent generates contextual emails at each stage using GPT-4o-mini. Each email includes the invoice number, amount, and due date. + +### Handling replies + +When a customer replies "paid," the agent confirms and stops sending reminders. Disputes and payment plan requests get escalated to a human. + +Full code: [github.com/agentmail-to/collections-agent](https://github.com/agentmail-to/collections-agent) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Built an AI collections agent that sends payment reminders and handles responses via email. + +Friendly -> firm -> urgent -> final notice. All automated. + +**Tweet 2:** +Feed it a CSV of overdue invoices. It creates its own inbox via @AgentMailTo and sends reminders on a schedule. + +Each reminder is contextual: includes the invoice number, amount, and days overdue. + +**Tweet 3:** +When customers reply, the agent classifies: paid, dispute, payment plan. Paid = stop reminders. Dispute = escalate to human. + +**Tweet 4:** +Why email? Because collections follow-up is inherently email-based. This agent fits the existing workflow without requiring portals or new tools. + +**Tweet 5:** +Repo: github.com/agentmail-to/collections-agent + +Python, MIT licensed. ~250 lines. diff --git a/collections-agent/LICENSE b/collections-agent/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/collections-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/collections-agent/README.md b/collections-agent/README.md new file mode 100644 index 0000000..aac24ea --- /dev/null +++ b/collections-agent/README.md @@ -0,0 +1,62 @@ +# Collections Agent + +An AI agent that handles payment collection follow-ups via email. It sends payment reminders, escalates overdue accounts, and tracks payment status. Built with AgentMail and OpenAI. + +## What It Does + +- Creates a collections inbox +- Sends initial payment reminder emails from an invoice list +- Escalates with progressively firmer follow-ups on a schedule +- Classifies replies: `paid`, `dispute`, `payment-plan`, `no-response` +- Tracks each account through the collections pipeline with labels +- Generates summary reports of collection status + +![Demo](assets/demo.gif) + +## Why This Exists + +Chasing overdue invoices is tedious but critical for cash flow. This agent sends consistent, professional reminders on schedule and handles the common responses, so your team only intervenes for disputes and payment plan negotiations. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/collections-agent.git +cd collections-agent +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +1. Add invoices to `invoices.csv` (columns: `name`, `email`, `amount`, `due_date`, `invoice_id`). + +2. Run: + +```bash +python src/main.py +``` + +The agent sends reminders, monitors replies, and escalates automatically. + +## How to Deploy + +```bash +docker build -t collections-agent . +docker run --env-file .env collections-agent +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Sending Messages](https://docs.agentmail.to/api-reference/messages/send-message) +- [Labels](https://docs.agentmail.to/features/labels) + +## License + +MIT diff --git a/collections-agent/invoices.csv b/collections-agent/invoices.csv new file mode 100644 index 0000000..6e00c65 --- /dev/null +++ b/collections-agent/invoices.csv @@ -0,0 +1,3 @@ +name,email,amount,due_date,invoice_id +Alice Brown,alice@example.com,1500.00,2025-04-15,INV-001 +Charlie Davis,charlie@example.com,3200.00,2025-04-01,INV-002 diff --git a/collections-agent/requirements.txt b/collections-agent/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/collections-agent/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/collections-agent/src/main.py b/collections-agent/src/main.py new file mode 100644 index 0000000..b48af2f --- /dev/null +++ b/collections-agent/src/main.py @@ -0,0 +1,172 @@ +import os +import csv +import json +import time +from datetime import datetime, timedelta, timezone + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "120")) +COMPANY_NAME = os.environ.get("COMPANY_NAME", "Your Company") +ESCALATION_EMAIL = os.environ.get("ESCALATION_EMAIL", "finance@yourcompany.com") + +REMINDER_SCHEDULE = [ + {"days_overdue": 1, "tone": "friendly", "label": "reminder-1"}, + {"days_overdue": 7, "tone": "firm", "label": "reminder-2"}, + {"days_overdue": 14, "tone": "urgent", "label": "reminder-3"}, + {"days_overdue": 30, "tone": "final-notice", "label": "final-notice"}, +] + +REMINDER_PROMPT = """Write a {tone} payment reminder email. + +Details: +- Customer: {name} +- Invoice: #{invoice_id} +- Amount: ${amount} +- Due date: {due_date} +- Days overdue: {days_overdue} + +Keep it under 100 words. Professional. Sign as {company} Accounts Receivable.""" + +CLASSIFY_PROMPT = """Classify this reply to a payment collection email. +Return JSON: {{"category": "paid"|"dispute"|"payment_plan"|"question"|"other", "summary": "one sentence summary"}} + +Reply: {text}""" + + +def llm_text(prompt: str) -> str: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + ) + return resp.choices[0].message.content + + +def llm_json(prompt: str) -> dict: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + ) + return json.loads(resp.choices[0].message.content) + + +def load_invoices(path: str = "invoices.csv") -> list[dict]: + with open(path) as f: + return list(csv.DictReader(f)) + + +def get_days_overdue(due_date_str: str) -> int: + due = datetime.strptime(due_date_str, "%Y-%m-%d").replace(tzinfo=timezone.utc) + return (datetime.now(timezone.utc) - due).days + + +def send_reminders(inbox_id: str, tracker: dict): + for email, data in tracker.items(): + if data.get("resolved"): + continue + + days = get_days_overdue(data["due_date"]) + if days < 1: + continue + + current_stage = data.get("current_stage", -1) + for i, stage in enumerate(REMINDER_SCHEDULE): + if i <= current_stage: + continue + if days < stage["days_overdue"]: + break + + reminder = llm_text(REMINDER_PROMPT.format( + tone=stage["tone"], + name=data["name"], + invoice_id=data["invoice_id"], + amount=data["amount"], + due_date=data["due_date"], + days_overdue=days, + company=COMPANY_NAME, + )) + agentmail.messages.send( + inbox_id=inbox_id, + to=[email], + subject=f"Payment reminder: Invoice #{data['invoice_id']} - ${data['amount']}", + text=reminder, + labels=[stage["label"], "collections"], + ) + data["current_stage"] = i + print(f"Sent {stage['label']} to {data['name']} ({days} days overdue, ${data['amount']})") + + +def handle_replies(inbox_id: str, tracker: dict): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + sender = msg.from_address + if sender not in tracker: + continue + + result = llm_json(CLASSIFY_PROMPT.format(text=msg.text or "")) + category = result.get("category", "other") + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=[category], + remove_labels=["unread"], + ) + + if category == "paid": + tracker[sender]["resolved"] = True + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Thank you for your payment on Invoice #{tracker[sender]['invoice_id']}. We appreciate your prompt attention to this matter.\n\n{COMPANY_NAME} Accounts Receivable", + ) + print(f"Marked {sender} as paid") + elif category in ("dispute", "payment_plan"): + agentmail.messages.send( + inbox_id=inbox_id, + to=[ESCALATION_EMAIL], + subject=f"[Collections Escalation] {category}: Invoice #{tracker[sender]['invoice_id']}", + text=f"Customer: {tracker[sender]['name']} ({sender})\nCategory: {category}\nSummary: {result.get('summary', '')}\n\nOriginal reply:\n{msg.text}", + labels=["escalated"], + ) + print(f"Escalated {category} from {sender} to {ESCALATION_EMAIL}") + + +def print_report(tracker: dict): + total = len(tracker) + resolved = sum(1 for d in tracker.values() if d.get("resolved")) + print(f"\n--- Collections Report: {resolved}/{total} resolved ---") + + +def main(): + inbox = agentmail.inboxes.create(display_name=f"{COMPANY_NAME} Collections") + print(f"Created collections inbox: {inbox.email}") + + invoices = load_invoices() + tracker = {} + for inv in invoices: + tracker[inv["email"]] = { + "name": inv["name"], + "amount": inv["amount"], + "due_date": inv["due_date"], + "invoice_id": inv["invoice_id"], + "current_stage": -1, + "resolved": False, + } + + print(f"Tracking {len(tracker)} invoices. Monitoring...\n") + + while True: + send_reminders(inbox.id, tracker) + handle_replies(inbox.id, tracker) + print_report(tracker) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/contract-redline-agent/.env.example b/contract-redline-agent/.env.example new file mode 100644 index 0000000..2e1b63e --- /dev/null +++ b/contract-redline-agent/.env.example @@ -0,0 +1,3 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +POLL_INTERVAL_SECONDS=30 diff --git a/contract-redline-agent/CROSSPOST.md b/contract-redline-agent/CROSSPOST.md new file mode 100644 index 0000000..efae7ae --- /dev/null +++ b/contract-redline-agent/CROSSPOST.md @@ -0,0 +1,53 @@ +# Crosspost Plan: Contract Redline Agent + +## Show HN Post + +**Title:** Show HN: Forward a contract to an AI agent, get a redline review back + +**Body:** +Built an agent that reviews contracts against your standard terms and replies with a redline summary. Forward the contract text to its email, and it flags risky clauses: unlimited liability, auto-renewal, one-sided indemnity, IP assignment. + +Not a replacement for legal counsel. A first-pass filter that catches the obvious issues in seconds. + +Configure your standard terms in a JSON file. The agent compares each contract against them. + +AgentMail (https://agentmail.to) + OpenAI. Python, ~200 lines. + +Repo: https://github.com/agentmail-to/contract-redline-agent + +--- + +## Dev.to Article + +**Title:** Build a Contract Review Agent That Catches Risky Clauses via Email + +**Tags:** python, ai, legal, automation + +--- + +Contract review is expensive and slow. This agent provides a first-pass review in seconds by comparing contracts against your standard terms. + +Forward a contract to the agent's email. It replies with flagged clauses, risk levels, and suggested alternative language. + +Full code: [github.com/agentmail-to/contract-redline-agent](https://github.com/agentmail-to/contract-redline-agent) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Built an AI agent that reviews contracts via email. Forward a contract, get a redline review back in seconds. + +**Tweet 2:** +It compares every clause against your standard terms (configured in JSON). Flags: unlimited liability, auto-renewal, IP grabs, one-sided indemnity. + +**Tweet 3:** +Each flagged clause gets a risk level and suggested alternative language. The agent replies with a full summary. + +**Tweet 4:** +Not replacing lawyers. Giving them a first-pass filter so they focus on what matters. Built on @AgentMailTo. + +**Tweet 5:** +Repo: github.com/agentmail-to/contract-redline-agent + +Python, MIT. ~200 lines. diff --git a/contract-redline-agent/LICENSE b/contract-redline-agent/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/contract-redline-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contract-redline-agent/README.md b/contract-redline-agent/README.md new file mode 100644 index 0000000..43239ca --- /dev/null +++ b/contract-redline-agent/README.md @@ -0,0 +1,62 @@ +# Contract Redline Agent + +An AI agent that receives contracts via email, reviews them against your standard terms, and replies with redline suggestions. Built with AgentMail and OpenAI. + +## What It Does + +- Creates a contract review inbox +- Monitors for incoming emails with contract text or attachments +- Compares contract clauses against your configured standard terms +- Identifies risky clauses: unlimited liability, auto-renewal, non-compete, IP assignment +- Replies with a redline summary: what to accept, what to push back on, and suggested language +- Labels threads: `reviewed`, `high-risk`, `acceptable`, `needs-negotiation` + +![Demo](assets/demo.gif) + +## Why This Exists + +Contract review is slow and expensive. This agent provides a first-pass review in seconds, flagging the clauses that need human attention. It does not replace legal counsel but it makes the review process faster by highlighting what matters. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/contract-redline-agent.git +cd contract-redline-agent +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +1. Edit `standard_terms.json` with your preferred contract terms. + +2. Run: + +```bash +python src/main.py +``` + +Forward a contract to the agent's email. It replies with a redline summary. + +## How to Deploy + +```bash +docker build -t contract-redline-agent . +docker run --env-file .env contract-redline-agent +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Replying to Messages](https://docs.agentmail.to/api-reference/messages/reply-to-message) +- [Attachments](https://docs.agentmail.to/api-reference/messages/get-attachment) + +## License + +MIT diff --git a/contract-redline-agent/requirements.txt b/contract-redline-agent/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/contract-redline-agent/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/contract-redline-agent/src/main.py b/contract-redline-agent/src/main.py new file mode 100644 index 0000000..7d11805 --- /dev/null +++ b/contract-redline-agent/src/main.py @@ -0,0 +1,130 @@ +import os +import json +import time + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "30")) + +REVIEW_PROMPT = """You are a contract review assistant. Compare the following contract text against our standard terms and identify issues. + +Our standard terms: +{standard_terms} + +Contract text: +{contract_text} + +Return JSON: +{{ + "risk_level": "high"|"medium"|"low", + "clauses": [ + {{ + "clause": "quoted or paraphrased clause from the contract", + "issue": "what is problematic", + "risk": "high"|"medium"|"low", + "suggestion": "suggested alternative language or action" + }} + ], + "summary": "2-3 sentence overall assessment", + "accept_as_is": true|false +}}""" + + +def load_standard_terms(path: str = "standard_terms.json") -> str: + with open(path) as f: + terms = json.load(f) + return "\n".join(f"- {t['area']}: {t['requirement']}" for t in terms) + + +def review_contract(contract_text: str, standard_terms: str) -> dict: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": REVIEW_PROMPT.format( + standard_terms=standard_terms, contract_text=contract_text + )}], + response_format={"type": "json_object"}, + ) + return json.loads(resp.choices[0].message.content) + + +def format_review(review: dict) -> str: + lines = [ + f"CONTRACT REVIEW - Risk Level: {review['risk_level'].upper()}", + "=" * 50, + "", + review["summary"], + "", + ] + + if review.get("accept_as_is"): + lines.append("Recommendation: This contract is acceptable as-is.") + else: + lines.append("FLAGGED CLAUSES:") + lines.append("") + for i, clause in enumerate(review.get("clauses", []), 1): + lines.append(f"{i}. [{clause['risk'].upper()} RISK] {clause['issue']}") + lines.append(f" Clause: \"{clause['clause']}\"") + lines.append(f" Suggestion: {clause['suggestion']}") + lines.append("") + + lines.append("---") + lines.append("This is an automated first-pass review. Consult legal counsel for final decisions.") + return "\n".join(lines) + + +def handle_messages(inbox_id: str, standard_terms: str): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + sender = msg.from_address or "" + contract_text = msg.text or msg.html or "" + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + remove_labels=["unread"], + ) + + if len(contract_text.strip()) < 100: + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text="Please forward the contract text in the email body. The message received was too short to review.", + ) + continue + + print(f"Reviewing contract from {sender}...") + review = review_contract(contract_text, standard_terms) + formatted = format_review(review) + + risk = review.get("risk_level", "medium") + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=formatted, + ) + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=["reviewed", f"risk-{risk}"], + ) + print(f" Risk: {risk}, Clauses flagged: {len(review.get('clauses', []))}") + + +def main(): + inbox = agentmail.inboxes.create(display_name="Contract Reviewer") + print(f"Contract review inbox: {inbox.email}") + print(f"Forward contracts to this address for review.\n") + + standard_terms = load_standard_terms() + + while True: + handle_messages(inbox.id, standard_terms) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/contract-redline-agent/standard_terms.json b/contract-redline-agent/standard_terms.json new file mode 100644 index 0000000..3f19ab9 --- /dev/null +++ b/contract-redline-agent/standard_terms.json @@ -0,0 +1,10 @@ +[ + {"area": "Liability", "requirement": "Liability capped at 12 months of fees paid. No unlimited liability."}, + {"area": "Termination", "requirement": "Either party can terminate with 30 days written notice."}, + {"area": "Auto-renewal", "requirement": "No auto-renewal. Contract requires explicit renewal."}, + {"area": "IP ownership", "requirement": "We retain ownership of all pre-existing IP. Work product IP transfers on final payment."}, + {"area": "Non-compete", "requirement": "No non-compete clauses. Non-solicitation limited to 12 months."}, + {"area": "Payment terms", "requirement": "Net 30 payment terms. No upfront payment exceeding 25% of total."}, + {"area": "Indemnification", "requirement": "Mutual indemnification only. No one-sided indemnity clauses."}, + {"area": "Governing law", "requirement": "Governed by Delaware law. Disputes resolved by arbitration."} +] diff --git a/email-to-cli/.env.example b/email-to-cli/.env.example new file mode 100644 index 0000000..0d7a2eb --- /dev/null +++ b/email-to-cli/.env.example @@ -0,0 +1,2 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +POLL_INTERVAL_SECONDS=10 diff --git a/email-to-cli/CROSSPOST.md b/email-to-cli/CROSSPOST.md new file mode 100644 index 0000000..fb058ea --- /dev/null +++ b/email-to-cli/CROSSPOST.md @@ -0,0 +1,55 @@ +# Crosspost Plan: Email to CLI + +## Show HN Post + +**Title:** Show HN: Run shell commands on a remote machine by sending an email + +**Body:** +A bridge between email and your terminal. Send an email with a command in the subject line, get the output back as a reply. + +Security: allowlisted commands only, sender verification, no shell expansion, configurable timeout. This is not a backdoor; it is a controlled remote exec interface with an email audit trail. + +Uses AgentMail (https://agentmail.to) for the inbox. Python, ~120 lines. + +Use cases: IoT devices, air-gapped systems, remote monitoring when SSH is not available. + +Repo: https://github.com/agentmail-to/email-to-cli + +--- + +## Dev.to Article + +**Title:** Build an Email-to-CLI Bridge: Run Remote Commands via Email + +**Tags:** python, devops, iot, automation + +--- + +SSH is great until it is not available. Firewalls, NAT, IoT devices with no SSH daemon. Email goes everywhere. + +This tutorial builds a bridge that lets you run commands on a remote machine by emailing an agent. Command goes in the subject line, output comes back as a reply. + +Full code: [github.com/agentmail-to/email-to-cli](https://github.com/agentmail-to/email-to-cli) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Built an email-to-CLI bridge. Send a command in the email subject, get stdout back as a reply. + +For when SSH is not available. ~120 lines of Python. + +**Tweet 2:** +Security: allowlisted commands, sender verification, no shell expansion, 30s timeout. Not a backdoor. A controlled interface with an email audit trail. + +**Tweet 3:** +The agent creates its own inbox via @AgentMailTo. Poll-based, so it works behind NAT and firewalls. + +**Tweet 4:** +Use cases: IoT monitoring, air-gapped systems, remote server health checks, teaching tools. + +**Tweet 5:** +Repo: github.com/agentmail-to/email-to-cli + +Python, MIT licensed. diff --git a/email-to-cli/LICENSE b/email-to-cli/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/email-to-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/email-to-cli/README.md b/email-to-cli/README.md new file mode 100644 index 0000000..0b7aa3d --- /dev/null +++ b/email-to-cli/README.md @@ -0,0 +1,67 @@ +# Email to CLI + +A bridge that lets you control CLI tools via email. Send an email with a command, and the agent executes it in a sandboxed environment and emails back the output. Built with AgentMail. + +## What It Does + +- Creates a command inbox +- Monitors for incoming emails containing shell commands +- Executes allowed commands in a sandboxed subprocess +- Emails the output (stdout/stderr) back to the sender +- Supports an allowlist of safe commands +- Labels threads: `executed`, `blocked`, `error` + +![Demo](assets/demo.gif) + +## Why This Exists + +Remote server management via email. When SSH is not available or you need an audit trail of every command, email provides both. Useful for IoT devices, air-gapped systems, or as a quick remote exec interface for agents. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/email-to-cli.git +cd email-to-cli +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +```bash +python src/main.py +``` + +Email the agent with a subject like `ls -la /tmp` and it will reply with the output. + +## Security + +Commands run in a restricted subprocess with: +- An allowlist of permitted commands (configurable in `config.py`) +- Timeout per command (default: 30s) +- No shell expansion by default +- Sender allowlist to restrict who can send commands + +**Do not run this in production without reviewing the security configuration.** + +## How to Deploy + +```bash +docker build -t email-to-cli . +docker run --env-file .env email-to-cli +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Listing Messages](https://docs.agentmail.to/api-reference/messages/list-messages) +- [Replying to Messages](https://docs.agentmail.to/api-reference/messages/reply-to-message) + +## License + +MIT diff --git a/email-to-cli/requirements.txt b/email-to-cli/requirements.txt new file mode 100644 index 0000000..4fda45b --- /dev/null +++ b/email-to-cli/requirements.txt @@ -0,0 +1 @@ +agentmail>=0.1.0 diff --git a/email-to-cli/src/config.py b/email-to-cli/src/config.py new file mode 100644 index 0000000..c585f8b --- /dev/null +++ b/email-to-cli/src/config.py @@ -0,0 +1,22 @@ +ALLOWED_COMMANDS = [ + "ls", + "pwd", + "whoami", + "uptime", + "df", + "free", + "date", + "hostname", + "cat", + "head", + "tail", + "wc", + "echo", + "env", + "uname", + "ps", +] + +COMMAND_TIMEOUT = 30 + +ALLOWED_SENDERS = [] diff --git a/email-to-cli/src/main.py b/email-to-cli/src/main.py new file mode 100644 index 0000000..781cd82 --- /dev/null +++ b/email-to-cli/src/main.py @@ -0,0 +1,122 @@ +import os +import subprocess +import time +import shlex + +from agentmail import AgentMail + +from config import ALLOWED_COMMANDS, COMMAND_TIMEOUT, ALLOWED_SENDERS + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) + +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "10")) + + +def is_command_allowed(command: str) -> bool: + parts = shlex.split(command) + if not parts: + return False + base_cmd = parts[0] + return base_cmd in ALLOWED_COMMANDS + + +def is_sender_allowed(sender: str) -> bool: + if not ALLOWED_SENDERS: + return True + return sender in ALLOWED_SENDERS + + +def execute_command(command: str) -> tuple[str, int]: + try: + parts = shlex.split(command) + result = subprocess.run( + parts, + capture_output=True, + text=True, + timeout=COMMAND_TIMEOUT, + shell=False, + ) + output = result.stdout + if result.stderr: + output += f"\nSTDERR:\n{result.stderr}" + return output.strip() or "(no output)", result.returncode + except subprocess.TimeoutExpired: + return f"Command timed out after {COMMAND_TIMEOUT} seconds", 1 + except Exception as e: + return f"Error: {str(e)}", 1 + + +def handle_messages(inbox_id: str): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + sender = msg.from_address + command = (msg.subject or "").strip() + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + remove_labels=["unread"], + ) + + if not command: + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text="Put the command in the email subject line.", + ) + continue + + if not is_sender_allowed(sender): + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=["blocked"], + ) + print(f"Blocked command from unauthorized sender: {sender}") + continue + + if not is_command_allowed(command): + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Command not allowed: {shlex.split(command)[0]}\n\nAllowed commands: {', '.join(ALLOWED_COMMANDS)}", + ) + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=["blocked"], + ) + print(f"Blocked disallowed command: {command}") + continue + + print(f"Executing: {command} (from {sender})") + output, exit_code = execute_command(command) + + status = "OK" if exit_code == 0 else f"EXIT {exit_code}" + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"$ {command}\n\n{output}\n\n[{status}]", + ) + label = "executed" if exit_code == 0 else "error" + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=[label], + ) + print(f" Result: {status}") + + +def main(): + inbox = agentmail.inboxes.create(display_name="CLI Agent") + print(f"CLI inbox created: {inbox.email}") + print(f"Send commands in the email subject line.") + print(f"Allowed commands: {', '.join(ALLOWED_COMMANDS)}\n") + + while True: + handle_messages(inbox.id) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/hiring-screener-agent/.env.example b/hiring-screener-agent/.env.example new file mode 100644 index 0000000..32d8cd4 --- /dev/null +++ b/hiring-screener-agent/.env.example @@ -0,0 +1,5 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +HIRING_MANAGER_EMAIL=hiring@yourcompany.com +PASS_THRESHOLD=0.7 +POLL_INTERVAL_SECONDS=30 diff --git a/hiring-screener-agent/CROSSPOST.md b/hiring-screener-agent/CROSSPOST.md new file mode 100644 index 0000000..7b70896 --- /dev/null +++ b/hiring-screener-agent/CROSSPOST.md @@ -0,0 +1,59 @@ +# Crosspost Plan: Hiring Screener Agent + +## Show HN Post + +**Title:** Show HN: AI hiring screener that interviews candidates via email + +**Body:** +Built an agent that screens job applicants through email. Post its email as the application address. When someone applies, it sends personalized screening questions, scores the responses, and forwards qualified candidates to the hiring manager. + +The screening questions are generated based on the applicant's background, not generic. The scoring considers technical fit, communication quality, and your configured criteria. + +Unqualified candidates get a polite rejection. Qualified ones get forwarded with a summary and score. + +Python, AgentMail (https://agentmail.to) + OpenAI. + +Repo: https://github.com/agentmail-to/hiring-screener-agent + +--- + +## Dev.to Article + +**Title:** Build an AI Hiring Screener That Interviews Candidates via Email + +**Tags:** python, ai, hiring, automation + +--- + +The first filter in hiring is the most repetitive. Every applicant needs the same 3-5 questions answered before a human should look at them. This agent handles that filter. + +### Flow + +1. Candidate emails the agent to apply +2. Agent sends personalized screening questions +3. Candidate replies with answers +4. Agent scores responses and decides: advance or reject +5. Qualified candidates forwarded to hiring manager with summary + +Full code: [github.com/agentmail-to/hiring-screener-agent](https://github.com/agentmail-to/hiring-screener-agent) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Built an AI agent that screens job applicants via email. Sends personalized questions, scores responses, forwards qualified candidates. + +**Tweet 2:** +Post the agent's email as your application address. It handles the first filter so hiring managers only see pre-qualified candidates. + +**Tweet 3:** +Questions are personalized based on the applicant's background, not generic templates. Scoring considers technical fit and communication. + +**Tweet 4:** +Qualified candidates get forwarded with a score and summary. Unqualified get a polite rejection. All automatic. + +**Tweet 5:** +Repo: github.com/agentmail-to/hiring-screener-agent + +Python, MIT. Built on @AgentMailTo. diff --git a/hiring-screener-agent/LICENSE b/hiring-screener-agent/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/hiring-screener-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hiring-screener-agent/README.md b/hiring-screener-agent/README.md new file mode 100644 index 0000000..1d5a777 --- /dev/null +++ b/hiring-screener-agent/README.md @@ -0,0 +1,62 @@ +# Hiring Screener Agent + +An AI agent that screens job applicants via email. It receives applications, asks screening questions, scores responses, and forwards qualified candidates to the hiring manager. Built with AgentMail and OpenAI. + +## What It Does + +- Creates a hiring inbox (e.g., `apply@agentmail.to`) +- Receives applications and sends a set of screening questions +- Scores candidate responses against configurable criteria +- Forwards qualified candidates to the hiring manager with a summary +- Sends polite rejections to unqualified candidates +- Labels threads: `applied`, `screening`, `qualified`, `rejected`, `forwarded` + +![Demo](assets/demo.gif) + +## Why This Exists + +Screening applicants is the bottleneck in hiring. Most applications need the same 3-5 questions answered before a human needs to look at them. This agent handles that first filter, so hiring managers only see pre-qualified candidates. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/hiring-screener-agent.git +cd hiring-screener-agent +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +1. Edit `job_config.json` with the role details and screening questions. + +2. Run: + +```bash +python src/main.py +``` + +Post the agent's email as the application address. It screens candidates automatically. + +## How to Deploy + +```bash +docker build -t hiring-screener-agent . +docker run --env-file .env hiring-screener-agent +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Sending Messages](https://docs.agentmail.to/api-reference/messages/send-message) +- [Threading](https://docs.agentmail.to/api-reference/threads) + +## License + +MIT diff --git a/hiring-screener-agent/job_config.json b/hiring-screener-agent/job_config.json new file mode 100644 index 0000000..26aa828 --- /dev/null +++ b/hiring-screener-agent/job_config.json @@ -0,0 +1,6 @@ +{ + "role_title": "Senior Backend Engineer", + "role_description": "Build and scale API infrastructure for a high-growth startup. Python and distributed systems experience required.", + "required_skills": ["Python", "distributed systems", "API design", "PostgreSQL"], + "screening_criteria": "Strong systems design thinking, hands-on experience with high-scale services, clear communication" +} diff --git a/hiring-screener-agent/requirements.txt b/hiring-screener-agent/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/hiring-screener-agent/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/hiring-screener-agent/src/main.py b/hiring-screener-agent/src/main.py new file mode 100644 index 0000000..d1bf503 --- /dev/null +++ b/hiring-screener-agent/src/main.py @@ -0,0 +1,157 @@ +import os +import json +import time + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +HIRING_MANAGER_EMAIL = os.environ["HIRING_MANAGER_EMAIL"] +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "30")) +PASS_THRESHOLD = float(os.environ.get("PASS_THRESHOLD", "0.7")) + +SCREENING_PROMPT = """Generate screening questions for a candidate who applied for this role. + +Role: {role_title} +Description: {role_description} +Required skills: {required_skills} + +The candidate's application: +{application_text} + +Return JSON: +{{ + "personalized_intro": "one sentence acknowledging something specific from their application", + "questions": ["question 1", "question 2", "question 3"] +}}""" + +SCORE_PROMPT = """Score this candidate's screening responses for the role of {role_title}. + +Required skills: {required_skills} +Screening criteria: {criteria} + +Candidate responses: +{responses} + +Return JSON: +{{ + "overall_score": 0.0 to 1.0, + "strengths": ["strength 1", "strength 2"], + "concerns": ["concern 1"], + "summary": "2-3 sentence assessment", + "recommendation": "advance"|"reject"|"maybe" +}}""" + + +def load_job_config(path: str = "job_config.json") -> dict: + with open(path) as f: + return json.load(f) + + +def llm_json(prompt: str) -> dict: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + ) + return json.loads(resp.choices[0].message.content) + + +def handle_messages(inbox_id: str, config: dict, tracker: dict): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + sender = msg.from_address or "" + text = msg.text or "" + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + remove_labels=["unread"], + ) + + if sender not in tracker: + result = llm_json(SCREENING_PROMPT.format( + role_title=config["role_title"], + role_description=config["role_description"], + required_skills=", ".join(config["required_skills"]), + application_text=text, + )) + + questions_text = "\n".join(f"{i+1}. {q}" for i, q in enumerate(result["questions"])) + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=( + f"Thank you for applying for the {config['role_title']} position.\n\n" + f"{result['personalized_intro']}\n\n" + f"To move forward, please answer these screening questions:\n\n" + f"{questions_text}\n\n" + f"Please reply to this email with your answers." + ), + ) + tracker[sender] = {"stage": "screening", "message_id": msg.id} + agentmail.messages.update(inbox_id=inbox_id, message_id=msg.id, add_labels=["applied", "screening"]) + print(f"Sent screening questions to {sender}") + + elif tracker[sender]["stage"] == "screening": + score = llm_json(SCORE_PROMPT.format( + role_title=config["role_title"], + required_skills=", ".join(config["required_skills"]), + criteria=config.get("screening_criteria", "general fit"), + responses=text, + )) + + tracker[sender]["stage"] = "scored" + tracker[sender]["score"] = score + + if score.get("recommendation") == "advance" or score.get("overall_score", 0) >= PASS_THRESHOLD: + agentmail.messages.send( + inbox_id=inbox_id, + to=[HIRING_MANAGER_EMAIL], + subject=f"[Qualified Candidate] {config['role_title']}: {sender}", + text=( + f"Candidate: {sender}\n" + f"Score: {score['overall_score']:.0%}\n" + f"Recommendation: {score['recommendation']}\n\n" + f"Strengths: {', '.join(score.get('strengths', []))}\n" + f"Concerns: {', '.join(score.get('concerns', []))}\n\n" + f"Summary: {score['summary']}\n\n" + f"Screening responses:\n{text}" + ), + labels=["qualified", "forwarded"], + ) + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Thank you for your responses. We are impressed with your background and would like to move forward. Our hiring manager will be in touch shortly.\n\nBest regards", + ) + agentmail.messages.update(inbox_id=inbox_id, message_id=msg.id, add_labels=["qualified"]) + print(f"Qualified: {sender} (score: {score['overall_score']:.0%})") + else: + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Thank you for taking the time to answer our questions. After careful review, we have decided to move forward with other candidates whose experience more closely aligns with our current needs. We wish you the best in your search.\n\nBest regards", + ) + agentmail.messages.update(inbox_id=inbox_id, message_id=msg.id, add_labels=["rejected"]) + print(f"Rejected: {sender} (score: {score['overall_score']:.0%})") + + +def main(): + config = load_job_config() + inbox = agentmail.inboxes.create(display_name=f"Apply: {config['role_title']}") + print(f"Hiring inbox: {inbox.email}") + print(f"Post this as the application email for: {config['role_title']}") + print(f"Qualified candidates forwarded to: {HIRING_MANAGER_EMAIL}\n") + + tracker: dict = {} + + while True: + handle_messages(inbox.id, config, tracker) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/legal-intake-agent/.env.example b/legal-intake-agent/.env.example new file mode 100644 index 0000000..1c687d5 --- /dev/null +++ b/legal-intake-agent/.env.example @@ -0,0 +1,4 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +FIRM_NAME=Smith & Associates +POLL_INTERVAL_SECONDS=30 diff --git a/legal-intake-agent/CROSSPOST.md b/legal-intake-agent/CROSSPOST.md new file mode 100644 index 0000000..8cc19e7 --- /dev/null +++ b/legal-intake-agent/CROSSPOST.md @@ -0,0 +1,76 @@ +# Crosspost Plan: Legal Intake Agent + +## Show HN Post + +**Title:** Show HN: AI legal intake agent that qualifies leads via email 24/7 + +**Body:** +Built an agent that handles legal intake through its own email address. Potential clients email the agent, it sends a structured questionnaire, classifies the case type with GPT-4o-mini, and routes qualified leads to the right attorney. + +The problem it solves: law firms lose leads because intake is slow. Someone emails at 11pm about a car accident, and nobody responds until Monday. This agent responds in seconds, collects the right information, and routes the case. + +It uses AgentMail (https://agentmail.to) for the inbox infrastructure, so the agent has a dedicated address and thread history separate from the firm's main email. + +Python, ~200 lines. Routing is configured via a JSON file mapping case types to attorney emails. + +Repo: https://github.com/agentmail-to/legal-intake-agent + +--- + +## Dev.to Article + +**Title:** Build a 24/7 Legal Intake Agent That Qualifies Leads via Email + +**Tags:** python, ai, legal, automation + +--- + +Law firms have a lead problem. Not volume, but speed. A potential client emails about an injury case at 11pm. By Monday, they have called three other firms. + +This tutorial builds an AI agent that handles intake immediately, around the clock, through its own email inbox. + +### What the agent does + +1. Receives inquiry from potential client +2. Sends structured intake questionnaire +3. Extracts and classifies case details +4. Routes qualified leads to the right attorney +5. Politely declines cases outside the firm's practice areas + +### Routing logic + +```json +{ + "personal-injury": {"name": "Jane Smith", "email": "jsmith@firm.com"}, + "employment": {"name": "Bob Johnson", "email": "bjohnson@firm.com"} +} +``` + +Full code: [github.com/agentmail-to/legal-intake-agent](https://github.com/agentmail-to/legal-intake-agent) + +--- + +## X Thread (5 tweets) + +**Tweet 1:** +Built an AI legal intake agent that qualifies leads via email, 24/7. + +Responds in seconds. Collects case details. Routes to the right attorney. ~200 lines of Python. + +**Tweet 2:** +Someone emails about a car accident at 11pm. The agent responds immediately with an intake questionnaire. + +No more lost leads because the office was closed. + +**Tweet 3:** +GPT-4o-mini classifies the case (personal injury, employment, contract, etc.) and checks for statute of limitations concerns. + +Qualified leads get routed to the right attorney. + +**Tweet 4:** +The agent runs on its own inbox via @AgentMailTo. Separate from the firm's main email. Full thread history and label tracking. + +**Tweet 5:** +Repo: github.com/agentmail-to/legal-intake-agent + +Python, MIT licensed. Configure attorneys.json and run. diff --git a/legal-intake-agent/LICENSE b/legal-intake-agent/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/legal-intake-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/legal-intake-agent/README.md b/legal-intake-agent/README.md new file mode 100644 index 0000000..ec4e16f --- /dev/null +++ b/legal-intake-agent/README.md @@ -0,0 +1,62 @@ +# Legal Intake Agent + +An AI agent that handles initial legal intake via email. It collects case details from potential clients, classifies case types, and routes qualified leads to the right attorney. Built with AgentMail and OpenAI. + +## What It Does + +- Creates an intake inbox (e.g., `intake@agentmail.to`) +- Responds to initial inquiries with a structured questionnaire +- Extracts case details from client responses +- Classifies case type: `personal-injury`, `employment`, `contract`, `family`, `criminal`, `other` +- Checks for conflicts and statute of limitations concerns +- Routes qualified cases to the appropriate attorney email +- Labels threads by status: `new`, `questionnaire-sent`, `details-received`, `qualified`, `routed` + +![Demo](assets/demo.gif) + +## Why This Exists + +Law firms lose leads because intake is slow. Potential clients email at night and on weekends when no one is there to respond. This agent responds immediately, collects the right information, and routes the case, so attorneys start their day with qualified leads instead of raw inquiries. + +## Prerequisites + +- Python 3.10+ +- [AgentMail](https://agentmail.to) API key +- [OpenAI](https://platform.openai.com) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/legal-intake-agent.git +cd legal-intake-agent +pip install -r requirements.txt +cp .env.example .env +``` + +## Quickstart + +1. Edit `attorneys.json` with your routing rules. +2. Run: + +```bash +python src/main.py +``` + +Share the agent's email address on your website. It handles the rest. + +## How to Deploy + +```bash +docker build -t legal-intake-agent . +docker run --env-file .env legal-intake-agent +``` + +## Docs + +- [AgentMail Python SDK](https://docs.agentmail.to/sdks/python) +- [Replying to Messages](https://docs.agentmail.to/api-reference/messages/reply-to-message) +- [Threading](https://docs.agentmail.to/api-reference/threads) + +## License + +MIT diff --git a/legal-intake-agent/attorneys.json b/legal-intake-agent/attorneys.json new file mode 100644 index 0000000..86704f3 --- /dev/null +++ b/legal-intake-agent/attorneys.json @@ -0,0 +1,26 @@ +{ + "personal-injury": { + "name": "Jane Smith", + "email": "jsmith@firm.example.com" + }, + "employment": { + "name": "Bob Johnson", + "email": "bjohnson@firm.example.com" + }, + "contract": { + "name": "Alice Davis", + "email": "adavis@firm.example.com" + }, + "family": { + "name": "Carlos Rivera", + "email": "crivera@firm.example.com" + }, + "criminal": { + "name": "Diana Chen", + "email": "dchen@firm.example.com" + }, + "other": { + "name": "Jane Smith", + "email": "jsmith@firm.example.com" + } +} diff --git a/legal-intake-agent/requirements.txt b/legal-intake-agent/requirements.txt new file mode 100644 index 0000000..4c572bb --- /dev/null +++ b/legal-intake-agent/requirements.txt @@ -0,0 +1,2 @@ +agentmail>=0.1.0 +openai>=1.0.0 diff --git a/legal-intake-agent/src/main.py b/legal-intake-agent/src/main.py new file mode 100644 index 0000000..b5502d8 --- /dev/null +++ b/legal-intake-agent/src/main.py @@ -0,0 +1,156 @@ +import os +import json +import time + +from agentmail import AgentMail +from openai import OpenAI + +agentmail = AgentMail(api_key=os.environ["AGENTMAIL_API_KEY"]) +openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +FIRM_NAME = os.environ.get("FIRM_NAME", "Smith & Associates") +POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "30")) + +QUESTIONNAIRE = """Thank you for reaching out to {firm_name}. + +To help us understand your situation, please reply with the following information: + +1. Brief description of your legal issue +2. When did this issue arise? (approximate date) +3. Have you consulted with another attorney about this matter? +4. What outcome are you hoping to achieve? +5. Your full name and phone number + +We will review your information and have the appropriate attorney follow up within 24 hours. + +{firm_name} Intake Team""" + +CLASSIFY_PROMPT = """You are a legal intake assistant for {firm_name}. + +Analyze this potential client's intake response and extract structured information. + +Response: +{text} + +Return JSON: +{{ + "case_type": "personal-injury"|"employment"|"contract"|"family"|"criminal"|"other", + "summary": "2-3 sentence summary of the case", + "client_name": "extracted name or unknown", + "phone": "extracted phone or unknown", + "date_of_incident": "extracted date or unknown", + "urgency": "high"|"medium"|"low", + "statute_concern": true|false, + "qualified": true|false, + "qualification_reason": "why qualified or not" +}}""" + + +def load_attorneys(path: str = "attorneys.json") -> dict: + with open(path) as f: + return json.load(f) + + +def classify_intake(text: str) -> dict: + resp = openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": CLASSIFY_PROMPT.format( + firm_name=FIRM_NAME, text=text + )}], + response_format={"type": "json_object"}, + ) + return json.loads(resp.choices[0].message.content) + + +def route_to_attorney(inbox_id: str, case: dict, original_msg, attorneys: dict): + case_type = case.get("case_type", "other") + attorney = attorneys.get(case_type, attorneys.get("other")) + if not attorney: + print(f"No attorney configured for case type: {case_type}") + return + + agentmail.messages.send( + inbox_id=inbox_id, + to=[attorney["email"]], + subject=f"[New Lead] {case_type.title()}: {case.get('client_name', 'Unknown')}", + text=( + f"New intake from: {original_msg.from_address}\n" + f"Client: {case.get('client_name', 'Unknown')}\n" + f"Phone: {case.get('phone', 'Unknown')}\n" + f"Case type: {case_type}\n" + f"Urgency: {case.get('urgency', 'medium')}\n" + f"Statute concern: {'Yes' if case.get('statute_concern') else 'No'}\n\n" + f"Summary: {case.get('summary', '')}\n\n" + f"Original message:\n{original_msg.text}" + ), + labels=["routed", case_type], + ) + print(f"Routed to {attorney['name']} ({attorney['email']})") + + +def handle_messages(inbox_id: str, attorneys: dict): + messages = agentmail.messages.list(inbox_id=inbox_id, labels=["unread"]) + for msg in messages.data: + text = msg.text or "" + sender = msg.from_address + + has_questionnaire_response = len(text.strip()) > 50 + + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + remove_labels=["unread"], + ) + + if not has_questionnaire_response: + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=QUESTIONNAIRE.format(firm_name=FIRM_NAME), + ) + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=["questionnaire-sent"], + ) + print(f"Sent questionnaire to {sender}") + else: + case = classify_intake(text) + labels = ["details-received", case.get("case_type", "other")] + if case.get("qualified"): + labels.append("qualified") + route_to_attorney(inbox_id, case, msg, attorneys) + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Thank you for providing that information. An attorney from our {case.get('case_type', '').replace('-', ' ')} team will contact you within 24 hours.\n\n{FIRM_NAME}", + ) + labels.append("routed") + else: + agentmail.messages.reply( + inbox_id=inbox_id, + message_id=msg.id, + text=f"Thank you for reaching out. After reviewing your information, we may not be the best fit for your matter. We recommend consulting with a specialist in this area.\n\n{FIRM_NAME}", + ) + agentmail.messages.update( + inbox_id=inbox_id, + message_id=msg.id, + add_labels=labels, + ) + print(f"Processed intake from {sender}: {case.get('case_type')} (qualified: {case.get('qualified')})") + + +def main(): + inbox = agentmail.inboxes.create(display_name=f"{FIRM_NAME} Intake") + print(f"Intake inbox created: {inbox.email}") + + attorneys = load_attorneys() + print(f"Routing configured for {len(attorneys)} case types\n") + + while True: + handle_messages(inbox.id, attorneys) + time.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + main() diff --git a/nextjs-agentmail-starter/.env.example b/nextjs-agentmail-starter/.env.example new file mode 100644 index 0000000..bd635fe --- /dev/null +++ b/nextjs-agentmail-starter/.env.example @@ -0,0 +1 @@ +AGENTMAIL_API_KEY=your_agentmail_api_key_here diff --git a/nextjs-agentmail-starter/CROSSPOST.md b/nextjs-agentmail-starter/CROSSPOST.md new file mode 100644 index 0000000..314dccd --- /dev/null +++ b/nextjs-agentmail-starter/CROSSPOST.md @@ -0,0 +1,90 @@ +# Crosspost Plan: Next.js AgentMail Starter + +## Show HN Post + +**Title:** Show HN: Next.js starter for building AI email agents with AgentMail + +**Body:** +A Next.js template for building AI agents that send and receive email. Includes API routes for inbox creation, message sending, thread listing, and a webhook endpoint for real-time handling. + +Ships with a simple dashboard UI. Fork it, add your agent logic to the webhook handler, deploy to Vercel. + +Stack: Next.js 14, TypeScript, AgentMail SDK. + +One-click deploy to Vercel. API key as the only env var. + +Repo: https://github.com/agentmail-to/nextjs-agentmail-starter + +--- + +## Dev.to Article + +**Title:** Build an AI Email Agent with Next.js and AgentMail + +**Tags:** nextjs, typescript, ai, email + +--- + +If you are building an AI agent that needs email, you need: inbox creation, message sending, thread management, and real-time webhook handling. This starter template gives you all four as Next.js API routes. + +### What you get + +- `POST /api/agentmail/inboxes` - create agent inboxes +- `POST /api/agentmail/send` - send messages +- `GET /api/agentmail/threads` - list conversation threads +- `POST /api/agentmail/webhook` - handle incoming messages in real time + +### Quick start + +```bash +npx create-next-app --example https://github.com/agentmail-to/nextjs-agentmail-starter my-agent +cd my-agent +echo "AGENTMAIL_API_KEY=your_key" > .env.local +npm run dev +``` + +### Adding agent logic + +The webhook handler is where your agent lives. When a message arrives: + +```typescript +case "message.received": + // Classify the message + // Generate a response with your LLM + // Reply via the AgentMail SDK + break; +``` + +Full code: [github.com/agentmail-to/nextjs-agentmail-starter](https://github.com/agentmail-to/nextjs-agentmail-starter) + +--- + +## X Thread (6 tweets) + +**Tweet 1:** +Built a Next.js starter template for AI email agents. + +Fork, add your agent logic, deploy to Vercel. Handles all the email plumbing. + +**Tweet 2:** +Four API routes: +- Create inboxes +- Send messages +- List threads +- Webhook for real-time incoming messages + +Plus a dashboard UI. + +**Tweet 3:** +The webhook handler is where your agent lives. Incoming message -> classify -> generate response -> reply. All via the @AgentMailTo SDK. + +**Tweet 4:** +One env var: AGENTMAIL_API_KEY. Deploy to Vercel in 30 seconds. + +**Tweet 5:** +Stack: Next.js 14, TypeScript, AgentMail SDK. MIT licensed. + +**Tweet 6:** +Repo: github.com/agentmail-to/nextjs-agentmail-starter + +npm install && npm run dev diff --git a/nextjs-agentmail-starter/LICENSE b/nextjs-agentmail-starter/LICENSE new file mode 100644 index 0000000..c0d9603 --- /dev/null +++ b/nextjs-agentmail-starter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AgentMail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nextjs-agentmail-starter/README.md b/nextjs-agentmail-starter/README.md new file mode 100644 index 0000000..c07673e --- /dev/null +++ b/nextjs-agentmail-starter/README.md @@ -0,0 +1,90 @@ +# Next.js AgentMail Starter + +A Next.js starter template for building AI email agents with AgentMail. Includes inbox creation, message sending, webhook handling, and a dashboard UI. Built with Next.js 14, TypeScript, and the AgentMail SDK. + +## What It Does + +- Dashboard to view and manage agent inboxes +- API routes for creating inboxes, sending messages, and listing threads +- Webhook endpoint for real-time message handling +- Simple UI to compose and send emails from agent inboxes +- Ready to deploy on Vercel + +![Demo](assets/demo.gif) + +## Why This Exists + +The fastest way to build an email-enabled AI agent with a web interface. Fork this, add your agent logic, and deploy. Handles all the AgentMail plumbing so you can focus on the agent behavior. + +## Prerequisites + +- Node.js 18+ +- [AgentMail](https://agentmail.to) API key + +## Install + +```bash +git clone https://github.com/agentmail-to/nextjs-agentmail-starter.git +cd nextjs-agentmail-starter +npm install +cp .env.example .env.local +# Add your AGENTMAIL_API_KEY to .env.local +``` + +## Quickstart + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). You will see a dashboard where you can: + +1. Create a new agent inbox +2. View threads for any inbox +3. Compose and send emails +4. See incoming messages in real time (via polling or webhooks) + +## Project Structure + +``` +src/ + app/ + page.tsx - Dashboard UI + api/ + agentmail/ + inboxes/route.ts - Create and list inboxes + send/route.ts - Send messages + threads/route.ts - List threads + webhook/route.ts - Webhook handler for incoming messages +``` + +## How to Deploy + +### Vercel (recommended) + +1. Push to GitHub +2. Import in Vercel +3. Add `AGENTMAIL_API_KEY` to environment variables +4. Deploy + +### Other platforms + +Any platform that supports Next.js: Railway, Render, Fly.io, Docker. + +## Webhook Setup + +To receive real-time notifications of incoming messages: + +1. Deploy the app +2. Set your webhook URL in the AgentMail dashboard to `https://your-domain.com/api/agentmail/webhook` + +## Docs + +- [AgentMail TypeScript SDK](https://docs.agentmail.to/sdks/typescript) +- [Webhooks](https://docs.agentmail.to/features/webhooks) +- [Creating Inboxes](https://docs.agentmail.to/api-reference/inboxes/create-inbox) +- [Next.js Documentation](https://nextjs.org/docs) + +## License + +MIT diff --git a/nextjs-agentmail-starter/next.config.js b/nextjs-agentmail-starter/next.config.js new file mode 100644 index 0000000..d918f80 --- /dev/null +++ b/nextjs-agentmail-starter/next.config.js @@ -0,0 +1,3 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; +module.exports = nextConfig; diff --git a/nextjs-agentmail-starter/package.json b/nextjs-agentmail-starter/package.json new file mode 100644 index 0000000..a37d8b7 --- /dev/null +++ b/nextjs-agentmail-starter/package.json @@ -0,0 +1,21 @@ +{ + "name": "nextjs-agentmail-starter", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "agentmail": "^0.1.0", + "next": "^14.2.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "typescript": "^5.4.0" + } +} diff --git a/nextjs-agentmail-starter/src/app/api/agentmail/inboxes/route.ts b/nextjs-agentmail-starter/src/app/api/agentmail/inboxes/route.ts new file mode 100644 index 0000000..9c82f74 --- /dev/null +++ b/nextjs-agentmail-starter/src/app/api/agentmail/inboxes/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ + apiKey: process.env.AGENTMAIL_API_KEY!, +}); + +export async function GET() { + const inboxes = await client.inboxes.list(); + return NextResponse.json(inboxes); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const inbox = await client.inboxes.create({ + displayName: body.displayName || "New Agent Inbox", + }); + return NextResponse.json(inbox, { status: 201 }); +} diff --git a/nextjs-agentmail-starter/src/app/api/agentmail/send/route.ts b/nextjs-agentmail-starter/src/app/api/agentmail/send/route.ts new file mode 100644 index 0000000..07273f0 --- /dev/null +++ b/nextjs-agentmail-starter/src/app/api/agentmail/send/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ + apiKey: process.env.AGENTMAIL_API_KEY!, +}); + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { inboxId, to, subject, text } = body; + + if (!inboxId || !to || !subject || !text) { + return NextResponse.json( + { error: "Missing required fields: inboxId, to, subject, text" }, + { status: 400 } + ); + } + + const message = await client.messages.send(inboxId, { + to: Array.isArray(to) ? to : [to], + subject, + text, + }); + + return NextResponse.json(message, { status: 201 }); +} diff --git a/nextjs-agentmail-starter/src/app/api/agentmail/threads/route.ts b/nextjs-agentmail-starter/src/app/api/agentmail/threads/route.ts new file mode 100644 index 0000000..b5ffbb7 --- /dev/null +++ b/nextjs-agentmail-starter/src/app/api/agentmail/threads/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ + apiKey: process.env.AGENTMAIL_API_KEY!, +}); + +export async function GET(request: NextRequest) { + const inboxId = request.nextUrl.searchParams.get("inboxId"); + + if (!inboxId) { + return NextResponse.json( + { error: "Missing required query param: inboxId" }, + { status: 400 } + ); + } + + const threads = await client.inboxes.threads.list(inboxId); + return NextResponse.json(threads); +} diff --git a/nextjs-agentmail-starter/src/app/api/agentmail/webhook/route.ts b/nextjs-agentmail-starter/src/app/api/agentmail/webhook/route.ts new file mode 100644 index 0000000..425085e --- /dev/null +++ b/nextjs-agentmail-starter/src/app/api/agentmail/webhook/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + const payload = await request.json(); + + const { event, data } = payload; + + switch (event) { + case "message.received": + console.log( + `New message in inbox ${data.inbox_id} from ${data.from_address}: ${data.subject}` + ); + // Add your agent logic here: + // - Classify the message + // - Generate a response + // - Send a reply + break; + + case "message.sent": + console.log(`Message sent from inbox ${data.inbox_id}: ${data.subject}`); + break; + + default: + console.log(`Unhandled event: ${event}`); + } + + return NextResponse.json({ received: true }); +} diff --git a/nextjs-agentmail-starter/src/app/layout.tsx b/nextjs-agentmail-starter/src/app/layout.tsx new file mode 100644 index 0000000..6a60638 --- /dev/null +++ b/nextjs-agentmail-starter/src/app/layout.tsx @@ -0,0 +1,12 @@ +export const metadata = { + title: "AgentMail Dashboard", + description: "Manage AI agent email inboxes", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/nextjs-agentmail-starter/src/app/page.tsx b/nextjs-agentmail-starter/src/app/page.tsx new file mode 100644 index 0000000..d17556c --- /dev/null +++ b/nextjs-agentmail-starter/src/app/page.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useState, useEffect } from "react"; + +interface Inbox { + id: string; + email: string; + display_name: string; +} + +export default function Dashboard() { + const [inboxes, setInboxes] = useState([]); + const [selectedInbox, setSelectedInbox] = useState(""); + const [newInboxName, setNewInboxName] = useState(""); + const [composeTo, setComposeTo] = useState(""); + const [composeSubject, setComposeSubject] = useState(""); + const [composeBody, setComposeBody] = useState(""); + const [status, setStatus] = useState(""); + + useEffect(() => { + fetchInboxes(); + }, []); + + async function fetchInboxes() { + const res = await fetch("/api/agentmail/inboxes"); + const data = await res.json(); + setInboxes(data.data || []); + } + + async function createInbox() { + const res = await fetch("/api/agentmail/inboxes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ displayName: newInboxName }), + }); + if (res.ok) { + setNewInboxName(""); + fetchInboxes(); + setStatus("Inbox created"); + } + } + + async function sendMessage() { + if (!selectedInbox || !composeTo || !composeSubject || !composeBody) { + setStatus("Fill in all fields"); + return; + } + const res = await fetch("/api/agentmail/send", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + inboxId: selectedInbox, + to: composeTo, + subject: composeSubject, + text: composeBody, + }), + }); + if (res.ok) { + setComposeTo(""); + setComposeSubject(""); + setComposeBody(""); + setStatus("Message sent"); + } else { + setStatus("Failed to send"); + } + } + + return ( +
+

AgentMail Dashboard

+ +
+

Inboxes

+
+ setNewInboxName(e.target.value)} + style={{ padding: 8, flex: 1 }} + /> + +
+
    + {inboxes.map((inbox) => ( +
  • + {inbox.display_name} ({inbox.email}) + +
  • + ))} +
+
+ +
+

Compose

+ {selectedInbox ? ( +
+ setComposeTo(e.target.value)} + style={{ padding: 8 }} + /> + setComposeSubject(e.target.value)} + style={{ padding: 8 }} + /> +