human hand off fix

This commit is contained in:
debu-2 2026-10-06 07:42:41 +05:30
parent 7b9e574b86
commit 0981262612
5 changed files with 154 additions and 24 deletions

View file

@ -1,3 +1,4 @@
from datetime import datetime
from html import escape
@ -25,13 +26,72 @@ def format_sender_line(sender: dict) -> str:
name = (sender.get("name") or "").strip()
address = (sender.get("address") or "").strip()
if not address:
return "unknown sender"
return name or "unknown sender"
local = address.split("@", 1)[0].lower()
if name and name.lower() not in {local, address.lower()}:
return f"{name} <{address}>"
return address
def sender_display_name(sender: dict | None) -> str:
sender = sender or {}
name = (sender.get("name") or "").strip()
address = (sender.get("address") or "").strip()
if name:
return name
if address:
return address.split("@", 1)[0].replace(".", " ").replace("_", " ").replace("-", " ").title()
return "unknown sender"
def _attribution_date(value) -> str:
if not isinstance(value, datetime):
return ""
return value.strftime("%a, %b %d, %Y at %I:%M %p").replace(" 0", " ")
def build_gmail_quote(
*,
sender: dict | None,
event_at,
body_text: str | None,
body_html: str | None,
) -> tuple[str, str]:
identity = format_sender_line(sender or {})
date = _attribution_date(event_at)
attribution = f"On {date} {identity} wrote:" if date else f"{identity} wrote:"
text_source = (body_text or "").rstrip()
quoted_lines = [f"> {line}" if line else ">" for line in text_source.splitlines()]
quoted_text = f"{attribution}\n" + "\n".join(quoted_lines)
inner_html = (body_html or "").strip() or (
f"<div>{escape(text_source).replace(chr(10), '<br>')}</div>"
)
quoted_html = (
f'<div class="gmail_quote">'
f'<div class="gmail_attr">{escape(attribution)}<br></div>'
f'<blockquote class="gmail_quote" '
f'style="margin:0 0 0 .8ex;border-left:1px solid #ccc;padding-left:1ex">'
f"{inner_html}"
f"</blockquote></div>"
)
return quoted_text, quoted_html
def attach_quoted_history(
*,
new_text: str,
new_html: str | None,
quoted_text: str,
quoted_html: str,
) -> tuple[str, str]:
body_text = f"{new_text.rstrip()}\n\n{quoted_text}".strip()
visible_html = (new_html or "").strip() or (
f"<div>{escape(new_text).replace(chr(10), '<br>')}</div>"
)
body_html = f"{visible_html}<br>{quoted_html}"
return body_text, body_html
def _footer_html(agent_email: str) -> str:
return f"""
<div class="gentic-handoff-footer" style="background:#F8F7FC;border-top:1px solid #E9E5F2;padding:12px 20px;">

View file

@ -22,6 +22,12 @@ from app.services.handoff_delivery import (
JOB_RELAY,
enqueue_handoff_delivery,
)
from app.services.handoff_email_templates import (
attach_quoted_history,
build_gmail_quote,
format_sender_line,
sender_display_name,
)
from app.services.handoff_evaluator import evaluate_handoff
from app.services.handoff_mirror import load_mirror_payload
from app.services.handoff_mirror_policy import (
@ -89,13 +95,15 @@ def _safe_subject_part(value: str, fallback: str) -> str:
def build_handoff_subject(
*,
rule_name: str,
sender_name: str,
external_sender: str,
external_subject: str,
rule_name: str | None = None,
) -> str:
del rule_name
subject = " · ".join(
[
_safe_subject_part(rule_name, "Human"),
_safe_subject_part(sender_name, "unknown sender"),
"handoff",
_safe_subject_part(external_sender, "unknown sender"),
_safe_subject_part(external_subject, "(No subject)"),
@ -472,10 +480,11 @@ async def _create_handoff_mapping(
rule: dict,
) -> dict:
sender = external_message.get("sender") or {}
sender_text = sender.get("address") or sender.get("name") or "unknown sender"
sender_email = (sender.get("address") or "").strip()
customer_name = sender_display_name(sender)
private_subject = build_handoff_subject(
rule_name=rule.get("name") or rule.get("owner_name") or "Human",
external_sender=sender_text,
sender_name=customer_name,
external_sender=sender_email or customer_name,
external_subject=external_message.get("subject", ""),
)
thread_id = await create_handoff_thread(
@ -494,7 +503,8 @@ async def _create_handoff_mapping(
"human_email": rule["owner_email"].lower(),
"human_name": rule.get("owner_name") or rule["owner_email"].split("@", 1)[0],
"agent_email": agent_email.lower(),
"external_sender": sender.get("address", "").lower(),
"external_sender": sender_email.lower(),
"external_sender_name": customer_name,
"external_subject": external_message.get("subject", ""),
"private_subject": private_subject,
"allow_replies": rule.get("allow_replies", True),
@ -710,6 +720,20 @@ async def process_human_reply(
return human_message
def _mirror_from_identity(source: dict, agent_name: str) -> str:
if source.get("direction") == "inbound":
return format_sender_line(source.get("sender") or {})
return agent_name
def _mirror_body(source: dict, payload) -> tuple[str, str | None]:
new_text = (source.get("new_reply_content") or "").strip()
new_html = (source.get("new_reply_html") or "").strip() or None
if new_text:
return new_text, new_html
return payload.body_text, payload.body_html
async def _deliver_mirror(database, job: dict, handoff: dict, source: dict) -> ObjectId:
existing = await database.messages.find_one(
{
@ -724,6 +748,8 @@ async def _deliver_mirror(database, job: dict, handoff: dict, source: dict) -> O
job["account_id"], handoff["agent_email"]
)
payload = await load_mirror_payload(source)
body_text, body_html = _mirror_body(source, payload)
from_name = _mirror_from_identity(source, sender_name)
parent = await _latest_thread_message(
database,
account_id=job["account_id"],
@ -736,13 +762,13 @@ async def _deliver_mirror(database, job: dict, handoff: dict, source: dict) -> O
subject = f"Re: {subject}"
internet_message_id = await deliver_email(
sender=handoff["agent_email"],
sender_name=sender_name,
sender_name=from_name,
recipients=[handoff["human_email"]],
cc=[],
bcc=[],
subject=subject,
body_text=payload.body_text,
body_html=payload.body_html,
body_text=body_text,
body_html=body_html,
domain=domain,
in_reply_to=threading.get("in_reply_to"),
references=threading.get("references"),
@ -758,7 +784,7 @@ async def _deliver_mirror(database, job: dict, handoff: dict, source: dict) -> O
account_id=job["account_id"],
thread_id=handoff["handoff_thread_id"],
direction="outbound",
sender={"name": sender_name, "address": handoff["agent_email"]},
sender={"name": from_name, "address": handoff["agent_email"]},
recipients=[
{
"name": handoff.get("human_name", ""),
@ -766,8 +792,8 @@ async def _deliver_mirror(database, job: dict, handoff: dict, source: dict) -> O
}
],
subject=subject,
body_text=payload.body_text,
body_html=payload.body_html,
body_text=body_text,
body_html=body_html,
internet_message_id=internet_message_id,
in_reply_to=threading.get("in_reply_to"),
references=threading.get("references", []),
@ -844,10 +870,26 @@ async def _deliver_relay(database, job: dict, handoff: dict, source: dict) -> Ob
rich = extract_new_reply_mime_content(raw)
inline_parts = rich.inline_parts
file_attachments = rich.file_attachments
quoted_text, quoted_html = build_gmail_quote(
sender=external_parent.get("sender") or {},
event_at=(
external_parent.get("sent_at")
or external_parent.get("received_at")
or external_parent.get("created_at")
),
body_text=external_parent.get("body_text") or external_parent.get("snippet") or "",
body_html=external_parent.get("body_html"),
)
relay_text, relay_html = attach_quoted_history(
new_text=new_text,
new_html=body_html,
quoted_text=quoted_text,
quoted_html=quoted_html,
)
plan = build_external_reply_plan(
parent_external_message=external_parent,
body_text=new_text,
body_html=body_html,
body_text=relay_text,
body_html=relay_html,
subject_override=None,
recipients=[recipient],
parent_internet_id_override=parent_internet_id,