responsiveness fix
This commit is contained in:
parent
4ad05bf137
commit
f7a4753217
9 changed files with 396 additions and 13 deletions
|
|
@ -58,6 +58,7 @@ from app.services.reply_extractor import (
|
|||
)
|
||||
from app.services.thread_service import (
|
||||
attach_message_to_thread,
|
||||
create_thread,
|
||||
message_event_at,
|
||||
threading_fields_from_parent,
|
||||
)
|
||||
|
|
@ -102,8 +103,8 @@ def build_handoff_subject(
|
|||
del rule_name
|
||||
subject = " · ".join(
|
||||
[
|
||||
_safe_subject_part(sender_name, "unknown sender"),
|
||||
"handoff",
|
||||
_safe_subject_part(sender_name, "unknown sender"),
|
||||
_safe_subject_part(external_sender, "unknown sender"),
|
||||
_safe_subject_part(external_subject, "(No subject)"),
|
||||
]
|
||||
|
|
@ -111,6 +112,48 @@ def build_handoff_subject(
|
|||
return subject[:998].rstrip()
|
||||
|
||||
|
||||
_EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
_SUBJECT_PREFIX = re.compile(r"^(?:re|fw|fwd)\s*:\s*", re.IGNORECASE)
|
||||
_HANDOFF_OPENER = re.compile(
|
||||
r"^handoff(?:\s*·\s*|\s*\|\s*|\s+-\s+)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_HANDOFF_SEPARATOR = re.compile(r"\s*·\s*|\s*\|\s*|\s+-\s+")
|
||||
|
||||
|
||||
def parse_human_initiated_subject(subject: str | None) -> dict | None:
|
||||
"""Read `handoff · Name · email · subject` from a human-started email.
|
||||
|
||||
The word handoff must come first. The three parts after it are the
|
||||
customer name, the customer email, and the subject the customer should
|
||||
see. Separate them with ·, |, or a spaced hyphen.
|
||||
"""
|
||||
cleaned = re.sub(r"[\r\n]+", " ", subject or "").strip()
|
||||
while True:
|
||||
without_prefix = _SUBJECT_PREFIX.sub("", cleaned, count=1).strip()
|
||||
if without_prefix == cleaned:
|
||||
break
|
||||
cleaned = without_prefix
|
||||
opener = _HANDOFF_OPENER.match(cleaned)
|
||||
if opener is None:
|
||||
return None
|
||||
parts = _HANDOFF_SEPARATOR.split(cleaned[opener.end() :], maxsplit=2)
|
||||
if len(parts) < 3:
|
||||
return None
|
||||
customer_name = parts[0].strip()
|
||||
customer_email = parts[1].strip().lower()
|
||||
customer_subject = parts[2].strip()
|
||||
if not customer_name or not customer_subject:
|
||||
return None
|
||||
if _EMAIL_PATTERN.fullmatch(customer_email) is None:
|
||||
return None
|
||||
return {
|
||||
"customer_name": customer_name,
|
||||
"customer_email": customer_email,
|
||||
"subject": customer_subject,
|
||||
}
|
||||
|
||||
|
||||
def redact_human_address(
|
||||
value: str | None,
|
||||
*,
|
||||
|
|
@ -304,6 +347,223 @@ async def resolve_handoff_from_inbound(
|
|||
return None
|
||||
|
||||
|
||||
async def start_human_initiated_handoff(
|
||||
database,
|
||||
*,
|
||||
account_id: str,
|
||||
agent_email: str,
|
||||
parsed: dict,
|
||||
body_text: str,
|
||||
body_html: str | None,
|
||||
raw_mime: dict,
|
||||
delivery_key: str,
|
||||
) -> bool:
|
||||
"""Send the first customer email when a Human Owner writes to Gentic.
|
||||
|
||||
Subject shape: handoff · Customer name · customer@email · Subject
|
||||
The customer receives a normal new email. Later replies from that
|
||||
customer are mirrored back on the same handoff thread.
|
||||
"""
|
||||
initiated = parse_human_initiated_subject(parsed.get("subject"))
|
||||
if initiated is None:
|
||||
return False
|
||||
sender = parsed.get("sender") or {}
|
||||
human_email = (sender.get("address") or "").strip().lower()
|
||||
if not human_email:
|
||||
return False
|
||||
rules = (
|
||||
await database.human_handoff_rules.find(
|
||||
{
|
||||
"account_id": account_id,
|
||||
"owner_email": human_email,
|
||||
"active": True,
|
||||
}
|
||||
)
|
||||
.sort([("priority", 1), ("created_at", 1), ("_id", 1)])
|
||||
.to_list(length=1)
|
||||
)
|
||||
if not rules:
|
||||
return False
|
||||
rule = rules[0]
|
||||
|
||||
delivery_marker = f"handoff-init:{account_id}:{delivery_key}"
|
||||
if await database.messages.find_one({"handoff_delivery_key": delivery_marker}):
|
||||
return True
|
||||
|
||||
extracted = extract_new_reply_content(body_text=body_text, body_html=body_html)
|
||||
rich = None
|
||||
try:
|
||||
raw = await get_raw_mime_storage().get(raw_mime)
|
||||
rich = extract_new_reply_mime_content(raw)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"handoff_initiate_rich_extract_failed delivery=%s",
|
||||
delivery_key,
|
||||
)
|
||||
new_text = (
|
||||
rich.body_text if rich and rich.body_text else extracted["new_reply_content"]
|
||||
).strip()
|
||||
new_html = rich.body_html if rich and rich.body_html else None
|
||||
new_text = (
|
||||
redact_human_address(
|
||||
new_text,
|
||||
human_email=human_email,
|
||||
agent_email=agent_email,
|
||||
)
|
||||
or ""
|
||||
).strip()
|
||||
new_html = redact_human_address(
|
||||
new_html,
|
||||
human_email=human_email,
|
||||
agent_email=agent_email,
|
||||
)
|
||||
if not new_text or not validate_external_reply_body(new_text):
|
||||
logger.info(
|
||||
"handoff_initiate_not_sent account_id=%s reason=empty_or_private_body",
|
||||
account_id,
|
||||
)
|
||||
return False
|
||||
|
||||
_, domain, sender_name = await _account_and_domain(account_id, agent_email)
|
||||
customer_name = initiated["customer_name"]
|
||||
customer_email = initiated["customer_email"]
|
||||
customer_subject = initiated["subject"]
|
||||
internet_message_id = await deliver_email(
|
||||
sender=agent_email,
|
||||
sender_name=sender_name,
|
||||
recipients=[customer_email],
|
||||
cc=[],
|
||||
bcc=[],
|
||||
subject=customer_subject,
|
||||
body_text=new_text,
|
||||
body_html=new_html,
|
||||
domain=domain,
|
||||
reply_to=agent_email,
|
||||
envelope_sender=agent_email,
|
||||
inline_parts=getattr(rich, "inline_parts", None),
|
||||
file_attachments=getattr(rich, "file_attachments", None),
|
||||
)
|
||||
now = _now()
|
||||
normalized_id = canonical_message_id(internet_message_id)
|
||||
external = {
|
||||
"direction": "outbound",
|
||||
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
||||
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
|
||||
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
|
||||
"account_id": account_id,
|
||||
"sender": {"name": sender_name, "address": agent_email.lower()},
|
||||
"to": [{"name": customer_name, "address": customer_email}],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"subject": customer_subject,
|
||||
"snippet": _snippet(new_text),
|
||||
"new_reply_content": new_text,
|
||||
"body_text": new_text,
|
||||
"body_html": new_html,
|
||||
"attachments": [],
|
||||
"created_at": now,
|
||||
"sent_at": now,
|
||||
"status": "accepted",
|
||||
"thread_relation": "new_thread",
|
||||
"thread_resolution_method": "human_handoff_initiate",
|
||||
**message_id_fields(normalized_id),
|
||||
}
|
||||
inserted = await database.messages.insert_one(external)
|
||||
external["_id"] = inserted.inserted_id
|
||||
thread_id = await create_thread(
|
||||
database,
|
||||
account_id=account_id,
|
||||
subject=customer_subject,
|
||||
message_id=external["_id"],
|
||||
event_at=now,
|
||||
)
|
||||
await database.mailbox_entries.insert_one(
|
||||
{
|
||||
"account_id": account_id,
|
||||
"message_id": external["_id"],
|
||||
"thread_id": thread_id,
|
||||
"mailbox": "sent",
|
||||
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
||||
"direction": "outbound",
|
||||
"sender": external["sender"],
|
||||
"recipients": external["to"],
|
||||
"subject": customer_subject,
|
||||
"snippet": external["snippet"],
|
||||
"attachments": [],
|
||||
"status": "accepted",
|
||||
"is_read": True,
|
||||
"is_starred": False,
|
||||
"event_at": now,
|
||||
"created_at": now,
|
||||
"trashed_at": None,
|
||||
"deletion_state": None,
|
||||
}
|
||||
)
|
||||
handoff = await _create_handoff_mapping(
|
||||
database,
|
||||
account_id=account_id,
|
||||
agent_email=agent_email,
|
||||
external_message={
|
||||
"_id": external["_id"],
|
||||
"sender": {"name": customer_name, "address": customer_email},
|
||||
"subject": customer_subject,
|
||||
"message_id": normalized_id,
|
||||
},
|
||||
external_thread_id=thread_id,
|
||||
rule=rule,
|
||||
)
|
||||
human_message = await store_handoff_message(
|
||||
database,
|
||||
account_id=account_id,
|
||||
thread_id=handoff["handoff_thread_id"],
|
||||
direction="inbound",
|
||||
sender={"name": sender.get("name") or "", "address": human_email},
|
||||
recipients=[{"name": sender_name, "address": agent_email.lower()}],
|
||||
subject=handoff["private_subject"],
|
||||
body_text=body_text,
|
||||
body_html=body_html,
|
||||
internet_message_id=parsed.get("message_id"),
|
||||
in_reply_to=parsed.get("in_reply_to"),
|
||||
references=parsed.get("references") or [],
|
||||
handoff_id=handoff["_id"],
|
||||
event_at=parsed.get("sent_at") or now,
|
||||
raw_mime=raw_mime,
|
||||
handoff_delivery_key=delivery_marker,
|
||||
new_reply_content=new_text,
|
||||
new_reply_html=new_html,
|
||||
quoted_content=extracted.get("quoted_content", ""),
|
||||
message_origin=MESSAGE_ORIGIN_HUMAN_OWNER,
|
||||
generated_by=GENERATED_BY_HUMAN,
|
||||
relay_status=RELAY_STATUS_SENT,
|
||||
)
|
||||
await database.messages.update_one(
|
||||
{"_id": human_message["_id"]},
|
||||
{"$set": {"generated_external_message_id": external["_id"]}},
|
||||
)
|
||||
await database.messages.update_one(
|
||||
{"_id": external["_id"]},
|
||||
{"$set": {"generated_from_handoff_message_id": human_message["_id"]}},
|
||||
)
|
||||
await database.human_handoffs.update_one(
|
||||
{"_id": handoff["_id"]},
|
||||
{
|
||||
"$set": {
|
||||
"status": HANDOFF_STATUS_HUMAN_REPLIED,
|
||||
"current_owner": OWNER_AGENT,
|
||||
"last_human_message_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"handoff_initiated account_id=%s handoff_id=%s customer=%s",
|
||||
account_id,
|
||||
handoff["_id"],
|
||||
customer_email,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def _record_link(
|
||||
database,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from app.services.human_handoff_service import (
|
|||
maybe_trigger_handoff,
|
||||
process_human_reply,
|
||||
resolve_handoff_from_inbound,
|
||||
start_human_initiated_handoff,
|
||||
)
|
||||
from app.services.message_ids import canonical_message_id, message_id_fields
|
||||
from app.services.mime_parser import parse_mime
|
||||
|
|
@ -154,6 +155,23 @@ class InboundHandler:
|
|||
references=parsed.get("references", []),
|
||||
)
|
||||
if handoff is None:
|
||||
started = await start_human_initiated_handoff(
|
||||
mongo_db,
|
||||
account_id=account_id,
|
||||
agent_email=recipient,
|
||||
parsed=parsed,
|
||||
body_text=body_text,
|
||||
body_html=body_html,
|
||||
raw_mime=raw_mime,
|
||||
delivery_key=delivery_key,
|
||||
)
|
||||
if started:
|
||||
logger.info(
|
||||
"handoff_initiated_from_human account_id=%s message_id=%s",
|
||||
account_id,
|
||||
internet_message_id,
|
||||
)
|
||||
continue
|
||||
external_recipients.append(recipient)
|
||||
continue
|
||||
await process_human_reply(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ TEXT_QUOTE_MARKERS = [
|
|||
PRIVATE_LEAK_MARKERS = [
|
||||
"human handoff",
|
||||
" · handoff · ",
|
||||
"handoff · ",
|
||||
"handoff - ",
|
||||
"handoff | ",
|
||||
"a conversation needs your attention",
|
||||
"why this was handed off",
|
||||
"why you're receiving this",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue