Skip to content

feat(email): voice & writing-style profile learned from past mail (mig 94) - #176

Merged
vjvarada merged 1 commit into
mainfrom
claude/voice-writing-style-profile-xempl3
Jul 23, 2026
Merged

feat(email): voice & writing-style profile learned from past mail (mig 94)#176
vjvarada merged 1 commit into
mainfrom
claude/voice-writing-style-profile-xempl3

Conversation

@vjvarada

@vjvarada vjvarada commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

  1. Builder — choose sources (Sent mail on by default; Drafts opt-in, with a caution that the folder can contain assistant-written drafts), pick a date range (Last 3/6/12 months, All time, or custom from→to dates), and see a live, debounced count of how many emails the build would study (capped at the 150 most recent). Optional toggle: also propose knowledge entries.
  2. Building — the build runs as a background job; the dialog polls live progress (collecting → analyzing batches → synthesizing → knowledge) with a progress bar, and can be closed mid-build.
  3. Overview — the finished profile: trait chips (tone, formality, typical length, emoji use), greeting/sign-off/recurring-phrase panels, an editable narrative style guide (the text the drafter actually follows), an enable/disable toggle, "Try it" (generate a sample email in the learned voice for preset or custom scenarios), a suggested-knowledge review list (approve/dismiss), plus Rebuild and Remove.

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_knowledge gains source ('manual' | 'voice_profile') and status ('active' | 'suggested'); existing rows backfill as manual+active.

Backend (gateway/routes/email/automation/voice_profile.py)

  • Build job: fetch messages in range → strip quoted chains (split_quoted_text) so only the owner's own prose is studied → per-batch observation extraction on tier-balanced → one synthesis call on tier-powerful producing whitelisted traits + style guide → knowledge candidates stored as suggested (never clobbering existing titles). Progress via the shared JobTracker; the profile row's status survives restarts, and a BUILDING row with no live job reads as FAILED instead of stuck.
  • Endpoints: 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.
  • Prompt contract: _load_assistant_about injects <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 includes status='active' entries. JSONB traits are parsed defensively (asyncpg may return str).
  • voice-profile/sample added to the Next proxy's AI_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:

  • ComposePanel had 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 at calc(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.
  • App shell reserved pb-14 but 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-nav utility reserves calc(3.5rem + env(safe-area-inset-bottom)).
  • Modals capped at 85/88vh — mobile 100vh includes browser chrome, so a "fits in 85vh" card could overrun the visible viewport and clip its action row. The shared automation Modal, TaskCaptureModal (the commitment popup after sending a reply), EmailPreviewModal, and MessageTimelineModal now cap against 100dvh.
  • The undo-send and captured-to-Tasks toasts are lifted above the bottom bar on mobile so "Undo" is tappable during the send window.

Desktop geometry is unchanged (sm: caps preserve the previous values).

Tests

  • New 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_about ordering (<writing_style><voice_profile><learned_writing_style>).
  • Updated test_email_knowledge.py fakes for the extra profile query.
  • Full unit suite: 1620 passed (the 5 failing integration tests fail on main too — they need live Postgres/services). Frontend: tsc --noEmit clean; ESLint clean on all touched files; next build passes and the generated CSS was inspected for the new dvh/safe-area rules.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QYXnVMP3RLBNSwQd5taGZg

…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
vjvarada merged commit 9ab3639 into main Jul 23, 2026
3 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants