"""Customer Agent - Handles notifications, tracking, and customer communication.""" import uuid from datetime import datetime, timedelta from typing import Dict, List, Any, Optional from dataclasses import dataclass from enum import Enum import aiohttp from core.agent import SpecializedAgent from core.types import AgentTask, MessageType, OrderStatus from core.logger import logger from core.http_client import api_post, api_get from config.system_config import GO_API_BASE_URL, INTERNAL_API_KEY class NotificationChannel(str, Enum): SMS = "sms" EMAIL = "email" PUSH = "push" WHATSAPP = "whatsapp" class NotificationType(str, Enum): ORDER_CONFIRMED = "order_confirmed" PICKUP_SCHEDULED = "pickup_scheduled" PICKED_UP = "picked_up" IN_TRANSIT = "in_transit" ARRIVED_AT_HUB = "arrived_at_hub" OUT_FOR_DELIVERY = "out_for_delivery" DELIVERED = "delivered" DELAYED = "delayed" RESCHEDULED = "rescheduled" CANCELLED = "cancelled" @dataclass class Notification: notification_id: str order_id: str customer_id: str channel: NotificationChannel notification_type: NotificationType message: str sent_at: datetime delivered_at: Optional[datetime] status: str # pending, sent, delivered, failed class CustomerAgent(SpecializedAgent): """ Customer Agent - Manages customer communications and notifications. Sends via Go backend (POST /api/v1/internal/notify → real FCM/SMS/WhatsApp). Keeps local notification history for audit / query. """ def __init__(self): super().__init__( agent_id="CUSTOMER_AGENT", domain="customer_communication", description="Handles notifications, tracking updates, and customer communication" ) self._customers: Dict[str, Dict] = {} self._notifications: Dict[str, List[Notification]] = {} self._templates = self._init_templates() self._preferences: Dict[str, Dict] = {} def _init_templates(self) -> Dict[str, Dict]: return { NotificationType.ORDER_CONFIRMED: { "sms": "Your order {order_id} has been confirmed! Estimated delivery: {eta}", "email": "Hi {customer_name}, your order {order_id} is confirmed. Track at: {tracking_url}", "whatsapp": "Order {order_id} confirmed! Delivery by {eta}. Track: {tracking_url}" }, NotificationType.PICKUP_SCHEDULED: { "sms": "Pickup scheduled for {pickup_time}. Driver will arrive within 30 mins.", "email": "Your pickup is scheduled for {pickup_time}", "whatsapp": "Pickup scheduled for {pickup_time}. Driver arrives in 30 mins" }, NotificationType.PICKED_UP: { "sms": "Order {order_id} picked up and on its way!", "email": "Your package has been picked up and is in transit.", "whatsapp": "Order {order_id} picked up! Heading to {destination}" }, NotificationType.IN_TRANSIT: { "sms": "Your order {order_id} is now in transit.", "email": "Update: Your order is on the move!", "whatsapp": "Order {order_id} in transit. Current stop: {current_location}" }, NotificationType.ARRIVED_AT_HUB: { "sms": "Your order {order_id} has arrived at {hub_name}.", "email": "Your package has arrived at the {hub_name} facility.", "whatsapp": "Order {order_id} arrived at {hub_name}" }, NotificationType.OUT_FOR_DELIVERY: { "sms": "Out for delivery! Driver: {driver_name}, Contact: {driver_phone}", "email": "Your order is out for delivery!", "whatsapp": "Out for delivery! Driver: {driver_name} | {driver_phone}" }, NotificationType.DELIVERED: { "sms": "Order {order_id} delivered successfully! Thank you for choosing us.", "email": "Your order has been delivered! We hope you enjoy your purchase.", "whatsapp": "Delivered! Order {order_id}. Thank you for shopping with us!" }, NotificationType.DELAYED: { "sms": "Delay alert: Order {order_id} may arrive later than expected. New ETA: {new_eta}", "email": "We apologize for the delay. Your order's new ETA is {new_eta}", "whatsapp": "Delay update: Order {order_id}. New ETA: {new_eta}" }, NotificationType.RESCHEDULED: { "sms": "Delivery rescheduled. New date: {new_date}", "email": "Your delivery has been rescheduled to {new_date}", "whatsapp": "Rescheduled! New delivery: {new_date}" }, NotificationType.CANCELLED: { "sms": "Order {order_id} has been cancelled. Refund processing: {refund_info}", "email": "Your order has been cancelled. {refund_info}", "whatsapp": "Cancelled: Order {order_id}. {refund_info}" } } async def handle_task(self, task: AgentTask) -> Dict[str, Any]: handlers = { "send_confirmation": self._send_confirmation, "send_notification": self._send_notification, "update_tracking": self._update_tracking, "get_tracking": self._get_tracking, "send_bulk_notification": self._send_bulk_notification, "get_customer_history": self._get_customer_history, "handle_query": self._handle_query, } handler = handlers.get(task.task_type, self._unknown_task) return await handler(task) async def _send_confirmation(self, task: AgentTask) -> Dict[str, Any]: order_data = task.data.get("order", {}) order_id = order_data.get("order_id") customer_phone = order_data.get("customer_phone") customer_email = order_data.get("customer_email") customer_name = order_data.get("customer_name", "Customer") eta = task.data.get("eta", "tomorrow") logger.info(f"Customer Agent: Sending confirmation for order {order_id}") if customer_phone and customer_phone not in self._customers: self._customers[customer_phone] = { "name": customer_name, "email": customer_email, "orders": [] } if customer_phone in self._customers: self._customers[customer_phone]["orders"].append(order_id) template_vars = { "order_id": order_id, "customer_name": customer_name, "eta": eta, "tracking_url": f"https://track.doormile.com/{order_id}" } notifications_sent = [] if customer_phone: notifications_sent.append(await self._send_via_channel( order_id=order_id, customer_id=customer_phone, channel=NotificationChannel.SMS, notification_type=NotificationType.ORDER_CONFIRMED, template_vars=template_vars, )) notifications_sent.append(await self._send_via_channel( order_id=order_id, customer_id=customer_phone, channel=NotificationChannel.WHATSAPP, notification_type=NotificationType.ORDER_CONFIRMED, template_vars=template_vars, )) if customer_email: notifications_sent.append(await self._send_via_channel( order_id=order_id, customer_id=customer_email, channel=NotificationChannel.EMAIL, notification_type=NotificationType.ORDER_CONFIRMED, template_vars=template_vars, )) return {"status": "sent", "order_id": order_id, "notifications": notifications_sent} async def _send_notification(self, task: AgentTask) -> Dict[str, Any]: order_id = task.data.get("order_id") notification_type = task.data.get("notification_type") template_vars = task.data.get("template_vars", {}) customer_id = task.data.get("customer_id", "unknown") logger.info(f"Customer Agent: Sending {notification_type} for order {order_id}") notification = await self._send_via_channel( order_id=order_id, customer_id=customer_id, channel=NotificationChannel.SMS, notification_type=NotificationType(notification_type), template_vars=template_vars, ) return {"status": "sent", "notification": notification} async def _update_tracking(self, task: AgentTask) -> Dict[str, Any]: order_id = task.data.get("order_id") status = task.data.get("status") location = task.data.get("location") eta = task.data.get("eta") logger.info(f"Customer Agent: Updating tracking for order {order_id} | status={status} location={location}") status_notification_map = { "picked_up": NotificationType.PICKED_UP, "in_transit": NotificationType.IN_TRANSIT, "at_hub": NotificationType.ARRIVED_AT_HUB, "out_for_delivery": NotificationType.OUT_FOR_DELIVERY, "delivered": NotificationType.DELIVERED, "delayed": NotificationType.DELAYED, } notification_type = status_notification_map.get(status) if notification_type: await self._send_via_channel( order_id=order_id, customer_id=task.data.get("customer_id", ""), channel=NotificationChannel.SMS, notification_type=notification_type, template_vars={ "order_id": order_id, "current_location": location, "new_eta": eta, "hub_name": location, }, ) return {"status": "updated", "order_id": order_id, "new_status": status, "location": location} async def _get_tracking(self, task: AgentTask) -> Dict[str, Any]: booking_id = task.data.get("booking_id") or task.data.get("order_id") result = await api_get( f"{GO_API_BASE_URL}/api/v1/bookings/cache/{booking_id}", headers={"X-Internal-Key": INTERNAL_API_KEY}, ) if not result: logger.warning(f"Tracking lookup failed for booking {booking_id}") return {"status": "not_found", "order_id": booking_id} data = result.get("data") or result return { "order_id": booking_id, "current_status": data.get("status", "unknown"), "miler_id": data.get("assignedmileruserid"), "miler_name": data.get("milername"), "miler_phone": data.get("milerphone"), "estimated_delivery": data.get("estimateddelivery"), "pickup_address": data.get("pickupaddress"), "delivery_address": data.get("deliveryaddress"), "last_update": data.get("updatedat") or datetime.now().isoformat(), } async def _send_bulk_notification(self, task: AgentTask) -> Dict[str, Any]: order_ids = task.data.get("order_ids", []) notification_type = task.data.get("notification_type") template_vars = task.data.get("template_vars", {}) logger.info(f"Customer Agent: Sending bulk notification to {len(order_ids)} customers") results = [] for order_id in order_ids: result = await self._send_notification(AgentTask( task_id=f"bulk_{order_id}", agent_type="customer", task_type="send_notification", data={ "order_id": order_id, "notification_type": notification_type, "template_vars": template_vars, } )) results.append(result) return { "status": "bulk_sent", "total": len(order_ids), "successful": sum(1 for r in results if r.get("status") == "sent"), "failed": sum(1 for r in results if r.get("status") == "failed"), } async def _get_customer_history(self, task: AgentTask) -> Dict[str, Any]: customer_id = task.data.get("customer_id") notifications = self._notifications.get(customer_id, []) return { "customer_id": customer_id, "total_notifications": len(notifications), "notifications": [self._format_notification(n) for n in notifications[-20:]], } async def _handle_query(self, task: AgentTask) -> Dict[str, Any]: query = task.data.get("query", "").lower() order_id = task.data.get("order_id") logger.info(f"Customer Agent: Handling query '{query}' for order {order_id}") if "where" in query or "track" in query or "status" in query: return await self._get_tracking(AgentTask( task_id="query_tracking", agent_type="customer", task_type="get_tracking", data={"order_id": order_id}, )) if "delay" in query or "late" in query: return { "response": "I understand your concern about the delay. Let me check the current status and get back to you with updated information.", "action": "check_status", } if "cancel" in query: return { "response": f"I can help you with cancellation. Please confirm you want to cancel order {order_id}. Note: Cancellation is only possible before dispatch.", "action": "await_confirmation", } return { "response": "I'm here to help! You can ask about your order status, tracking, delays, or cancellation. How can I assist you?", "action": "provide_options", } async def _send_via_channel( self, order_id: str, customer_id: str, channel: NotificationChannel, notification_type: NotificationType, template_vars: Dict[str, Any], ) -> Dict[str, Any]: """Render template and call Go backend (POST /api/v1/internal/notify).""" template = self._templates.get(notification_type, {}).get(channel.value, "") message = template for key, value in template_vars.items(): message = message.replace(f"{{{key}}}", str(value)) notification_id = f"NOTIF-{uuid.uuid4().hex[:8].upper()}" result = await api_post( f"{GO_API_BASE_URL}/api/v1/internal/notify", json={ "notification_id": notification_id, "order_id": order_id, "customer_id": customer_id, "channel": channel.value, "notification_type": notification_type.value, "message": message, }, headers={"X-Internal-Key": INTERNAL_API_KEY}, timeout=aiohttp.ClientTimeout(total=10), ) if result is None: status = "failed" logger.warning(f"Notify API call failed for {notification_id}") else: status = "sent" notification = Notification( notification_id=notification_id, order_id=order_id, customer_id=customer_id, channel=channel, notification_type=notification_type, message=message, sent_at=datetime.now(), delivered_at=None, status=status, ) if customer_id not in self._notifications: self._notifications[customer_id] = [] self._notifications[customer_id].append(notification) logger.info(f"[{channel.value.upper()}] {notification_type.value} -> {customer_id[:20]} [{status}]") return { "notification_id": notification_id, "channel": channel.value, "status": status, "message": message, } def _format_notification(self, notification: Notification) -> Dict[str, Any]: return { "id": notification.notification_id, "type": notification.notification_type.value, "channel": notification.channel.value, "message": notification.message, "sent_at": notification.sent_at.isoformat(), "status": notification.status, } async def _unknown_task(self, task: AgentTask) -> Dict[str, Any]: return {"status": "error", "message": f"Unknown task: {task.task_type}"} async def think(self, context: str, options: List[str] = None) -> str: return f"[CUSTOMER_AGENT reasoning]: {context}"