Intermediate

Step 3: Draft Generation

In this lesson, you will build a draft generation engine that creates context-aware email replies. The system matches your writing tone using few-shot examples, supports multiple reply styles (professional, friendly, brief), and includes a template system for common response patterns like meeting confirmations and acknowledgments.

Draft Generation Architecture

The draft generator takes the original email, optional thread context, a desired tone, and generates a reply draft. It uses a carefully crafted system prompt with few-shot examples to match your personal writing style.

Original Email + Thread Context
    |
    v
[Tone Selection: professional | friendly | brief | custom]
    |
    v
[System Prompt + Few-Shot Examples]
    |
    v
[OpenAI gpt-4o-mini]
    |
    v
Draft Reply (stored in DB as "pending")

The Draft Generator Module

# app/ai/drafter.py
"""AI-powered email draft generation engine."""
import json
from openai import OpenAI

from app.config import config
from app.database import SessionLocal, Email, Draft, Classification
from app.gmail.parser import get_thread_context
from app.gmail.client import GmailClient

client = OpenAI(api_key=config.openai_api_key)

# Tone-specific system prompts with few-shot examples
TONE_PROMPTS = {
    "professional": {
        "description": "Formal, business-appropriate tone",
        "system": """You are drafting a professional email reply. Use formal but
warm business language. Be concise and action-oriented. Include a proper
greeting and sign-off. Do not use slang or overly casual language.

Example style:
"Hi Sarah,

Thank you for sharing the Q3 report. I have reviewed the key metrics and
have a few observations I would like to discuss.

Could we schedule a 30-minute call this week to go over the revenue
projections in Section 3? I think there may be an opportunity to adjust
our targets based on the latest pipeline data.

Best regards"
""",
    },
    "friendly": {
        "description": "Warm, conversational but still professional",
        "system": """You are drafting a friendly email reply. Use warm,
conversational language while remaining professional. Be approachable and
personable. You can use light humor where appropriate.

Example style:
"Hey Mike,

Thanks for the heads up on the deadline change - that actually works out
perfectly for us! We were hoping for a few extra days to polish the demo.

I will loop in the design team and we will have everything ready by Thursday.
Let me know if anything else comes up!

Cheers"
""",
    },
    "brief": {
        "description": "Short, to-the-point replies",
        "system": """You are drafting a brief email reply. Keep it extremely
concise - 2-3 sentences maximum. Get straight to the point. Use a short
greeting and no elaborate sign-off.

Example style:
"Hi Tom,

Got it - I will have the updated specs to you by EOD Wednesday. Let me know
if the timeline changes.

Thanks"
""",
    },
}

DRAFT_PROMPT = """Draft a reply to this email. Write ONLY the email body
(no subject line). Match the tone specified in the system message.

{thread_context}

--- ORIGINAL EMAIL ---
From: {sender}
Subject: {subject}
Date: {date}

{body}
--- END EMAIL ---

{additional_instructions}

Write the reply now:"""


def generate_draft(
    email: Email,
    tone: str = "professional",
    additional_instructions: str = "",
    include_thread: bool = True
) -> str:
    """
    Generate a draft reply for an email.

    Args:
        email: Email ORM object to reply to.
        tone: Reply tone ('professional', 'friendly', 'brief').
        additional_instructions: Extra instructions for the LLM.
        include_thread: Whether to include thread context.

    Returns:
        Generated draft reply text.
    """
    tone_config = TONE_PROMPTS.get(tone, TONE_PROMPTS["professional"])

    # Build thread context if available
    thread_context = ""
    if include_thread and email.thread_id:
        try:
            gmail_client = GmailClient()
            thread = gmail_client.get_thread(email.thread_id)
            from app.gmail.parser import parse_message
            messages = [parse_message(m) for m in thread.get("messages", [])]
            context = get_thread_context(messages, email.gmail_id)
            if context:
                thread_context = (
                    f"--- PREVIOUS MESSAGES IN THREAD ---\n"
                    f"{context}\n"
                    f"--- END THREAD CONTEXT ---\n"
                )
        except Exception as e:
            print(f"Could not fetch thread context: {e}")

    # Build the prompt
    body = (email.body_text or "")[:3000]
    instructions = ""
    if additional_instructions:
        instructions = f"Additional instructions: {additional_instructions}"

    prompt = DRAFT_PROMPT.format(
        thread_context=thread_context,
        sender=f"{email.sender_name} <{email.sender}>",
        subject=email.subject or "(no subject)",
        date=email.date.strftime("%Y-%m-%d %H:%M") if email.date else "Unknown",
        body=body,
        additional_instructions=instructions,
    )

    response = client.chat.completions.create(
        model=config.openai_model,
        messages=[
            {"role": "system", "content": tone_config["system"]},
            {"role": "user", "content": prompt},
        ],
        temperature=0.7,  # More creative than classification
        max_tokens=500,
    )

    draft_text = response.choices[0].message.content.strip()
    return draft_text


