ReplyFlow / app /services /handoff.py
nvikou's picture
Initial commit
582520a verified
Raw History Blame Contribute Delete
2.99 kB
"""Human handoff detection and notification."""
from __future__ import annotations
import logging
from typing import Any
import httpx
from app.config import get_settings
logger = logging.getLogger(__name__)
SENSITIVE_HINTS = (
"complaint",
"réclamation",
"reclamation",
"litige",
"lawsuit",
"lawyer",
"avocat",
"refund",
"remboursement",
"scam",
"fraud",
"мошен",
"жалоб",
"претенз",
"возврат",
"суд",
)
HANDOFF_PHRASES = (
"human",
"agent",
"operator",
"speak to someone",
"real person",
"humain",
"conseiller",
"parler à quelqu'un",
"оператор",
"человек",
"живой",
"специалист",
)
def should_handoff(
*,
message: str,
failure_streak: int,
grounded: bool,
) -> tuple[bool, str]:
"""Decide whether to escalate to a human."""
lowered = message.lower()
if any(p in lowered for p in HANDOFF_PHRASES):
return True, "explicit_request"
if any(p in lowered for p in SENSITIVE_HINTS):
return True, "sensitive_topic"
if not grounded and failure_streak >= 2:
return True, "consecutive_failures"
return False, ""
async def notify_handoff(
*,
conversation_id: int,
reason: str,
history: list[dict[str, str]],
lead: dict[str, Any],
) -> None:
"""Send handoff payload to webhook if configured."""
settings = get_settings()
payload = {
"conversation_id": conversation_id,
"reason": reason,
"lead": lead,
"history": history[-20:],
"app": settings.app_name,
}
if not settings.handoff_webhook_url:
logger.info(
"Handoff (no webhook): conversation=%s reason=%s",
conversation_id,
reason,
)
return
try:
async with httpx.AsyncClient(timeout=10) as client:
await client.post(
settings.handoff_webhook_url, json=payload
)
except Exception as exc: # noqa: BLE001
logger.exception("Handoff webhook failed: %s", exc)
def handoff_message(language: str, reason: str) -> str:
"""User-facing handoff acknowledgement."""
messages = {
"en": (
"I am transferring you to a human teammate. "
"They will see this conversation and follow up "
f"shortly. (reason: {reason.replace('_', ' ')})"
),
"fr": (
"Je vous transfère à un conseiller humain. "
"Il verra cet échange et vous répondra bientôt. "
f"(motif : {reason.replace('_', ' ')})"
),
"ru": (
"Передаю вас живому специалисту. Он увидит "
"этот диалог и скоро свяжется с вами. "
f"(причина: {reason.replace('_', ' ')})"
),
}
return messages.get(language, messages["en"])