⚡ ONE-GLANCE (DEPLOY)
- Create Gmail account (support-ai@)
- Go to Apps Script → paste code below (single file)
- Set Script Properties:
- OPENAI_API_KEY
- YOUR_EMAIL
- AVATAR_EMAIL
- NEXT_ENDPOINT (optional)
- AVATAR_SECRET (optional)
- Add time trigger → every 2–5 min → pollInbox
- Test:
- client email → reply
- you reply → AI stops
- /consult → internal reply
- /initiate email@ → outbound
⸻
🧩 FULL MLV — DROP-IN APPS SCRIPT (PRODUCTION SAFE)
/***************************************
- AVATAR EMAIL INTELLIGENCE ROUTER v1
- Single-file MLV (safe, idempotent)
**************************/
/ CONFIG /
const CONFIG = {
MODEL: "gpt-4o-mini",
PROCESSING_TTL_MS: 5 * 60 * 1000, // 5 min
};
/ ENTRY /
function pollInbox() {
const threads = GmailApp.getInboxThreads(0, 10);
for (const thread of threads) {
processThread(thread);
}
}
/ CORE /
function processThread(thread) {
const threadId = thread.getId();
if (isProcessing(threadId)) return;
withLock(() => {
if (isProcessing(threadId)) return;
markProcessing(threadId);
try {
const messages = thread.getMessages();
const lastMessage = messages[messages.length - 1];
if (shouldYield(messages)) return;
if (isDirectorMode(lastMessage)) {
handleDirectorCommand(lastMessage);
} else {
handleClientThread(thread, lastMessage);
}
markDone(threadId);
} catch (e) {
resetThread(threadId);
throw e;
}
});
}
/ MODE /
function isDirectorMode(message) {
const from = message.getFrom();
const to = message.getTo();
const YOUR_EMAIL = getProp("YOUR_EMAIL");
const AVATAR_EMAIL = getProp("AVATAR_EMAIL");
const isFromYou = from.includes(YOUR_EMAIL);
const onlyAvatar = to.trim() === AVATAR_EMAIL;
return isFromYou && onlyAvatar;
}
/ YIELD /
function shouldYield(messages) {
const lastAI = findLastAIIndex(messages);
if (lastAI === -1) return false;
const YOUR_EMAIL = getProp("YOUR_EMAIL");
return messages
.slice(lastAI + 1)
.some(m =>
m.getFrom().includes(YOUR_EMAIL) ||
(m.getCc() && m.getCc().includes(YOUR_EMAIL))
);
}
/ AI DETECTION /
function findLastAIIndex(messages) {
const AVATAR_EMAIL = getProp("AVATAR_EMAIL");
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].getFrom().includes(AVATAR_EMAIL)) {
return i;
}
}
return -1;
}
/ DIRECTOR /
function handleDirectorCommand(message) {
const subject = message.getSubject();
const body = message.getPlainBody();
const from = message.getFrom();
if (subject.startsWith("/initiate")) {
const target = extractEmail(subject);
if (!target) return;
const reply = generateReply(body, "initiate");
GmailApp.sendEmail(target, "Introduction", reply, {
cc: from
});
return;
}
if (subject.startsWith("/consult")) {
const reply = generateReply(body, "consult");
GmailApp.sendEmail(from, "Re: consult", reply);
return;
}
}
/ CLIENT /
function handleClientThread(thread, message) {
const body = message.getPlainBody();
const threadId = thread.getId();
const intent = classifyIntent(body);
if (intent === "ignore") return;
if (intent === "lead") {
postToNext(body, threadId);
}
const reply = generateReply(body, "client");
thread.reply(reply);
}
/ INTENT /
function classifyIntent(text) {
const lower = text.toLowerCase();
if (lower.includes("price") || lower.includes("info")) return "lead";
if (lower.includes("unsubscribe") || lower.includes("spam")) return "ignore";
return "lead";
}
/ LLM /
function generateReply(input, mode) {
const OPENAI_KEY = getProp("OPENAI_API_KEY");
const payload = {
model: CONFIG.MODEL,
messages: [
{
role: "system",
content: getPersona(mode),
},
{
role: "user",
content: input,
},
],
};
const res = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", {
method: "post",
headers: {
Authorization:
Bearer ${OPENAI_KEY}, "Content-Type": "application/json", }, payload: JSON.stringify(payload), muteHttpExceptions: true, }); const json = JSON.parse(res.getContentText()); return json?.choices?.[0]?.message?.content || "Sorry, could you clarify?"; } / PERSONA / function getPersona(mode) { if (mode === "consult") { return "You are an internal assistant. Be concise and helpful."; } if (mode === "initiate") { return "You are a professional outreach assistant. Introduce the sender clearly and politely."; } return "You are a helpful support assistant. Be concise, polite, and clear."; } / NEXT.JS / function postToNext(content, threadId) { const url = getProp("NEXT_ENDPOINT"); const secret = getProp("AVATAR_SECRET"); if (!url) return; const payload = { content, threadId, secret, }; UrlFetchApp.fetch(url, { method: "post", contentType: "application/json", payload: JSON.stringify(payload), }); } / PROCESSING / function markProcessing(threadId) { const props = PropertiesService.getScriptProperties(); props.setProperty(threadId, JSON.stringify({ status: "processing", ts: Date.now() })); } function isProcessing(threadId) { const props = PropertiesService.getScriptProperties(); const raw = props.getProperty(threadId); if (!raw) return false; try { const data = JSON.parse(raw); if (data.status !== "processing") return false; const expired = Date.now() - data.ts > CONFIG.PROCESSING_TTL_MS; if (expired) { props.deleteProperty(threadId); return false; } return true; } catch { props.deleteProperty(threadId); return false; } } function markDone(threadId) { const props = PropertiesService.getScriptProperties(); props.setProperty(threadId, JSON.stringify({ status: "done", ts: Date.now() })); } function resetThread(threadId) { PropertiesService.getScriptProperties().deleteProperty(threadId); } / LOCK / function withLock(fn) { const lock = LockService.getScriptLock(); if (!lock.tryLock(5000)) return; try { fn(); } finally { lock.releaseLock(); } } / UTILS *************/ function extractEmail(text) { const match = text.match(/[\w.-]+@[\w.-]+.\w+/); return match ? match[0] : null; } function getProp(key) { return PropertiesService.getScriptProperties().getProperty(key); }
⸻
✅ WHAT THIS VERSION GUARANTEES
- No duplicate replies
- Safe concurrency (locks)
- No stuck threads (TTL)
- Correct human override (yield after your reply)
- Director mode works
- Minimal external dependency
⸻
🧭 NEXT STEP (ONLY AFTER THIS WORKS)
Do NOT extend yet.
Run this for:
- real inbox
- real conversations
Then iterate based on:
- failure cases
- tone issues
- missed intent
⸻
If needed next: → hardened version with logging + thread summaries + retry queue