feat: use send instead of draft for email reply agent

This commit is contained in:
Richard Tang
2026-03-03 12:04:44 -08:00
committed by bryan
parent f490038e36
commit f0899bb35d
4 changed files with 23 additions and 23 deletions
@@ -18,7 +18,7 @@ from .nodes import intake_node, search_node, confirm_draft_node
goal = Goal(
id="email-reply-goal",
name="Email Reply Agent",
description="Filter unreplied emails by user criteria, confirm recipients, draft personalized replies.",
description="Filter unreplied emails by user criteria, confirm recipients, send personalized replies.",
success_criteria=[
SuccessCriterion(
id="sc-filter",
@@ -29,15 +29,15 @@ goal = Goal(
),
SuccessCriterion(
id="sc-confirm",
description="User confirms recipient list before drafting",
description="User confirms recipient list before sending",
metric="Confirmation rate",
target="100%",
weight=0.25,
),
SuccessCriterion(
id="sc-personalize",
description="Drafts are personalized based on email content and tone guidance",
metric="User satisfaction with draft relevance",
description="Replies are personalized based on email content and tone guidance",
metric="User satisfaction with reply relevance",
target="85%",
weight=0.40,
),
@@ -45,7 +45,7 @@ goal = Goal(
constraints=[
Constraint(
id="c-privacy",
description="Never auto-send emails; all replies go to drafts for user review",
description="Never send emails without explicit user confirmation; always present recipient list and get approval first",
constraint_type="hard",
category="functional",
),
@@ -103,7 +103,7 @@ terminal_nodes = []
# Module-level vars read by AgentRunner.load()
conversation_mode = "continuous"
identity_prompt = "You are a helpful email reply assistant that filters unreplied emails and drafts personalized responses."
identity_prompt = "You are a helpful email reply assistant that filters unreplied emails and sends personalized responses."
loop_config = {
"max_iterations": 100,
"max_tool_calls_per_turn": 30,
@@ -36,7 +36,7 @@ default_config = RuntimeConfig()
class AgentMetadata:
name: str = "Email Reply Agent"
version: str = "1.0.0"
description: str = "Filter unreplied emails, confirm recipients, draft personalized replies."
description: str = "Filter unreplied emails, confirm recipients, send personalized replies."
intro_message: str = "Tell me which emails you want to reply to (e.g., 'emails from @company.com in the last week')."
@@ -74,39 +74,39 @@ If no emails found, set empty array: set_output("email_list", [])
tools=["gmail_list_messages", "gmail_get_message", "gmail_batch_get_messages"],
)
# Node 3: Confirm & Draft (client-facing)
# Node 3: Confirm & Reply (client-facing)
confirm_draft_node = NodeSpec(
id="confirm-draft",
name="Confirm & Draft",
description="Present emails for confirmation, draft personalized replies",
name="Confirm & Reply",
description="Present emails for confirmation, send personalized replies",
node_type="event_loop",
client_facing=True,
max_node_visits=0,
input_keys=["email_list", "filter_criteria"],
output_keys=["batch_complete", "restart"],
nullable_output_keys=["batch_complete", "restart"],
success_criteria="User confirmed recipients and personalized drafts created for each.",
success_criteria="User confirmed recipients and personalized replies sent for each.",
system_prompt="""\
You are a Gmail reply drafter. Present emails for confirmation, then draft personalized replies.
You are a Gmail reply assistant. Present emails for confirmation, then send personalized replies.
**STEP 1 Present for confirmation (text only, NO tool calls):**
1. Show the email list in readable format:
- #. Sender Name <email> - Subject (Date)
- Snippet: first 150 chars
2. Ask: "These are the people to reply to. Confirm? Any tone preferences or specific messages?"
2. Ask: "These are the emails to reply to. Confirm? Any tone preferences or specific messages?"
3. Wait for user response
**STEP 2 Handle user response:**
If user CONFIRMS (says yes, go ahead, sounds good, etc.):
For EACH email in email_list:
1. Read the sender, subject, and snippet
1. Read the subject and snippet
2. Use tone_guidance from filter_criteria + any user-specified preferences
3. Call gmail_create_draft with:
- to: sender email
- subject: "Re: " + original_subject
- body: personalized 2-4 sentence reply based on email context
4. After all drafts created, call: set_output("batch_complete", True)
3. Call gmail_reply_email with:
- message_id: the email's message_id
- html: personalized 2-4 sentence reply based on email context
(The tool automatically handles recipient, subject, and threading)
4. After all replies sent, call: set_output("batch_complete", True)
If user wants to CHANGE LOGIC/FILTER (says change filter, different criteria, not these emails, wrong emails, etc.):
1. Acknowledge their request
@@ -118,7 +118,7 @@ Personalization rules:
- If tone_guidance specifies style, follow it
- Keep replies concise but warm
""",
tools=["gmail_create_draft"],
tools=["gmail_reply_email"],
)
__all__ = ["intake_node", "search_node", "confirm_draft_node"]
@@ -60,10 +60,10 @@ class TestAgentStructure:
assert "gmail_list_messages" in search_node.tools
assert "gmail_get_message" in search_node.tools
def test_confirm_draft_node_has_draft_tool(self, agent_module):
"""Confirm-draft node has draft creation tool."""
def test_confirm_draft_node_has_reply_tool(self, agent_module):
"""Confirm-draft node has reply tool."""
draft_node = next(n for n in agent_module.nodes if n.id == "confirm-draft")
assert "gmail_create_draft" in draft_node.tools
assert "gmail_reply_email" in draft_node.tools
def test_confirm_draft_node_has_restart_output(self, agent_module):
"""Confirm-draft node has restart output key for logic changes."""