handoff fixes
This commit is contained in:
parent
83f0eba64d
commit
8653f319f7
9 changed files with 30 additions and 1309 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue