174 lines
6 KiB
Python
174 lines
6 KiB
Python
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()
|
|
],
|
|
}
|