feat(email): voice & writing-style profile learned from past mail (mig 94) - #176
Merged
Merged
Conversation
…g 94)
AI Settings > Advanced Settings gains a "Voice profile" card (Your voice
section): the assistant studies emails the user already wrote — Sent and/or
Drafts, over a chosen date range (presets or custom, with a live count of
what a build would read) — and distils a structured trait set (tone,
formality, greetings, sign-offs, recurring phrases, dos/don'ts) plus an
editable narrative style guide. The same pass can propose knowledge-base
entries (role, products, policies…) which land as 'suggested' and only feed
drafts once approved.
Backend (gateway/routes/email/automation/voice_profile.py):
- email_voice_profiles table (mig 94) — one profile per account with
status/enabled/traits JSONB/style_guide/range/sources; email_knowledge
gains source + status provenance ('suggested' entries are excluded from
the drafting prompt until approved).
- Build runs as a BackgroundTask with JobTracker progress (collect →
analyze per-batch on tier-balanced → synthesize on tier-powerful →
knowledge suggestions); quoted chains are stripped so only the owner's
own prose is studied; a BUILDING row with no live job reads as FAILED.
- Endpoints: GET/PUT/DELETE /voice-profile, GET /voice-profile/preview
(per-source counts for the range picker), POST /voice-profile/build,
GET /voice-profile/status, POST /voice-profile/sample ("Try it"),
POST /knowledge/{id}/approve.
- Prompt contract: _load_assistant_about injects <voice_profile> between
the explicit <writing_style> (outranks it) and the auto-derived
<learned_writing_style>; both drafter system prompts updated.
Frontend (VoiceProfileDialog.tsx + SettingsTab wiring):
- Builder view: source checkboxes (drafts opt-in, with a caution that the
folder can hold assistant-written drafts), date-range presets + custom
dates, debounced live email counts, knowledge-extraction toggle.
- Building view: phase + batch progress, polled; survives dialog close.
- Overview: trait chips, greeting/sign-off/phrase panels, editable style
guide, enable toggle, sample generator with scenario presets, suggested-
knowledge review (approve/dismiss), rebuild + remove.
- Knowledge-base manager shows Suggested/Learned badges with inline
approve; suggested entries sort first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYXnVMP3RLBNSwQd5taGZg
vjvarada
added a commit
that referenced
this pull request
Jul 23, 2026
…file 94) (#185) Two migrations both landed as 94: 94_email_voice_profile.sql (#176) and 94_note_taker.sql (#181). #176 merged and DEPLOYED first — email_voice_profiles exists on prod — so it keeps 94; #181's note-taker deploy failed on the prefix-uniqueness guard and its tables were never applied, so it renumbers to 95 with zero data impact (apply_migrations replays every numbered file idempotently on each deploy — ordering only). Reference comments in notes/core.py, acb_graph/models.py, and the note-taker spec updated to match. Unblocks main (the collision was failing the unit-test gate and blocking all deploys). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
AI Settings → Advanced Settings gains a Voice profile card (Your voice section): the assistant learns a writing-style profile — plus knowledge-base suggestions — from emails the user has already written, over a date range and source folders the user picks.
Also includes (second commit): mobile layout fixes for the email compose/reply UI, whose buttons could hide behind the bottom nav bar — see the last section.
UX flow
The Knowledge base manager also shows Suggested / Learned provenance badges with inline approve; suggested entries sort first and never feed drafts until approved.
How
DB (
infra/postgres/94_email_voice_profile.sql, idempotent)email_voice_profiles— one row per account:status(EMPTY/BUILDING/READY/FAILED),enabled,traits JSONB,style_guide,sources,range_start/end,analyzed_count,last_error,built_at.email_knowledgegainssource('manual' | 'voice_profile') andstatus('active' | 'suggested'); existing rows backfill as manual+active.Backend (
gateway/routes/email/automation/voice_profile.py)split_quoted_text) so only the owner's own prose is studied → per-batch observation extraction ontier-balanced→ one synthesis call ontier-powerfulproducing whitelisted traits + style guide → knowledge candidates stored assuggested(never clobbering existing titles). Progress via the sharedJobTracker; the profile row's status survives restarts, and a BUILDING row with no live job reads as FAILED instead of stuck.GET/PUT/DELETE /email/voice-profile,GET /voice-profile/preview(per-source counts),POST /voice-profile/build,GET /voice-profile/status,POST /voice-profile/sample,POST /knowledge/{id}/approve._load_assistant_aboutinjects<voice_profile>between the explicit<writing_style>(outranks it) and the auto-derived<learned_writing_style>; both drafting system prompts (_llm_draft_reply,_llm_compose_assist) updated. The KB block now only includesstatus='active'entries. JSONB traits are parsed defensively (asyncpg may return str).voice-profile/sampleadded to the Next proxy'sAI_SLOW_POST_PATHS(120s timeout).Mobile fix: compose/reply UI vs bottom nav bar (commit 2)
Reported from phone use: while drafting, sending, and replying, buttons/sections sat behind or past the mobile bottom menu bar. Three layout causes, all fixed:
ComposePanelhad no height cap and no internal scrolling — on a phone (especially with the keyboard open) the card ran past the screen, taking Discard/Send with it. Now a flex column capped atcalc(100dvh-6.5rem)with a scrollable middle and pinned header/footer. Verified in a Playwright harness at 390×500 (keyboard-open height): old structure puts Send at y=527 (off-screen), new keeps it at y=412.pb-14but the nav also pads itself with the safe-area inset — on devices with a home indicator the last inset-height of page content (the inline reply composer's Send row) hid behind the bar. New.pb-navutility reservescalc(3.5rem + env(safe-area-inset-bottom)).100vhincludes browser chrome, so a "fits in 85vh" card could overrun the visible viewport and clip its action row. The shared automationModal,TaskCaptureModal(the commitment popup after sending a reply),EmailPreviewModal, andMessageTimelineModalnow cap against100dvh.Desktop geometry is unchanged (
sm:caps preserve the previous values).Tests
tests/unit/test_email_voice_profile.py(13 tests): quote-strip sample prep, observation merge/dedupe, trait whitelisting,<voice_profile>block rendering (incl. str-JSONB), stale-BUILDING → FAILED, and_load_assistant_aboutordering (<writing_style>→<voice_profile>→<learned_writing_style>).test_email_knowledge.pyfakes for the extra profile query.maintoo — they need live Postgres/services). Frontend:tsc --noEmitclean; ESLint clean on all touched files;next buildpasses and the generated CSS was inspected for the newdvh/safe-area rules.🤖 Generated with Claude Code
https://claude.ai/code/session_01QYXnVMP3RLBNSwQd5taGZg