outreach: voice by-purpose (larger sample) + Tier-B Gmail draft creation (v0.1.0:71)
(1) Voice: _voice_examples now picks the sender's prior sent emails OF THE SAME PURPOSE (PURPOSE_PATTERNS keyword cues per outreach type), larger sample (8) weighted by purpose then recency — not just recent. meta carries on_topic for transparency. (2) Tier-B sending (gmail.compose now authorized in Workspace DWD). New email_integration/compose.py create_outreach_draft: mints a compose-scoped DWD token for the sender (credentials._mint/access_token_for parameterized by scope; GMAIL_COMPOSE_SCOPE), builds an RFC822 message, and POSTs gmail.drafts.create into the SENDER's mailbox — as an in-thread reply (threadId + In-Reply-To/References, recipient = matched LP address) when there's an active thread, else a fresh email. NEVER sends — the human sends from Gmail (guardrails #4, #6). Route POST /api/outreach/gmail-draft; UI "Create Gmail draft" button + "Open Gmail Drafts" link. Tests: test_compose.py (parse/reply-target/RFC822+threading). Message construction unit-verified; the live drafts.create runs on the box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1915,6 +1915,8 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
return self.handle_architect_ground(user, body)
|
||||
if path == '/api/outreach/draft':
|
||||
return self.handle_outreach_draft(user, body)
|
||||
if path == '/api/outreach/gmail-draft':
|
||||
return self.handle_outreach_gmail_draft(user, body)
|
||||
if re.match(r'^/api/activity/proposals/[^/]+/approve$', path):
|
||||
return self.handle_decide_activity_proposal(user, path.split('/')[-2], 'approve', body)
|
||||
if re.match(r'^/api/activity/proposals/[^/]+/dismiss$', path):
|
||||
@@ -3972,6 +3974,40 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
conn.close()
|
||||
return self.send_json({"data": res})
|
||||
|
||||
def handle_outreach_gmail_draft(self, user, body):
|
||||
"""Create a Gmail DRAFT from an approved outreach draft (in-thread reply when there
|
||||
is an active thread). Never sends — the human sends from Gmail (guardrails #4, #6)."""
|
||||
body = body or {}
|
||||
inv = body.get('investor_id')
|
||||
text = body.get('draft') or ''
|
||||
if not inv or not text.strip():
|
||||
return self.send_error_json("investor_id and draft required", 400)
|
||||
try:
|
||||
from email_integration import compose as _compose
|
||||
except Exception:
|
||||
return self.send_error_json("Gmail compose unavailable", 503)
|
||||
conn = get_db()
|
||||
try:
|
||||
sender_email = None
|
||||
try:
|
||||
r = conn.execute("SELECT email FROM users WHERE id=?", (user.get('user_id'),)).fetchone()
|
||||
sender_email = r[0] if r else None
|
||||
except Exception:
|
||||
pass
|
||||
res = _compose.create_outreach_draft(conn, sender_email, inv, text)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO interaction_log (id, ts, actor_type, actor_id, action, target_type, target_id, payload, source, created_at) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(generate_id(), now(), "human", user.get('user_id'), "outreach.gmail_draft_created",
|
||||
"fundraising_investor", inv, json.dumps({"status": res.get('status')}), "crm_ui", now()))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
return self.send_json({"data": res})
|
||||
|
||||
# ─── Architect thesis (Phase 1) ───
|
||||
def handle_list_thesis_lines(self, user):
|
||||
if thesis_review is None:
|
||||
|
||||
Reference in New Issue
Block a user