From 3f4bcc26d9a14bdb5e99be1ac0c0cc4f0da7bef3 Mon Sep 17 00:00:00 2001 From: debu-2 Date: Tue, 6 Oct 2026 02:07:13 +0530 Subject: [PATCH] handoff fixes --- backend/app/services/handoff_evaluator.py | 33 ++++++++++-- backend/tests/test_handoff_evaluator.py | 34 +++++++++++- .../src/components/HumanHandoffDrawer.jsx | 54 ++++++++++++++----- frontend/src/utils/handoffUtils.js | 44 +++++++++++---- 4 files changed, 137 insertions(+), 28 deletions(-) diff --git a/backend/app/services/handoff_evaluator.py b/backend/app/services/handoff_evaluator.py index 79e442d..51bebe9 100644 --- a/backend/app/services/handoff_evaluator.py +++ b/backend/app/services/handoff_evaluator.py @@ -1,8 +1,14 @@ 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 = { + "all": { + "title": "All types", + "patterns": "", + }, "utility": { "title": "Utility", "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_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] + all_mail = ALL_MAIL_LABEL in labels + 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) - matched_types = [item for item in email_types if item in inferred_types] if email_types else inferred_types[:1] + label_ok = all_mail or not selected_labels or any( + 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) would_handoff = label_ok and type_ok and instruction_ok diff --git a/backend/tests/test_handoff_evaluator.py b/backend/tests/test_handoff_evaluator.py index ac6fecc..d4ffcb7 100644 --- a/backend/tests/test_handoff_evaluator.py +++ b/backend/tests/test_handoff_evaluator.py @@ -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(): @@ -31,3 +31,35 @@ def test_handoff_does_not_match_unrelated_instruction(): instructions="Forward when a refund requires approval.", ) 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 diff --git a/frontend/src/components/HumanHandoffDrawer.jsx b/frontend/src/components/HumanHandoffDrawer.jsx index 3372887..0a3cf74 100644 --- a/frontend/src/components/HumanHandoffDrawer.jsx +++ b/frontend/src/components/HumanHandoffDrawer.jsx @@ -2,6 +2,8 @@ import { Mail, UserRound, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import AgentSpark from "./AgentSpark"; import { + ALL_MAIL_LABEL, + ALL_TYPES_ID, buildHandoffSummary, EMAIL_TYPE_OPTIONS, HANDOFF_LABELS, @@ -62,19 +64,33 @@ export default function HumanHandoffDrawer({ ); function toggleLabel(label) { - setLabels((current) => ( - current.includes(label) - ? current.filter((item) => item !== label) - : [...current, label] - )); + if (label === ALL_MAIL_LABEL) { + setLabels((current) => ( + current.includes(ALL_MAIL_LABEL) ? [] : [ALL_MAIL_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) { - setEmailTypes((current) => ( - current.includes(typeId) - ? current.filter((item) => item !== typeId) - : [...current, typeId] - )); + if (typeId === ALL_TYPES_ID) { + setEmailTypes((current) => ( + current.includes(ALL_TYPES_ID) ? [] : [ALL_TYPES_ID] + )); + 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) { @@ -137,7 +153,10 @@ export default function HumanHandoffDrawer({

Hand off emails with these labels

-

An email matching any selected label will be eligible for handoff.

+

+ These are content-based handoff categories, not inbox tabs. Gentic infers them from + subject and body text. Choose All mail to hand off every incoming email. +

setLabelQuery(event.target.value)} /> - + +
{filteredLabels.map((label) => ( @@ -163,7 +188,10 @@ export default function HumanHandoffDrawer({

Hand off these types of email

-

Emails matching any selected type can be considered for handoff.

+

+ Types are also inferred from email content. Choose All types if you + do not want to restrict by category. +

{EMAIL_TYPE_OPTIONS.map((type) => (