human handoffs
This commit is contained in:
parent
79188e0f59
commit
36a55930f9
35 changed files with 4039 additions and 139 deletions
117
backend/app/services/handoff_evaluator.py
Normal file
117
backend/app/services/handoff_evaluator.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import re
|
||||
|
||||
HANDOFF_LABELS = ["Important", "Finance", "Clients", "Projects", "Personal", "Support"]
|
||||
|
||||
EMAIL_TYPE_META = {
|
||||
"utility": {
|
||||
"title": "Utility",
|
||||
"patterns": r"account update|alert|notice|security|verification|password|service information|noreply|no-reply",
|
||||
},
|
||||
"transactional": {
|
||||
"title": "Transactional",
|
||||
"patterns": r"order|payment|invoice|receipt|billing|subscription|shipped|confirmation|refund|approval",
|
||||
},
|
||||
"promotional": {
|
||||
"title": "Promotional",
|
||||
"patterns": r"offer|deal|discount|promo|sale|limited time|save \d+%",
|
||||
},
|
||||
"marketing": {
|
||||
"title": "Marketing",
|
||||
"patterns": r"newsletter|campaign|marketing|outreach|webinar|subscribe|unsubscribe",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _email_text(email: dict) -> str:
|
||||
parts = [
|
||||
email.get("subject", ""),
|
||||
email.get("snippet", ""),
|
||||
email.get("body_text", ""),
|
||||
]
|
||||
return " ".join(parts).lower()
|
||||
|
||||
|
||||
def infer_labels(email: dict) -> list[str]:
|
||||
text = _email_text(email)
|
||||
matched: list[str] = []
|
||||
if re.search(r"urgent|important|asap|priority", text) or email.get("is_starred"):
|
||||
matched.append("Important")
|
||||
if re.search(r"invoice|bill|payment|finance|receipt", text):
|
||||
matched.append("Finance")
|
||||
if re.search(r"client|customer", text):
|
||||
matched.append("Clients")
|
||||
if re.search(r"project|proposal|contract", text):
|
||||
matched.append("Projects")
|
||||
if re.search(r"personal|family", text):
|
||||
matched.append("Personal")
|
||||
if re.search(r"support|help desk|ticket|assistance", text):
|
||||
matched.append("Support")
|
||||
return matched
|
||||
|
||||
|
||||
def infer_email_types(email: dict) -> list[str]:
|
||||
text = _email_text(email)
|
||||
matched: list[str] = []
|
||||
for key, meta in EMAIL_TYPE_META.items():
|
||||
if re.search(meta["patterns"], text, flags=re.IGNORECASE):
|
||||
matched.append(key)
|
||||
return matched
|
||||
|
||||
|
||||
def instruction_matches(instructions: str, email: dict) -> tuple[bool, str]:
|
||||
text = _email_text(email)
|
||||
instructions = (instructions or "").strip()
|
||||
if not instructions:
|
||||
return True, "No handoff instructions were set, so this step passes."
|
||||
|
||||
keywords = re.findall(r"[a-z]{4,}", instructions.lower())
|
||||
topic_hits = [word for word in keywords if word in text and len(word) > 4]
|
||||
if topic_hits:
|
||||
return True, f"Mentions topics related to your instructions ({', '.join(topic_hits[:3])})."
|
||||
|
||||
intent_patterns = [
|
||||
(r"refund|chargeback", "This email appears to involve a refund or payment issue."),
|
||||
(r"approval|authorize|sign off", "This email appears to require approval."),
|
||||
(r"human|speak with|talk to|call me", "The sender appears to be asking for a person."),
|
||||
(r"not confident|unsure|escalat", "Your instructions mention uncertain situations."),
|
||||
(r"urgent|asap|immediately", "The message appears urgent."),
|
||||
]
|
||||
for pattern, reason in intent_patterns:
|
||||
if re.search(pattern, instructions, flags=re.IGNORECASE) and re.search(pattern, text, flags=re.IGNORECASE):
|
||||
return True, reason
|
||||
|
||||
if re.search(r"refund|approval|human|speak with|person|payment", instructions, flags=re.IGNORECASE):
|
||||
if re.search(r"refund|approval|human|speak|person|payment|decision", text, flags=re.IGNORECASE):
|
||||
return True, "The email situation matches your handoff instructions."
|
||||
|
||||
return False, "This email does not clearly match your handoff instructions."
|
||||
|
||||
|
||||
def evaluate_handoff(
|
||||
*,
|
||||
email: dict,
|
||||
labels: list[str],
|
||||
email_types: list[str],
|
||||
instructions: str,
|
||||
) -> dict:
|
||||
inferred_labels = infer_labels(email)
|
||||
inferred_types = infer_email_types(email)
|
||||
|
||||
label_ok = not labels or any(label in inferred_labels for label in labels)
|
||||
matched_labels = [label for label in labels if label in inferred_labels] if labels else inferred_labels[:1]
|
||||
|
||||
type_ok = not email_types or any(item in inferred_types for item in email_types)
|
||||
matched_types = [item for item in email_types if item in inferred_types] if email_types else inferred_types[:1]
|
||||
|
||||
instruction_ok, instruction_reason = instruction_matches(instructions, email)
|
||||
would_handoff = label_ok and type_ok and instruction_ok
|
||||
|
||||
return {
|
||||
"label_matched": label_ok,
|
||||
"matched_labels": matched_labels,
|
||||
"type_matched": type_ok,
|
||||
"matched_types": matched_types,
|
||||
"instruction_matched": instruction_ok,
|
||||
"instruction_reason": instruction_reason,
|
||||
"would_handoff": would_handoff,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue