Ashgen12's picture
Brevo + faster drafts + adaptive chart + fuzzy lookup
504c58a verified
Raw
History Blame Contribute Delete
9.63 kB
"""Email drafting + sending MCP tool.
Two modes:
* ``mode="draft"`` — given a free-form natural-language brief, returns three
tonal variants (formal / casual / polite) that the user can pick from.
* ``mode="send"`` — given a final subject + body + recipient(s), delivers the
email through the configured SMTP relay (reusing the same settings as the
interview-invite flow).
The 3 tone variants are produced by Gemini using the project's ``GEMINI_API_KEY``.
If Gemini isn't reachable we fall back to deterministic template wrappers so the
flow still works for demos.
"""
from __future__ import annotations
import json
import logging
import re
from fastmcp import FastMCP
from core.config import get_settings
from services.notification_service import send_plain_email
logger = logging.getLogger(__name__)
_TONE_LABELS = ("formal", "casual", "polite")
def _strip_code_fences(text: str) -> str:
fence = re.search(r"```(?:json)?\s*(\{[\s\S]*\}|\[[\s\S]*\])\s*```", text)
if fence:
return fence.group(1)
return text.strip()
def _fallback_draft(brief: str, tone_label: str, recipient_name: str | None) -> dict:
name = recipient_name or "there"
if tone_label == "formal":
opener = f"Dear {name},"
closer = "Best regards,\nRecruitment OS"
elif tone_label == "casual":
opener = f"Hi {name.split()[0] if recipient_name else 'there'},"
closer = "Cheers,\nRecruitment OS"
else:
opener = f"Hello {name},"
closer = "Thank you for your time,\nRecruitment OS"
subject = brief.strip().split("\n")[0][:90] or "Following up"
body = f"{opener}\n\n{brief.strip()}\n\n{closer}"
return {"tone": tone_label, "subject": subject, "body": body}
def _build_drafts(brief: str, recipient_name: str | None) -> list[dict]:
"""Generate all three tonal variants in a single Gemini call.
Previously we did three sequential ``generate_content`` calls (one per tone).
On the HF Space free tier this took 30–60 s wall-clock and looked like the
UI was hanging. One JSON-array call cuts it to a single request.
"""
settings = get_settings()
if not settings.gemini_api_key:
return [_fallback_draft(brief, tone, recipient_name) for tone in _TONE_LABELS]
addressee = recipient_name or "the recipient"
prompt = (
"You are drafting a professional recruiter email. Produce THREE tonal "
"variants of the same email — formal, casual, and polite — based on the "
"brief below.\n\n"
f"USER BRIEF:\n{brief}\n\n"
f"Addressee placeholder: {addressee}\n\n"
"Return STRICT JSON only — a JSON array of three objects, in this exact "
"order: formal, casual, polite. Each object has keys: tone, subject, "
"body. The body must end with a sign-off line. No prose outside the JSON.\n\n"
"Example shape:\n"
'[{"tone":"formal","subject":"…","body":"…"},'
'{"tone":"casual","subject":"…","body":"…"},'
'{"tone":"polite","subject":"…","body":"…"}]'
)
try:
from google import genai
client = genai.Client(api_key=settings.gemini_api_key)
response = client.models.generate_content(
model=settings.gemini_model or "gemini-2.5-pro",
contents=[prompt],
)
raw = (getattr(response, "text", None) or "").strip()
if not raw:
raise RuntimeError("Gemini returned an empty response.")
data = json.loads(_strip_code_fences(raw))
if not isinstance(data, list) or len(data) < 3:
raise RuntimeError("Gemini response was not a 3-element array.")
drafts: list[dict] = []
for tone, item in zip(_TONE_LABELS, data[:3]):
if not isinstance(item, dict):
drafts.append(_fallback_draft(brief, tone, recipient_name))
continue
drafts.append(
{
"tone": tone,
"subject": str(item.get("subject") or "").strip()
or _fallback_draft(brief, tone, recipient_name)["subject"],
"body": str(item.get("body") or "").strip()
or _fallback_draft(brief, tone, recipient_name)["body"],
}
)
return drafts
except Exception as exc:
logger.warning("Gemini email draft failed; using template fallback: %s", exc)
return [_fallback_draft(brief, tone, recipient_name) for tone in _TONE_LABELS]
def register(mcp: FastMCP) -> None:
@mcp.tool()
def email_compose(
mode: str,
brief: str | None = None,
recipient_name: str | None = None,
recipient_email: str | None = None,
cc: str | None = None,
subject: str | None = None,
body: str | None = None,
tone: str | None = None,
) -> dict:
"""Draft or send recruiter emails.
``mode="draft"`` requires ``brief``. Returns three tone variants
(formal / casual / polite) that the user can choose from.
``mode="send"`` requires ``recipient_email`` (comma-separated allowed),
``subject`` and ``body`` and delivers the message via SMTP.
"""
normalized = (mode or "").strip().lower()
if normalized == "draft":
text = (brief or "").strip()
if not text:
return {
"status": "error",
"message": "brief is required when mode='draft'.",
}
drafts = _build_drafts(text, recipient_name)
cards = []
for draft in drafts:
preview = draft["body"][:200] + ("…" if len(draft["body"]) > 200 else "")
cards.append(
{
"title": f"{draft['tone'].title()} draft",
"subtitle": f"Subject: {draft['subject']}",
"tags": [draft["tone"].title()],
"meta": {"preview": preview},
"actions": [
{
"label": f"Use {draft['tone']} tone",
"action": (
f"Use the {draft['tone']} tone draft. "
"What's the recipient's email address?"
),
}
],
}
)
markdown_sections = [f"# Email drafts for: {text[:90]}\n"]
for draft in drafts:
markdown_sections.append(
f"## {draft['tone'].title()} tone\n"
f"**Subject:** {draft['subject']}\n\n"
f"{draft['body']}\n"
)
markdown_body = "\n---\n".join(markdown_sections)
return {
"status": "success",
"drafts": drafts,
"ui": {
"summary": (
"Three tone variants ready. Reply with your choice — "
"'use formal', 'use casual', or 'use polite' — and the "
"recipient email to send."
),
"cards": cards,
"markdown": markdown_body,
},
}
if normalized == "send":
if not recipient_email or not subject or not body:
return {
"status": "error",
"message": "recipient_email, subject, and body are required when mode='send'.",
}
to_list = [addr.strip() for addr in recipient_email.split(",") if addr.strip()]
cc_list = [addr.strip() for addr in (cc or "").split(",") if addr.strip()]
result = send_plain_email(
to=to_list,
subject=subject,
body=body,
cc=cc_list,
)
tone_label = (tone or "selected").lower()
return {
"status": "success" if result.sent else "error",
"delivery": result.delivery,
"message": result.detail,
"ui": {
"summary": (
f"Email ({tone_label} tone) {'sent' if result.sent else 'NOT sent'} — "
f"{result.detail}"
),
"cards": [
{
"title": "Email delivery",
"subtitle": result.detail,
"tags": [
tone_label.title() or "Email",
"Delivered" if result.sent else "Failed",
],
"meta": {
"to": ", ".join(to_list),
"cc": ", ".join(cc_list) if cc_list else "—",
"subject": subject,
},
"actions": [],
}
],
"markdown": (
f"# {subject}\n\n"
f"**To:** {', '.join(to_list)} \n"
f"**Cc:** {', '.join(cc_list) if cc_list else '—'}\n\n"
f"{body}"
),
},
}
return {
"status": "error",
"message": "mode must be 'draft' or 'send'.",
}