Files
zeekay 363ebca369 fix(rust): vendor the email recipe into the crate
builtin_email_recipe_yaml include_str!'d
../../../../bot/extensions/recipes-brain/recipes/email.yaml — a path
that climbs out of this repo into a sibling checkout of hanzoai/bot.
That resolves only on a workstation with both repos side by side, so
every CI target failed to compile the lib:

  couldn't read `src/brain/../../../../bot/.../email.yaml`

Copy the file to rust/recipes/email.yaml (byte-identical, sha256
f4577ec92d696a426d8ec230af55747e81a505c79f9d02266f9e2e0538ed38ae) and
include it from there. This is also what the module doc already claimed:
recipes live at <crate_dir>/recipes/*.yaml.

Verified by building all five targets from a copy containing only this
crate's tracked files, with no sibling repo present.
2026-07-18 20:41:21 -07:00

93 lines
2.9 KiB
YAML

# Hanzo Brain — Email Recipe
#
# Daily Gmail pull → classify → draft response → enqueue for swipe-review
# in the Hanzo app (~/work/hanzo/bot/apps/{ios,android,macos}).
#
# Run:
# hanzo-bot brain run-recipe email
# hanzo-bot brain drafts list
# hanzo-bot brain drafts approve <id> # sends via gmail
# hanzo-bot brain drafts reject <id> # logs as decline-fact for training
#
# Trains your reply tone over time. Every swipe is a signal.
recipe: email
version: 1
backend: gmail
auth:
provider: hanzo.id # OIDC via hanzo IAM — single sign-on
scopes:
- https://www.googleapis.com/auth/gmail.readonly
- https://www.googleapis.com/auth/gmail.compose
cron: "*/30 * * * *" # every 30 min — adjust per volume
ingest:
source: inbox
since: 24h
max: 100
filter:
has_thread: any
skip_labels: [SPAM, TRASH, PROMOTIONS]
classify:
model: zen-2-haiku # cheap classifier, fast
schema:
needs_reply: bool
priority: one_of [P0, P1, P2, P3]
intent: one_of [question, fyi, scheduling, ask, update, spam]
entities: [person, company, deal]
sentiment: one_of [positive, neutral, negative]
action_items: list
draft:
when: needs_reply == true
model: zen-2 # main model — bigger context, better tone
context:
- brain.recall(from_email) # who is this person (facts)
- brain.search(thread_id, limit=10) # past replies in thread (RAG)
- brain.facts({entity:from_email, since:30d, limit:30})
- brain.style(sender:me, recent:200) # 200-shot style examples from outbox
prompt: |
You are drafting a reply for me. Match my tone exactly — terse,
direct, no exclamation marks, no emoji. Answer the question in the
fewest words that are still useful. Sign off only if I usually do.
Never invent commitments. If you're unsure, suggest "let me check
and get back to you" and add an action_item.
output_schema:
subject: string
body: string
action_items: list
confidence: number # 0..1, used for triage ranking
enqueue:
queue: drafts.email
ttl: 7d
rank_by: [priority, confidence]
fact_on_enqueue:
subject: from_email
predicate: thread_active
object: true
source: thread_id
notify:
channel: hanzo-app
badge: count_unswiped
push: drafts.email.new
# After the user swipes in the app, the result feeds back to the brain:
on_swipe_send:
- send via gmail
- upsertFact: { subject: from_email, predicate: replied, object: thread_id, ts: now }
- learn: append (received_msg, sent_draft) to style examples
on_swipe_reject:
- delete draft
- upsertFact: { subject: from_email, predicate: declined_reply, object: thread_id, ts: now }
- learn: down-weight similar drafts in future ranking
on_swipe_edit:
- open editor with the draft pre-filled
- on save: send + learn (received_msg, edited_draft) as positive example