209 lines
6.5 KiB
Python
209 lines
6.5 KiB
Python
from datetime import datetime
|
|
from html import escape
|
|
|
|
|
|
def _clean_external_subject(subject: str) -> str:
|
|
value = (subject or "(No subject)").strip()
|
|
for prefix in ("Re: ", "RE: ", "re: "):
|
|
if value.startswith(prefix):
|
|
value = value[len(prefix) :].strip()
|
|
for prefix in ("Human Handoff · ", "Human Handoff: "):
|
|
if value.lower().startswith(prefix.lower()):
|
|
value = value[len(prefix) :].strip()
|
|
return value or "(No subject)"
|
|
|
|
|
|
def handoff_email_subject(external_subject: str) -> str:
|
|
return f"Human Handoff · {_clean_external_subject(external_subject)}"
|
|
|
|
|
|
def external_reply_subject(external_subject: str) -> str:
|
|
cleaned = _clean_external_subject(external_subject)
|
|
return f"Re: {cleaned}"
|
|
|
|
|
|
def format_sender_line(sender: dict) -> str:
|
|
name = (sender.get("name") or "").strip()
|
|
address = (sender.get("address") or "").strip()
|
|
if not address:
|
|
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;">
|
|
<p style="margin:0 0 6px;font-size:12px;font-weight:650;color:#6750E8;">↩ Reply normally</p>
|
|
<p style="margin:0;font-size:11px;line-height:1.45;color:#77758A;">
|
|
Your reply will be sent to the original sender through
|
|
<span style="color:#292536;">{escape(agent_email)}</span>.
|
|
Only the new text you write will be sent.
|
|
</p>
|
|
</div>"""
|
|
|
|
|
|
def _footer_text(agent_email: str) -> str:
|
|
return (
|
|
"↩ Reply normally\n\n"
|
|
f"Your reply will be sent to the original sender through {agent_email}.\n"
|
|
"Only the new text you write will be sent."
|
|
)
|
|
|
|
|
|
def _header_html(sender_line: str, external_subject: str) -> str:
|
|
subject = escape(_clean_external_subject(external_subject))
|
|
return f"""
|
|
<div class="gentic-handoff-header" style="padding:16px 20px 12px;border-bottom:1px solid #ECE9EF;">
|
|
<div style="font-size:14px;font-weight:650;color:#6750E8;">✦ Human Handoff</div>
|
|
<div style="margin-top:5px;font-size:12px;color:#6F6B7C;">
|
|
<span>{escape(sender_line)}</span>
|
|
<span style="color:#292536;font-weight:550;"> | {subject}</span>
|
|
</div>
|
|
</div>"""
|
|
|
|
|
|
def _header_text(sender_line: str, external_subject: str) -> str:
|
|
return f"✦ Human Handoff\n{sender_line} | {_clean_external_subject(external_subject)}"
|
|
|
|
|
|
def build_mirror_handoff_html(
|
|
*,
|
|
sender_line: str,
|
|
external_subject: str,
|
|
original_html: str,
|
|
agent_email: str,
|
|
) -> str:
|
|
return f"""<!DOCTYPE html>
|
|
<html><body style="margin:0;padding:0;background:#FFFFFF;">
|
|
{_header_html(sender_line, external_subject)}
|
|
<div class="gentic-handoff-original" style="padding:20px 20px 24px;">
|
|
{original_html}
|
|
</div>
|
|
{_footer_html(agent_email)}
|
|
</body></html>"""
|
|
|
|
|
|
def build_mirror_handoff_text(
|
|
*,
|
|
sender_line: str,
|
|
external_subject: str,
|
|
original_body_text: str,
|
|
agent_email: str,
|
|
) -> str:
|
|
return "\n\n".join(
|
|
[
|
|
_header_text(sender_line, external_subject),
|
|
"─" * 56,
|
|
original_body_text.strip(),
|
|
"─" * 56,
|
|
_footer_text(agent_email),
|
|
]
|
|
)
|
|
|
|
|
|
def build_mirror_update_html(
|
|
*,
|
|
sender_line: str,
|
|
external_subject: str,
|
|
original_html: str,
|
|
agent_email: str,
|
|
) -> str:
|
|
subject = escape(_clean_external_subject(external_subject))
|
|
return f"""<!DOCTYPE html>
|
|
<html><body style="margin:0;padding:0;background:#FFFFFF;">
|
|
<div class="gentic-handoff-header" style="padding:16px 20px 12px;border-bottom:1px solid #ECE9EF;">
|
|
<div style="font-size:14px;font-weight:650;color:#6750E8;">✦ Customer replied</div>
|
|
<div style="margin-top:5px;font-size:12px;color:#6F6B7C;">
|
|
<span>{escape(sender_line)}</span>
|
|
<span style="color:#292536;font-weight:550;"> | {subject}</span>
|
|
</div>
|
|
</div>
|
|
<div class="gentic-handoff-original" style="padding:20px 20px 24px;">
|
|
{original_html}
|
|
</div>
|
|
{_footer_html(agent_email)}
|
|
</body></html>"""
|
|
|
|
|
|
def build_mirror_update_text(
|
|
*,
|
|
sender_line: str,
|
|
external_subject: str,
|
|
original_body_text: str,
|
|
agent_email: str,
|
|
) -> str:
|
|
return "\n\n".join(
|
|
[
|
|
f"✦ Customer replied\n{sender_line} | {_clean_external_subject(external_subject)}",
|
|
"─" * 56,
|
|
original_body_text.strip(),
|
|
"─" * 56,
|
|
_footer_text(agent_email),
|
|
]
|
|
)
|
|
|
|
|
|
def build_relay_confirmation_text(customer_body: str) -> str:
|
|
return f"Sent to customer:\n\n{customer_body.strip()}"
|