MolinoPro

new-assistant-gmail-pattern.skill

Master Codebase Guidebook
Markdown + HTML Dev-Docs Renderer - Frontend Client Module

Default Index
Open README.md
Root: README.mdskills
Milestones

⚡ ONE-GLANCE (DEPLOY)

  1. Create Gmail account (support-ai@)
  2. Go to Apps Script → paste code below (single file)
  3. Set Script Properties:
    • OPENAI_API_KEY
    • YOUR_EMAIL
    • AVATAR_EMAIL
    • NEXT_ENDPOINT (optional)
    • AVATAR_SECRET (optional)
  4. Add time trigger → every 2–5 min → pollInbox
  5. 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