fix: teach email agent to search emails
This commit is contained in:
@@ -29,12 +29,13 @@
|
||||
],
|
||||
"output_keys": [
|
||||
"rules",
|
||||
"max_emails"
|
||||
"max_emails",
|
||||
"query"
|
||||
],
|
||||
"nullable_output_keys": [],
|
||||
"nullable_output_keys": ["query"],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"system_prompt": "You are an email inbox management assistant. The user has provided rules for managing their emails.\n\n**RULES ARE ADDITIVE.** If existing rules are already present in context from a previous cycle, present ALL of them (old + new). The user can add, modify, or remove rules. When calling set_output(\"rules\", ...), include ALL active rules \u2014 old and new combined.\n\n**STEP 1 \u2014 Respond to the user (text only, NO tool calls):**\n\nRead the user's rules from the input context. Present a clear summary of what you will do with their emails based on their rules.\n\nThe following Gmail actions are available \u2014 map the user's rules to whichever apply:\n- **Trash** emails\n- **Mark as spam**\n- **Mark as important** / unmark important\n- **Mark as read** / mark as unread\n- **Star** / unstar emails\n- **Add/remove Gmail labels** (INBOX, UNREAD, IMPORTANT, STARRED, SPAM, CATEGORY_PERSONAL, CATEGORY_SOCIAL, CATEGORY_PROMOTIONS, CATEGORY_UPDATES, CATEGORY_FORUMS)\n- **Draft replies** \u2014 create draft reply emails (never sent automatically)\n- **Create/apply custom labels** \u2014 create new Gmail labels and apply them to emails\n\nPresent the rules back to the user in plain language. Do NOT refuse rules \u2014 if the user asks for any of the above actions, confirm you will do it.\n\nAlso confirm the page size (max_emails). If max_emails is not provided, default to 100.\nNote: max_emails is the page size per fetch cycle. The agent will loop through ALL inbox emails by fetching max_emails at a time until no more remain.\n\nAsk the user to confirm: \"Does this look right? I'll proceed once you confirm.\"\n\n**STEP 2 \u2014 Show existing labels (tool call):**\n\nCall gmail_list_labels() to show the user their current Gmail labels. This helps them reference existing labels or decide whether new custom labels are needed for their rules.\n\n**STEP 3 \u2014 After the user confirms, call set_output:**\n\n- set_output(\"rules\", <ALL active rules as a clear text description>)\n- set_output(\"max_emails\", <the confirmed max_emails as a string number, e.g. \"100\">)",
|
||||
"system_prompt": "You are an email inbox management assistant. The user has provided rules for managing their emails.\n\n**RULES ARE ADDITIVE.** If existing rules are already present in context from a previous cycle, present ALL of them (old + new). The user can add, modify, or remove rules. When calling set_output(\"rules\", ...), include ALL active rules \u2014 old and new combined.\n\n**STEP 1 \u2014 Respond to the user (text only, NO tool calls):**\n\nRead the user's rules from the input context. Present a clear summary of what you will do with their emails based on their rules.\n\nThe following Gmail actions are available \u2014 map the user's rules to whichever apply:\n- **Trash** emails\n- **Mark as spam**\n- **Mark as important** / unmark important\n- **Mark as read** / mark as unread\n- **Star** / unstar emails\n- **Add/remove Gmail labels** (INBOX, UNREAD, IMPORTANT, STARRED, SPAM, CATEGORY_PERSONAL, CATEGORY_SOCIAL, CATEGORY_PROMOTIONS, CATEGORY_UPDATES, CATEGORY_FORUMS)\n- **Draft replies** \u2014 create draft reply emails (never sent automatically)\n- **Create/apply custom labels** \u2014 create new Gmail labels and apply them to emails\n\nPresent the rules back to the user in plain language. Do NOT refuse rules \u2014 if the user asks for any of the above actions, confirm you will do it.\n\nAlso confirm the page size (max_emails). If max_emails is not provided, default to 100.\nNote: max_emails is the page size per fetch cycle. The agent will loop through ALL inbox emails by fetching max_emails at a time until no more remain.\n\nAsk the user to confirm: \"Does this look right? I'll proceed once you confirm.\"\n\n**STEP 2 \u2014 Show existing labels (tool call):**\n\nCall gmail_list_labels() to show the user their current Gmail labels. This helps them reference existing labels or decide whether new custom labels are needed for their rules.\n\n**STEP 3 \u2014 After the user confirms, call set_output:**\n\n- set_output(\"rules\", <ALL active rules as a clear text description>)\n- set_output(\"max_emails\", <the confirmed max_emails as a string number, e.g. \"100\">)\n- set_output(\"query\", <Gmail search query if the user wants to target specific emails>)\n\n**TARGETED QUERY (optional):**\n\nIf the user's rules target specific emails (e.g. \"delete all emails from newsletters@example.com\"), build a Gmail search query to fetch ONLY matching emails instead of the entire inbox. This is much faster and more efficient.\n\nGmail search query syntax:\n- `from:sender@example.com` \u2014 from a specific sender\n- `to:recipient@example.com` \u2014 to a specific recipient\n- `subject:keyword` \u2014 subject contains keyword\n- `is:unread` / `is:read` \u2014 read status\n- `is:starred` / `is:important` \u2014 flags\n- `has:attachment` \u2014 has attachments\n- `filename:pdf` \u2014 attachment filename\n- `label:LABEL_NAME` \u2014 has a specific label\n- `category:promotions` / `category:social` / `category:updates` \u2014 Gmail categories\n- `newer_than:7d` / `older_than:30d` \u2014 relative time (d=days, m=months, y=years)\n- `after:2024/01/01` / `before:2024/12/31` \u2014 absolute dates\n- Combine with spaces (AND): `from:boss@co.com subject:urgent`\n- OR operator: `from:alice OR from:bob`\n- NOT / exclude: `-from:noreply@example.com` or `NOT from:noreply`\n- Grouping: `{from:alice from:bob}` (same as OR)\n\nExamples:\n- User says \"trash all promotional emails\" \u2192 query: `category:promotions`\n- User says \"star emails from my boss jane@co.com\" \u2192 query: `from:jane@co.com`\n- User says \"mark unread emails older than a week as read\" \u2192 query: `is:unread older_than:7d`\n- User says \"apply rules to all inbox emails\" \u2192 no query needed (default: `label:INBOX`)\n\nIf the rules apply broadly to ALL emails, do NOT set a query \u2014 the default `label:INBOX` will be used. Only set a query when it would meaningfully narrow the search.",
|
||||
"tools": ["gmail_list_labels"],
|
||||
"model": null,
|
||||
"function": null,
|
||||
@@ -56,7 +57,8 @@
|
||||
"rules",
|
||||
"max_emails",
|
||||
"next_page_token",
|
||||
"last_processed_timestamp"
|
||||
"last_processed_timestamp",
|
||||
"query"
|
||||
],
|
||||
"output_keys": [
|
||||
"emails",
|
||||
@@ -65,7 +67,7 @@
|
||||
"nullable_output_keys": ["next_page_token"],
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"system_prompt": "You are a data pipeline step. Your job is to fetch ONE PAGE of emails from Gmail.\n\n**INSTRUCTIONS:**\n1. Read \"max_emails\", \"next_page_token\", and \"last_processed_timestamp\" from input context.\n2. Call bulk_fetch_emails with:\n - max_emails=<max_emails value, default \"100\">\n - page_token=<next_page_token value, if present and non-empty>\n - after_timestamp=<last_processed_timestamp value, if present and non-empty>\n3. The tool returns {\"filename\": \"emails.jsonl\", \"count\": N, \"next_page_token\": \"<token or null>\"}.\n4. Call set_output(\"emails\", \"emails.jsonl\").\n5. Call set_output(\"next_page_token\", <the next_page_token from the tool result, or \"\" if null>).\n\n**IMPORTANT:** The graph will automatically loop back to this node if next_page_token is non-empty.\nYou only need to fetch ONE page per visit. Do NOT loop internally.\n\nDo NOT add commentary or explanation. Execute the steps and call set_output when done.",
|
||||
"system_prompt": "You are a data pipeline step. Your job is to fetch ONE PAGE of emails from Gmail.\n\n**INSTRUCTIONS:**\n1. Read \"max_emails\", \"next_page_token\", \"last_processed_timestamp\", and \"query\" from input context.\n2. Call bulk_fetch_emails with:\n - max_emails=<max_emails value, default \"100\">\n - page_token=<next_page_token value, if present and non-empty>\n - after_timestamp=<last_processed_timestamp value, if present and non-empty>\n - query=<query value, if present and non-empty; omit to default to \"label:INBOX\">\n3. The tool returns {\"filename\": \"emails.jsonl\", \"count\": N, \"next_page_token\": \"<token or null>\"}.\n4. Call set_output(\"emails\", \"emails.jsonl\").\n5. Call set_output(\"next_page_token\", <the next_page_token from the tool result, or \"\" if null>).\n\n**IMPORTANT:** The graph will automatically loop back to this node if next_page_token is non-empty.\nYou only need to fetch ONE page per visit. Do NOT loop internally.\n\nDo NOT add commentary or explanation. Execute the steps and call set_output when done.",
|
||||
"tools": [
|
||||
"bulk_fetch_emails"
|
||||
],
|
||||
|
||||
@@ -15,7 +15,8 @@ intake_node = NodeSpec(
|
||||
client_facing=True,
|
||||
max_node_visits=0,
|
||||
input_keys=["rules", "max_emails"],
|
||||
output_keys=["rules", "max_emails"],
|
||||
output_keys=["rules", "max_emails", "query"],
|
||||
nullable_output_keys=["query"],
|
||||
system_prompt="""\
|
||||
You are an inbox management assistant. The user has provided rules for managing their emails.
|
||||
|
||||
@@ -53,6 +54,39 @@ Call gmail_list_labels() to show the user their current Gmail labels. This helps
|
||||
|
||||
- set_output("rules", <ALL active rules as a clear text description>)
|
||||
- set_output("max_emails", <the confirmed max_emails as a string number, e.g. "100">)
|
||||
- set_output("query", <Gmail search query if the user wants to target specific emails>)
|
||||
|
||||
**TARGETED QUERY (optional):**
|
||||
|
||||
If the user's rules target specific emails (e.g. "delete all emails from newsletters@example.com"),
|
||||
build a Gmail search query to fetch ONLY matching emails instead of the entire inbox. This is much
|
||||
faster and more efficient.
|
||||
|
||||
Gmail search query syntax:
|
||||
- `from:sender@example.com` — from a specific sender
|
||||
- `to:recipient@example.com` — to a specific recipient
|
||||
- `subject:keyword` — subject contains keyword
|
||||
- `is:unread` / `is:read` — read status
|
||||
- `is:starred` / `is:important` — flags
|
||||
- `has:attachment` — has attachments
|
||||
- `filename:pdf` — attachment filename
|
||||
- `label:LABEL_NAME` — has a specific label
|
||||
- `category:promotions` / `category:social` / `category:updates` — Gmail categories
|
||||
- `newer_than:7d` / `older_than:30d` — relative time (d=days, m=months, y=years)
|
||||
- `after:2024/01/01` / `before:2024/12/31` — absolute dates
|
||||
- Combine with spaces (AND): `from:boss@co.com subject:urgent`
|
||||
- OR operator: `from:alice OR from:bob`
|
||||
- NOT / exclude: `-from:noreply@example.com` or `NOT from:noreply`
|
||||
- Grouping: `{from:alice from:bob}` (same as OR)
|
||||
|
||||
Examples:
|
||||
- User says "trash all promotional emails" → query: `category:promotions`
|
||||
- User says "star emails from my boss jane@co.com" → query: `from:jane@co.com`
|
||||
- User says "mark unread emails older than a week as read" → query: `is:unread older_than:7d`
|
||||
- User says "apply rules to all inbox emails" → no query needed (default: `label:INBOX`)
|
||||
|
||||
If the rules apply broadly to ALL emails, do NOT set a query — the default `label:INBOX` will be used.
|
||||
Only set a query when it would meaningfully narrow the search.
|
||||
|
||||
""",
|
||||
tools=["gmail_list_labels"],
|
||||
@@ -72,18 +106,19 @@ fetch_emails_node = NodeSpec(
|
||||
node_type="event_loop",
|
||||
client_facing=False,
|
||||
max_node_visits=0,
|
||||
input_keys=["rules", "max_emails", "next_page_token", "last_processed_timestamp"],
|
||||
input_keys=["rules", "max_emails", "next_page_token", "last_processed_timestamp", "query"],
|
||||
output_keys=["emails", "next_page_token"],
|
||||
nullable_output_keys=["next_page_token"],
|
||||
system_prompt="""\
|
||||
You are a data pipeline step. Your job is to fetch ONE PAGE of emails from Gmail.
|
||||
|
||||
**INSTRUCTIONS:**
|
||||
1. Read "max_emails", "next_page_token", and "last_processed_timestamp" from input context.
|
||||
1. Read "max_emails", "next_page_token", "last_processed_timestamp", and "query" from input context.
|
||||
2. Call bulk_fetch_emails with:
|
||||
- max_emails=<max_emails value, default "100">
|
||||
- page_token=<next_page_token value, if present and non-empty>
|
||||
- after_timestamp=<last_processed_timestamp value, if present and non-empty>
|
||||
- query=<query value, if present and non-empty; omit to default to "label:INBOX">
|
||||
3. The tool returns {"filename": "emails.jsonl", "count": N, "next_page_token": "<token or null>"}.
|
||||
4. Call set_output("emails", "emails.jsonl").
|
||||
5. Call set_output("next_page_token", <the next_page_token from the tool result, or "" if null>).
|
||||
|
||||
@@ -31,9 +31,10 @@ TOOLS = {
|
||||
"bulk_fetch_emails": Tool(
|
||||
name="bulk_fetch_emails",
|
||||
description=(
|
||||
"Fetch emails from the Gmail inbox and write them to a JSONL file. "
|
||||
"Fetch emails from Gmail and write them to a JSONL file. "
|
||||
"Returns {filename, count, next_page_token}. Pass next_page_token "
|
||||
"from a previous call to fetch the next page."
|
||||
"from a previous call to fetch the next page. "
|
||||
"Supports Gmail search query syntax via the 'query' parameter."
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
@@ -63,6 +64,18 @@ TOOLS = {
|
||||
"Required when multiple Google accounts are connected."
|
||||
),
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Gmail search query. Defaults to 'label:INBOX'. Supports full Gmail "
|
||||
"search syntax: from:, to:, subject:, is:unread, is:starred, "
|
||||
"has:attachment, label:, newer_than:, older_than:, category:, "
|
||||
"filename:, and boolean operators (AND, OR, NOT, -, {}). "
|
||||
"Examples: 'from:boss@example.com', 'subject:invoice is:unread', "
|
||||
"'label:INBOX -from:noreply'. The after_timestamp parameter is "
|
||||
"appended automatically if provided."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
@@ -151,8 +164,9 @@ def _bulk_fetch_emails(
|
||||
page_token: str = "",
|
||||
after_timestamp: str = "",
|
||||
account: str = "",
|
||||
query: str = "",
|
||||
) -> dict:
|
||||
"""Fetch inbox emails and write them to emails.jsonl.
|
||||
"""Fetch emails from Gmail and write them to emails.jsonl.
|
||||
|
||||
Uses synchronous httpx.Client since this runs as a tool call inside
|
||||
an already-running async event loop.
|
||||
@@ -162,6 +176,8 @@ def _bulk_fetch_emails(
|
||||
page_token: Gmail API page token for pagination. Omit for the first page.
|
||||
after_timestamp: Unix epoch seconds — only fetch emails after this time.
|
||||
account: Account alias (e.g. 'timothy-home') for multi-account routing.
|
||||
query: Gmail search query. Defaults to 'label:INBOX'. Supports full
|
||||
Gmail search syntax (from:, subject:, is:, label:, etc.).
|
||||
|
||||
Returns:
|
||||
Dict with {filename, count, next_page_token}.
|
||||
@@ -177,9 +193,9 @@ def _bulk_fetch_emails(
|
||||
}
|
||||
|
||||
# Build Gmail query
|
||||
query = "label:INBOX"
|
||||
gmail_query = query.strip() if query and query.strip() else "label:INBOX"
|
||||
if after_timestamp and after_timestamp.strip():
|
||||
query += f" after:{after_timestamp.strip()}"
|
||||
gmail_query += f" after:{after_timestamp.strip()}"
|
||||
|
||||
message_ids: list[str] = []
|
||||
current_page_token: str | None = page_token if page_token else None
|
||||
@@ -192,7 +208,7 @@ def _bulk_fetch_emails(
|
||||
page_size = min(remaining, 500)
|
||||
|
||||
params: dict[str, str | int] = {
|
||||
"q": query,
|
||||
"q": gmail_query,
|
||||
"maxResults": page_size,
|
||||
}
|
||||
if current_page_token:
|
||||
@@ -315,6 +331,7 @@ def tool_executor(tool_use: ToolUse) -> ToolResult:
|
||||
page_token=tool_use.input.get("page_token", ""),
|
||||
after_timestamp=tool_use.input.get("after_timestamp", ""),
|
||||
account=tool_use.input.get("account", ""),
|
||||
query=tool_use.input.get("query", ""),
|
||||
)
|
||||
return ToolResult(
|
||||
tool_use_id=tool_use.id,
|
||||
|
||||
Reference in New Issue
Block a user