def generate_and_store_draft(
    email_id: int,
    tone: str = "professional",
    additional_instructions: str = ""
) -> Draft:
    """
    Generate a draft and save it to the database.

    Args:
        email_id: Database ID of the email to reply to.
        tone: Reply tone.
        additional_instructions: Extra instructions.

    Returns:
        Draft ORM object.
    """
    session = SessionLocal()
    try:
        email = session.query(Email).get(email_id)
        if not email:
            raise ValueError(f"Email {email_id} not found")

        draft_text = generate_draft(
            email, tone=tone,
            additional_instructions=additional_instructions
        )

        draft = Draft(
            email_id=email.id,
            content=draft_text,
            tone=tone,
            status="pending",
        )
        session.add(draft)
        session.commit()

        print(f"Draft generated for: {email.subject[:50]}")
        return draft

    except Exception as e:
        session.rollback()
        raise
    finally:
        session.close()

Template System for Common Responses

For frequently occurring email types, templates are faster and more consistent than full LLM generation:

# Add to app/ai/drafter.py

RESPONSE_TEMPLATES = {
    "acknowledge": {
        "name": "Acknowledge Receipt",
        "template": """Hi {sender_first_name},

Thank you for sending this over. I have received it and will review
it {timeframe}. I will follow up with any questions or feedback.

Best regards""",
        "defaults": {"timeframe": "by end of day"},
    },
    "meeting_accept": {
        "name": "Accept Meeting",
        "template": """Hi {sender_first_name},

Thanks for the invite! The proposed time works well for me.
I will be there.{agenda_note}

See you then""",
        "defaults": {"agenda_note": ""},
    },
    "meeting_decline": {
        "name": "Decline Meeting",
        "template": """Hi {sender_first_name},

Thank you for the invitation. Unfortunately, I have a conflict at
that time and will not be able to attend.{alternative}

I hope the meeting goes well. Please share any notes or action items
afterward, and I will follow up on anything relevant.

Best regards""",
        "defaults": {"alternative": ""},
    },
    "delegate": {
        "name": "Delegate to Someone",
        "template": """Hi {sender_first_name},

Thanks for reaching out. I am looping in {delegate_name} who is
better positioned to help with this. {delegate_name}, could you
take a look?

{context}

Thanks""",
        "defaults": {"delegate_name": "[Name]", "context": ""},
    },
    "need_more_info": {
        "name": "Request More Information",
        "template": """Hi {sender_first_name},

Thank you for the update. Before I can proceed, I need a bit more
information:

{questions}

Once I have these details, I will be able to {next_step}.

Thanks""",
        "defaults": {
            "questions": "- [Question 1]\n- [Question 2]",
            "next_step": "move forward with this",
        },
    },
    "follow_up": {
        "name": "Follow Up",
        "template": """Hi {sender_first_name},

I wanted to follow up on {topic}. Have you had a chance to
{action}?

No rush, but I wanted to make sure this does not fall through
the cracks. Let me know if you need anything from my end.

Thanks""",
        "defaults": {
            "topic": "our previous conversation",
            "action": "look into this",
        },
    },
}


