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.')
"
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_draftsfunction 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.
Lilly Tech Systems