"""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'.", }