def generate_from_template(
    email: Email,
    template_key: str,
    overrides: dict = None
) -> str:
    """
    Generate a draft from a predefined template.

    Args:
        email: Email ORM object.
        template_key: Key from RESPONSE_TEMPLATES.
        overrides: Dict to override template defaults.

    Returns:
        Formatted draft text.
    """
    template_data = RESPONSE_TEMPLATES.get(template_key)
    if not template_data:
        raise ValueError(
            f"Unknown template: {template_key}. "
            f"Available: {list(RESPONSE_TEMPLATES.keys())}"
        )

    # Build template variables
    sender_first_name = (
        email.sender_name.split()[0] if email.sender_name else "there"
    )

    variables = {
        "sender_first_name": sender_first_name,
        **template_data["defaults"],
    }
    if overrides:
        variables.update(overrides)

    draft = template_data["template"].format(**variables)
    return draft


def auto_generate_drafts(
    priorities: list = None,
    tone: str = "professional",
    limit: int = 20
) -> list:
    """
    Auto-generate drafts for high-priority emails that need replies.

    Args:
        priorities: List of priority levels to process.
        tone: Default tone for generated drafts.
        limit: Maximum drafts to generate.

    Returns:
        List of Draft objects created.
    """
    if priorities is None:
        priorities = ["urgent", "high"]

    session = SessionLocal()
    drafts = []

    try:
        # Find classified emails without drafts
        emails = (
            session.query(Email)
            .join(Classification)
            .outerjoin(Draft)
            .filter(
                Classification.priority.in_(priorities),
                Classification.category.in_(
                    ["task", "question", "meeting"]
                ),
                Draft.id.is_(None),
            )
            .order_by(Email.date.desc())
            .limit(limit)
            .all()
        )

        print(f"Generating drafts for {len(emails)} emails...")

        for email in emails:
            try:
                draft_text = generate_draft(email, tone=tone)
                draft = Draft(
                    email_id=email.id,
                    content=draft_text,
                    tone=tone,
                    status="pending",
                )
                session.add(draft)
                drafts.append(draft)
                print(f"  Draft: {email.subject[:50]}")
            except Exception as e:
                print(f"  Error drafting for '{email.subject}': {e}")
                continue

        session.commit()
        print(f"Generated {len(drafts)} drafts")

    except Exception as e:
        session.rollback()
        raise
    finally:
        session.close()

    return drafts

Testing Draft Generation

# Test generating a draft for the most recent email
python -c "
from app.database import SessionLocal, Email
from app.ai.drafter import generate_draft, generate_from_template

session = SessionLocal()
email = session.query(Email).order_by(Email.date.desc()).first()
session.close()

if email:
    print(f'Email: {email.subject}')
    print(f'From: {email.sender_name} <{email.sender}>')
    print()

    # Generate LLM draft
    print('=== Professional Draft ===')
    draft = generate_draft(email, tone='professional')
    print(draft)
    print()

    # Generate brief draft
    print('=== Brief Draft ===')
    draft = generate_draft(email, tone='brief')
    print(draft)
    print()

    # Template-based draft
    print('=== Acknowledge Template ===')
    template_draft = generate_from_template(
        email, 'acknowledge',
        overrides={'timeframe': 'within the next 24 hours'}
    )
    print(template_draft)
else:
    print('No emails found. Run email integration first.')
"
💡
Tone Customization: To train the model on your specific writing style, add 3-5 examples of your actual email replies to the few-shot examples in the tone prompts. The more representative the examples, the better the generated drafts will match your voice.

Key Takeaways

  • Thread context improves draft quality dramatically. The LLM can reference previous messages in the conversation.
  • Temperature 0.7 provides creative variety while staying relevant. Lower values produce more formulaic responses.
  • Few-shot examples in the system prompt are the most effective way to match a personal writing tone.
  • Templates handle common response patterns faster and cheaper than LLM generation.
  • The auto_generate_drafts function processes only high-priority emails that actually need replies (tasks, questions, meetings).

What Is Next

In the next lesson, you will build smart features — email summarization, action item extraction, follow-up reminders, and daily digest generation.