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( goal = Goal(
id="email-reply-goal", id="email-reply-goal",
name="Email Reply Agent", 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=[ success_criteria=[
SuccessCriterion( SuccessCriterion(
id="sc-filter", id="sc-filter",
@@ -29,15 +29,15 @@ goal = Goal(
), ),
SuccessCriterion( SuccessCriterion(
id="sc-confirm", id="sc-confirm",
description="User confirms recipient list before drafting", description="User confirms recipient list before sending",
metric="Confirmation rate", metric="Confirmation rate",
target="100%", target="100%",
weight=0.25, weight=0.25,
), ),
SuccessCriterion( SuccessCriterion(
id="sc-personalize", id="sc-personalize",
description="Drafts are personalized based on email content and tone guidance", description="Replies are personalized based on email content and tone guidance",
metric="User satisfaction with draft relevance", metric="User satisfaction with reply relevance",
target="85%", target="85%",
weight=0.40, weight=0.40,
), ),
@@ -45,7 +45,7 @@ goal = Goal(
constraints=[ constraints=[
Constraint( Constraint(
id="c-privacy", 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", constraint_type="hard",
category="functional", category="functional",
), ),
@@ -103,7 +103,7 @@ terminal_nodes = []
# Module-level vars read by AgentRunner.load() # Module-level vars read by AgentRunner.load()
conversation_mode = "continuous" 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 = { loop_config = {
"max_iterations": 100, "max_iterations": 100,
"max_tool_calls_per_turn": 30, "max_tool_calls_per_turn": 30,
@@ -36,7 +36,7 @@ default_config = RuntimeConfig()
class AgentMetadata: class AgentMetadata:
name: str = "Email Reply Agent" name: str = "Email Reply Agent"
version: str = "1.0.0" 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')." 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"], 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( confirm_draft_node = NodeSpec(
id="confirm-draft", id="confirm-draft",
name="Confirm & Draft", name="Confirm & Reply",
description="Present emails for confirmation, draft personalized replies", description="Present emails for confirmation, send personalized replies",
node_type="event_loop", node_type="event_loop",
client_facing=True, client_facing=True,
max_node_visits=0, max_node_visits=0,
input_keys=["email_list", "filter_criteria"], input_keys=["email_list", "filter_criteria"],
output_keys=["batch_complete", "restart"], output_keys=["batch_complete", "restart"],
nullable_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="""\ 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):** **STEP 1 Present for confirmation (text only, NO tool calls):**
1. Show the email list in readable format: 1. Show the email list in readable format:
- #. Sender Name <email> - Subject (Date) - #. Sender Name <email> - Subject (Date)
- Snippet: first 150 chars - 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 3. Wait for user response
**STEP 2 Handle user response:** **STEP 2 Handle user response:**
If user CONFIRMS (says yes, go ahead, sounds good, etc.): If user CONFIRMS (says yes, go ahead, sounds good, etc.):
For EACH email in email_list: 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 2. Use tone_guidance from filter_criteria + any user-specified preferences
3. Call gmail_create_draft with: 3. Call gmail_reply_email with:
- to: sender email - message_id: the email's message_id
- subject: "Re: " + original_subject - html: personalized 2-4 sentence reply based on email context
- body: personalized 2-4 sentence reply based on email context (The tool automatically handles recipient, subject, and threading)
4. After all drafts created, call: set_output("batch_complete", True) 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.): If user wants to CHANGE LOGIC/FILTER (says change filter, different criteria, not these emails, wrong emails, etc.):
1. Acknowledge their request 1. Acknowledge their request
@@ -118,7 +118,7 @@ Personalization rules:
- If tone_guidance specifies style, follow it - If tone_guidance specifies style, follow it
- Keep replies concise but warm - Keep replies concise but warm
""", """,
tools=["gmail_create_draft"], tools=["gmail_reply_email"],
) )
__all__ = ["intake_node", "search_node", "confirm_draft_node"] __all__ = ["intake_node", "search_node", "confirm_draft_node"]
@@ -60,10 +60,10 @@ class TestAgentStructure:
assert "gmail_list_messages" in search_node.tools assert "gmail_list_messages" in search_node.tools
assert "gmail_get_message" in search_node.tools assert "gmail_get_message" in search_node.tools
def test_confirm_draft_node_has_draft_tool(self, agent_module): def test_confirm_draft_node_has_reply_tool(self, agent_module):
"""Confirm-draft node has draft creation tool.""" """Confirm-draft node has reply tool."""
draft_node = next(n for n in agent_module.nodes if n.id == "confirm-draft") 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): def test_confirm_draft_node_has_restart_output(self, agent_module):
"""Confirm-draft node has restart output key for logic changes.""" """Confirm-draft node has restart output key for logic changes."""