handoff fixes

This commit is contained in:
debu-2 2026-10-06 02:07:13 +05:30
parent aabb63f947
commit 3f4bcc26d9
4 changed files with 137 additions and 28 deletions

View file

@ -1,8 +1,14 @@
import re import re
HANDOFF_LABELS = ["Important", "Finance", "Clients", "Projects", "Personal", "Support"] ALL_MAIL_LABEL = "All mail"
HANDOFF_LABELS = [ALL_MAIL_LABEL, "Important", "Finance", "Clients", "Projects", "Personal", "Support"]
ALL_TYPES_ID = "all"
EMAIL_TYPE_META = { EMAIL_TYPE_META = {
"all": {
"title": "All types",
"patterns": "",
},
"utility": { "utility": {
"title": "Utility", "title": "Utility",
"patterns": r"account update|alert|notice|security|verification|password|service information|noreply|no-reply", "patterns": r"account update|alert|notice|security|verification|password|service information|noreply|no-reply",
@ -97,11 +103,28 @@ def evaluate_handoff(
inferred_labels = infer_labels(email) inferred_labels = infer_labels(email)
inferred_types = infer_email_types(email) inferred_types = infer_email_types(email)
label_ok = not labels or any(label in inferred_labels for label in labels) all_mail = ALL_MAIL_LABEL in labels
matched_labels = [label for label in labels if label in inferred_labels] if labels else inferred_labels[:1] all_types = ALL_TYPES_ID in email_types
selected_labels = [label for label in labels if label != ALL_MAIL_LABEL]
selected_types = [item for item in email_types if item != ALL_TYPES_ID]
type_ok = not email_types or any(item in inferred_types for item in email_types) label_ok = all_mail or not selected_labels or any(
matched_types = [item for item in email_types if item in inferred_types] if email_types else inferred_types[:1] label in inferred_labels for label in selected_labels
)
matched_labels = (
[ALL_MAIL_LABEL]
if all_mail
else [label for label in selected_labels if label in inferred_labels]
)
type_ok = all_types or not selected_types or any(
item in inferred_types for item in selected_types
)
matched_types = (
[ALL_TYPES_ID]
if all_types
else [item for item in selected_types if item in inferred_types]
)
instruction_ok, instruction_reason = instruction_matches(instructions, email) instruction_ok, instruction_reason = instruction_matches(instructions, email)
would_handoff = label_ok and type_ok and instruction_ok would_handoff = label_ok and type_ok and instruction_ok

View file

@ -1,4 +1,4 @@
from app.services.handoff_evaluator import evaluate_handoff from app.services.handoff_evaluator import ALL_MAIL_LABEL, ALL_TYPES_ID, evaluate_handoff
def test_handoff_requires_all_selected_filters(): def test_handoff_requires_all_selected_filters():
@ -31,3 +31,35 @@ def test_handoff_does_not_match_unrelated_instruction():
instructions="Forward when a refund requires approval.", instructions="Forward when a refund requires approval.",
) )
assert result["would_handoff"] is False assert result["would_handoff"] is False
def test_handoff_all_mail_matches_generic_inbound():
email = {
"subject": "test handoff",
"snippet": "did you receive this?",
"body_text": "did you receive this?",
}
result = evaluate_handoff(
email=email,
labels=[ALL_MAIL_LABEL],
email_types=[ALL_TYPES_ID],
instructions="",
)
assert result["would_handoff"] is True
assert result["label_matched"] is True
assert result["type_matched"] is True
def test_handoff_selected_categories_do_not_match_generic_email():
email = {
"subject": "hi",
"snippet": "check the inbox please",
"body_text": "check the inbox please",
}
result = evaluate_handoff(
email=email,
labels=["Important", "Finance", "Clients"],
email_types=["transactional"],
instructions="",
)
assert result["would_handoff"] is False

View file

@ -2,6 +2,8 @@ import { Mail, UserRound, X } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import AgentSpark from "./AgentSpark"; import AgentSpark from "./AgentSpark";
import { import {
ALL_MAIL_LABEL,
ALL_TYPES_ID,
buildHandoffSummary, buildHandoffSummary,
EMAIL_TYPE_OPTIONS, EMAIL_TYPE_OPTIONS,
HANDOFF_LABELS, HANDOFF_LABELS,
@ -62,19 +64,33 @@ export default function HumanHandoffDrawer({
); );
function toggleLabel(label) { function toggleLabel(label) {
if (label === ALL_MAIL_LABEL) {
setLabels((current) => ( setLabels((current) => (
current.includes(label) current.includes(ALL_MAIL_LABEL) ? [] : [ALL_MAIL_LABEL]
? current.filter((item) => item !== label)
: [...current, label]
)); ));
return;
}
setLabels((current) => {
const withoutAll = current.filter((item) => item !== ALL_MAIL_LABEL);
return withoutAll.includes(label)
? withoutAll.filter((item) => item !== label)
: [...withoutAll, label];
});
} }
function toggleEmailType(typeId) { function toggleEmailType(typeId) {
if (typeId === ALL_TYPES_ID) {
setEmailTypes((current) => ( setEmailTypes((current) => (
current.includes(typeId) current.includes(ALL_TYPES_ID) ? [] : [ALL_TYPES_ID]
? current.filter((item) => item !== typeId)
: [...current, typeId]
)); ));
return;
}
setEmailTypes((current) => {
const withoutAll = current.filter((item) => item !== ALL_TYPES_ID);
return withoutAll.includes(typeId)
? withoutAll.filter((item) => item !== typeId)
: [...withoutAll, typeId];
});
} }
function insertExample(example) { function insertExample(example) {
@ -137,7 +153,10 @@ export default function HumanHandoffDrawer({
<section className="handoff-form-section"> <section className="handoff-form-section">
<h3>Hand off emails with these labels</h3> <h3>Hand off emails with these labels</h3>
<p className="handoff-helper">An email matching any selected label will be eligible for handoff.</p> <p className="handoff-helper">
These are content-based handoff categories, not inbox tabs. Gentic infers them from
subject and body text. Choose <strong>All mail</strong> to hand off every incoming email.
</p>
<div className="handoff-label-toolbar"> <div className="handoff-label-toolbar">
<input <input
type="search" type="search"
@ -145,7 +164,13 @@ export default function HumanHandoffDrawer({
value={labelQuery} value={labelQuery}
onChange={(event) => setLabelQuery(event.target.value)} onChange={(event) => setLabelQuery(event.target.value)}
/> />
<button type="button" onClick={() => setLabels([...HANDOFF_LABELS])}>Select all</button> <button type="button" onClick={() => setLabels([ALL_MAIL_LABEL])}>All mail</button>
<button
type="button"
onClick={() => setLabels(HANDOFF_LABELS.filter((label) => label !== ALL_MAIL_LABEL))}
>
Select categories
</button>
</div> </div>
<div className="handoff-label-grid"> <div className="handoff-label-grid">
{filteredLabels.map((label) => ( {filteredLabels.map((label) => (
@ -163,7 +188,10 @@ export default function HumanHandoffDrawer({
<section className="handoff-form-section"> <section className="handoff-form-section">
<h3>Hand off these types of email</h3> <h3>Hand off these types of email</h3>
<p className="handoff-helper">Emails matching any selected type can be considered for handoff.</p> <p className="handoff-helper">
Types are also inferred from email content. Choose <strong>All types</strong> if you
do not want to restrict by category.
</p>
<div className="handoff-type-grid"> <div className="handoff-type-grid">
{EMAIL_TYPE_OPTIONS.map((type) => ( {EMAIL_TYPE_OPTIONS.map((type) => (
<label key={type.id} className={`handoff-type-card${emailTypes.includes(type.id) ? " selected" : ""}`}> <label key={type.id} className={`handoff-type-card${emailTypes.includes(type.id) ? " selected" : ""}`}>

View file

@ -1,6 +1,22 @@
export const HANDOFF_LABELS = ["Important", "Finance", "Clients", "Projects", "Personal", "Support"]; export const ALL_MAIL_LABEL = "All mail";
export const ALL_TYPES_ID = "all";
export const HANDOFF_LABELS = [
ALL_MAIL_LABEL,
"Important",
"Finance",
"Clients",
"Projects",
"Personal",
"Support",
];
export const EMAIL_TYPE_OPTIONS = [ export const EMAIL_TYPE_OPTIONS = [
{
id: ALL_TYPES_ID,
title: "All types",
description: "Any incoming email, regardless of category",
},
{ {
id: "utility", id: "utility",
title: "Utility", title: "Utility",
@ -46,10 +62,14 @@ export function buildHandoffSummary({
emailTypes, emailTypes,
instructions, instructions,
}) { }) {
const labelText = labels?.length const labelText = labels?.includes(ALL_MAIL_LABEL)
? "All incoming emails"
: labels?.length
? `${joinOrNone(labels)} emails` ? `${joinOrNone(labels)} emails`
: "eligible emails"; : "eligible emails";
const typeText = emailTypes?.length const typeText = emailTypes?.includes(ALL_TYPES_ID)
? "any type"
: emailTypes?.length
? typeTitles(emailTypes).join(" or ").toLowerCase() ? typeTitles(emailTypes).join(" or ").toLowerCase()
: "any type"; : "any type";
const instructionText = instructions?.trim() const instructionText = instructions?.trim()
@ -61,8 +81,14 @@ export function buildHandoffSummary({
export function buildRuleCardSummary(rule) { export function buildRuleCardSummary(rule) {
const parts = []; const parts = [];
if (rule.labels?.length) parts.push(rule.labels.join(", ")); if (rule.labels?.includes(ALL_MAIL_LABEL)) {
if (rule.email_types?.length) { parts.push("All mail");
} else if (rule.labels?.length) {
parts.push(rule.labels.join(", "));
}
if (rule.email_types?.includes(ALL_TYPES_ID)) {
parts.push("All types");
} else if (rule.email_types?.length) {
parts.push(typeTitles(rule.email_types).join(", ")); parts.push(typeTitles(rule.email_types).join(", "));
} }
const meta = parts.length ? parts.join(" • ") : "All eligible emails"; const meta = parts.length ? parts.join(" • ") : "All eligible emails";