import re from datetime import datetime, timezone from bson import ObjectId from fastapi import APIRouter, Depends, HTTPException, status from pydantic import EmailStr, Field from app.database import get_mongo_database from app.models import Account from app.routers.auth import get_current_account from app.schemas import ( HandoffRuleCreate, HandoffRuleRead, HandoffRuleUpdate, HandoffTestRequest, HandoffTestResult, ) from app.services.handoff_evaluator import EMAIL_TYPE_META, HANDOFF_LABELS, evaluate_handoff router = APIRouter(prefix="/agent/handoff", tags=["agent-handoff"]) def _now() -> datetime: return datetime.now(timezone.utc) def _owner_name_from_email(email: str) -> str: local = email.split("@", 1)[0] return re.sub(r"[._-]+", " ", local).strip().title() or email def _serialize(document: dict) -> HandoffRuleRead: return HandoffRuleRead( id=str(document["_id"]), owner_email=document["owner_email"], owner_name=document.get("owner_name") or _owner_name_from_email(document["owner_email"]), labels=document.get("labels", []), email_types=document.get("email_types", []), instructions=document.get("instructions", ""), allow_replies=document.get("allow_replies", True), reply_as=document.get("reply_as", "agent"), active=document.get("active", True), created_at=document["created_at"], updated_at=document["updated_at"], ) @router.get("", response_model=list[HandoffRuleRead]) async def list_handoff_rules( database=Depends(get_mongo_database), account: Account = Depends(get_current_account), ): cursor = database.human_handoff_rules.find( {"account_id": str(account.id)} ).sort("updated_at", -1) return [_serialize(document) async for document in cursor] @router.post("", response_model=HandoffRuleRead, status_code=status.HTTP_201_CREATED) async def create_handoff_rule( payload: HandoffRuleCreate, database=Depends(get_mongo_database), account: Account = Depends(get_current_account), ): now = _now() document = { "_id": ObjectId(), "account_id": str(account.id), "owner_email": str(payload.owner_email).lower(), "owner_name": payload.owner_name or _owner_name_from_email(str(payload.owner_email)), "labels": payload.labels, "email_types": payload.email_types, "instructions": payload.instructions.strip(), "allow_replies": payload.allow_replies, "reply_as": payload.reply_as, "active": payload.active, "created_at": now, "updated_at": now, } await database.human_handoff_rules.insert_one(document) return _serialize(document) @router.patch("/{rule_id}", response_model=HandoffRuleRead) async def update_handoff_rule( rule_id: str, payload: HandoffRuleUpdate, database=Depends(get_mongo_database), account: Account = Depends(get_current_account), ): if not ObjectId.is_valid(rule_id): raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid rule ID.") patch = payload.model_dump(exclude_unset=True) if "owner_email" in patch and patch["owner_email"]: patch["owner_email"] = str(patch["owner_email"]).lower() patch["updated_at"] = _now() result = await database.human_handoff_rules.find_one_and_update( {"_id": ObjectId(rule_id), "account_id": str(account.id)}, {"$set": patch}, return_document=True, ) if result is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Handoff rule not found.") return _serialize(result) @router.delete("/{rule_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_handoff_rule( rule_id: str, database=Depends(get_mongo_database), account: Account = Depends(get_current_account), ): if not ObjectId.is_valid(rule_id): raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid rule ID.") result = await database.human_handoff_rules.delete_one( {"_id": ObjectId(rule_id), "account_id": str(account.id)} ) if result.deleted_count == 0: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Handoff rule not found.") @router.post("/test", response_model=HandoffTestResult) async def test_handoff( payload: HandoffTestRequest, database=Depends(get_mongo_database), account: Account = Depends(get_current_account), ): labels = payload.labels email_types = payload.email_types instructions = payload.instructions owner_email = payload.owner_email if payload.rule_id: if not ObjectId.is_valid(payload.rule_id): raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid rule ID.") rule = await database.human_handoff_rules.find_one( {"_id": ObjectId(payload.rule_id), "account_id": str(account.id)} ) if rule is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Handoff rule not found.") labels = rule.get("labels", []) email_types = rule.get("email_types", []) instructions = rule.get("instructions", "") owner_email = rule.get("owner_email", owner_email) evaluation = evaluate_handoff( email=payload.email.model_dump(), labels=labels, email_types=email_types, instructions=instructions or "", ) return HandoffTestResult( owner_email=owner_email, **evaluation, result_path=( "Would match handoff rule (configuration only)" if evaluation["would_handoff"] else "Would not match handoff rule" ), reason=evaluation["instruction_reason"], ) @router.get("/meta") async def handoff_meta(): return { "labels": HANDOFF_LABELS, "email_types": [ { "id": key, "title": meta["title"], } for key, meta in EMAIL_TYPE_META.items() ], }