handoff fixes

This commit is contained in:
debu-2 2026-10-06 03:24:57 +05:30
parent 83f0eba64d
commit 8653f319f7
9 changed files with 30 additions and 1309 deletions

View file

@ -8,18 +8,12 @@ from app.database import get_mongo_database
from app.models import Account
from app.routers.auth import get_current_account
from app.schemas import (
HandoffConversationRead,
HandoffConversationSummary,
HandoffLinkedExternal,
HandoffMessageRead,
HandoffRuleCreate,
HandoffRuleRead,
HandoffRuleUpdate,
HandoffTestRequest,
HandoffTestResult,
)
from app.services.handoff_mirror_policy import is_relay_acknowledgment_message
from app.services.human_handoff_service import MESSAGE_SCOPE_HANDOFF
from app.services.handoff_evaluator import EMAIL_TYPE_META, HANDOFF_LABELS, evaluate_handoff
router = APIRouter(prefix="/agent/handoff", tags=["agent-handoff"])
@ -34,20 +28,6 @@ def _owner_name_from_email(email: str) -> str:
return re.sub(r"[._-]+", " ", local).strip().title() or email
def _status_label(status: str, current_owner: str) -> str:
if status == "WAITING_FOR_HUMAN":
return "Waiting for Human"
if status == "HUMAN_REPLIED":
return "Needs Agent Reply"
if status == "AGENT_REPLIED":
return "Replied"
if status == "CLOSED":
return "Closed"
if current_owner == "HUMAN":
return "Waiting for Human"
return "Active"
def _serialize(document: dict) -> HandoffRuleRead:
return HandoffRuleRead(
id=str(document["_id"]),
@ -171,150 +151,15 @@ async def test_handoff(
return HandoffTestResult(
owner_email=owner_email,
**evaluation,
result_path="Agent → Human Owner" if evaluation["would_handoff"] else "Agent handles email",
result_path=(
"Would match handoff rule (configuration only)"
if evaluation["would_handoff"]
else "Would not match handoff rule"
),
reason=evaluation["instruction_reason"],
)
@router.get("/conversations", response_model=list[HandoffConversationSummary])
async def list_handoff_conversations(
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
account_id = str(account.id)
cursor = database.human_handoffs.find({"account_id": account_id}).sort("updated_at", -1)
items: list[HandoffConversationSummary] = []
async for handoff in cursor:
handoff_thread_id = handoff["handoff_thread_id"]
count = await database.mailbox_entries.count_documents(
{
"account_id": account_id,
"thread_id": handoff_thread_id,
"message_scope": MESSAGE_SCOPE_HANDOFF,
}
)
latest = await database.mailbox_entries.find_one(
{
"account_id": account_id,
"thread_id": handoff_thread_id,
"message_scope": MESSAGE_SCOPE_HANDOFF,
},
sort=[("event_at", -1), ("_id", -1)],
)
snippet = latest.get("snippet", "") if latest else ""
items.append(
HandoffConversationSummary(
id=str(handoff["_id"]),
external_subject=handoff.get("external_subject", ""),
external_sender=handoff.get("external_sender", ""),
human_email=handoff.get("human_email", ""),
status=handoff.get("status", "ACTIVE"),
status_label=_status_label(
handoff.get("status", "ACTIVE"),
handoff.get("current_owner", "AGENT"),
),
current_owner=handoff.get("current_owner", "AGENT"),
snippet=snippet,
message_count=count,
updated_at=handoff.get("updated_at") or handoff.get("created_at"),
)
)
return items
@router.get("/conversations/{handoff_id}", response_model=HandoffConversationRead)
async def get_handoff_conversation(
handoff_id: str,
database=Depends(get_mongo_database),
account: Account = Depends(get_current_account),
):
if not ObjectId.is_valid(handoff_id):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid handoff ID.")
account_id = str(account.id)
handoff = await database.human_handoffs.find_one(
{"_id": ObjectId(handoff_id), "account_id": account_id}
)
if handoff is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Handoff conversation not found.")
pipeline = [
{
"$match": {
"account_id": account_id,
"thread_id": handoff["handoff_thread_id"],
"message_scope": MESSAGE_SCOPE_HANDOFF,
}
},
{"$sort": {"event_at": 1, "_id": 1}},
{
"$lookup": {
"from": "messages",
"localField": "message_id",
"foreignField": "_id",
"as": "message",
}
},
{"$unwind": "$message"},
]
cursor = await database.mailbox_entries.aggregate(pipeline)
rows = [row async for row in cursor]
human_email = handoff.get("human_email", "").lower()
messages: list[HandoffMessageRead] = []
for row in rows:
message = row["message"]
if is_relay_acknowledgment_message(message):
continue
sender_address = (message.get("sender") or {}).get("address", "").lower()
if message.get("mirrored_from_external_message_id"):
role = "customer"
elif sender_address == human_email:
role = "human"
else:
role = "agent"
event_at = row.get("event_at") or message.get("received_at") or message.get("sent_at") or message["created_at"]
raw_body = message.get("raw_body")
new_reply_content = message.get("new_reply_content")
display_body = new_reply_content or message.get("body_text", "")
messages.append(
HandoffMessageRead(
id=str(message["_id"]),
role=role,
subject=message.get("subject", ""),
snippet=message.get("snippet", "") or display_body[:240],
body_text=display_body,
new_reply_content=new_reply_content,
raw_body=raw_body,
has_quoted_text=bool(
raw_body
and new_reply_content
and raw_body.strip() != new_reply_content.strip()
),
relay_status=message.get("relay_status") if role == "human" else None,
event_at=event_at,
)
)
return HandoffConversationRead(
id=str(handoff["_id"]),
external_subject=handoff.get("external_subject", ""),
external_sender=handoff.get("external_sender", ""),
human_email=handoff.get("human_email", ""),
handoff_reason=handoff.get("handoff_reason", ""),
status=handoff.get("status", "ACTIVE"),
status_label=_status_label(
handoff.get("status", "ACTIVE"),
handoff.get("current_owner", "AGENT"),
),
current_owner=handoff.get("current_owner", "AGENT"),
linked_external=HandoffLinkedExternal(
thread_id=str(handoff["external_thread_id"]),
subject=handoff.get("external_subject", ""),
sender=handoff.get("external_sender", ""),
),
messages=messages,
)
@router.get("/meta")
async def handoff_meta():
return {