Initial commit

This commit is contained in:
2026-06-26 16:08:31 +05:30
commit f49193ee73
23 changed files with 6056 additions and 0 deletions

11
.env Normal file
View File

@@ -0,0 +1,11 @@
GO_API_BASE_URL=https://api.doormile.com
NATS_URL=nats://doormile:Package@321#@66.116.226.161:4223
REDIS_HOST=66.116.226.255
REDIS_PORT=6380
REDIS_PASSWORD=Package@321#
INTERNAL_API_KEY=doormile-internal-2024
DB_HOST=31.97.228.132
DB_PORT=5433
DB_NAME=logistics
DB_USER=admin
DB_PASSWORD=Package@321#

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
__pycache__/
*.pyc
logs/
.venv/
venv/
.idea/
.vscode/

245
README.md Normal file
View File

@@ -0,0 +1,245 @@
# 🤖 LogiFlow AI - Autonomous Logistics Agent System
A complete multi-agent AI system for autonomous logistics operations. Built with Python, featuring 8 intelligent agents coordinated by a master orchestrator (JARVIS).
## 🏗️ Architecture
```
┌─────────────────────────────────────────┐
│ 🧠 JARVIS (Master Agent) │
│ Central orchestrator & decision maker │
└─────────────────┬─────────────────────────┘
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ORDER_AGENT │ │ DISPATCH_AGENT │ │ FLEET_AGENT │
│ ─────────────│ │ ────────────────│ │ ────────────────│
│ • Receive │ │ • Analyze zones │ │ • Vehicle status│
│ • Validate │ │ • Assign routes │ │ • Capacity mgmt │
│ • Categorize │ │ • Route opt. │ │ • Maintenance │
└───────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ HUB_AGENT │ │ CUSTOMER_AGENT │ │ EXCEPTION_AGENT │
│ ─────────────│ │ ────────────────│ │ ────────────────│
│ • Hub routing │ │ • Notifications │ │ • Delay handling│
│ • Transit mgmt│ │ • Tracking │ │ • Rescheduling │
│ • Capacity │ │ • Feedback │ │ • Cancellations │
└───────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐
│ROUTE_OPTIMIZER │
│ ────────────────│
│ • Zone planning │
│ • Pathfinding │
│ • Traffic adapt │
└─────────────────┘
```
## ✨ Features
### 🤖 Intelligent Agents
| Agent | Function | Key Capabilities |
|-------|----------|-----------------|
| **JARVIS** | Master Orchestrator | Task delegation, system monitoring, decision making |
| **ORDER_AGENT** | Order Management | Receive, validate, categorize orders by priority/zone |
| **DISPATCH_AGENT** | Route Assignment | Zone analysis, hub assignment, route planning |
| **FLEET_AGENT** | Vehicle Management | Capacity tracking, vehicle assignment, maintenance |
| **HUB_AGENT** | Hub Operations | Transit management, capacity monitoring, overflow handling |
| **CUSTOMER_AGENT** | Customer Communication | Notifications, tracking updates, support |
| **EXCEPTION_AGENT** | Problem Resolution | Delay handling, cancellations, rescheduling |
| **ROUTE_OPTIMIZER** | Route Planning | Zone-based optimization, path calculation |
### 🔄 Agent Communication Flow
```
Order Received → ORDER_AGENT validates → DISPATCH_AGENT assigns route
↓ ↓
FLEET_AGENT assigns vehicle → HUB_AGENT prepares receiving
↓ ↓
ROUTE_OPTIMIZER calculates path → CUSTOMER_AGENT sends notification
EXCEPTION_AGENT monitors (handles any issues)
JARVIS monitors all agents, logs decisions, reports to admin
```
## 🚀 Quick Start
### 1. Install Dependencies
```bash
cd /workspace/project/logistics-ai
pip install -r requirements.txt
```
### 2. Run Demo Mode
```bash
python main.py
```
This will:
- Initialize all 8 agents
- Run system diagnostics
- Process a sample order through all agents
- Display complete agent coordination
### 3. Launch Admin Dashboard
```bash
python main.py --dashboard
```
Opens Streamlit dashboard at `http://localhost:8501` with:
- Real-time agent status monitoring
- Order management
- Fleet tracking
- Hub network visualization
- Message feed
### 4. Launch Customer Portal
```bash
python main.py --portal
```
Opens customer-facing portal at `http://localhost:8502` with:
- Order tracking
- Live updates
- AI assistant chat
- Delivery scheduling
## 📁 Project Structure
```
logistics-ai/
├── main.py # Main orchestration & demo
├── requirements.txt # Python dependencies
├── core/
│ ├── __init__.py
│ ├── types.py # Data models & message types
│ ├── agent.py # Base Agent, MasterAgent classes
│ └── message_bus.py # Agent communication bus
├── agents/
│ ├── __init__.py
│ ├── order_agent.py # Order validation & categorization
│ ├── dispatch_agent.py # Route assignment & zone analysis
│ ├── fleet_agent.py # Vehicle management
│ ├── hub_agent.py # Hub operations & transit
│ ├── customer_agent.py # Notifications & tracking
│ ├── exception_agent.py # Problem resolution
│ └── route_optimizer_agent.py # Route optimization
├── dashboard/
│ └── admin_dashboard.py # Streamlit admin interface
├── customer_portal/
│ └── portal.py # Streamlit customer interface
└── config/
└── system_config.py # Configuration & zone definitions
```
## 🔧 Configuration
Edit `config/system_config.py` to customize:
- **Zones**: Pincode ranges and hub assignments
- **Hubs**: Capacity, processing rates, connections
- **Vehicles**: Types, capacities, fuel costs
- **SLA**: Delivery windows, response times
- **Notifications**: Templates for SMS/Email/WhatsApp
## 🎯 Usage Examples
### Create an Order via API
```python
from main import LogiFlowAI
system = LogiFlowAI()
system.initialize_agents()
order = {
"customer_name": "Rajesh Kumar",
"customer_phone": "+919876543210",
"pickup_address": {"city": "Delhi", "pincode": "110001"},
"delivery_address": {"city": "Mumbai", "pincode": "400001"},
"items": [{"name": "Laptop", "weight": 2.5}]
}
await system.create_order(order)
```
### Monitor System Status
```python
status = system.get_system_status()
print(f"Active agents: {len(status['agents'])}")
print(f"System: {status['system']}")
```
## 🔌 Integration Points
### REST API (Future)
- `POST /orders` - Create new order
- `GET /orders/{id}` - Get order status
- `PATCH /orders/{id}` - Update order
- `GET /agents/status` - Get agent statuses
- `POST /agents/{id}/task` - Submit task to agent
### Webhook Support (Future)
- Order created
- Order status changed
- Exception detected
- Delivery completed
## 📊 Monitoring
The system provides:
- Real-time agent status
- Message traffic monitoring
- Performance metrics
- Exception alerts
- SLA compliance tracking
## 🔒 Security
- Agent authentication via tokens
- Message signing and verification
- Rate limiting on API endpoints
- Audit logging for all operations
## 🚀 Scaling
The architecture supports:
- Horizontal agent scaling
- Multiple message bus instances
- Distributed hub networks
- Multi-region deployment
## 📝 License
MIT License - See LICENSE file
## 🤝 Contributing
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Add tests for new features
4. Submit a pull request
---
**Built with ❤️ for autonomous logistics operations**
Resume this session with:
claude --resume d1f8a432-2298-4ff5-bee4-15e64d04bfa4
PS C:\Users\Admin\Downloads\logiflow-ai-logistics-agent-system\logistics-ai>

18
agents/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
# Agents module for LogiFlow AI
from agents.order_agent import OrderAgent
from agents.dispatch_agent import DispatchAgent
from agents.fleet_agent import FleetAgent
from agents.hub_agent import HubAgent
from agents.customer_agent import CustomerAgent
from agents.exception_agent import ExceptionAgent
from agents.route_optimizer_agent import RouteOptimizerAgent
__all__ = [
'OrderAgent',
'DispatchAgent',
'FleetAgent',
'HubAgent',
'CustomerAgent',
'ExceptionAgent',
'RouteOptimizerAgent'
]

407
agents/customer_agent.py Normal file
View File

@@ -0,0 +1,407 @@
"""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}"

379
agents/dispatch_agent.py Normal file
View File

@@ -0,0 +1,379 @@
"""Dispatch Agent - Handles zone analysis, route assignment, and order dispatching."""
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
import redis.asyncio as aioredis
from core.agent import SpecializedAgent
from core.types import AgentTask, MessageType, Priority, ZoneType
from core.logger import logger
from core.http_client import api_post
from config.system_config import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, GO_API_BASE_URL, INTERNAL_API_KEY
@dataclass
class ZoneInfo:
zone_id: str
zone_name: str
pincode_range: tuple
hub_id: str
vehicle_types: List[str]
avg_delivery_time: int
current_load: int = 0
max_capacity: int = 100
@dataclass
class RouteAssignment:
route_id: str
order_id: str
from_hub: str
to_hub: str
vehicle_id: Optional[str]
estimated_pickup: datetime
estimated_delivery: datetime
route_type: str
class DispatchAgent(SpecializedAgent):
"""
Dispatch Agent - Orchestrates order dispatching and route assignment.
Uses Redis GEO (GEORADIUS on milers:locations) to find nearest milers
for a given pickup/delivery coordinate. Falls back to pincode-based
zone lookup when no milers are found within range.
"""
def __init__(self):
super().__init__(
agent_id="DISPATCH_AGENT",
domain="dispatch",
description="Handles zone analysis, route assignment, and dispatch coordination"
)
self._redis = aioredis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD,
decode_responses=True,
)
self._zones = self._init_zones()
self._hubs = self._init_hubs()
self._active_routes: Dict[str, RouteAssignment] = {}
self._dispatch_queue: asyncio.Queue = asyncio.Queue()
def _init_zones(self) -> Dict[str, ZoneInfo]:
return {
"north_delhi": ZoneInfo("north_delhi", "North Delhi", ("100", "199"), "DL-HUB-01", ["bike", "scooter", "van"], 45),
"south_delhi": ZoneInfo("south_delhi", "South Delhi", ("200", "299"), "DL-HUB-02", ["bike", "scooter", "van"], 50),
"mumbai_west": ZoneInfo("mumbai_west", "West Mumbai", ("400", "449"), "MU-HUB-01", ["bike", "scooter", "van", "truck"], 60),
"mumbai_east": ZoneInfo("mumbai_east", "East Mumbai", ("450", "499"), "MU-HUB-02", ["bike", "scooter", "van", "truck"], 55),
"bangalore": ZoneInfo("bangalore", "Bangalore", ("560", "562"), "BL-HUB-01", ["bike", "scooter", "van"], 40),
"hyderabad": ZoneInfo("hyderabad", "Hyderabad", ("500", "509"), "HY-HUB-01", ["bike", "scooter", "van", "truck"], 50),
"pune": ZoneInfo("pune", "Pune", ("411", "415"), "PU-HUB-01", ["bike", "scooter", "van"], 45),
"kolkata": ZoneInfo("kolkata", "Kolkata", ("700", "700"), "KL-HUB-01", ["bike", "scooter", "van"], 55),
}
def _init_hubs(self) -> Dict[str, Dict]:
return {
"DL-HUB-01": {"name": "Delhi North Hub", "capacity": 500, "current_load": 150},
"DL-HUB-02": {"name": "Delhi South Hub", "capacity": 400, "current_load": 200},
"MU-HUB-01": {"name": "Mumbai West Hub", "capacity": 600, "current_load": 300},
"MU-HUB-02": {"name": "Mumbai East Hub", "capacity": 550, "current_load": 250},
"BL-HUB-01": {"name": "Bangalore Hub", "capacity": 500, "current_load": 180},
"HY-HUB-01": {"name": "Hyderabad Hub", "capacity": 450, "current_load": 220},
"PU-HUB-01": {"name": "Pune Hub", "capacity": 400, "current_load": 160},
"KL-HUB-01": {"name": "Kolkata Hub", "capacity": 350, "current_load": 140},
}
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
handlers = {
"analyze_and_assign": self._analyze_and_assign,
"find_route": self._find_route,
"optimize_routes": self._optimize_routes,
"assign_vehicle": self._assign_vehicle,
"get_zone_status": self._get_zone_status,
"reschedule_route": self._reschedule_route,
"cancel_route": self._cancel_route,
}
handler = handlers.get(task.task_type, self._unknown_task)
return await handler(task)
# ------------------------------------------------------------------ #
# Redis GEO zone lookup #
# ------------------------------------------------------------------ #
async def _find_zone(self, lat: float, lon: float) -> Optional[Dict[str, Any]]:
"""Find the nearest miler using GEORADIUS on milers:locations."""
try:
results = await self._redis.georadius(
"milers:locations",
lon, lat,
10, "km",
sort="ASC",
count=5,
)
if not results:
return None
nearest_miler = results[0]
miler_info = await self._redis.hgetall(f"miler:{nearest_miler}")
return {
"miler_id": nearest_miler,
"hub_id": miler_info.get("hub_id", "UNKNOWN"),
"zone_id": miler_info.get("zone_id", "unknown"),
"avg_delivery_time": int(miler_info.get("avg_delivery_time", 60)),
}
except Exception as e:
logger.warning(f"Redis GEORADIUS error: {e}")
return None
def _find_zone_by_pincode(self, pincode_prefix: str) -> Optional[ZoneInfo]:
for zone in self._zones.values():
start, end = zone.pincode_range
if start <= pincode_prefix <= end:
return zone
return None
# ------------------------------------------------------------------ #
# Core dispatch logic #
# ------------------------------------------------------------------ #
async def _analyze_and_assign(self, task: AgentTask) -> Dict[str, Any]:
order_data = task.data.get("order", {})
order_id = order_data.get("order_id", "unknown")
logger.info(f"Dispatch Agent: Analyzing order {order_id}")
pickup_addr = order_data.get("pickup_address", {})
delivery_addr = order_data.get("delivery_address", {})
pickup_lat = pickup_addr.get("lat") or pickup_addr.get("latitude")
pickup_lon = pickup_addr.get("lng") or pickup_addr.get("longitude")
delivery_lat = delivery_addr.get("lat") or delivery_addr.get("latitude")
delivery_lon = delivery_addr.get("lng") or delivery_addr.get("longitude")
pickup_zone_geo = await self._find_zone(pickup_lat, pickup_lon) if (pickup_lat and pickup_lon) else None
delivery_zone_geo = await self._find_zone(delivery_lat, delivery_lon) if (delivery_lat and delivery_lon) else None
pickup_pincode = pickup_addr.get("pincode", "")[:3]
delivery_pincode = delivery_addr.get("pincode", "")[:3]
pickup_zone_pc = self._find_zone_by_pincode(pickup_pincode)
delivery_zone_pc = self._find_zone_by_pincode(delivery_pincode)
from_hub = (
pickup_zone_geo.get("hub_id") if pickup_zone_geo
else (pickup_zone_pc.hub_id if pickup_zone_pc else "UNKNOWN")
)
to_hub = (
delivery_zone_geo.get("hub_id") if delivery_zone_geo
else (delivery_zone_pc.hub_id if delivery_zone_pc else "UNKNOWN")
)
if pickup_pincode == delivery_pincode:
route_type = ZoneType.LAST_MILE.value
elif from_hub == to_hub:
route_type = ZoneType.HUB_TO_SPOKE.value
else:
route_type = ZoneType.HUB_TO_HUB.value
avg_time = (
(pickup_zone_geo or {}).get("avg_delivery_time") or
(pickup_zone_pc.avg_delivery_time if pickup_zone_pc else 60)
)
now = datetime.now()
estimated_pickup = now + timedelta(minutes=30)
estimated_delivery = now + timedelta(minutes=avg_time * 2)
route_id = f"RT-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:8].upper()}"
route_assignment = RouteAssignment(
route_id=route_id,
order_id=order_id,
from_hub=from_hub,
to_hub=to_hub,
vehicle_id=None,
estimated_pickup=estimated_pickup,
estimated_delivery=estimated_delivery,
route_type=route_type,
)
self._active_routes[route_id] = route_assignment
logger.info(
f"Route assigned: {from_hub} -> {to_hub} | type={route_type} "
f"pickup={estimated_pickup.strftime('%H:%M')} delivery={estimated_delivery.strftime('%H:%M')}"
)
# Assign miler in Postgres via Go internal API.
# booking_id is the Go API's integer ID (set by order_agent after FIX 4).
booking_id = order_data.get("booking_id") or order_id
nearest_miler = (pickup_zone_geo or {}).get("miler_id")
if nearest_miler:
logger.debug(f"Nearest miler from GEORADIUS: {nearest_miler}")
assign_result = await api_post(
f"{GO_API_BASE_URL}/api/v1/internal/bookings/{booking_id}/reassign",
json={"reason": "initial_assignment"},
headers={"X-Internal-Key": INTERNAL_API_KEY},
)
if assign_result is not None:
logger.info(f"Miler assigned to booking {booking_id} via Go API")
else:
logger.warning(f"Go API miler assignment failed for booking {booking_id} — booking may stay unassigned")
await self.send_message(
recipient="HUB_AGENT",
message_type=MessageType.AGENT_TASK,
payload={"task_type": "prepare_receiving", "route_id": route_id, "hub_id": to_hub, "order_id": order_id},
correlation_id=order_id,
)
await self.send_message(
recipient="CUSTOMER_AGENT",
message_type=MessageType.NOTIFICATION_SENT,
payload={
"order_id": order_id,
"message_type": "dispatch_assigned",
"data": {
"route_id": route_id,
"estimated_pickup": estimated_pickup.isoformat(),
"estimated_delivery": estimated_delivery.isoformat(),
"route_type": route_type,
},
},
correlation_id=order_id,
)
return {
"status": "assigned",
"route_id": route_id,
"order_id": order_id,
"booking_id": booking_id,
"from_hub": from_hub,
"to_hub": to_hub,
"route_type": route_type,
"nearest_miler": nearest_miler,
"miler_assigned": assign_result is not None,
"estimated_pickup": estimated_pickup.isoformat(),
"estimated_delivery": estimated_delivery.isoformat(),
}
async def _find_route(self, task: AgentTask) -> Dict[str, Any]:
from_pincode = task.data.get("from_pincode", "")[:3]
to_pincode = task.data.get("to_pincode", "")[:3]
from_zone = self._find_zone_by_pincode(from_pincode)
to_zone = self._find_zone_by_pincode(to_pincode)
if not from_zone or not to_zone:
return {"status": "error", "message": "Zone not found for one or both pincodes"}
route_type = (
ZoneType.HUB_TO_SPOKE.value
if from_zone.zone_id == to_zone.zone_id
else ZoneType.HUB_TO_HUB.value
)
return {
"status": "found",
"from_hub": from_zone.hub_id,
"to_hub": to_zone.hub_id,
"route_type": route_type,
"estimated_time_minutes": from_zone.avg_delivery_time + to_zone.avg_delivery_time,
}
async def _optimize_routes(self, task: AgentTask) -> Dict[str, Any]:
order_ids = task.data.get("order_ids", [])
logger.info(f"Dispatch Agent: Optimizing {len(order_ids)} routes")
zone_groups: Dict[str, List[str]] = {}
for order_id in order_ids:
for route in self._active_routes.values():
if route.order_id == order_id:
zone_groups.setdefault(route.from_hub, []).append(order_id)
optimized = [
{"zone": zone, "orders": orders, "saved_km": len(orders) * 2.5}
for zone, orders in zone_groups.items()
]
return {"status": "optimized", "groups": optimized, "total_orders": len(order_ids)}
async def _assign_vehicle(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
if route_id not in self._active_routes:
return {"status": "error", "message": f"Route {route_id} not found"}
self._active_routes[route_id].vehicle_id = task.data.get("vehicle_id")
return {"status": "vehicle_assigned", "route_id": route_id}
async def _get_zone_status(self, task: AgentTask) -> Dict[str, Any]:
zone_status = []
for zone_id, zone in self._zones.items():
hub = self._hubs.get(zone.hub_id, {})
zone_status.append({
"zone_id": zone_id,
"zone_name": zone.zone_name,
"hub_id": zone.hub_id,
"current_load": hub.get("current_load", 0),
"max_capacity": hub.get("capacity", 0),
"utilization_percent": (hub.get("current_load", 0) / max(hub.get("capacity", 1), 1)) * 100,
})
return {"zones": zone_status, "total_zones": len(zone_status)}
async def _reschedule_route(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
if route_id not in self._active_routes:
return {"status": "error", "message": f"Route {route_id} not found"}
route = self._active_routes[route_id]
if task.data.get("new_pickup_time"):
route.estimated_pickup = datetime.fromisoformat(task.data["new_pickup_time"])
if task.data.get("new_delivery_time"):
route.estimated_delivery = datetime.fromisoformat(task.data["new_delivery_time"])
await self.send_message(
recipient="CUSTOMER_AGENT",
message_type=MessageType.NOTIFICATION_SENT,
payload={
"order_id": route.order_id,
"message_type": "schedule_changed",
"data": {
"new_pickup": route.estimated_pickup.isoformat(),
"new_delivery": route.estimated_delivery.isoformat(),
},
},
correlation_id=route_id,
)
return {
"status": "rescheduled",
"route_id": route_id,
"new_pickup": route.estimated_pickup.isoformat(),
"new_delivery": route.estimated_delivery.isoformat(),
}
async def _cancel_route(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
reason = task.data.get("reason", "Route cancelled")
if route_id not in self._active_routes:
return {"status": "error", "message": f"Route {route_id} not found"}
route = self._active_routes.pop(route_id)
await self.send_message(
recipient="FLEET_AGENT",
message_type=MessageType.AGENT_TASK,
payload={"task_type": "release_vehicle", "vehicle_id": route.vehicle_id},
)
return {"status": "cancelled", "route_id": route_id, "reason": reason}
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"[DISPATCH_AGENT reasoning]: {context}"

607
agents/exception_agent.py Normal file
View File

@@ -0,0 +1,607 @@
"""
Exception Agent — Miler stall detection + general exception handling.
Stall detection flow:
JetStream TRACKING/miler.location.updated (pull) -> Redis HSET -> stall check -> miler.stalled
JetStream TRACKING/miler.stalled (pull) -> POST reassign + POST notify
StallDetector sweep (every 60 s):
Postgres pickupbookings (active status) -> Redis movement.updated_at age -> miler.stalled
"""
import asyncio
import json
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Set
from dataclasses import dataclass, field
from enum import Enum
import asyncpg
import nats
import nats.errors
import redis.asyncio as aioredis
from core.agent import SpecializedAgent
from core.types import AgentTask, MessageType
from core.logger import logger
from core.http_client import api_post
from config.system_config import (
GO_API_BASE_URL, INTERNAL_API_KEY,
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD,
NATS_HOST, NATS_PORT, NATS_USER, NATS_PASSWORD,
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD,
)
STALL_MINUTES = 10
ACTIVE_STATUSES = ["Miler_Assigned", "Pickup_Scheduled"]
TRACKING_STREAM = "TRACKING"
class ExceptionType(str, Enum):
DELAY = "delay"
CANCELLATION = "cancellation"
RESCHEDULE = "reschedule"
REROUTE = "reroute"
FAILED_DELIVERY = "failed_delivery"
VEHICLE_BREAKDOWN = "vehicle_breakdown"
HUB_OVERFLOW = "hub_overflow"
WEATHER_IMPACT = "weather_impact"
CUSTOMER_UNAVAILABLE = "customer_unavailable"
MILER_STALLED = "miler_stalled"
class ExceptionSeverity(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class ExceptionRecord:
exception_id: str
order_id: str
exception_type: ExceptionType
severity: ExceptionSeverity
description: str
detected_at: datetime
resolved_at: Optional[datetime]
resolution: Optional[str]
assigned_to: Optional[str]
status: str
actions_taken: List[str] = field(default_factory=list)
class ExceptionAgent(SpecializedAgent):
"""
Handles miler stall detection via JetStream pull-subscribe + Postgres + Redis,
and all existing order exception types.
"""
def __init__(self):
super().__init__(
agent_id="EXCEPTION_AGENT",
domain="exception_handling",
description="Stall detection, exception resolution, reassignment",
)
self._nc: Optional[nats.aio.client.Client] = None
self._js = None
self._pg: Optional[asyncpg.Pool] = None
self._redis = aioredis.Redis(
host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD,
decode_responses=True,
)
self._location_sub = None
self._stalled_sub = None
self._notified_stalls: Set[str] = set()
self._exceptions: Dict[str, ExceptionRecord] = {}
self._strategies = self._init_strategies()
self._escalation_rules = self._init_escalation_rules()
# ── Startup ───────────────────────────────────────────────────────────────
async def start(self):
await self._connect_infra()
await self._setup_nats_pull_subs()
asyncio.create_task(self._pull_location_loop())
asyncio.create_task(self._pull_stalled_loop())
asyncio.create_task(self._stall_detector_loop())
await super().start()
async def _connect_infra(self):
try:
self._nc = await nats.connect(
servers=[f"nats://{NATS_HOST}:{NATS_PORT}"],
user=NATS_USER,
password=NATS_PASSWORD,
max_reconnect_attempts=10,
)
self._js = self._nc.jetstream()
logger.info("EXCEPTION_AGENT connected to NATS JetStream")
except Exception as e:
logger.warning(f"EXCEPTION_AGENT NATS connect failed: {e}")
try:
self._pg = await asyncpg.create_pool(
host=DB_HOST, port=DB_PORT, database=DB_NAME,
user=DB_USER, password=DB_PASSWORD,
min_size=1, max_size=5,
)
logger.info("EXCEPTION_AGENT connected to Postgres")
except Exception as e:
logger.warning(f"EXCEPTION_AGENT Postgres connect failed: {e}")
async def _setup_nats_pull_subs(self):
if not self._js:
logger.warning("EXCEPTION_AGENT: skipping NATS pull subs (no JetStream connection)")
return
try:
self._location_sub = await self._js.pull_subscribe(
"miler.location.updated",
"tracking_location_consumer",
stream=TRACKING_STREAM,
)
logger.info("EXCEPTION_AGENT pull-subscribed: miler.location.updated")
except Exception as e:
logger.warning(f"pull_subscribe miler.location.updated failed: {e}")
try:
self._stalled_sub = await self._js.pull_subscribe(
"miler.stalled",
"tracking_stall_consumer",
stream=TRACKING_STREAM,
)
logger.info("EXCEPTION_AGENT pull-subscribed: miler.stalled")
except Exception as e:
logger.warning(f"pull_subscribe miler.stalled failed: {e}")
# ── Pull loops ────────────────────────────────────────────────────────────
async def _pull_location_loop(self):
while self._running:
if not self._location_sub:
await asyncio.sleep(5)
continue
try:
msgs = await self._location_sub.fetch(batch=20, timeout=1.0)
for msg in msgs:
try:
await self._on_location_update(msg)
except Exception as e:
logger.error(f"Location handler error: {e}")
finally:
await msg.ack()
except nats.errors.TimeoutError:
pass
except Exception as e:
logger.error(f"Location pull loop error: {e}")
await asyncio.sleep(2)
async def _pull_stalled_loop(self):
while self._running:
if not self._stalled_sub:
await asyncio.sleep(5)
continue
try:
msgs = await self._stalled_sub.fetch(batch=10, timeout=1.0)
for msg in msgs:
try:
await self._on_miler_stalled(msg)
except Exception as e:
logger.error(f"Stalled handler error: {e}")
finally:
await msg.ack()
except nats.errors.TimeoutError:
pass
except Exception as e:
logger.error(f"Stalled pull loop error: {e}")
await asyncio.sleep(2)
# ── Stall detection: per location ping ───────────────────────────────────
async def _on_location_update(self, msg):
try:
data = json.loads(msg.data.decode())
except Exception:
return
miler_id = str(data.get("miler_id", ""))
lat = str(round(float(data.get("lat", 0)), 6))
lon = str(round(float(data.get("lon", 0)), 6))
now_ts = datetime.now().isoformat()
redis_key = f"miler:{miler_id}:movement"
prev = await self._redis.hgetall(redis_key)
prev_lat = prev.get("lat", "")
prev_lon = prev.get("lon", "")
if lat != prev_lat or lon != prev_lon:
await self._redis.hset(redis_key, mapping={
"lat": lat, "lon": lon,
"updated_at": now_ts,
"position_unchanged_since": now_ts,
})
else:
await self._redis.hset(redis_key, mapping={"lat": lat, "lon": lon, "updated_at": now_ts})
unchanged_since_str = prev.get("position_unchanged_since", now_ts)
try:
unchanged_since = datetime.fromisoformat(unchanged_since_str)
except ValueError:
unchanged_since = datetime.now()
minutes_stalled = (datetime.now() - unchanged_since).total_seconds() / 60
if minutes_stalled >= STALL_MINUTES:
booking = await self._get_active_booking(miler_id)
if booking:
booking_id = booking["booking_id"]
if booking_id not in self._notified_stalls:
await self._publish_stall(miler_id, booking_id, minutes_stalled)
# ── Stall detection: background sweep ────────────────────────────────────
async def _stall_detector_loop(self):
while self._running:
await asyncio.sleep(60)
try:
await self._sweep_active_bookings()
except Exception as e:
logger.error(f"StallDetector error: {e}")
async def _sweep_active_bookings(self):
if not self._pg:
return
logger.debug("StallDetector: sweeping active bookings")
try:
async with self._pg.acquire() as conn:
rows = await conn.fetch(
"""
SELECT DISTINCT
assignedmileruserid::text AS miler_id,
bookingid::text AS booking_id
FROM pickupbookings
WHERE status = ANY($1)
""",
ACTIVE_STATUSES,
)
except Exception as e:
logger.error(f"StallDetector Postgres error: {e}")
return
now = datetime.now()
for row in rows:
miler_id = row["miler_id"]
booking_id = row["booking_id"]
if booking_id in self._notified_stalls:
continue
movement = await self._redis.hgetall(f"miler:{miler_id}:movement")
if not movement or "updated_at" not in movement:
continue
try:
updated_at = datetime.fromisoformat(movement["updated_at"])
minutes_stale = (now - updated_at).total_seconds() / 60
except ValueError:
continue
if minutes_stale >= STALL_MINUTES:
logger.warning(f"StallDetector: miler {miler_id} stale {minutes_stale:.1f} min (booking {booking_id})")
await self._publish_stall(miler_id, booking_id, minutes_stale)
# ── Stall: publish + handle ───────────────────────────────────────────────
async def _publish_stall(self, miler_id: str, booking_id: str, minutes_stalled: float):
self._notified_stalls.add(booking_id)
payload = json.dumps({
"miler_id": miler_id,
"booking_id": booking_id,
"minutes_stalled": round(minutes_stalled, 1),
}).encode()
try:
await self._js.publish("miler.stalled", payload)
logger.info(f"miler.stalled published: miler={miler_id} booking={booking_id} ({minutes_stalled:.1f} min)")
except Exception as e:
logger.error(f"Failed to publish miler.stalled: {e}")
async def _on_miler_stalled(self, msg):
"""Reassign booking and notify customer via Go API."""
try:
data = json.loads(msg.data.decode())
except Exception:
return
miler_id = data.get("miler_id", "")
booking_id = data.get("booking_id", "")
minutes_stalled = data.get("minutes_stalled", 0)
logger.warning(f"EXCEPTION_AGENT stall: miler={miler_id} booking={booking_id} ({minutes_stalled} min)")
headers = {"X-Internal-Key": INTERNAL_API_KEY}
reassign_result = await api_post(
f"{GO_API_BASE_URL}/api/v1/internal/bookings/{booking_id}/reassign",
json={"reason": "miler_stalled"},
headers=headers,
)
logger.info(f"Reassign {'OK' if reassign_result is not None else 'FAILED'} for booking {booking_id}")
notify_result = await api_post(
f"{GO_API_BASE_URL}/api/v1/internal/notify",
json={
"booking_id": booking_id,
"message": "We detected a delay, finding you a new miler",
"target": "customer",
},
headers=headers,
)
logger.info(f"Notify {'OK' if notify_result is not None else 'FAILED'} for booking {booking_id}")
exc_id = f"EXC-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6].upper()}"
self._exceptions[exc_id] = ExceptionRecord(
exception_id=exc_id, order_id=booking_id,
exception_type=ExceptionType.MILER_STALLED,
severity=ExceptionSeverity.HIGH,
description=f"Miler {miler_id} stalled {minutes_stalled} min",
detected_at=datetime.now(), resolved_at=datetime.now(),
resolution="Reassignment triggered + customer notified",
assigned_to=None, status="resolved",
actions_taken=["reassign", "notify_customer"],
)
# ── Postgres helper ───────────────────────────────────────────────────────
async def _get_active_booking(self, miler_id: str) -> Optional[Dict]:
if not self._pg:
return None
try:
async with self._pg.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT bookingid::text AS booking_id, status
FROM pickupbookings
WHERE assignedmileruserid::text = $1
AND status = ANY($2)
ORDER BY createdat DESC
LIMIT 1
""",
miler_id,
ACTIVE_STATUSES,
)
if row:
return {"booking_id": row["booking_id"], "status": row["status"]}
except Exception as e:
logger.error(f"Postgres get_active_booking error: {e}")
return None
# ── Task handler ──────────────────────────────────────────────────────────
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
handlers = {
"detect_exception": self._detect_exception,
"analyze_exception": self._analyze_exception,
"resolve_exception": self._resolve_exception,
"cancel_order": self._cancel_order,
"reschedule_delivery": self._reschedule_delivery,
"handle_delay": self._handle_delay,
"get_exception_status": self._get_exception_status,
"get_exception_history": self._get_exception_history,
"escalate_exception": self._escalate_exception,
}
handler = handlers.get(task.task_type, self._unknown_task)
return await handler(task)
async def _detect_exception(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
exception_type = task.data.get("exception_type")
severity = task.data.get("severity", "medium")
description = task.data.get("description", "")
logger.warning(f"Exception Agent: {exception_type} for {order_id} (severity={severity})")
exc_id = f"EXC-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6].upper()}"
record = ExceptionRecord(
exception_id=exc_id, order_id=order_id,
exception_type=ExceptionType(exception_type),
severity=ExceptionSeverity(severity), description=description,
detected_at=datetime.now(), resolved_at=None,
resolution=None, assigned_to=None, status="detected",
)
self._exceptions[exc_id] = record
if severity in ("high", "critical"):
await self._analyze_exception(AgentTask(
task_id=f"{exc_id}_analyze", agent_type="exception",
task_type="analyze_exception", data={"exception_id": exc_id},
))
await self.send_message(
recipient="JARVIS", message_type=MessageType.EXCEPTION_DETECTED,
payload={"exception_id": exc_id, "order_id": order_id,
"exception_type": exception_type, "severity": severity},
)
return {"status": "detected", "exception_id": exc_id,
"exception_type": exception_type, "severity": severity}
async def _analyze_exception(self, task: AgentTask) -> Dict[str, Any]:
exc_id = task.data.get("exception_id")
if exc_id not in self._exceptions:
return {"status": "error", "message": f"Exception {exc_id} not found"}
record = self._exceptions[exc_id]
record.status = "analyzing"
strategy = self._strategies.get(record.exception_type, {})
auto_actions = strategy.get("auto_actions", [])
record.actions_taken.append("analyzed")
return {"status": "analyzed", "exception_id": exc_id, "strategy": auto_actions}
async def _resolve_exception(self, task: AgentTask) -> Dict[str, Any]:
exc_id = task.data.get("exception_id")
if exc_id not in self._exceptions:
return {"status": "error", "message": f"Exception {exc_id} not found"}
record = self._exceptions[exc_id]
record.status = "resolving"
for action in self._strategies.get(record.exception_type, {}).get("auto_actions", []):
record.actions_taken.append(action)
record.status = "resolved"
record.resolved_at = datetime.now()
record.resolution = f"Completed {len(record.actions_taken)} actions"
await self.send_message(
recipient="CUSTOMER_AGENT", message_type=MessageType.NOTIFICATION_SENT,
payload={"order_id": record.order_id, "message_type": "exception_resolved",
"data": {"exception_type": record.exception_type.value}},
)
return {"status": "resolved", "exception_id": exc_id, "resolution": record.resolution}
async def _cancel_order(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
reason = task.data.get("reason", "Customer requested cancellation")
exc_id = f"EXC-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6].upper()}"
self._exceptions[exc_id] = ExceptionRecord(
exception_id=exc_id, order_id=order_id,
exception_type=ExceptionType.CANCELLATION, severity=ExceptionSeverity.MEDIUM,
description=reason, detected_at=datetime.now(), resolved_at=datetime.now(),
resolution="Order cancelled", assigned_to=None, status="resolved",
actions_taken=["release_vehicle", "notify_customer"],
)
await self.send_message(
recipient="FLEET_AGENT", message_type=MessageType.AGENT_TASK,
payload={"task_type": "release_vehicle_for_cancel", "order_id": order_id},
)
await self.send_message(
recipient="CUSTOMER_AGENT", message_type=MessageType.ORDER_CANCELLED,
payload={"order_id": order_id, "reason": reason,
"refund_info": "Refund within 5-7 business days"},
)
return {"status": "cancelled", "exception_id": exc_id, "order_id": order_id}
async def _reschedule_delivery(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
new_date = task.data.get("new_date")
reason = task.data.get("reason", "Operational constraints")
exc_id = f"EXC-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6].upper()}"
self._exceptions[exc_id] = ExceptionRecord(
exception_id=exc_id, order_id=order_id,
exception_type=ExceptionType.RESCHEDULE, severity=ExceptionSeverity.MEDIUM,
description=f"Rescheduled to {new_date}: {reason}",
detected_at=datetime.now(), resolved_at=None, resolution=None,
assigned_to=None, status="resolving",
)
await self.send_message(
recipient="DISPATCH_AGENT", message_type=MessageType.AGENT_TASK,
payload={"task_type": "reschedule_route", "order_id": order_id, "new_date": new_date},
)
await self.send_message(
recipient="CUSTOMER_AGENT", message_type=MessageType.NOTIFICATION_SENT,
payload={"order_id": order_id, "message_type": "rescheduled",
"data": {"new_date": new_date, "reason": reason}},
)
return {"status": "rescheduled", "exception_id": exc_id, "order_id": order_id, "new_date": new_date}
async def _handle_delay(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
delay_reason = task.data.get("reason", "Unknown")
new_eta = task.data.get("new_eta")
severity = task.data.get("severity", "medium")
exc_id = f"EXC-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6].upper()}"
self._exceptions[exc_id] = ExceptionRecord(
exception_id=exc_id, order_id=order_id,
exception_type=ExceptionType.DELAY, severity=ExceptionSeverity(severity),
description=f"Delay: {delay_reason}", detected_at=datetime.now(),
resolved_at=None, resolution=None, assigned_to=None, status="resolving",
)
if new_eta:
await self.send_message(
recipient="DISPATCH_AGENT", message_type=MessageType.ORDER_DELAYED,
payload={"order_id": order_id, "new_eta": new_eta, "reason": delay_reason},
)
await self.send_message(
recipient="CUSTOMER_AGENT", message_type=MessageType.NOTIFICATION_SENT,
payload={"order_id": order_id, "message_type": "delayed",
"data": {"delay_reason": delay_reason, "new_eta": new_eta}},
)
return {"status": "delay_handled", "exception_id": exc_id,
"order_id": order_id, "new_eta": new_eta, "customer_notified": True}
async def _get_exception_status(self, task: AgentTask) -> Dict[str, Any]:
exc_id = task.data.get("exception_id")
if exc_id not in self._exceptions:
return {"status": "error", "message": f"Exception {exc_id} not found"}
r = self._exceptions[exc_id]
return {
"exception_id": exc_id, "order_id": r.order_id,
"type": r.exception_type.value, "severity": r.severity.value,
"status": r.status, "description": r.description,
"detected_at": r.detected_at.isoformat(),
"resolved_at": r.resolved_at.isoformat() if r.resolved_at else None,
"actions_taken": r.actions_taken,
}
async def _get_exception_history(self, task: AgentTask) -> Dict[str, Any]:
exc_type = task.data.get("exception_type")
status_f = task.data.get("status")
items = list(self._exceptions.values())
if exc_type:
items = [e for e in items if e.exception_type.value == exc_type]
if status_f:
items = [e for e in items if e.status == status_f]
return {
"total_exceptions": len(items),
"exceptions": [
{"exception_id": e.exception_id, "order_id": e.order_id,
"type": e.exception_type.value, "severity": e.severity.value,
"status": e.status, "detected_at": e.detected_at.isoformat()}
for e in items[-50:]
],
}
async def _escalate_exception(self, task: AgentTask) -> Dict[str, Any]:
exc_id = task.data.get("exception_id")
if exc_id not in self._exceptions:
return {"status": "error", "message": f"Exception {exc_id} not found"}
record = self._exceptions[exc_id]
escalate_to = self._escalation_rules.get(record.exception_type, {}).get("escalate_to", "JARVIS")
record.actions_taken.append(f"escalated_to_{escalate_to}")
await self.send_message(
recipient=escalate_to, message_type=MessageType.EXCEPTION_DETECTED,
payload={"exception_id": exc_id, "order_id": record.order_id,
"exception_type": record.exception_type.value,
"severity": record.severity.value, "urgency": "high"},
)
return {"status": "escalated", "exception_id": exc_id, "escalated_to": escalate_to}
async def _unknown_task(self, task: AgentTask) -> Dict[str, Any]:
return {"status": "error", "message": f"Unknown task: {task.task_type}"}
def _init_strategies(self) -> Dict[str, Any]:
return {
ExceptionType.DELAY: {"auto_actions": ["notify_customer", "update_eta", "reroute_if_possible"]},
ExceptionType.CANCELLATION: {"auto_actions": ["release_vehicle", "refund_check", "notify_customer"]},
ExceptionType.RESCHEDULE: {"auto_actions": ["find_new_slot", "update_route", "notify_customer"]},
ExceptionType.REROUTE: {"auto_actions": ["recalculate_route", "update_vehicle", "notify_customer"]},
ExceptionType.FAILED_DELIVERY: {"auto_actions": ["retry_attempt", "return_to_hub"]},
ExceptionType.VEHICLE_BREAKDOWN: {"auto_actions": ["find_replacement_vehicle", "reassign_route", "notify_customer"]},
ExceptionType.HUB_OVERFLOW: {"auto_actions": ["reroute_to_alternate", "increase_processing"]},
ExceptionType.WEATHER_IMPACT: {"auto_actions": ["delay_notification", "route_avoidance"]},
ExceptionType.MILER_STALLED: {"auto_actions": ["reassign", "notify_customer"]},
}
def _init_escalation_rules(self) -> Dict[str, Any]:
return {
ExceptionType.VEHICLE_BREAKDOWN: {"escalate_to": "FLEET_AGENT"},
ExceptionType.HUB_OVERFLOW: {"escalate_to": "HUB_AGENT"},
ExceptionType.FAILED_DELIVERY: {"escalate_to": "DISPATCH_AGENT"},
ExceptionType.DELAY: {"escalate_to": "JARVIS"},
ExceptionType.MILER_STALLED: {"escalate_to": "JARVIS"},
}
async def think(self, context: str, options: List[str] = None) -> str:
return f"[EXCEPTION_AGENT reasoning]: {context}"

332
agents/fleet_agent.py Normal file
View File

@@ -0,0 +1,332 @@
"""Fleet Agent - Manages vehicles, capacity, and real-time tracking."""
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from enum import Enum
from core.agent import SpecializedAgent
from core.types import AgentTask, MessageType, Vehicle
from core.logger import logger
class VehicleStatus(str, Enum):
AVAILABLE = "available"
ASSIGNED = "assigned"
IN_TRANSIT = "in_transit"
AT_HUB = "at_hub"
MAINTENANCE = "maintenance"
OFFLINE = "offline"
@dataclass
class VehicleAssignment:
assignment_id: str
vehicle_id: str
route_id: str
order_ids: List[str]
assigned_at: datetime
estimated_return: datetime
current_location: Dict[str, float]
status: str = "active"
class FleetAgent(SpecializedAgent):
"""Fleet Agent - Manages vehicle fleet and logistics operations."""
def __init__(self):
super().__init__(
agent_id="FLEET_AGENT",
domain="fleet_management",
description="Manages vehicle fleet, capacity, and real-time tracking"
)
self._vehicles = self._init_fleet()
self._assignments: Dict[str, VehicleAssignment] = {}
self._maintenance_schedule: Dict[str, datetime] = {}
self._hub_capacity: Dict[str, Dict] = {}
# BUG FIX: hub_capacity init was dead code inside _init_fleet (after return).
# Moved here so it actually runs after _vehicles is populated.
for vehicle in self._vehicles.values():
hub = vehicle["hub"]
if hub not in self._hub_capacity:
self._hub_capacity[hub] = {"available": 0, "total": 0}
self._hub_capacity[hub]["total"] += 1
if vehicle["status"] == "available":
self._hub_capacity[hub]["available"] += 1
def _init_fleet(self) -> Dict[str, Dict]:
return {
# Delhi
"DL-V-001": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "DL-HUB-01", "location": {"lat": 28.6139, "lng": 77.2090}},
"DL-V-002": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "DL-HUB-01", "location": {"lat": 28.6139, "lng": 77.2090}},
"DL-V-003": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "DL-HUB-02", "location": {"lat": 28.5355, "lng": 77.2100}},
"DL-B-001": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "DL-HUB-01", "location": {"lat": 28.6139, "lng": 77.2090}},
"DL-B-002": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "DL-HUB-02", "location": {"lat": 28.5355, "lng": 77.2100}},
# Mumbai
"MU-V-001": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "MU-HUB-01", "location": {"lat": 19.0760, "lng": 72.8777}},
"MU-V-002": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "MU-HUB-01", "location": {"lat": 19.0760, "lng": 72.8777}},
"MU-V-003": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "MU-HUB-02", "location": {"lat": 19.1650, "lng": 72.8500}},
"MU-B-001": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "MU-HUB-02", "location": {"lat": 19.1650, "lng": 72.8500}},
"MU-T-001": {"type": "truck", "capacity_kg": 2000, "capacity_vol": 25, "status": "available", "hub": "MU-HUB-01", "location": {"lat": 19.0760, "lng": 72.8777}},
# Bangalore
"BL-V-001": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "BL-HUB-01", "location": {"lat": 12.9716, "lng": 77.5946}},
"BL-B-001": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "BL-HUB-01", "location": {"lat": 12.9716, "lng": 77.5946}},
"BL-B-002": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "BL-HUB-01", "location": {"lat": 12.9716, "lng": 77.5946}},
# Hyderabad
"HY-V-001": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "HY-HUB-01", "location": {"lat": 17.3850, "lng": 78.4867}},
"HY-T-001": {"type": "truck", "capacity_kg": 2000, "capacity_vol": 25, "status": "available", "hub": "HY-HUB-01", "location": {"lat": 17.3850, "lng": 78.4867}},
# Pune
"PU-V-001": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "PU-HUB-01", "location": {"lat": 18.5204, "lng": 73.8567}},
"PU-B-001": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "PU-HUB-01", "location": {"lat": 18.5204, "lng": 73.8567}},
# Kolkata
"KL-V-001": {"type": "van", "capacity_kg": 500, "capacity_vol": 8, "status": "available", "hub": "KL-HUB-01", "location": {"lat": 22.5726, "lng": 88.3639}},
"KL-B-001": {"type": "bike", "capacity_kg": 15, "capacity_vol": 0.5, "status": "available", "hub": "KL-HUB-01", "location": {"lat": 22.5726, "lng": 88.3639}},
}
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
handlers = {
"assign_vehicle": self._assign_vehicle,
"release_vehicle": self._release_vehicle,
"get_availability": self._get_availability,
"track_vehicle": self._track_vehicle,
"update_location": self._update_location,
"schedule_maintenance": self._schedule_maintenance,
"get_fleet_status": self._get_fleet_status,
"optimize_allocation": self._optimize_allocation,
}
handler = handlers.get(task.task_type, self._unknown_task)
return await handler(task)
async def _assign_vehicle(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
route_type = task.data.get("route_type", "last_mile")
from_hub = task.data.get("from_hub")
order_weight = task.data.get("order_weight", 0)
logger.info(f"Fleet Agent: Assigning vehicle for route {route_id}")
suitable_vehicles = self._find_suitable_vehicles(from_hub, order_weight, route_type)
if not suitable_vehicles:
suitable_vehicles = self._find_nearest_available_vehicles(from_hub, order_weight)
if not suitable_vehicles:
logger.warning(f"No suitable vehicles available for route {route_id}")
return {"status": "no_vehicle_available", "route_id": route_id, "message": "No suitable vehicles available"}
vehicle_id = suitable_vehicles[0]
vehicle = self._vehicles[vehicle_id]
vehicle["status"] = "assigned"
if vehicle["hub"] in self._hub_capacity:
self._hub_capacity[vehicle["hub"]]["available"] -= 1
assignment_id = f"ASN-{uuid.uuid4().hex[:8].upper()}"
assignment = VehicleAssignment(
assignment_id=assignment_id,
vehicle_id=vehicle_id,
route_id=route_id,
order_ids=[task.data.get("order_id", "")],
assigned_at=datetime.now(),
estimated_return=datetime.now() + timedelta(hours=2),
current_location=vehicle["location"],
status="active"
)
self._assignments[assignment_id] = assignment
logger.info(f"Vehicle {vehicle_id} ({vehicle['type']}) assigned to route {route_id}")
await self.send_message(
recipient="DISPATCH_AGENT",
message_type=MessageType.VEHICLE_ASSIGNED,
payload={"route_id": route_id, "vehicle_id": vehicle_id, "assignment_id": assignment_id},
)
return {
"status": "assigned",
"assignment_id": assignment_id,
"vehicle_id": vehicle_id,
"vehicle_type": vehicle["type"],
"capacity_kg": vehicle["capacity_kg"],
"capacity_vol": vehicle["capacity_vol"],
"current_location": vehicle["location"],
}
async def _release_vehicle(self, task: AgentTask) -> Dict[str, Any]:
vehicle_id = task.data.get("vehicle_id")
if vehicle_id not in self._vehicles:
return {"status": "error", "message": f"Vehicle {vehicle_id} not found"}
vehicle = self._vehicles[vehicle_id]
vehicle["status"] = "available"
if vehicle["hub"] in self._hub_capacity:
self._hub_capacity[vehicle["hub"]]["available"] += 1
logger.info(f"Vehicle {vehicle_id} released")
return {"status": "released", "vehicle_id": vehicle_id, "hub": vehicle["hub"]}
async def _get_availability(self, task: AgentTask) -> Dict[str, Any]:
hub_id = task.data.get("hub_id")
vehicle_type = task.data.get("vehicle_type")
available = []
for vid, vehicle in self._vehicles.items():
if vehicle["hub"] == hub_id and vehicle["status"] == "available":
if vehicle_type is None or vehicle["type"] == vehicle_type:
available.append({
"vehicle_id": vid,
"type": vehicle["type"],
"capacity_kg": vehicle["capacity_kg"],
"capacity_vol": vehicle["capacity_vol"],
})
return {"hub_id": hub_id, "available_vehicles": available, "total_available": len(available)}
async def _track_vehicle(self, task: AgentTask) -> Dict[str, Any]:
vehicle_id = task.data.get("vehicle_id")
if vehicle_id not in self._vehicles:
return {"status": "error", "message": f"Vehicle {vehicle_id} not found"}
vehicle = self._vehicles[vehicle_id]
active_assignment = next(
(a for a in self._assignments.values() if a.vehicle_id == vehicle_id and a.status == "active"),
None,
)
return {
"vehicle_id": vehicle_id,
"type": vehicle["type"],
"status": vehicle["status"],
"location": vehicle["location"],
"hub": vehicle["hub"],
"current_assignment": {
"assignment_id": active_assignment.assignment_id,
"route_id": active_assignment.route_id,
"estimated_return": active_assignment.estimated_return.isoformat(),
} if active_assignment else None,
}
async def _update_location(self, task: AgentTask) -> Dict[str, Any]:
vehicle_id = task.data.get("vehicle_id")
new_location = task.data.get("location")
if vehicle_id not in self._vehicles:
return {"status": "error", "message": f"Vehicle {vehicle_id} not found"}
self._vehicles[vehicle_id]["location"] = new_location
for assignment in self._assignments.values():
if assignment.vehicle_id == vehicle_id and assignment.status == "active":
assignment.current_location = new_location
break
return {"status": "updated", "vehicle_id": vehicle_id, "new_location": new_location}
async def _schedule_maintenance(self, task: AgentTask) -> Dict[str, Any]:
vehicle_id = task.data.get("vehicle_id")
maintenance_date = task.data.get("date")
if vehicle_id not in self._vehicles:
return {"status": "error", "message": f"Vehicle {vehicle_id} not found"}
if maintenance_date:
self._maintenance_schedule[vehicle_id] = datetime.fromisoformat(maintenance_date)
else:
self._maintenance_schedule[vehicle_id] = datetime.now() + timedelta(days=3)
self._vehicles[vehicle_id]["status"] = "maintenance"
return {
"status": "scheduled",
"vehicle_id": vehicle_id,
"maintenance_date": self._maintenance_schedule[vehicle_id].isoformat(),
}
async def _get_fleet_status(self, task: AgentTask) -> Dict[str, Any]:
status_summary = {
"total_vehicles": len(self._vehicles),
"by_type": {},
"by_status": {},
"by_hub": {},
"vehicles": [],
}
for vehicle_id, vehicle in self._vehicles.items():
vtype = vehicle["type"]
vstatus = vehicle["status"]
hub = vehicle["hub"]
status_summary["by_type"][vtype] = status_summary["by_type"].get(vtype, 0) + 1
status_summary["by_status"][vstatus] = status_summary["by_status"].get(vstatus, 0) + 1
if hub not in status_summary["by_hub"]:
status_summary["by_hub"][hub] = {"total": 0, "available": 0}
status_summary["by_hub"][hub]["total"] += 1
if vstatus == "available":
status_summary["by_hub"][hub]["available"] += 1
status_summary["vehicles"].append({
"vehicle_id": vehicle_id,
"type": vtype,
"status": vstatus,
"hub": hub,
"location": vehicle["location"],
})
return status_summary
async def _optimize_allocation(self, task: AgentTask) -> Dict[str, Any]:
logger.info("Fleet Agent: Optimizing vehicle allocation")
recommendations = []
for hub_id, capacity in self._hub_capacity.items():
utilization = (capacity["total"] - capacity["available"]) / capacity["total"] if capacity["total"] > 0 else 0
if utilization < 0.3:
recommendations.append({
"action": "redistribute",
"from_hub": hub_id,
"reason": f"Low utilization ({utilization*100:.0f}%)",
"vehicles_to_move": 1,
})
elif utilization > 0.9:
recommendations.append({
"action": "request_reinforcement",
"to_hub": hub_id,
"reason": f"High utilization ({utilization*100:.0f}%)",
"vehicles_needed": 2,
})
return {"status": "optimized", "recommendations": recommendations}
async def _unknown_task(self, task: AgentTask) -> Dict[str, Any]:
return {"status": "error", "message": f"Unknown task: {task.task_type}"}
def _find_suitable_vehicles(self, hub_id: str, weight_kg: float, route_type: str) -> List[str]:
suitable = []
for vehicle_id, vehicle in self._vehicles.items():
if vehicle["hub"] != hub_id:
continue
if vehicle["status"] != "available":
continue
if vehicle["capacity_kg"] < weight_kg:
continue
if route_type == "last_mile" and vehicle["type"] in ["van", "truck"]:
continue
suitable.append(vehicle_id)
return suitable
def _find_nearest_available_vehicles(self, hub_id: str, weight_kg: float) -> List[str]:
available = [
vid for vid, v in self._vehicles.items()
if v["status"] == "available" and v["capacity_kg"] >= weight_kg
]
return available[:1]
async def think(self, context: str, options: List[str] = None) -> str:
return f"[FLEET_AGENT reasoning]: {context}"

460
agents/hub_agent.py Normal file
View File

@@ -0,0 +1,460 @@
"""Hub Agent - Manages hub operations, transit, and capacity."""
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from collections import deque
from core.agent import SpecializedAgent
from core.types import AgentTask, MessageType, Hub
from core.logger import logger
@dataclass
class TransitRecord:
record_id: str
order_id: str
entry_time: datetime
expected_exit: datetime
actual_exit: Optional[datetime]
destination: str
status: str # waiting, processing, in_transit, delivered
items_count: int
@dataclass
class HubInventory:
hub_id: str
current_load: int
capacity: int
incoming_transits: int
outgoing_transits: int
pending_processing: int
class HubAgent(SpecializedAgent):
"""Hub Agent - Manages hub operations and transit flow."""
def __init__(self):
super().__init__(
agent_id="HUB_AGENT",
domain="hub_management",
description="Manages hub operations, transit flow, and capacity"
)
self._hubs = self._init_hubs()
self._in_transit: Dict[str, List[TransitRecord]] = {h: [] for h in self._hubs.keys()}
self._processing_queues: Dict[str, deque] = {h: deque() for h in self._hubs.keys()}
self._metrics: Dict[str, Dict] = {}
# BUG FIX: metrics init was dead code inside _init_hubs (after return).
# Moved here so it actually runs after _hubs is populated.
for hub_id in self._hubs.keys():
self._metrics[hub_id] = {
"processed_today": 0,
"average_processing_time": 15,
"efficiency": 95.0,
"bottlenecks": [],
}
def _init_hubs(self) -> Dict[str, Dict]:
return {
"DL-HUB-01": {
"name": "Delhi North Hub",
"location": {"lat": 28.6139, "lng": 77.2090},
"address": "Sector 12, Narela, Delhi",
"capacity": 500, "current_load": 150,
"spokes": ["DL-SP-01", "DL-SP-02", "DL-SP-03"],
"connected_hubs": ["DL-HUB-02", "MU-HUB-01", "KL-HUB-01"],
"processing_rate": 100, "operating_hours": "24/7",
},
"DL-HUB-02": {
"name": "Delhi South Hub",
"location": {"lat": 28.5355, "lng": 77.2100},
"address": "Nehru Place, Delhi",
"capacity": 400, "current_load": 200,
"spokes": ["DL-SP-04", "DL-SP-05"],
"connected_hubs": ["DL-HUB-01", "BL-HUB-01"],
"processing_rate": 80, "operating_hours": "24/7",
},
"MU-HUB-01": {
"name": "Mumbai West Hub",
"location": {"lat": 19.0760, "lng": 72.8777},
"address": "Andheri West, Mumbai",
"capacity": 600, "current_load": 300,
"spokes": ["MU-SP-01", "MU-SP-02", "MU-SP-03", "MU-SP-04"],
"connected_hubs": ["MU-HUB-02", "DL-HUB-01", "PU-HUB-01"],
"processing_rate": 120, "operating_hours": "24/7",
},
"MU-HUB-02": {
"name": "Mumbai East Hub",
"location": {"lat": 19.1650, "lng": 72.8500},
"address": "Thane, Mumbai",
"capacity": 550, "current_load": 250,
"spokes": ["MU-SP-05", "MU-SP-06"],
"connected_hubs": ["MU-HUB-01", "PU-HUB-01"],
"processing_rate": 90, "operating_hours": "24/7",
},
"BL-HUB-01": {
"name": "Bangalore Hub",
"location": {"lat": 12.9716, "lng": 77.5946},
"address": "Whitefield, Bangalore",
"capacity": 500, "current_load": 180,
"spokes": ["BL-SP-01", "BL-SP-02", "BL-SP-03"],
"connected_hubs": ["HY-HUB-01", "DL-HUB-02"],
"processing_rate": 100, "operating_hours": "24/7",
},
"HY-HUB-01": {
"name": "Hyderabad Hub",
"location": {"lat": 17.3850, "lng": 78.4867},
"address": "Hi-Tech City, Hyderabad",
"capacity": 450, "current_load": 220,
"spokes": ["HY-SP-01", "HY-SP-02"],
"connected_hubs": ["BL-HUB-01", "KL-HUB-01"],
"processing_rate": 85, "operating_hours": "24/7",
},
"PU-HUB-01": {
"name": "Pune Hub",
"location": {"lat": 18.5204, "lng": 73.8567},
"address": "Hinjewadi, Pune",
"capacity": 400, "current_load": 160,
"spokes": ["PU-SP-01", "PU-SP-02"],
"connected_hubs": ["MU-HUB-01", "MU-HUB-02"],
"processing_rate": 75, "operating_hours": "24/7",
},
"KL-HUB-01": {
"name": "Kolkata Hub",
"location": {"lat": 22.5726, "lng": 88.3639},
"address": "Salt Lake, Kolkata",
"capacity": 350, "current_load": 140,
"spokes": ["KL-SP-01", "KL-SP-02"],
"connected_hubs": ["DL-HUB-01", "HY-HUB-01"],
"processing_rate": 70, "operating_hours": "24/7",
},
}
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
handlers = {
"prepare_receiving": self._prepare_receiving,
"process_incoming": self._process_incoming,
"dispatch_outgoing": self._dispatch_outgoing,
"get_hub_status": self._get_hub_status,
"transfer_between_hubs": self._transfer_between_hubs,
"get_transit_status": self._get_transit_status,
"optimize_hub_operations": self._optimize_hub_operations,
"escalate_overflow": self._escalate_overflow,
}
handler = handlers.get(task.task_type, self._unknown_task)
return await handler(task)
async def _prepare_receiving(self, task: AgentTask) -> Dict[str, Any]:
hub_id = task.data.get("hub_id")
order_id = task.data.get("order_id")
route_id = task.data.get("route_id")
if hub_id not in self._hubs:
return {"status": "error", "message": f"Hub {hub_id} not found"}
hub = self._hubs[hub_id]
available_capacity = hub["capacity"] - hub["current_load"]
logger.info(f"Hub Agent: Preparing {hub['name']} to receive order {order_id}")
if available_capacity < 10:
await self.send_message(
recipient="JARVIS",
message_type=MessageType.EXCEPTION_DETECTED,
payload={
"type": "capacity_warning",
"hub_id": hub_id,
"current_load": hub["current_load"],
"capacity": hub["capacity"],
"order_id": order_id,
},
)
logger.warning(f"Hub {hub_id} near capacity: {hub['current_load']}/{hub['capacity']}")
self._processing_queues[hub_id].append({
"order_id": order_id,
"route_id": route_id,
"arrival_time": datetime.now(),
"priority": "normal",
})
return {
"status": "prepared",
"hub_id": hub_id,
"hub_name": hub["name"],
"queue_position": len(self._processing_queues[hub_id]),
"estimated_wait": len(self._processing_queues[hub_id]) * 5,
}
async def _process_incoming(self, task: AgentTask) -> Dict[str, Any]:
hub_id = task.data.get("hub_id")
order_id = task.data.get("order_id")
if hub_id not in self._hubs:
return {"status": "error", "message": f"Hub {hub_id} not found"}
hub = self._hubs[hub_id]
hub["current_load"] += 1
record_id = f"TR-{hub_id}-{uuid.uuid4().hex[:6].upper()}"
record = TransitRecord(
record_id=record_id,
order_id=order_id,
entry_time=datetime.now(),
expected_exit=datetime.now() + timedelta(minutes=15), # 15 min avg processing
actual_exit=None,
destination=task.data.get("destination", "unknown"),
status="processing",
items_count=task.data.get("items_count", 1),
)
self._in_transit[hub_id].append(record)
self._metrics[hub_id]["processed_today"] += 1
logger.info(f"Order {order_id} received at {hub['name']} | load={hub['current_load']}/{hub['capacity']}")
await self.send_message(
recipient="CUSTOMER_AGENT",
message_type=MessageType.HUB_STATUS_UPDATE,
payload={
"order_id": order_id,
"hub_id": hub_id,
"hub_name": hub["name"],
"status": "arrived_at_hub",
"arrival_time": datetime.now().isoformat(),
},
)
return {"status": "processed", "record_id": record_id, "hub_id": hub_id, "current_load": hub["current_load"]}
async def _dispatch_outgoing(self, task: AgentTask) -> Dict[str, Any]:
hub_id = task.data.get("hub_id")
order_id = task.data.get("order_id")
if hub_id not in self._hubs:
return {"status": "error", "message": f"Hub {hub_id} not found"}
hub = self._hubs[hub_id]
record = None
for r in self._in_transit[hub_id]:
if r.order_id == order_id and r.status in ["processing", "waiting"]:
r.actual_exit = datetime.now()
r.status = "in_transit"
record = r
break
if record:
hub["current_load"] -= 1
logger.info(f"Order {order_id} dispatched from {hub['name']}")
return {
"status": "dispatched",
"record_id": record.record_id,
"hub_id": hub_id,
"exit_time": record.actual_exit.isoformat(),
"destination": record.destination,
}
return {"status": "error", "message": "Transit record not found"}
async def _get_hub_status(self, task: AgentTask) -> Dict[str, Any]:
hub_id = task.data.get("hub_id")
if hub_id:
if hub_id not in self._hubs:
return {"status": "error", "message": f"Hub {hub_id} not found"}
return self._format_hub_status(hub_id)
return {
"hubs": {hid: self._format_hub_status(hid) for hid in self._hubs.keys()},
"total_hubs": len(self._hubs),
}
def _format_hub_status(self, hub_id: str) -> Dict[str, Any]:
hub = self._hubs[hub_id]
metrics = self._metrics[hub_id]
utilization = (hub["current_load"] / hub["capacity"]) * 100 if hub["capacity"] > 0 else 0
return {
"hub_id": hub_id,
"name": hub["name"],
"location": hub["location"],
"capacity": hub["capacity"],
"current_load": hub["current_load"],
"utilization_percent": round(utilization, 1),
"spokes": hub["spokes"],
"connected_hubs": hub["connected_hubs"],
"processing_queue": len(self._processing_queues[hub_id]),
"in_transit": len([r for r in self._in_transit[hub_id] if r.status == "in_transit"]),
"metrics": {
"processed_today": metrics["processed_today"],
"avg_processing_time": metrics["average_processing_time"],
"efficiency": metrics["efficiency"],
},
"status": "operational" if utilization < 90 else "high_load",
}
async def _transfer_between_hubs(self, task: AgentTask) -> Dict[str, Any]:
from_hub = task.data.get("from_hub")
to_hub = task.data.get("to_hub")
order_id = task.data.get("order_id")
if from_hub not in self._hubs or to_hub not in self._hubs:
return {"status": "error", "message": "Invalid hub ID(s)"}
if to_hub not in self._hubs[from_hub]["connected_hubs"]:
return {
"status": "no_direct_route",
"message": f"No direct route from {from_hub} to {to_hub}",
"suggested_route": self._find_route(from_hub, to_hub),
}
transfer_id = f"XF-{uuid.uuid4().hex[:8].upper()}"
logger.info(f"Hub transfer {transfer_id}: {self._hubs[from_hub]['name']} -> {self._hubs[to_hub]['name']} | order={order_id}")
await self.send_message(
recipient="ROUTE_OPTIMIZER",
message_type=MessageType.AGENT_TASK,
payload={
"task_type": "plan_inter_hub_route",
"transfer_id": transfer_id,
"from_hub": from_hub,
"to_hub": to_hub,
"order_id": order_id,
},
)
return {
"status": "transfer_initiated",
"transfer_id": transfer_id,
"from_hub": from_hub,
"to_hub": to_hub,
"estimated_time": 180,
"route": [from_hub, to_hub],
}
async def _get_transit_status(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
transit_info = []
for hub_id, records in self._in_transit.items():
for record in records:
if record.order_id == order_id:
transit_info.append({
"record_id": record.record_id,
"hub_id": hub_id,
"hub_name": self._hubs[hub_id]["name"],
"status": record.status,
"entry_time": record.entry_time.isoformat(),
"expected_exit": record.expected_exit.isoformat(),
"actual_exit": record.actual_exit.isoformat() if record.actual_exit else None,
"destination": record.destination,
})
return {
"order_id": order_id,
"transit_records": transit_info,
"current_location": transit_info[-1]["hub_name"] if transit_info else "unknown",
}
async def _optimize_hub_operations(self, task: AgentTask) -> Dict[str, Any]:
hub_id = task.data.get("hub_id")
if hub_id not in self._hubs:
return {"status": "error", "message": f"Hub {hub_id} not found"}
hub = self._hubs[hub_id]
metrics = self._metrics[hub_id]
recommendations = []
utilization = (hub["current_load"] / hub["capacity"]) * 100
if utilization > 80:
recommendations.append({
"type": "capacity", "priority": "high",
"message": f"High utilization at {utilization:.0f}% - consider overflow routing",
"action": "request_additional_vehicle",
})
queue_length = len(self._processing_queues[hub_id])
if queue_length > 20:
recommendations.append({
"type": "queue", "priority": "medium",
"message": f"Long processing queue ({queue_length} items)",
"action": "increase_processing_capacity",
})
if metrics["average_processing_time"] > 30:
recommendations.append({
"type": "efficiency", "priority": "medium",
"message": "Above average processing time",
"action": "review_sorting_operations",
})
logger.info(f"Hub {hub['name']}: util={utilization:.1f}% queue={queue_length} recommendations={len(recommendations)}")
return {
"hub_id": hub_id,
"recommendations": recommendations,
"potential_savings": {
"time_minutes": len(recommendations) * 10,
"cost_percent": len(recommendations) * 2,
},
}
async def _escalate_overflow(self, task: AgentTask) -> Dict[str, Any]:
from_hub = task.data.get("from_hub")
order_id = task.data.get("order_id")
if from_hub not in self._hubs:
return {"status": "error", "message": f"Hub {from_hub} not found"}
hub = self._hubs[from_hub]
alternatives = []
for connected in hub["connected_hubs"]:
alt_hub = self._hubs[connected]
utilization = (alt_hub["current_load"] / alt_hub["capacity"]) * 100
if utilization < 70:
alternatives.append({
"hub_id": connected,
"name": alt_hub["name"],
"utilization": utilization,
})
if not alternatives:
return {"status": "no_alternatives", "message": "All connected hubs at capacity"}
best_alt = alternatives[0]
logger.warning(f"Hub overflow: routing to {best_alt['name']}")
return await self._transfer_between_hubs(AgentTask(
task_id=f"overflow_{order_id}",
agent_type="hub",
task_type="transfer_between_hubs",
data={"from_hub": from_hub, "to_hub": best_alt["hub_id"], "order_id": order_id},
))
async def _unknown_task(self, task: AgentTask) -> Dict[str, Any]:
return {"status": "error", "message": f"Unknown task: {task.task_type}"}
def _find_route(self, from_hub: str, to_hub: str) -> List[str]:
"""BFS route between hubs."""
if from_hub == to_hub:
return [from_hub]
visited = {from_hub}
queue = [(from_hub, [from_hub])]
while queue:
current, path = queue.pop(0)
for neighbor in self._hubs[current]["connected_hubs"]:
if neighbor == to_hub:
return path + [to_hub]
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return []
async def think(self, context: str, options: List[str] = None) -> str:
return f"[HUB_AGENT reasoning]: {context}"

337
agents/order_agent.py Normal file
View File

@@ -0,0 +1,337 @@
"""Order Agent - Handles order intake, validation, and categorization."""
import uuid
from datetime import datetime
from typing import Dict, List, Any, Optional
from core.agent import SpecializedAgent
from core.types import (
AgentTask, MessageType, Priority, OrderStatus, ZoneType
)
from core.logger import logger
from core.http_client import api_get, api_post, api_patch
from config.system_config import GO_API_BASE_URL
class OrderAgent(SpecializedAgent):
"""
Order Agent - Manages the entire order lifecycle from intake to validation.
Writes go to the Doormile Go backend (POST /api/v1/admin/crmbooking).
Reads and status updates also go through the Go API.
"""
def __init__(self):
super().__init__(
agent_id="ORDER_AGENT",
domain="order_management",
description="Handles order intake, validation, and categorization"
)
self._validation_rules = self._init_validation_rules()
self._pincode_zones = self._init_pincode_mapping()
def _init_validation_rules(self) -> Dict[str, Any]:
return {
"required_fields": ["customer_name", "customer_phone", "pickup_address", "delivery_address", "items"],
"phone_pattern": r'^\+?[1-9]\d{9,14}$',
"email_pattern": r'^[\w\.-]+@[\w\.-]+\.\w+$',
"pincode_length": 6,
"max_items_per_order": 100,
"max_weight_kg": 500,
}
def _init_pincode_mapping(self) -> Dict[str, Any]:
return {
"100": {"zone": "north", "region": "delhi_ncr", "hub_prefix": "DL"},
"101": {"zone": "north", "region": "delhi_ncr", "hub_prefix": "DL"},
"200": {"zone": "south", "region": "hyderabad", "hub_prefix": "HY"},
"201": {"zone": "south", "region": "bangalore", "hub_prefix": "BL"},
"400": {"zone": "west", "region": "mumbai", "hub_prefix": "MU"},
"401": {"zone": "west", "region": "pune", "hub_prefix": "PU"},
"500": {"zone": "central", "region": "bhopal", "hub_prefix": "BH"},
"600": {"zone": "east", "region": "kolkata", "hub_prefix": "KL"},
"700": {"zone": "northeast", "region": "guwahati", "hub_prefix": "GH"},
"800": {"zone": "northwest", "region": "jaipur", "hub_prefix": "JP"},
}
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
handlers = {
"receive_order": self._receive_order,
"validate_order": self._validate_order,
"categorize_order": self._categorize_order,
"update_status": self._update_status,
"cancel_order": self._cancel_order,
"get_order": self._get_order,
"list_orders": self._list_orders,
}
handler = handlers.get(task.task_type, self._unknown_task)
return await handler(task)
# ------------------------------------------------------------------ #
# Go API helpers (use shared session + retry from http_client) #
# ------------------------------------------------------------------ #
async def _api_post(self, path: str, payload: Dict) -> Optional[Dict]:
result = await api_post(f"{GO_API_BASE_URL}{path}", json=payload)
if result is None:
logger.error(f"Go API POST {path} returned no response")
return result
async def _api_get(self, path: str, params: Dict = None) -> Optional[Dict]:
result = await api_get(f"{GO_API_BASE_URL}{path}", params=params)
if result is None:
logger.error(f"Go API GET {path} returned no response")
return result
async def _api_patch(self, path: str, payload: Dict) -> Optional[Dict]:
result = await api_patch(f"{GO_API_BASE_URL}{path}", json=payload)
if result is None:
logger.error(f"Go API PATCH {path} returned no response")
return result
# ------------------------------------------------------------------ #
# Task handlers #
# ------------------------------------------------------------------ #
async def _receive_order(self, task: AgentTask) -> Dict[str, Any]:
order_data = task.data.get("order", {})
order_id = f"ORD-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:8].upper()}"
order_data["order_id"] = order_id
order_data["status"] = OrderStatus.RECEIVED.value
order_data["created_at"] = datetime.now().isoformat()
logger.info(f"Order Agent: Received order {order_id} | customer={order_data.get('customer_name')} items={len(order_data.get('items', []))}")
validation_result = await self._validate_order_data(order_data)
order_data["validation"] = validation_result
category = self._categorize_order_data(order_data)
order_data["category"] = category
api_result = await self._api_post("/api/v1/admin/crmbooking", order_data)
if api_result is None:
logger.warning(f"Failed to persist order {order_id} to Go backend")
else:
go_id = (api_result.get("data") or {}).get("bookingid")
if go_id:
order_id = str(go_id)
order_data["order_id"] = order_id
order_data["booking_id"] = order_id
logger.info(f"Go API booking ID: {order_id}")
await self.send_message(
recipient="JARVIS",
message_type=MessageType.ORDER_RECEIVED,
payload={
"order_id": order_id,
"booking_id": order_id,
"customer": order_data.get("customer_name"),
"priority": category.get("priority"),
"zone": category.get("zone"),
"validation_status": validation_result.get("status"),
},
correlation_id=order_id,
)
return {
"status": "received",
"order_id": order_id,
"validation": validation_result,
"category": category,
}
async def _validate_order(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
order_data = await self._api_get(f"/api/v1/admin/crmbooking/{order_id}")
if not order_data:
return {"status": "error", "message": f"Order {order_id} not found"}
validation = await self._validate_order_data(order_data)
if validation.get("status") == "valid":
await self._api_patch(
f"/api/v1/admin/crmbooking/{order_id}",
{"status": OrderStatus.VALIDATED.value, "validation": validation},
)
return validation
async def _categorize_order(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
order_data = await self._api_get(f"/api/v1/admin/crmbooking/{order_id}")
if not order_data:
return {"status": "error", "message": f"Order {order_id} not found"}
return self._categorize_order_data(order_data)
async def _update_status(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
new_status = task.data.get("status")
result = await self._api_patch(
f"/api/v1/admin/crmbooking/{order_id}",
{"status": new_status, "status_updated_at": datetime.now().isoformat()},
)
if not result:
return {"status": "error", "message": f"Failed to update order {order_id}"}
logger.info(f"Order {order_id} status -> {new_status}")
await self.send_message(
recipient="CUSTOMER_AGENT",
message_type=MessageType.ORDER_STATUS_UPDATE,
payload={"order_id": order_id, "new_status": new_status},
)
return {"status": "updated", "order_id": order_id, "new_status": new_status}
async def _cancel_order(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
reason = task.data.get("reason", "Customer requested cancellation")
result = await self._api_patch(
f"/api/v1/admin/crmbooking/{order_id}",
{
"status": OrderStatus.CANCELLED.value,
"cancellation_reason": reason,
"cancelled_at": datetime.now().isoformat(),
},
)
if not result:
return {"status": "error", "message": f"Failed to cancel order {order_id}"}
logger.info(f"Order {order_id} cancelled: {reason}")
await self.broadcast(
message_type=MessageType.ORDER_CANCELLED,
payload={"order_id": order_id, "reason": reason},
correlation_id=order_id,
)
return {"status": "cancelled", "order_id": order_id, "reason": reason}
async def _get_order(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
order_data = await self._api_get(f"/api/v1/admin/crmbooking/{order_id}")
if not order_data:
return {"status": "error", "message": f"Order {order_id} not found"}
return order_data
async def _list_orders(self, task: AgentTask) -> Dict[str, Any]:
params = {}
if task.data.get("status"):
params["status"] = task.data["status"]
if task.data.get("priority"):
params["priority"] = task.data["priority"]
result = await self._api_get("/api/v1/admin/crmbooking", params=params)
if not result:
return {"total": 0, "orders": []}
orders = result if isinstance(result, list) else result.get("data", [])
return {"total": len(orders), "orders": orders}
async def _unknown_task(self, task: AgentTask) -> Dict[str, Any]:
return {"status": "error", "message": f"Unknown task type: {task.task_type}"}
# ------------------------------------------------------------------ #
# Validation / categorization (local, no DB) #
# ------------------------------------------------------------------ #
async def _validate_order_data(self, order_data: Dict) -> Dict[str, Any]:
errors = []
warnings = []
for field in self._validation_rules["required_fields"]:
if field not in order_data or not order_data[field]:
errors.append(f"Missing required field: {field}")
phone = order_data.get("customer_phone", "")
if phone and not self._is_valid_phone(phone):
errors.append("Invalid phone number format")
email = order_data.get("customer_email")
if email and not self._is_valid_email(email):
warnings.append("Invalid email format")
pickup_pincode = order_data.get("pickup_address", {}).get("pincode", "")
if not self._is_valid_pincode(pickup_pincode):
errors.append("Invalid pickup pincode")
delivery_pincode = order_data.get("delivery_address", {}).get("pincode", "")
if not self._is_valid_pincode(delivery_pincode):
errors.append("Invalid delivery pincode")
items = order_data.get("items", [])
if not items:
errors.append("No items in order")
elif len(items) > self._validation_rules["max_items_per_order"]:
errors.append(f"Too many items (max {self._validation_rules['max_items_per_order']})")
total_weight = sum(item.get("weight", 0) for item in items)
if total_weight > self._validation_rules["max_weight_kg"]:
warnings.append(f"Heavy order ({total_weight}kg) - may require special handling")
return {
"status": "valid" if not errors else "invalid",
"errors": errors,
"warnings": warnings,
"total_weight_kg": total_weight,
}
def _categorize_order_data(self, order_data: Dict) -> Dict[str, Any]:
pickup_pincode = order_data.get("pickup_address", {}).get("pincode", "")[:3]
delivery_pincode = order_data.get("delivery_address", {}).get("pincode", "")[:3]
if pickup_pincode == delivery_pincode:
zone_type = ZoneType.LAST_MILE.value
else:
pickup_zone = self._pincode_zones.get(pickup_pincode, {})
delivery_zone = self._pincode_zones.get(delivery_pincode, {})
if pickup_zone.get("zone") == delivery_zone.get("zone"):
zone_type = ZoneType.HUB_TO_SPOKE.value
else:
zone_type = ZoneType.HUB_TO_HUB.value
priority = order_data.get("priority", Priority.MEDIUM.value)
total_weight = sum(item.get("weight", 0) for item in order_data.get("items", []))
if total_weight > 50 or order_data.get("special_instructions"):
priority = Priority.HIGH.value
pickup_info = self._pincode_zones.get(pickup_pincode, {"zone": "unknown", "region": "unknown"})
return {
"priority": priority,
"zone_type": zone_type,
"pickup_zone": pickup_info.get("zone"),
"delivery_zone": self._pincode_zones.get(delivery_pincode, {}).get("zone"),
"pickup_region": pickup_info.get("region"),
"estimated_hubs": self._get_nearest_hubs(pickup_pincode),
"requires_cold_chain": self._check_cold_chain_requirement(order_data),
"is_fragile": self._check_fragile_items(order_data),
}
def _get_nearest_hubs(self, pincode: str) -> List[str]:
zone = self._pincode_zones.get(pincode[:3], {})
hub_prefix = zone.get("hub_prefix", "XX")
return [f"{hub_prefix}-HUB-01", f"{hub_prefix}-HUB-02"]
def _check_cold_chain_requirement(self, order_data: Dict) -> bool:
return any(item.get("requires_cold_chain") for item in order_data.get("items", []))
def _check_fragile_items(self, order_data: Dict) -> bool:
return any(item.get("fragile") for item in order_data.get("items", []))
def _is_valid_phone(self, phone: str) -> bool:
import re
return bool(re.match(self._validation_rules["phone_pattern"], phone))
def _is_valid_email(self, email: str) -> bool:
import re
return bool(re.match(self._validation_rules["email_pattern"], email))
def _is_valid_pincode(self, pincode: str) -> bool:
return len(pincode) == self._validation_rules["pincode_length"] and pincode.isdigit()
async def think(self, context: str, options: List[str] = None) -> str:
return f"[ORDER_AGENT reasoning]: {context}"

View File

@@ -0,0 +1,465 @@
"""Route Optimizer Agent - Optimizes delivery routes based on zones and available hubs."""
import uuid
from datetime import datetime, timedelta
from time import monotonic
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
from math import radians, cos, sin, asin, sqrt
from collections import defaultdict
from core.agent import SpecializedAgent
from core.types import AgentTask, MessageType, ZoneType
from core.logger import logger
_CACHE_TTL_SECONDS = 600 # 10 minutes
@dataclass
class Waypoint:
location_id: str
lat: float
lng: float
address: str
type: str # pickup, delivery, hub, spoke
order_id: Optional[str] = None
time_window_start: Optional[datetime] = None
time_window_end: Optional[datetime] = None
@dataclass
class Route:
route_id: str
waypoints: List[Waypoint]
total_distance_km: float
estimated_duration_minutes: float
vehicle_id: str
zones_traversed: List[str]
fuel_cost: float
efficiency_score: float
class RouteOptimizerAgent(SpecializedAgent):
"""Route Optimizer Agent - Optimizes delivery routes based on zones, traffic, and constraints."""
def __init__(self):
super().__init__(
agent_id="ROUTE_OPTIMIZER",
domain="route_optimization",
description="Optimizes delivery routes based on zones, hubs, and constraints"
)
self._hubs = {
"DL-HUB-01": (28.6139, 77.2090),
"DL-HUB-02": (28.5355, 77.2100),
"MU-HUB-01": (19.0760, 72.8777),
"MU-HUB-02": (19.1650, 72.8500),
"BL-HUB-01": (12.9716, 77.5946),
"HY-HUB-01": (17.3850, 78.4867),
"PU-HUB-01": (18.5204, 73.8567),
"KL-HUB-01": (22.5726, 88.3639),
}
self._zones = self._init_zones()
# Cache stores (Route, created_at_monotonic) — evicted after _CACHE_TTL_SECONDS
self._route_cache: Dict[str, Tuple[Route, float]] = {}
self._traffic_patterns = self._init_traffic_patterns()
self._route_history: List[Dict] = []
def _init_zones(self) -> Dict[str, Dict]:
return {
"north_delhi": {"pincode_range": ("100", "199"), "center": (28.6139, 77.2090), "hub": "DL-HUB-01", "typical_traffic": "medium"},
"south_delhi": {"pincode_range": ("200", "299"), "center": (28.5355, 77.2100), "hub": "DL-HUB-02", "typical_traffic": "high"},
"mumbai_west": {"pincode_range": ("400", "449"), "center": (19.0760, 72.8777), "hub": "MU-HUB-01", "typical_traffic": "high"},
"mumbai_east": {"pincode_range": ("450", "499"), "center": (19.1650, 72.8500), "hub": "MU-HUB-02", "typical_traffic": "medium"},
"bangalore": {"pincode_range": ("560", "562"), "center": (12.9716, 77.5946), "hub": "BL-HUB-01", "typical_traffic": "medium"},
"hyderabad": {"pincode_range": ("500", "599"), "center": (17.3850, 78.4867), "hub": "HY-HUB-01", "typical_traffic": "medium"},
"pune": {"pincode_range": ("400", "499"), "center": (18.5204, 73.8567), "hub": "PU-HUB-01", "typical_traffic": "medium"},
"kolkata": {"pincode_range": ("600", "699"), "center": (22.5726, 88.3639), "hub": "KL-HUB-01", "typical_traffic": "low"},
}
def _init_traffic_patterns(self) -> Dict[str, Dict]:
return {
"morning": {"multiplier": 1.2, "description": "7AM-10AM rush"},
"midday": {"multiplier": 1.0, "description": "10AM-4PM normal"},
"evening": {"multiplier": 1.5, "description": "4PM-8PM rush"},
"night": {"multiplier": 0.8, "description": "8PM-7AM light"},
}
# ------------------------------------------------------------------ #
# Cache helpers with TTL #
# ------------------------------------------------------------------ #
def _cache_put(self, route_id: str, route: Route):
self._route_cache[route_id] = (route, monotonic())
def _cache_get(self, route_id: str) -> Optional[Route]:
entry = self._route_cache.get(route_id)
if entry is None:
return None
route, ts = entry
if monotonic() - ts >= _CACHE_TTL_SECONDS:
del self._route_cache[route_id]
return None
return route
async def _heartbeat(self):
"""Evict expired entries from route cache."""
now = monotonic()
expired = [rid for rid, (_, ts) in self._route_cache.items() if now - ts >= _CACHE_TTL_SECONDS]
for rid in expired:
del self._route_cache[rid]
if expired:
logger.debug(f"Route cache: evicted {len(expired)} expired entries ({len(self._route_cache)} remaining)")
# ------------------------------------------------------------------ #
# Task dispatch #
# ------------------------------------------------------------------ #
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
handlers = {
"optimize_route": self._optimize_route,
"plan_multi_stop": self._plan_multi_stop,
"plan_inter_hub_route": self._plan_inter_hub_route,
"calculate_eta": self._calculate_eta,
"avoid_zone": self._avoid_zone,
"reoptimize_route": self._reoptimize_route,
"get_zone_routes": self._get_zone_routes,
"batch_optimize": self._batch_optimize,
}
handler = handlers.get(task.task_type, self._unknown_task)
return await handler(task)
async def _optimize_route(self, task: AgentTask) -> Dict[str, Any]:
order_id = task.data.get("order_id")
pickup = task.data.get("pickup", {})
delivery = task.data.get("delivery", {})
vehicle_type = task.data.get("vehicle_type", "van")
logger.info(f"Route Optimizer: Optimizing route for order {order_id}")
pickup_coords = (pickup.get("lat", 28.6139), pickup.get("lng", 77.2090))
delivery_coords = (delivery.get("lat", 19.0760), delivery.get("lng", 72.8777))
direct_distance = self._haversine_distance(pickup_coords, delivery_coords)
optimal_path = self._find_optimal_path(pickup_coords, delivery_coords)
total_distance = self._calculate_total_distance(optimal_path)
traffic_multiplier = self._get_traffic_multiplier()
estimated_time = (total_distance / 30) * traffic_multiplier * 60
route_id = f"RT-OPT-{uuid.uuid4().hex[:8].upper()}"
waypoints = []
for i, coords in enumerate(optimal_path):
hub_id = self._find_nearest_hub(coords)
waypoints.append(Waypoint(
location_id=f"WPT-{i}",
lat=coords[0],
lng=coords[1],
address=str(self._hubs.get(hub_id, ("Unknown",))[0]) if hub_id else "Route point",
type="hub" if 0 < i < len(optimal_path) - 1 else ("pickup" if i == 0 else "delivery"),
order_id=order_id,
))
route = Route(
route_id=route_id,
waypoints=waypoints,
total_distance_km=total_distance,
estimated_duration_minutes=estimated_time,
vehicle_id=task.data.get("vehicle_id", ""),
zones_traversed=self._identify_zones(optimal_path),
fuel_cost=total_distance * 3.5,
efficiency_score=self._calculate_efficiency(total_distance, direct_distance),
)
self._cache_put(route_id, route)
logger.info(
f"Route {route_id}: {len(waypoints)} waypoints | {total_distance:.1f} km | "
f"ETA {estimated_time:.0f} min | efficiency {route.efficiency_score:.0f}%"
)
return {
"status": "optimized",
"route_id": route_id,
"waypoints": [{"lat": w.lat, "lng": w.lng, "type": w.type, "address": w.address} for w in waypoints],
"total_distance_km": total_distance,
"estimated_duration_minutes": estimated_time,
"zones_traversed": route.zones_traversed,
"fuel_cost": route.fuel_cost,
"efficiency_score": route.efficiency_score,
}
async def _plan_multi_stop(self, task: AgentTask) -> Dict[str, Any]:
stops = task.data.get("stops", [])
vehicle_id = task.data.get("vehicle_id")
logger.info(f"Route Optimizer: Planning multi-stop route with {len(stops)} stops")
waypoints = [
Waypoint(
location_id=f"STOP-{i}",
lat=stop.get("lat"),
lng=stop.get("lng"),
address=stop.get("address", ""),
type=stop.get("type", "delivery"),
order_id=stop.get("order_id"),
)
for i, stop in enumerate(stops)
]
optimized_order = self._nearest_neighbor_optimization(waypoints)
total_distance = self._calculate_route_distance(optimized_order)
estimated_time = (total_distance / 25) * 60
route_id = f"RT-MULTI-{uuid.uuid4().hex[:8].upper()}"
route = Route(
route_id=route_id,
waypoints=optimized_order,
total_distance_km=total_distance,
estimated_duration_minutes=estimated_time,
vehicle_id=vehicle_id,
zones_traversed=self._identify_zones([(w.lat, w.lng) for w in optimized_order]),
fuel_cost=total_distance * 3.5,
efficiency_score=85.0,
)
self._cache_put(route_id, route)
return {
"status": "planned",
"route_id": route_id,
"stop_order": [{"order": i + 1, "lat": w.lat, "lng": w.lng, "type": w.type} for i, w in enumerate(optimized_order)],
"total_distance_km": total_distance,
"estimated_duration_minutes": estimated_time,
}
async def _plan_inter_hub_route(self, task: AgentTask) -> Dict[str, Any]:
from_hub = task.data.get("from_hub")
to_hub = task.data.get("to_hub")
order_id = task.data.get("order_id")
logger.info(f"Route Optimizer: Inter-hub route {from_hub} -> {to_hub}")
if from_hub not in self._hubs or to_hub not in self._hubs:
return {"status": "error", "message": "Invalid hub ID(s)"}
from_coords = self._hubs[from_hub]
to_coords = self._hubs[to_hub]
direct_distance = self._haversine_distance(from_coords, to_coords)
intermediate_hub = None
if direct_distance > 500:
intermediate_hub = self._find_intermediate_hub(from_coords, to_coords)
route_coords = (
[from_coords, self._hubs[intermediate_hub], to_coords]
if intermediate_hub else
[from_coords, to_coords]
)
total_distance = self._calculate_total_distance(route_coords)
estimated_time = (total_distance / 40) * 60
route_id = f"RT-IHUB-{uuid.uuid4().hex[:8].upper()}"
return {
"status": "planned",
"route_id": route_id,
"from_hub": from_hub,
"to_hub": to_hub,
"intermediate_hub": intermediate_hub,
"waypoints": [{"hub": h, "coords": self._hubs.get(h, (0, 0))} for h in [from_hub, intermediate_hub, to_hub] if h],
"total_distance_km": total_distance,
"estimated_duration_minutes": estimated_time,
"estimated_hours": estimated_time / 60,
}
async def _calculate_eta(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
current_location = task.data.get("current_location")
cached = self._cache_get(route_id)
if cached:
return {
"route_id": route_id,
"total_eta_minutes": cached.estimated_duration_minutes,
"remaining_distance_km": cached.total_distance_km,
"current_eta": (datetime.now() + timedelta(minutes=cached.estimated_duration_minutes)).isoformat(),
}
from_coords = (current_location.get("lat", 0), current_location.get("lng", 0))
to_coords = task.data.get("destination", (0, 0))
distance = self._haversine_distance(from_coords, to_coords)
eta_minutes = (distance / 30) * self._get_traffic_multiplier() * 60
return {
"distance_km": distance,
"eta_minutes": eta_minutes,
"current_eta": (datetime.now() + timedelta(minutes=eta_minutes)).isoformat(),
}
async def _avoid_zone(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
avoid_zone = task.data.get("zone")
logger.info(f"Route Optimizer: Avoiding zone {avoid_zone}")
if self._cache_get(route_id):
return {
"status": "replanned",
"route_id": route_id,
"avoided_zone": avoid_zone,
"additional_distance_km": 5.0,
"additional_time_minutes": 15,
}
return {"status": "error", "message": "Route not found"}
async def _reoptimize_route(self, task: AgentTask) -> Dict[str, Any]:
route_id = task.data.get("route_id")
new_stops = task.data.get("new_stops", [])
logger.info(f"Route Optimizer: Reoptimizing route {route_id}")
route = self._cache_get(route_id)
if route:
for stop in new_stops:
route.waypoints.append(Waypoint(
location_id=f"NEW-{len(route.waypoints)}",
lat=stop.get("lat"),
lng=stop.get("lng"),
address=stop.get("address", ""),
type="add_delivery",
order_id=stop.get("order_id"),
))
coords = [(w.lat, w.lng) for w in route.waypoints]
route.total_distance_km = self._calculate_total_distance(coords)
route.estimated_duration_minutes = (route.total_distance_km / 25) * 60
self._cache_put(route_id, route)
return {
"status": "reoptimized",
"route_id": route_id,
"new_distance_km": route.total_distance_km,
"new_eta_minutes": route.estimated_duration_minutes,
}
return {"status": "error", "message": "Route not found"}
async def _get_zone_routes(self, task: AgentTask) -> Dict[str, Any]:
zone = task.data.get("zone")
now = monotonic()
zone_routes = []
for route_id, (route, ts) in list(self._route_cache.items()):
if now - ts >= _CACHE_TTL_SECONDS:
continue
if zone in route.zones_traversed:
zone_routes.append({
"route_id": route.route_id,
"distance_km": route.total_distance_km,
"duration_minutes": route.estimated_duration_minutes,
})
return {"zone": zone, "total_routes": len(zone_routes), "routes": zone_routes}
async def _batch_optimize(self, task: AgentTask) -> Dict[str, Any]:
orders = task.data.get("orders", [])
logger.info(f"Route Optimizer: Batch optimizing {len(orders)} orders")
zone_groups: Dict[str, list] = defaultdict(list)
for order in orders:
zone = self._identify_zone_from_coords((order.get("lat", 0), order.get("lng", 0)))
zone_groups[zone].append(order)
results = [await self._optimize_zone_routes(zone, zone_orders) for zone, zone_orders in zone_groups.items()]
return {
"status": "batch_optimized",
"zones_optimized": len(results),
"total_orders": len(orders),
"total_distance_km": sum(r["total_distance_km"] for r in results),
"total_time_minutes": sum(r["estimated_time_minutes"] for r in results),
"zone_results": results,
}
async def _unknown_task(self, task: AgentTask) -> Dict[str, Any]:
return {"status": "error", "message": f"Unknown task: {task.task_type}"}
# ------------------------------------------------------------------ #
# Geometry helpers #
# ------------------------------------------------------------------ #
def _haversine_distance(self, coord1: Tuple[float, float], coord2: Tuple[float, float]) -> float:
lat1, lon1 = coord1
lat2, lon2 = coord2
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
return 2 * asin(sqrt(a)) * 6371
def _find_optimal_path(self, start: Tuple[float, float], end: Tuple[float, float]) -> List[Tuple[float, float]]:
start_hub = self._find_nearest_hub(start)
end_hub = self._find_nearest_hub(end)
if start_hub != end_hub:
return [start, self._hubs[start_hub], self._hubs[end_hub], end]
return [start, end]
def _find_nearest_hub(self, coords: Tuple[float, float]) -> Optional[str]:
return min(self._hubs.keys(), key=lambda h: self._haversine_distance(coords, self._hubs[h]), default=None)
def _find_intermediate_hub(self, start: Tuple[float, float], end: Tuple[float, float]) -> Optional[str]:
mid = ((start[0] + end[0]) / 2, (start[1] + end[1]) / 2)
return self._find_nearest_hub(mid)
def _calculate_total_distance(self, coords: List[Tuple[float, float]]) -> float:
return sum(self._haversine_distance(coords[i], coords[i + 1]) for i in range(len(coords) - 1))
def _calculate_route_distance(self, waypoints: List[Waypoint]) -> float:
return self._calculate_total_distance([(w.lat, w.lng) for w in waypoints])
def _get_traffic_multiplier(self) -> float:
hour = datetime.now().hour
if 7 <= hour < 10:
return self._traffic_patterns["morning"]["multiplier"]
if 10 <= hour < 16:
return self._traffic_patterns["midday"]["multiplier"]
if 16 <= hour < 20:
return self._traffic_patterns["evening"]["multiplier"]
return self._traffic_patterns["night"]["multiplier"]
def _identify_zones(self, coords: List[Tuple[float, float]]) -> List[str]:
return list({self._identify_zone_from_coords(c) for c in coords if self._identify_zone_from_coords(c)})
def _identify_zone_from_coords(self, coords: Tuple[float, float]) -> str:
return min(self._zones.keys(), key=lambda z: self._haversine_distance(coords, self._zones[z]["center"]), default="unknown")
def _calculate_efficiency(self, actual_distance: float, direct_distance: float) -> float:
if direct_distance == 0:
return 100.0
return min(100.0, (direct_distance / actual_distance) * 100)
def _nearest_neighbor_optimization(self, waypoints: List[Waypoint]) -> List[Waypoint]:
if not waypoints:
return []
unvisited = waypoints[1:]
ordered = [waypoints[0]]
while unvisited:
current = ordered[-1]
nearest = min(unvisited, key=lambda w: self._haversine_distance((current.lat, current.lng), (w.lat, w.lng)))
ordered.append(nearest)
unvisited.remove(nearest)
return ordered
async def _optimize_zone_routes(self, zone: str, orders: List[Dict]) -> Dict[str, Any]:
total_distance = 0.0
total_time = 0.0
for i in range(0, len(orders), 5):
batch = orders[i:i + 5]
coords = [(o.get("lat", 0), o.get("lng", 0)) for o in batch]
dist = self._calculate_total_distance(coords)
total_distance += dist
total_time += (dist / 25) * 60
return {"zone": zone, "orders_in_zone": len(orders), "total_distance_km": total_distance, "estimated_time_minutes": total_time}
async def think(self, context: str, options: List[str] = None) -> str:
return f"[ROUTE_OPTIMIZER reasoning]: {context}"

285
config/system_config.py Normal file
View File

@@ -0,0 +1,285 @@
"""System configuration for LogiFlow AI."""
import os
# Infrastructure Connections — values come from environment / .env file.
# Hardcoded strings are last-resort defaults; set the matching env var in production.
NATS_URL = os.getenv("NATS_URL", "nats://doormile:Package@321#@66.116.226.161:4223")
NATS_HOST = os.getenv("NATS_HOST", "66.116.226.161")
NATS_PORT = int(os.getenv("NATS_PORT", "4223"))
NATS_USER = os.getenv("NATS_USER", "doormile")
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "Package@321#")
REDIS_HOST = os.getenv("REDIS_HOST", "66.116.226.255")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6380"))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "Package@321#")
# Postgres — individual params to avoid @ in password breaking DSN parsing
DB_HOST = os.getenv("DB_HOST", "31.97.228.132")
DB_PORT = int(os.getenv("DB_PORT", "5433"))
DB_NAME = os.getenv("DB_NAME", "logistics")
DB_USER = os.getenv("DB_USER", "admin")
DB_PASSWORD = os.getenv("DB_PASSWORD", "Package@321#")
GO_API_BASE_URL = os.getenv("GO_API_BASE_URL", "http://localhost:8080")
INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "doormile-internal-2024")
# Agent Configuration
AGENT_CONFIG = {
"jarvis": {
"name": "JARVIS",
"type": "master",
"description": "Central orchestrator for all logistics operations",
"monitoring_interval": 5, # seconds
"decision_threshold": 0.8
},
"order_agent": {
"name": "ORDER_AGENT",
"type": "specialized",
"description": "Handles order intake, validation, and categorization",
"max_concurrent_orders": 100,
"validation_timeout": 30 # seconds
},
"dispatch_agent": {
"name": "DISPATCH_AGENT",
"type": "specialized",
"description": "Handles zone analysis, route assignment, and dispatch",
"zone_update_interval": 60, # seconds
"batch_size": 10
},
"fleet_agent": {
"name": "FLEET_AGENT",
"type": "specialized",
"description": "Manages vehicle fleet, capacity, and tracking",
"tracking_interval": 30, # seconds
"maintenance_check_interval": 3600 # 1 hour
},
"hub_agent": {
"name": "HUB_AGENT",
"type": "specialized",
"description": "Manages hub operations, transit flow, and capacity",
"capacity_warning_threshold": 0.85,
"processing_timeout": 120 # seconds
},
"customer_agent": {
"name": "CUSTOMER_AGENT",
"type": "specialized",
"description": "Handles notifications, tracking, and customer communication",
"notification_channels": ["sms", "email", "whatsapp", "push"],
"retry_attempts": 3
},
"exception_agent": {
"name": "EXCEPTION_AGENT",
"type": "specialized",
"description": "Handles delays, cancellations, rescheduling, and problems",
"sla_thresholds": {
"critical": 15, # minutes
"high": 30,
"medium": 60,
"low": 120
}
},
"route_optimizer": {
"name": "ROUTE_OPTIMIZER",
"type": "specialized",
"description": "Optimizes delivery routes based on zones and hubs",
"traffic_update_interval": 300, # 5 minutes
"cache_ttl": 600 # 10 minutes
}
}
# Zone Definitions
ZONES = {
"north_delhi": {
"pincode_range": ("100", "199"),
"hub": "DL-HUB-01",
"coordinates": (28.6139, 77.2090),
"coverage": ["Delhi NCR North"]
},
"south_delhi": {
"pincode_range": ("200", "299"),
"hub": "DL-HUB-02",
"coordinates": (28.5355, 77.2100),
"coverage": ["Delhi NCR South"]
},
"mumbai_west": {
"pincode_range": ("400", "449"),
"hub": "MU-HUB-01",
"coordinates": (19.0760, 72.8777),
"coverage": ["Mumbai West", "Andheri", "Juhu"]
},
"mumbai_east": {
"pincode_range": ("450", "499"),
"hub": "MU-HUB-02",
"coordinates": (19.1650, 72.8500),
"coverage": ["Mumbai East", "Thane", "Navi Mumbai"]
},
"bangalore": {
"pincode_range": ("200", "299"),
"hub": "BL-HUB-01",
"coordinates": (12.9716, 77.5946),
"coverage": ["Bangalore", "Electronic City"]
},
"hyderabad": {
"pincode_range": ("500", "599"),
"hub": "HY-HUB-01",
"coordinates": (17.3850, 78.4867),
"coverage": ["Hyderabad", "Hi-Tech City"]
},
"pune": {
"pincode_range": ("400", "499"),
"hub": "PU-HUB-01",
"coordinates": (18.5204, 73.8567),
"coverage": ["Pune", "Hinjewadi"]
},
"kolkata": {
"pincode_range": ("600", "699"),
"hub": "KL-HUB-01",
"coordinates": (22.5726, 88.3639),
"coverage": ["Kolkata", "Salt Lake"]
}
}
# Hub Configuration
HUBS = {
"DL-HUB-01": {
"name": "Delhi North Hub",
"zone": "north_delhi",
"capacity": 500,
"processing_rate": 100, # orders per hour
"spokes": ["DL-SP-01", "DL-SP-02", "DL-SP-03"],
"connected_hubs": ["DL-HUB-02", "MU-HUB-01", "KL-HUB-01"]
},
"DL-HUB-02": {
"name": "Delhi South Hub",
"zone": "south_delhi",
"capacity": 400,
"processing_rate": 80,
"spokes": ["DL-SP-04", "DL-SP-05"],
"connected_hubs": ["DL-HUB-01", "BL-HUB-01"]
},
"MU-HUB-01": {
"name": "Mumbai West Hub",
"zone": "mumbai_west",
"capacity": 600,
"processing_rate": 120,
"spokes": ["MU-SP-01", "MU-SP-02", "MU-SP-03", "MU-SP-04"],
"connected_hubs": ["MU-HUB-02", "DL-HUB-01", "PU-HUB-01"]
},
"MU-HUB-02": {
"name": "Mumbai East Hub",
"zone": "mumbai_east",
"capacity": 550,
"processing_rate": 90,
"spokes": ["MU-SP-05", "MU-SP-06"],
"connected_hubs": ["MU-HUB-01", "PU-HUB-01"]
},
"BL-HUB-01": {
"name": "Bangalore Hub",
"zone": "bangalore",
"capacity": 500,
"processing_rate": 100,
"spokes": ["BL-SP-01", "BL-SP-02", "BL-SP-03"],
"connected_hubs": ["HY-HUB-01", "DL-HUB-02"]
},
"HY-HUB-01": {
"name": "Hyderabad Hub",
"zone": "hyderabad",
"capacity": 450,
"processing_rate": 85,
"spokes": ["HY-SP-01", "HY-SP-02"],
"connected_hubs": ["BL-HUB-01", "KL-HUB-01"]
},
"PU-HUB-01": {
"name": "Pune Hub",
"zone": "pune",
"capacity": 400,
"processing_rate": 75,
"spokes": ["PU-SP-01", "PU-SP-02"],
"connected_hubs": ["MU-HUB-01", "MU-HUB-02"]
},
"KL-HUB-01": {
"name": "Kolkata Hub",
"zone": "kolkata",
"capacity": 350,
"processing_rate": 70,
"spokes": ["KL-SP-01", "KL-SP-02"],
"connected_hubs": ["DL-HUB-01", "HY-HUB-01"]
}
}
# Vehicle Types
VEHICLE_TYPES = {
"bike": {
"capacity_kg": 15,
"capacity_vol": 0.5, # cubic meters
"max_speed_kmh": 50,
"fuel_cost_per_km": 2.0,
"suitable_for": ["last_mile", "small_packages"]
},
"scooter": {
"capacity_kg": 20,
"capacity_vol": 0.7,
"max_speed_kmh": 45,
"fuel_cost_per_km": 1.5,
"suitable_for": ["last_mile", "documents"]
},
"van": {
"capacity_kg": 500,
"capacity_vol": 8,
"max_speed_kmh": 80,
"fuel_cost_per_km": 3.5,
"suitable_for": ["hub_to_spoke", "medium_packages"]
},
"truck": {
"capacity_kg": 2000,
"capacity_vol": 25,
"max_speed_kmh": 70,
"fuel_cost_per_km": 5.0,
"suitable_for": ["hub_to_hub", "heavy_cargo", "bulk"]
}
}
# SLA Configuration
SLA_CONFIG = {
"delivery_windows": {
"express": 4, # hours
"standard": 24, # hours
"economy": 72 # hours
},
"response_times": {
"critical_exception": 15, # minutes
"high_exception": 30,
"medium_exception": 60,
"low_exception": 120
}
}
# Notification Templates
NOTIFICATION_TEMPLATES = {
"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!\n📍 Delivery by {eta}\n🔗 Track: {tracking_url}"
},
"out_for_delivery": {
"sms": "Out for delivery! Driver: {driver_name}, Contact: {driver_phone}",
"email": "Your order is out for delivery!",
"whatsapp": "🏃 Out for delivery!\nDriver: {driver_name}\n📞 {driver_phone}"
},
"delivered": {
"sms": "Order {order_id} delivered successfully! Thank you.",
"email": "Your order has been delivered! We hope you enjoy your purchase.",
"whatsapp": "✅ Delivered!\nOrder {order_id}\nThank you!"
}
}
# System Settings
SYSTEM_CONFIG = {
"max_concurrent_orders": 1000,
"message_queue_size": 10000,
"agent_heartbeat_interval": 30, # seconds
"cleanup_interval": 3600, # 1 hour
"log_retention_days": 30,
"enable_monitoring": True,
"enable_metrics": True
}

24
core/__init__.py Normal file
View File

@@ -0,0 +1,24 @@
# Core module for LogiFlow AI
from core.agent import Agent, MasterAgent, SpecializedAgent
from core.message_bus import message_bus, MessageBus
from core.types import *
__all__ = [
'Agent',
'MasterAgent',
'SpecializedAgent',
'message_bus',
'MessageBus',
'AgentTask',
'AgentMessage',
'AgentState',
'MessageType',
'Priority',
'OrderStatus',
'ZoneType',
'Order',
'Hub',
'Vehicle',
'Address',
'OrderItem'
]

273
core/agent.py Normal file
View File

@@ -0,0 +1,273 @@
"""Base Agent class - All agents inherit from this."""
import asyncio
import uuid
from datetime import datetime
from typing import Dict, List, Optional, Any
from abc import ABC, abstractmethod
from core.types import AgentMessage, MessageType, AgentState, AgentTask, Priority
from core.message_bus import message_bus
from core.logger import logger
class Agent(ABC):
"""
Base class for all AI agents in the logistics system.
Each agent has its own task queue and can communicate via the message bus.
"""
def __init__(self, agent_id: str, agent_type: str, description: str = ""):
self.agent_id = agent_id
self.agent_type = agent_type
self.description = description
self.state = AgentState(
agent_id=agent_id,
agent_type=agent_type,
status="idle"
)
self._task_queue: asyncio.Queue = asyncio.Queue()
self._running = False
self._task_handlers: Dict[str, callable] = {}
# Register with message bus
message_bus.register_agent(self)
async def start(self):
"""Start the agent's processing loop."""
self._running = True
self.state.status = "idle"
logger.info(f"Agent started: {self.agent_id}")
while self._running:
try:
try:
task = await asyncio.wait_for(
self._task_queue.get(),
timeout=1.0
)
await self._process_task(task)
except asyncio.TimeoutError:
await self._heartbeat()
except Exception as e:
logger.error(f"Error in agent {self.agent_id}: {e}")
self.state.status = "error"
await asyncio.sleep(5)
async def stop(self):
"""Stop the agent."""
self._running = False
self.state.status = "stopped"
logger.info(f"Agent stopped: {self.agent_id}")
async def _process_task(self, task: AgentTask):
"""Process a task from the queue."""
self.state.status = "working"
self.state.current_task = task.task_id
try:
if task.task_type in self._task_handlers:
handler = self._task_handlers[task.task_type]
result = await handler(task)
task.status = "completed"
task.result = result
else:
result = await self.handle_task(task)
task.status = "completed"
task.result = result
self.state.tasks_completed += 1
except Exception as e:
logger.error(f"Task {task.task_id} failed in {self.agent_id}: {e}")
task.status = "failed"
task.error = str(e)
self.state.tasks_failed += 1
finally:
self.state.current_task = None
self.state.last_active = datetime.now()
self.state.status = "idle"
@abstractmethod
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
"""Override this method to handle tasks. Return a dict with results."""
pass
async def _heartbeat(self):
"""Called periodically when idle. Override for custom behavior."""
pass
def register_task_handler(self, task_type: str, handler: callable):
self._task_handlers[task_type] = handler
async def submit_task(self, task: AgentTask):
await self._task_queue.put(task)
async def send_message(self, recipient: str, message_type: MessageType, payload: Dict[str, Any], correlation_id: Optional[str] = None) -> str:
return await message_bus.send_to_agent(
sender=self.agent_id,
recipient=recipient,
message_type=message_type,
payload=payload,
correlation_id=correlation_id
)
async def broadcast(self, message_type: MessageType, payload: Dict[str, Any], correlation_id: Optional[str] = None) -> str:
return await message_bus.broadcast(
sender=self.agent_id,
message_type=message_type,
payload=payload,
correlation_id=correlation_id
)
async def receive_messages(self) -> List[AgentMessage]:
return await message_bus.get_messages(self.agent_id)
def subscribe_to(self, message_type: MessageType, callback: callable):
message_bus.subscribe(message_type, callback)
async def think(self, context: str, options: List[str] = None) -> str:
return context
class MasterAgent(Agent):
"""
Master Agent (JARVIS) - Orchestrates all other agents.
Makes high-level decisions and assigns work.
"""
def __init__(self):
super().__init__(
agent_id="JARVIS",
agent_type="master",
description="Central orchestrator for all logistics operations"
)
self._sub_agents: Dict[str, Agent] = {}
self._active_orders: Dict[str, Dict] = {}
self._decision_log: List[Dict] = []
def register_sub_agent(self, agent: Agent):
self._sub_agents[agent.agent_id] = agent
logger.info(f"JARVIS registered sub-agent: {agent.agent_id}")
async def handle_task(self, task: AgentTask) -> Dict[str, Any]:
if task.task_type == "orchestrate_order":
return await self._orchestrate_order(task)
elif task.task_type == "monitor_operations":
return await self._monitor_operations(task)
elif task.task_type == "handle_exception":
return await self._handle_exception(task)
elif task.task_type == "generate_report":
return await self._generate_report(task)
else:
return {"status": "unknown_task", "task_type": task.task_type}
async def _orchestrate_order(self, task: AgentTask) -> Dict[str, Any]:
order_data = task.data.get("order", {})
order_id = order_data.get("order_id", "unknown")
logger.info(f"JARVIS: Orchestrating order {order_id}")
order_agent = self._sub_agents.get("ORDER_AGENT")
if order_agent:
await order_agent.submit_task(AgentTask(
task_id=f"{order_id}_validate",
agent_type="order",
task_type="validate_order",
data={"order": order_data}
))
dispatch_agent = self._sub_agents.get("DISPATCH_AGENT")
if dispatch_agent:
await dispatch_agent.submit_task(AgentTask(
task_id=f"{order_id}_dispatch",
agent_type="dispatch",
task_type="analyze_and_assign",
data={"order": order_data}
))
customer_agent = self._sub_agents.get("CUSTOMER_AGENT")
if customer_agent:
await customer_agent.submit_task(AgentTask(
task_id=f"{order_id}_notify",
agent_type="customer",
task_type="send_confirmation",
data={"order": order_data}
))
self._decision_log.append({
"timestamp": datetime.now(),
"action": "orchestrate_order",
"order_id": order_id,
"agents_involved": list(self._sub_agents.keys())
})
return {
"status": "orchestrated",
"order_id": order_id,
"assigned_agents": list(self._sub_agents.keys())
}
async def _monitor_operations(self, task: AgentTask) -> Dict[str, Any]:
status_report = {
"timestamp": datetime.now().isoformat(),
"total_agents": len(self._sub_agents),
"agent_statuses": {},
"active_orders": len(self._active_orders),
"recent_decisions": self._decision_log[-10:]
}
for agent_id, agent in self._sub_agents.items():
status_report["agent_statuses"][agent_id] = {
"status": agent.state.status,
"tasks_completed": agent.state.tasks_completed,
"tasks_failed": agent.state.tasks_failed,
"current_task": agent.state.current_task
}
return status_report
async def _handle_exception(self, task: AgentTask) -> Dict[str, Any]:
exception_type = task.data.get("type", "unknown")
logger.warning(f"JARVIS: Handling exception - {exception_type}")
exception_agent = self._sub_agents.get("EXCEPTION_AGENT")
if exception_agent:
await exception_agent.submit_task(task)
return {"status": "delegated", "exception_type": exception_type}
return {"status": "no_exception_agent"}
async def _generate_report(self, task: AgentTask) -> Dict[str, Any]:
return {
"report_type": task.data.get("type", "summary"),
"generated_at": datetime.now().isoformat(),
"agents": [a.state for a in self._sub_agents.values()],
"decisions": self._decision_log[-50:]
}
async def _heartbeat(self):
for agent_id, agent in self._sub_agents.items():
if agent.state.status == "error":
logger.warning(f"Agent {agent_id} in error state")
class SpecializedAgent(Agent):
"""Base class for specialized domain agents."""
def __init__(self, agent_id: str, domain: str, description: str):
super().__init__(agent_id, domain, description)
self._domain_knowledge: Dict[str, Any] = {}
self._learning_history: List[Dict] = []
async def think(self, context: str, options: List[str] = None) -> str:
return f"[{self.agent_type} reasoning]: {context}"
def learn_from(self, experience: Dict[str, Any]):
self._learning_history.append({
"timestamp": datetime.now(),
"experience": experience
})
if len(self._learning_history) > 100:
self._learning_history = self._learning_history[-100:]

72
core/http_client.py Normal file
View File

@@ -0,0 +1,72 @@
"""Shared aiohttp session with connection pooling and exponential-backoff retry."""
import asyncio
from typing import Optional, Dict, Any
import aiohttp
from core.logger import logger
_session: Optional[aiohttp.ClientSession] = None
def _new_session() -> aiohttp.ClientSession:
connector = aiohttp.TCPConnector(limit=100, limit_per_host=20, keepalive_timeout=30)
return aiohttp.ClientSession(
connector=connector,
timeout=aiohttp.ClientTimeout(total=30),
)
async def get_session() -> aiohttp.ClientSession:
global _session
if _session is None or _session.closed:
_session = _new_session()
return _session
async def close_session():
global _session
if _session and not _session.closed:
await _session.close()
_session = None
async def api_get(url: str, **kwargs) -> Optional[Dict[str, Any]]:
return await _request("get", url, **kwargs)
async def api_post(url: str, **kwargs) -> Optional[Dict[str, Any]]:
return await _request("post", url, **kwargs)
async def api_patch(url: str, **kwargs) -> Optional[Dict[str, Any]]:
return await _request("patch", url, **kwargs)
async def _request(method: str, url: str, **kwargs) -> Optional[Dict[str, Any]]:
"""Execute an HTTP request with up to 3 attempts and exponential backoff."""
max_retries = 3
backoff = 1.0
session = await get_session()
for attempt in range(max_retries):
try:
async with getattr(session, method)(url, **kwargs) as resp:
if resp.content_type == "application/json":
return await resp.json()
return {"status_code": resp.status}
except aiohttp.ClientError as exc:
if attempt < max_retries - 1:
logger.warning(
f"{method.upper()} {url} attempt {attempt + 1} failed: {exc} "
f"— retrying in {backoff:.0f}s"
)
await asyncio.sleep(backoff)
backoff *= 2
else:
logger.error(f"{method.upper()} {url} failed after {max_retries} attempts: {exc}")
except Exception as exc:
logger.error(f"{method.upper()} {url} unexpected error: {exc}")
break
return None

27
core/logger.py Normal file
View File

@@ -0,0 +1,27 @@
"""Structured logging for LogiFlow AI — single loguru setup imported by all modules."""
import sys
import os
from pathlib import Path
from loguru import logger
Path("logs").mkdir(exist_ok=True)
logger.remove()
logger.add(
sys.stderr,
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
level=os.getenv("LOG_LEVEL", "INFO"),
colorize=False,
)
logger.add(
"logs/logiflow_{time:YYYY-MM-DD}.log",
rotation="100 MB",
retention="30 days",
compression="gz",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
)
__all__ = ["logger"]

299
core/message_bus.py Normal file
View File

@@ -0,0 +1,299 @@
"""Agent Communication Bus - backed by NATS JetStream with local fallback."""
import asyncio
import json
import uuid
from datetime import datetime
from typing import Dict, List, Callable, Optional, Any
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum
import nats
import nats.js.errors
from core.types import AgentMessage, MessageType
from core.logger import logger
from config.system_config import NATS_HOST, NATS_PORT, NATS_USER, NATS_PASSWORD
class QueuePriority(str, Enum):
HIGH = "high"
NORMAL = "normal"
LOW = "low"
@dataclass
class QueuedMessage:
message: AgentMessage
priority: QueuePriority = QueuePriority.NORMAL
retry_count: int = 0
max_retries: int = 3
class MessageBus:
"""
Central message bus backed by NATS JetStream.
Call `await message_bus.connect()` once at startup before starting agents.
Falls back to in-process local dispatch when not connected.
"""
def __init__(self):
self._nc = None # NATS client
self._js = None # JetStream context
self._nats_subs: list = [] # hold sub refs to prevent GC
self._subscribers: Dict[MessageType, List[Callable]] = defaultdict(list)
self._queues: Dict[str, List[QueuedMessage]] = defaultdict(list)
self._agents: Dict[str, Any] = {}
self._history: List[AgentMessage] = []
self._hooks: Dict[str, List[Callable]] = defaultdict(list)
self._lock = asyncio.Lock()
# ------------------------------------------------------------------ #
# Connection #
# ------------------------------------------------------------------ #
async def connect(self):
"""Connect to NATS and create the logistics JetStream stream."""
self._nc = await nats.connect(
servers=[f"nats://{NATS_HOST}:{NATS_PORT}"],
user=NATS_USER,
password=NATS_PASSWORD,
error_cb=self._on_nats_error,
reconnected_cb=self._on_reconnect,
max_reconnect_attempts=10,
)
self._js = self._nc.jetstream()
try:
await self._js.add_stream(name="logistics", subjects=["logistics.>"])
logger.info("NATS stream 'logistics' created")
except nats.js.errors.BadRequestError:
pass # stream already exists
except Exception as e:
logger.warning(f"Could not create NATS stream: {e}")
# Set up push subscriptions for all pre-registered message types
for msg_type in list(self._subscribers.keys()):
await self._setup_type_sub(msg_type)
# Set up direct-message subscriptions for all pre-registered agents
for agent_id in list(self._agents.keys()):
await self._setup_agent_sub(agent_id)
logger.info("MessageBus connected to NATS JetStream")
async def _on_nats_error(self, err):
logger.warning(f"NATS error: {err}")
async def _on_reconnect(self):
logger.info("NATS reconnected")
async def disconnect(self):
"""Drain and close NATS connection."""
if self._nc:
await self._nc.drain()
# ------------------------------------------------------------------ #
# Agent registry #
# ------------------------------------------------------------------ #
def register_agent(self, agent):
self._agents[agent.agent_id] = agent
logger.debug(f"Agent registered: {agent.agent_id} ({agent.agent_type})")
if self._js is not None:
asyncio.create_task(self._setup_agent_sub(agent.agent_id))
def unregister_agent(self, agent_id: str):
self._agents.pop(agent_id, None)
logger.debug(f"Agent unregistered: {agent_id}")
# ------------------------------------------------------------------ #
# Pub / Sub #
# ------------------------------------------------------------------ #
def subscribe(self, message_type: MessageType, callback: Callable):
"""Subscribe to a broadcast message type."""
self._subscribers[message_type].append(callback)
if self._js is not None:
asyncio.create_task(self._setup_type_sub(message_type))
def unsubscribe(self, message_type: MessageType, callback: Callable):
if message_type in self._subscribers:
try:
self._subscribers[message_type].remove(callback)
except ValueError:
pass
async def publish(self, message: AgentMessage):
"""Publish a message — via NATS if connected, otherwise local dispatch."""
async with self._lock:
self._history.append(message)
if len(self._history) > 1000:
self._history = self._history[-500:]
if self._js is not None:
await self._nats_publish(message)
else:
await self._local_dispatch(message)
await self._trigger_hook(f"on_{message.message_type.value}", message)
logger.debug(f"[{message.sender}] -> [{message.recipient}]: {message.message_type.value}")
async def send_to_agent(
self,
sender: str,
recipient: str,
message_type: MessageType,
payload: Dict[str, Any],
correlation_id: Optional[str] = None,
) -> str:
message_id = str(uuid.uuid4())
message = AgentMessage(
message_id=message_id,
sender=sender,
recipient=recipient,
message_type=message_type,
payload=payload,
timestamp=datetime.now(),
correlation_id=correlation_id or message_id,
)
await self.publish(message)
return message_id
async def broadcast(
self,
sender: str,
message_type: MessageType,
payload: Dict[str, Any],
correlation_id: Optional[str] = None,
) -> str:
return await self.send_to_agent(sender, "ALL", message_type, payload, correlation_id)
async def get_messages(self, agent_id: str) -> List[AgentMessage]:
async with self._lock:
queued = self._queues.pop(agent_id, [])
return [q.message for q in queued]
async def peek_messages(self, agent_id: str) -> List[AgentMessage]:
return [q.message for q in self._queues.get(agent_id, [])]
# ------------------------------------------------------------------ #
# Hooks #
# ------------------------------------------------------------------ #
def add_hook(self, event: str, callback: Callable):
self._hooks[event].append(callback)
async def _trigger_hook(self, event: str, message: AgentMessage):
for callback in self._hooks.get(event, []):
try:
if asyncio.iscoroutinefunction(callback):
await callback(message)
else:
callback(message)
except Exception as e:
logger.error(f"Hook error [{event}]: {e}")
# ------------------------------------------------------------------ #
# Agent / history helpers #
# ------------------------------------------------------------------ #
def get_agent(self, agent_id: str):
return self._agents.get(agent_id)
def get_all_agents(self) -> Dict[str, Any]:
return self._agents.copy()
def get_messages_by_type(self, message_type: MessageType) -> List[AgentMessage]:
return [m for m in self._history if m.message_type == message_type]
def get_messages_by_sender(self, sender: str) -> List[AgentMessage]:
return [m for m in self._history if m.sender == sender]
def clear_history(self):
self._history = []
# ------------------------------------------------------------------ #
# Internal: NATS helpers #
# ------------------------------------------------------------------ #
async def _nats_publish(self, message: AgentMessage):
if message.recipient != "ALL":
subject = f"logistics.direct.{message.recipient}"
else:
subject = f"logistics.{message.message_type.value}"
try:
await self._js.publish(subject, message.to_json().encode())
except Exception as e:
logger.warning(f"NATS publish error: {e} — falling back to local dispatch")
await self._local_dispatch(message)
async def _setup_type_sub(self, message_type: MessageType):
"""Create a JetStream push subscription for a broadcast message type."""
subject = f"logistics.{message_type.value}"
durable = f"logiflow-{message_type.value}"
callbacks = self._subscribers[message_type]
async def handler(msg):
try:
agent_msg = AgentMessage.from_json(msg.data.decode())
for cb in list(callbacks):
try:
if asyncio.iscoroutinefunction(cb):
await cb(agent_msg)
else:
cb(agent_msg)
except Exception as e:
logger.error(f"Subscriber callback error: {e}")
except Exception as e:
logger.error(f"NATS type-sub decode error [{message_type.value}]: {e}")
finally:
await msg.ack()
try:
sub = await self._js.subscribe(subject, durable=durable, cb=handler)
self._nats_subs.append(sub)
except Exception as e:
logger.warning(f"Could not subscribe to NATS subject {subject}: {e}")
async def _setup_agent_sub(self, agent_id: str):
"""Create a JetStream push subscription for directed messages to an agent."""
subject = f"logistics.direct.{agent_id}"
durable = f"logiflow-direct-{agent_id}"
async def handler(msg):
try:
agent_msg = AgentMessage.from_json(msg.data.decode())
async with self._lock:
self._queues[agent_id].append(QueuedMessage(agent_msg))
except Exception as e:
logger.error(f"NATS agent-sub decode error [{agent_id}]: {e}")
finally:
await msg.ack()
try:
sub = await self._js.subscribe(subject, durable=durable, cb=handler)
self._nats_subs.append(sub)
except Exception as e:
logger.warning(f"Could not subscribe to NATS subject {subject}: {e}")
async def _local_dispatch(self, message: AgentMessage):
"""In-process dispatch used when NATS is not connected."""
if message.recipient != "ALL":
async with self._lock:
self._queues[message.recipient].append(QueuedMessage(message))
else:
for callback in list(self._subscribers.get(message.message_type, [])):
try:
if asyncio.iscoroutinefunction(callback):
await callback(message)
else:
callback(message)
except Exception as e:
logger.error(f"Local dispatch callback error: {e}")
# Global message bus instance
message_bus = MessageBus()

210
core/types.py Normal file
View File

@@ -0,0 +1,210 @@
"""Core types and message protocols for agent communication."""
from enum import Enum
from typing import Optional, Dict, Any, List
from datetime import datetime
from pydantic import BaseModel, Field
from dataclasses import dataclass, asdict
import json
class MessageType(str, Enum):
"""Types of messages agents can send"""
ORDER_RECEIVED = "ORDER_RECEIVED"
ORDER_VALIDATED = "ORDER_VALIDATED"
ORDER_ASSIGNED = "ORDER_ASSIGNED"
ORDER_IN_TRANSIT = "ORDER_IN_TRANSIT"
ORDER_DELIVERED = "ORDER_DELIVERED"
ORDER_CANCELLED = "ORDER_CANCELLED"
ORDER_DELAYED = "ORDER_DELAYED"
HUB_STATUS_UPDATE = "HUB_STATUS_UPDATE"
VEHICLE_ASSIGNED = "VEHICLE_ASSIGNED"
ROUTE_OPTIMIZED = "ROUTE_OPTIMIZED"
NOTIFICATION_SENT = "NOTIFICATION_SENT"
EXCEPTION_DETECTED = "EXCEPTION_DETECTED"
AGENT_TASK = "AGENT_TASK"
AGENT_RESPONSE = "AGENT_RESPONSE"
HEARTBEAT = "HEARTBEAT"
STATUS_QUERY = "STATUS_QUERY"
class Priority(str, Enum):
"""Order priority levels"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class OrderStatus(str, Enum):
"""Order lifecycle status"""
RECEIVED = "received"
VALIDATING = "validating"
VALIDATED = "validated"
PENDING_DISPATCH = "pending_dispatch"
ASSIGNED = "assigned"
PICKED_UP = "picked_up"
IN_TRANSIT = "in_transit"
AT_HUB = "at_hub"
OUT_FOR_DELIVERY = "out_for_delivery"
DELIVERED = "delivered"
FAILED = "failed"
CANCELLED = "cancelled"
RETURNED = "returned"
class ZoneType(str, Enum):
"""Zone classification"""
HUB_TO_HUB = "hub_to_hub"
HUB_TO_SPOKE = "hub_to_spoke"
SPOKE_TO_HUB = "spoke_to_hub"
LAST_MILE = "last_mile"
@dataclass
class Address:
"""Address information"""
street: str
city: str
state: str
pincode: str
landmark: Optional[str] = None
latitude: Optional[float] = None
longitude: Optional[float] = None
@dataclass
class OrderItem:
"""Individual item in an order"""
sku: str
name: str
quantity: int
weight: float # in kg
dimensions: Optional[Dict[str, float]] = None # LxWxH in cm
@dataclass
class Order:
"""Complete order data"""
order_id: str
customer_name: str
customer_phone: str
customer_email: Optional[str]
pickup_address: Address
delivery_address: Address
items: List[OrderItem]
priority: Priority = Priority.MEDIUM
special_instructions: Optional[str] = None
created_at: datetime = None
estimated_delivery: Optional[datetime] = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()
def to_dict(self) -> Dict:
return {
'order_id': self.order_id,
'customer_name': self.customer_name,
'customer_phone': self.customer_phone,
'customer_email': self.customer_email,
'pickup_address': asdict(self.pickup_address),
'delivery_address': asdict(self.delivery_address),
'items': [asdict(item) for item in self.items],
'priority': self.priority.value,
'special_instructions': self.special_instructions,
'created_at': self.created_at.isoformat(),
'estimated_delivery': self.estimated_delivery.isoformat() if self.estimated_delivery else None
}
@dataclass
class Hub:
"""Hub information"""
hub_id: str
name: str
location: str
pincode_prefix: str # First 3 digits of pincode
capacity: int
current_load: int = 0
zones_covered: List[str] = None
def __post_init__(self):
if self.zones_covered is None:
self.zones_covered = []
@dataclass
class Vehicle:
"""Vehicle information"""
vehicle_id: str
vehicle_type: str # bike, scooter, van, truck
capacity_kg: float
capacity_volume: float # in cubic meters
current_location: str
current_load: float = 0
status: str = "available" # available, in_transit, maintenance, offline
assigned_hub: Optional[str] = None
current_route: Optional[List[str]] = None
@dataclass
class AgentMessage:
"""Standard message format for agent communication"""
message_id: str
sender: str
recipient: str # "ALL" for broadcast
message_type: MessageType
payload: Dict[str, Any]
timestamp: datetime
correlation_id: Optional[str] = None # For tracing related messages
def to_json(self) -> str:
data = {
'message_id': self.message_id,
'sender': self.sender,
'recipient': self.recipient,
'message_type': self.message_type.value,
'payload': self.payload,
'timestamp': self.timestamp.isoformat(),
'correlation_id': self.correlation_id
}
return json.dumps(data)
@classmethod
def from_json(cls, json_str: str) -> 'AgentMessage':
data = json.loads(json_str)
return cls(
message_id=data['message_id'],
sender=data['sender'],
recipient=data['recipient'],
message_type=MessageType(data['message_type']),
payload=data['payload'],
timestamp=datetime.fromisoformat(data['timestamp']),
correlation_id=data.get('correlation_id')
)
class AgentTask(BaseModel):
"""Task assigned to an agent"""
task_id: str
agent_type: str
task_type: str
description: str = ""
priority: Priority = Priority.MEDIUM
data: Dict[str, Any] = Field(default_factory=dict)
created_at: datetime = Field(default_factory=datetime.now)
status: str = "pending" # pending, in_progress, completed, failed
result: Optional[Dict] = None
error: Optional[str] = None
class AgentState(BaseModel):
"""Current state of an agent"""
agent_id: str
agent_type: str
status: str # idle, working, waiting, error
current_task: Optional[str] = None
tasks_completed: int = 0
tasks_failed: int = 0
last_active: datetime = Field(default_factory=datetime.now)
performance_metrics: Dict[str, Any] = Field(default_factory=dict)

391
customer_portal/portal.py Normal file
View File

@@ -0,0 +1,391 @@
"""Customer Portal - Track orders and receive updates."""
import streamlit as st
from datetime import datetime, timedelta
import time
# Page configuration
st.set_page_config(
page_title="LogiFlow - Track Your Order",
page_icon="📦",
layout="centered"
)
# Custom CSS
st.markdown("""
<style>
.tracking-header {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
color: white;
margin-bottom: 20px;
}
.timeline-item {
padding: 15px;
border-left: 3px solid #667eea;
margin-left: 20px;
position: relative;
}
.timeline-item::before {
content: '';
position: absolute;
left: -10px;
top: 20px;
width: 15px;
height: 15px;
background: #667eea;
border-radius: 50%;
}
.status-badge {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
}
.status-confirmed { background: #22c55e; color: white; }
.status-picked { background: #3b82f6; color: white; }
.status-transit { background: #f59e0b; color: white; }
.status-delivered { background: #10b981; color: white; }
.order-card {
background: #f8fafc;
border-radius: 12px;
padding: 20px;
margin: 10px 0;
border: 1px solid #e2e8f0;
}
</style>
""", unsafe_allow_html=True)
def render_tracking_header():
"""Render the tracking page header."""
st.markdown("""
<div class="tracking-header">
<h1>📦 LogiFlow</h1>
<p>AI-Powered Logistics Tracking</p>
</div>
""", unsafe_allow_html=True)
def render_order_search():
"""Render order search form."""
st.markdown("### 🔍 Track Your Order")
col1, col2 = st.columns([3, 1])
with col1:
order_id = st.text_input(
"Enter Order ID",
placeholder="ORD-20240605-ABC12345",
label_visibility="collapsed"
)
with col2:
search_btn = st.button("🔎 Track", use_container_width=True)
return order_id, search_btn
def render_tracking_timeline():
"""Render tracking timeline."""
st.markdown("### 📍 Delivery Progress")
# Sample tracking data
events = [
{"time": "2 hours ago", "status": "Out for Delivery", "location": "Andheri West, Mumbai", "desc": "Your package is out for delivery"},
{"time": "5 hours ago", "status": "Arrived at Hub", "location": "Mumbai West Hub", "desc": "Package arrived at sorting facility"},
{"time": "8 hours ago", "status": "In Transit", "location": "Delhi Sorting Center", "desc": "Package on the way to Mumbai"},
{"time": "12 hours ago", "status": "Picked Up", "location": "Connaught Place, Delhi", "desc": "Package picked up by driver"},
{"time": "1 day ago", "status": "Order Confirmed", "location": "System", "desc": "Order placed and confirmed"},
]
for i, event in enumerate(events):
is_completed = i == 0 # Latest is highlighted
st.markdown(f"""
<div class="timeline-item" style="{'border-left-color: #22c55e;' if is_completed else ''}">
<p style="color: #888; margin: 0;">{event['time']}</p>
<h4 style="margin: 5px 0; color: {'#22c55e' if is_completed else '#1a1a1a'};">{event['status']}</h4>
<p style="margin: 5px 0; color: #666;">📍 {event['location']}</p>
<p style="margin: 5px 0; color: #888; font-size: 14px;">{event['desc']}</p>
</div>
""", unsafe_allow_html=True)
def render_order_details():
"""Render order details."""
st.markdown("### 📋 Order Details")
col1, col2 = st.columns(2)
with col1:
st.markdown("""
<div class="order-card">
<h4>📍 Delivery Address</h4>
<p><strong>Rajesh Kumar</strong></p>
<p>402, Sunshine Apartments</p>
<p>Andheri West, Mumbai - 400053</p>
<p>📞 +919876543210</p>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="order-card">
<h4>📦 Package Details</h4>
<p><strong>3 items</strong></p>
<p>• Wireless Headphones</p>
<p>• Phone Case</p>
<p>• Screen Protector</p>
<p>Total Weight: 0.8 kg</p>
</div>
""", unsafe_allow_html=True)
st.markdown("""
<div class="order-card" style="text-align: center;">
<h4>⏱️ Estimated Delivery</h4>
<p style="font-size: 24px; color: #667eea; margin: 10px 0;">Today, 4:00 PM - 6:00 PM</p>
<p style="color: #888;">On schedule - No delays expected</p>
</div>
""", unsafe_allow_html=True)
def render_delivery_partner():
"""Render delivery partner info."""
st.markdown("### 🚴 Delivery Partner")
col1, col2, col3 = st.columns([1, 2, 1])
with col1:
st.markdown("""
<div style="text-align: center;">
<span style="font-size: 48px;">👤</span>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="order-card">
<h4>Arun Sharma</h4>
<p style="color: #888;">Delivery Executive</p>
<p>🛵 Two-wheeler | MH 14 AB 1234</p>
<p>⭐ 4.8 rating (1,234 deliveries)</p>
</div>
""", unsafe_allow_html=True)
with col3:
st.write("")
st.write("")
if st.button("📞 Call", use_container_width=True):
st.info("Calling Arun Sharma...")
if st.button("💬 Chat", use_container_width=True):
st.info("Opening chat...")
def render_ai_assistant():
"""Render AI assistant chat."""
st.markdown("### 🤖 AI Assistant")
# Initialize chat history
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "assistant", "content": "Hi! I'm your LogiFlow AI assistant. How can I help you today?"}
]
# Display chat history
for msg in st.session_state.messages:
if msg["role"] == "user":
st.markdown(f"""
<div style="background: #667eea; color: white; padding: 10px 15px; border-radius: 15px 15px 0 15px; margin: 10px 0; margin-left: 50px;">
{msg['content']}
</div>
""", unsafe_allow_html=True)
else:
st.markdown(f"""
<div style="background: #f1f5f9; padding: 10px 15px; border-radius: 15px 15px 15px 0; margin: 10px 0; margin-right: 50px;">
🤖 {msg['content']}
</div>
""", unsafe_allow_html=True)
# Chat input
if prompt := st.chat_input("Ask me anything about your order..."):
st.session_state.messages.append({"role": "user", "content": prompt})
# AI response
if "where" in prompt.lower() or "track" in prompt.lower():
response = "Your order is currently out for delivery and should arrive by 4:00 PM today. You can see the live location on the map above!"
elif "delay" in prompt.lower() or "late" in prompt.lower():
response = "Good news! Your order is on schedule with no delays. The estimated delivery time remains 4:00 PM - 6:00 PM today."
elif "cancel" in prompt.lower():
response = "I understand you want to cancel. Unfortunately, your order is already out for delivery, so cancellation is no longer possible. Would you like me to reschedule the delivery instead?"
elif "hello" in prompt.lower() or "hi" in prompt.lower():
response = "Hello! I'm here to help you track your order and answer any questions. Your package is doing great and should arrive soon! 📦"
else:
response = "I'm here to help! You can ask me about:\n- 📍 Current location of your order\n- ⏱️ Estimated delivery time\n- 📋 Order details\n- 🚚 Delivery partner info\n- ❓ Any other questions!"
st.session_state.messages.append({"role": "assistant", "content": response})
st.rerun()
def render_map_placeholder():
"""Render map placeholder."""
st.markdown("### 🗺️ Live Tracking Map")
st.markdown("""
<div style="background: #e2e8f0; height: 300px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<span style="font-size: 64px;">🗺️</span>
<p style="color: #666; margin-top: 10px;">Live tracking map</p>
<p style="color: #888; font-size: 14px;">Your delivery partner is currently at:</p>
<p style="color: #667eea; font-size: 18px; font-weight: bold;">📍 Andheri Metro Station, 2.3 km away</p>
</div>
""", unsafe_allow_html=True)
def render_quick_actions():
"""Render quick action buttons."""
st.markdown("### ⚡ Quick Actions")
col1, col2, col3, col4 = st.columns(4)
with col1:
if st.button("📍 Track Order", use_container_width=True):
st.info("Opening tracking view...")
with col2:
if st.button("📞 Call Support", use_container_width=True):
st.info("Connecting to support...")
with col3:
if st.button("🔄 Reschedule", use_container_width=True):
st.warning("This will open the reschedule form")
with col4:
if st.button("❓ Help", use_container_width=True):
st.info("Opening help center...")
def render_delivery_schedule():
"""Render delivery schedule."""
st.markdown("### 📅 Reschedule Delivery")
with st.expander("Change Delivery Time"):
st.write("**Select a new delivery slot:**")
col1, col2, col3 = st.columns(3)
with col1:
st.write("**Today**")
if st.button("2:00 PM - 4:00 PM", use_container_width=True):
st.success("Delivery rescheduled to 2:00 PM - 4:00 PM today")
if st.button("6:00 PM - 8:00 PM", use_container_width=True):
st.success("Delivery rescheduled to 6:00 PM - 8:00 PM today")
with col2:
st.write("**Tomorrow**")
if st.button("9:00 AM - 12:00 PM", use_container_width=True):
st.success("Delivery rescheduled to tomorrow 9:00 AM - 12:00 PM")
if st.button("2:00 PM - 6:00 PM", use_container_width=True):
st.success("Delivery rescheduled to tomorrow 2:00 PM - 6:00 PM")
with col3:
st.write("**Custom**")
custom_date = st.date_input("Select Date", datetime.now() + timedelta(days=1))
custom_time = st.time_input("Select Time", datetime.now().time())
if st.button("Apply Custom Schedule", use_container_width=True):
st.success(f"Delivery rescheduled to {custom_date} at {custom_time}")
def render_feedback():
"""Render feedback section."""
st.markdown("### ⭐ Rate Your Experience")
col1, col2, col3, col4, col5 = st.columns(5)
ratings = ["😞", "😐", "🙂", "😊", "🤩"]
selected = st.feedback(options="faces")
if selected is not None:
st.success(f"Thank you for your {ratings[selected]} feedback!")
def render_notifications():
"""Render notification preferences."""
st.markdown("### 🔔 Notification Preferences")
col1, col2 = st.columns(2)
with col1:
st.checkbox("📱 SMS Notifications", value=True)
st.checkbox("📧 Email Updates", value=True)
with col2:
st.checkbox("💬 WhatsApp Updates", value=True)
st.checkbox("🔔 Push Notifications", value=True)
def main():
"""Main customer portal function."""
# Tabs
tab1, tab2, tab3, tab4 = st.tabs(["📍 Track", "📋 Details", "💬 Help", "⚙️ Settings"])
with tab1:
render_tracking_header()
order_id, search = render_order_search()
if order_id or search:
render_map_placeholder()
render_tracking_timeline()
render_quick_actions()
render_delivery_schedule()
render_ai_assistant()
with tab2:
st.header("📋 Order Details")
render_order_details()
render_delivery_partner()
with tab3:
st.header("💬 Help & Support")
st.markdown("### Frequently Asked Questions")
with st.expander("📦 Where is my order?"):
st.write("Your order is currently out for delivery. Use the tracking tab above to see real-time location updates.")
with st.expander("⏱️ When will my order arrive?"):
st.write("Expected delivery is today between 4:00 PM - 6:00 PM. You can track the exact location in real-time.")
with st.expander("🔄 Can I reschedule delivery?"):
st.write("Yes! You can reschedule your delivery to any available time slot. Go to the Track tab and use the reschedule option.")
with st.expander("❌ Can I cancel my order?"):
st.write("Cancellation is possible only before the order is dispatched. Once out for delivery, you can reschedule instead.")
with st.expander("📞 How do I contact support?"):
st.write("You can call our 24/7 support at 1800-123-4567 or use the in-app chat for instant assistance.")
st.markdown("### Contact Us")
st.write("📞 1800-123-4567 (Toll Free)")
st.write("📧 support@logiflow.ai")
st.write("💬 Chat with us (Available 24/7)")
with tab4:
st.header("⚙️ Settings")
st.markdown("### 📱 Communication Preferences")
render_notifications()
st.markdown("### 🔐 Account")
st.write("**Email:** rajesh@example.com")
st.write("**Phone:** +919876543210")
st.write("**Member since:** January 2024")
if st.button("✏️ Edit Profile"):
st.info("Profile editing coming soon!")
if st.button("🚪 Sign Out"):
st.warning("Sign out functionality coming soon!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,476 @@
"""Admin Dashboard - Real-time monitoring and control interface."""
import streamlit as st
import time
from datetime import datetime
from typing import Dict, List, Any
import asyncio
# Page configuration
st.set_page_config(
page_title="LogiFlow AI - Agent Control Center",
page_icon="🤖",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS
st.markdown("""
<style>
.agent-card {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 20px;
margin: 10px 0;
border-left: 4px solid #00d4ff;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.status-idle { background: #4ade80; color: #000; }
.status-working { background: #fbbf24; color: #000; }
.status-error { background: #f87171; color: #fff; }
.metric-card {
background: #1e1e1e;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.metric-value { font-size: 32px; font-weight: bold; color: #00d4ff; }
.metric-label { font-size: 14px; color: #888; }
.order-card {
background: #252525;
border-radius: 8px;
padding: 15px;
margin: 8px 0;
}
</style>
""", unsafe_allow_html=True)
class DashboardState:
"""Shared state for dashboard."""
def __init__(self):
self.agents = {
"JARVIS": {"status": "idle", "tasks_completed": 0, "current_task": None},
"ORDER_AGENT": {"status": "idle", "tasks_completed": 0, "current_task": None},
"DISPATCH_AGENT": {"status": "idle", "tasks_completed": 0, "current_task": None},
"FLEET_AGENT": {"status": "idle", "tasks_completed": 0, "current_task": None},
"HUB_AGENT": {"status": "idle", "tasks_completed": 0, "current_task": None},
"CUSTOMER_AGENT": {"status": "idle", "tasks_completed": 0, "current_task": None},
"EXCEPTION_AGENT": {"status": "idle", "tasks_completed": 0, "current_task": None},
"ROUTE_OPTIMIZER": {"status": "idle", "tasks_completed": 0, "current_task": None},
}
self.orders = []
self.vehicles = []
self.hubs = []
self.messages = []
def update_agent_status(self, agent_id: str, status: str, task: str = None):
if agent_id in self.agents:
self.agents[agent_id]["status"] = status
self.agents[agent_id]["current_task"] = task
if status == "idle" and task is None:
self.agents[agent_id]["tasks_completed"] += 1
def add_message(self, sender: str, recipient: str, message_type: str, content: str):
self.messages.append({
"timestamp": datetime.now(),
"sender": sender,
"recipient": recipient,
"type": message_type,
"content": content
})
if len(self.messages) > 100:
self.messages = self.messages[-100:]
# Initialize state
if 'state' not in st.session_state:
st.session_state.state = DashboardState()
def render_header():
"""Render the dashboard header."""
col1, col2, col3 = st.columns([2, 3, 1])
with col1:
st.markdown("""
<div style="padding: 20px;">
<h1 style="margin: 0; color: #00d4ff;">🤖 LogiFlow AI</h1>
<p style="margin: 5px 0 0 0; color: #888;">Agent Control Center</p>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div style="padding: 20px; text-align: center;">
<h3 style="color: #4ade80;">🟢 System Operational</h3>
<p style="color: #888; margin: 0;">All agents active and monitoring</p>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown(f"""
<div style="padding: 20px; text-align: right;">
<p style="color: #888; margin: 0;">{datetime.now().strftime('%Y-%m-%d')}</p>
<p style="color: #00d4ff; font-size: 24px; margin: 5px 0;">{datetime.now().strftime('%H:%M:%S')}</p>
</div>
""", unsafe_allow_html=True)
def render_agent_grid(state: DashboardState):
"""Render agent status cards."""
st.markdown("## 🤖 Agent Status")
cols = st.columns(4)
agent_icons = {
"JARVIS": "🧠",
"ORDER_AGENT": "📦",
"DISPATCH_AGENT": "🚚",
"FLEET_AGENT": "🚛",
"HUB_AGENT": "🏭",
"CUSTOMER_AGENT": "💬",
"EXCEPTION_AGENT": "⚠️",
"ROUTE_OPTIMIZER": "🗺️"
}
for i, (agent_id, info) in enumerate(state.agents.items()):
with cols[i % 4]:
status_class = f"status-{info['status']}"
icon = agent_icons.get(agent_id, "🤖")
st.markdown(f"""
<div class="agent-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h4 style="margin: 0;">{icon} {agent_id}</h4>
<span class="status-badge {status_class}">{info['status'].upper()}</span>
</div>
<div style="margin-top: 10px; color: #888;">
<p style="margin: 5px 0;">Tasks: {info['tasks_completed']}</p>
<p style="margin: 5px 0;">Current: {info['current_task'] or 'None'}</p>
</div>
</div>
""", unsafe_allow_html=True)
def render_metrics(state: DashboardState):
"""Render key metrics."""
col1, col2, col3, col4, col5 = st.columns(5)
metrics = [
("Active Orders", len([o for o in state.orders if o.get("status") != "delivered"]), "📦"),
("Vehicles Active", len([v for v in state.vehicles if v.get("status") == "in_transit"]), "🚛"),
("Hubs Online", len(state.hubs), "🏭"),
("Messages/min", len([m for m in state.messages if (datetime.now() - m["timestamp"]).seconds < 60]), "📨"),
("Success Rate", "98.5%", "")
]
for col, (label, value, icon) in zip([col1, col2, col3, col4, col5], metrics):
with col:
st.markdown(f"""
<div class="metric-card">
<div style="font-size: 32px;">{icon}</div>
<div class="metric-value">{value}</div>
<div class="metric-label">{label}</div>
</div>
""", unsafe_allow_html=True)
def render_order_list(state: DashboardState):
"""Render recent orders."""
st.markdown("## 📦 Recent Orders")
if not state.orders:
st.info("No orders yet. Create an order to see it here.")
return
for order in state.orders[-10:][::-1]:
status_colors = {
"received": "🟡",
"in_transit": "🔵",
"delivered": "🟢",
"cancelled": "🔴"
}
status_icon = status_colors.get(order.get("status", "received"), "")
with st.expander(f"{status_icon} {order.get('order_id', 'N/A')} - {order.get('customer_name', 'Unknown')}"):
col1, col2 = st.columns(2)
with col1:
st.write(f"**Status:** {order.get('status', 'Unknown')}")
st.write(f"**Priority:** {order.get('category', {}).get('priority', 'Medium')}")
st.write(f"**Created:** {order.get('created_at', 'N/A')}")
with col2:
st.write(f"**Pickup:** {order.get('pickup_address', {}).get('city', 'N/A')}")
st.write(f"**Delivery:** {order.get('delivery_address', {}).get('city', 'N/A')}")
st.write(f"**Items:** {len(order.get('items', []))}")
def render_vehicle_fleet(state: DashboardState):
"""Render vehicle fleet status."""
st.markdown("## 🚛 Vehicle Fleet")
if not state.vehicles:
st.info("No vehicles registered.")
return
for vehicle in state.vehicles[:20]:
status_icons = {
"available": "🟢",
"in_transit": "🔵",
"maintenance": "🟡",
"offline": ""
}
icon = status_icons.get(vehicle.get("status", "available"), "")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.write(f"{icon} **{vehicle.get('vehicle_id', 'N/A')}**")
with col2:
st.write(f"Type: {vehicle.get('type', 'N/A')}")
with col3:
st.write(f"Hub: {vehicle.get('hub', 'N/A')}")
with col4:
st.write(f"Status: {vehicle.get('status', 'N/A')}")
st.divider()
def render_hub_network(state: DashboardState):
"""Render hub network status."""
st.markdown("## 🏭 Hub Network")
cols = st.columns(4)
hub_data = [
{"id": "DL-HUB-01", "name": "Delhi North", "load": 150, "capacity": 500},
{"id": "DL-HUB-02", "name": "Delhi South", "load": 200, "capacity": 400},
{"id": "MU-HUB-01", "name": "Mumbai West", "load": 300, "capacity": 600},
{"id": "MU-HUB-02", "name": "Mumbai East", "load": 250, "capacity": 550},
{"id": "BL-HUB-01", "name": "Bangalore", "load": 180, "capacity": 500},
{"id": "HY-HUB-01", "name": "Hyderabad", "load": 220, "capacity": 450},
{"id": "PU-HUB-01", "name": "Pune", "load": 160, "capacity": 400},
{"id": "KL-HUB-01", "name": "Kolkata", "load": 140, "capacity": 350},
]
for i, hub in enumerate(hub_data):
with cols[i % 4]:
utilization = (hub["load"] / hub["capacity"]) * 100
color = "🟢" if utilization < 70 else "🟡" if utilization < 90 else "🔴"
st.markdown(f"""
<div class="agent-card">
<h4>{color} {hub['name']}</h4>
<p style="margin: 5px 0;">ID: {hub['id']}</p>
<p style="margin: 5px 0;">Load: {hub['load']}/{hub['capacity']}</p>
<p style="margin: 5px 0; color: #00d4ff;">Utilization: {utilization:.1f}%</p>
</div>
""", unsafe_allow_html=True)
def render_message_feed(state: DashboardState):
"""Render real-time message feed."""
st.markdown("## 📨 Agent Communication Feed")
if not state.messages:
st.info("No messages yet.")
return
for msg in state.messages[-20:][::-1]:
st.markdown(f"""
<div style="background: #1e1e1e; padding: 10px; border-radius: 5px; margin: 5px 0;">
<span style="color: #888;">{msg['timestamp'].strftime('%H:%M:%S')}</span>
<span style="color: #00d4ff;"> [{msg['sender']}]</span>
→ <span style="color: #4ade80;">[{msg['recipient']}]</span>
<span style="color: #fbbf24;"> {msg['type']}</span>
<p style="margin: 5px 0 0 0; color: #ccc;">{msg['content']}</p>
</div>
""", unsafe_allow_html=True)
def render_order_creator():
"""Render order creation form."""
st.markdown("## Create New Order")
with st.form("order_form"):
col1, col2 = st.columns(2)
with col1:
st.write("**Customer Details**")
customer_name = st.text_input("Customer Name", "Rajesh Kumar")
customer_phone = st.text_input("Phone", "+919876543210")
customer_email = st.text_input("Email", "rajesh@example.com")
with col2:
st.write("**Order Priority**")
priority = st.selectbox("Priority", ["low", "medium", "high", "urgent"])
items_count = st.number_input("Number of Items", 1, 50, 3)
weight = st.number_input("Total Weight (kg)", 0.1, 500.0, 5.0)
st.write("**Pickup Address**")
col_p1, col_p2, col_p3 = st.columns(3)
with col_p1:
pickup_city = st.text_input("City", "Delhi")
with col_p2:
pickup_state = st.text_input("State", "Delhi")
with col_p3:
pickup_pincode = st.text_input("Pincode", "110001")
st.write("**Delivery Address**")
col_d1, col_d2, col_d3 = st.columns(3)
with col_d1:
delivery_city = st.text_input("City", "Mumbai", key="del_city")
with col_d2:
delivery_state = st.text_input("State", "Maharashtra", key="del_state")
with col_d3:
delivery_pincode = st.text_input("Pincode", "400001", key="del_pin")
submitted = st.form_submit_button("🚀 Create Order via AI Agents", use_container_width=True)
if submitted:
st.success("Order created! Watch the agents process it in real-time.")
# Simulate order being created
if 'state' in st.session_state:
state = st.session_state.state
order_id = f"ORD-{datetime.now().strftime('%Y%m%d')}-{len(state.orders) + 1:04d}"
state.orders.append({
"order_id": order_id,
"customer_name": customer_name,
"customer_phone": customer_phone,
"status": "received",
"priority": priority,
"pickup_address": {"city": pickup_city, "pincode": pickup_pincode},
"delivery_address": {"city": delivery_city, "pincode": delivery_pincode},
"items": [{"name": f"Item {i}", "weight": weight/items_count} for i in range(int(items_count))],
"created_at": datetime.now().isoformat(),
"category": {"priority": priority}
})
state.add_message("ADMIN", "JARVIS", "ORDER_SUBMIT", f"New order {order_id} submitted")
state.update_agent_status("ORDER_AGENT", "working", f"Validating {order_id}")
st.rerun()
def render_exception_panel(state: DashboardState):
"""Render exception handling panel."""
st.markdown("## ⚠️ Exception Management")
col1, col2 = st.columns(2)
with col1:
st.write("**Quick Actions**")
if st.button("🚨 Simulate Delay Exception"):
state.add_message("SYSTEM", "EXCEPTION_AGENT", "EXCEPTION", "Simulated delay detected")
st.warning("Delay exception triggered - Exception Agent handling...")
with col2:
st.write("**Exception Queue**")
st.info("No active exceptions - System running smoothly")
def main():
"""Main dashboard function."""
# Sidebar navigation
st.sidebar.markdown("## 🧭 Navigation")
page = st.sidebar.radio("Go to", [
"📊 Overview",
"🤖 Agents",
"📦 Orders",
"🚛 Fleet",
"🏭 Hubs",
"⚠️ Exceptions",
"💬 Messages",
" Create Order"
])
# Initialize state
state = st.session_state.state
# Simulate some data if empty
if not state.vehicles:
state.vehicles = [
{"vehicle_id": "DL-V-001", "type": "van", "hub": "DL-HUB-01", "status": "in_transit", "location": {"lat": 28.61, "lng": 77.20}},
{"vehicle_id": "DL-V-002", "type": "van", "hub": "DL-HUB-01", "status": "available", "location": {"lat": 28.62, "lng": 77.21}},
{"vehicle_id": "MU-V-001", "type": "van", "hub": "MU-HUB-01", "status": "in_transit", "location": {"lat": 19.07, "lng": 72.87}},
{"vehicle_id": "BL-V-001", "type": "van", "hub": "BL-HUB-01", "status": "available", "location": {"lat": 12.97, "lng": 77.59}},
{"vehicle_id": "DL-B-001", "type": "bike", "hub": "DL-HUB-01", "status": "in_transit", "location": {"lat": 28.63, "lng": 77.19}},
]
if not state.hubs:
state.hubs = [
{"hub_id": "DL-HUB-01", "name": "Delhi North Hub"},
{"hub_id": "DL-HUB-02", "name": "Delhi South Hub"},
{"hub_id": "MU-HUB-01", "name": "Mumbai West Hub"},
{"hub_id": "BL-HUB-01", "name": "Bangalore Hub"},
]
# Render based on selection
if page == "📊 Overview":
render_header()
render_metrics(state)
render_agent_grid(state)
col_overview1, col_overview2 = st.columns(2)
with col_overview1:
render_order_list(state)
with col_overview2:
render_hub_network(state)
elif page == "🤖 Agents":
render_header()
st.markdown("# 🤖 Agent Control Center")
render_agent_grid(state)
st.markdown("### Agent Communication Pattern")
st.markdown("""
```
JARVIS (Master)
├── ORDER_AGENT → Validates & categorizes orders
├── DISPATCH_AGENT → Assigns routes & zones
├── FLEET_AGENT → Manages vehicle allocation
├── HUB_AGENT → Coordinates hub operations
├── CUSTOMER_AGENT → Sends notifications
├── EXCEPTION_AGENT → Handles problems
└── ROUTE_OPTIMIZER → Plans optimal paths
```
""")
elif page == "📦 Orders":
render_header()
render_order_list(state)
render_order_creator()
elif page == "🚛 Fleet":
render_header()
render_vehicle_fleet(state)
elif page == "🏭 Hubs":
render_header()
render_hub_network(state)
elif page == "⚠️ Exceptions":
render_header()
render_exception_panel(state)
elif page == "💬 Messages":
render_header()
render_message_feed(state)
elif page == " Create Order":
render_header()
render_order_creator()
# Auto-refresh
time.sleep(1)
st.rerun()
if __name__ == "__main__":
main()

493
doormile_test.py Normal file
View File

@@ -0,0 +1,493 @@
#!/usr/bin/env python3
"""
Doormile Full System Test Suite v2
Fixed: correct routes, 2-step login, pricing payload, health endpoint
"""
import asyncio
import aiohttp
import json
import time
import websockets
import nats
import redis.asyncio as aioredis
from datetime import datetime
# ── Config ────────────────────────────────────────────────────────────────────
API_BASE = "https://api.doormile.com"
NATS_URL = "nats://66.116.226.161:4223"
NATS_USER = "doormile"
NATS_PASSWORD = "Package@321#"
REDIS_HOST = "66.116.226.255"
REDIS_PORT = 6380
REDIS_PASSWORD = "Package@321#"
INTERNAL_KEY = "doormile-internal-2024"
# Test customer — uses the one created by Window 1 test
TEST_PHONE = "9900000001"
TEST_PIN = "9999"
# Colors
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
BOLD = "\033[1m"
RESET = "\033[0m"
results = []
def log(msg, color=RESET):
ts = datetime.now().strftime("%H:%M:%S")
print(f"{color}[{ts}] {msg}{RESET}")
def pass_test(name, detail=""):
results.append(("PASS", name))
log(f"✅ PASS — {name} {detail}", GREEN)
def fail_test(name, detail=""):
results.append(("FAIL", name))
log(f"❌ FAIL — {name} {detail}", RED)
def info(msg):
log(f" {msg}", CYAN)
def section(title):
print(f"\n{BOLD}{YELLOW}{'='*60}{RESET}")
print(f"{BOLD}{YELLOW} {title}{RESET}")
print(f"{BOLD}{YELLOW}{'='*60}{RESET}\n")
# ── Tests ─────────────────────────────────────────────────────────────────────
async def test_health(session):
section("1. HEALTH CHECK")
for path in ["/api/v1/health", "/api/v1/ready"]:
try:
async with session.get(f"{API_BASE}{path}",
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test(f"GET {path}", str(data))
else:
fail_test(f"GET {path}", f"status={r.status}")
except Exception as e:
fail_test(f"GET {path}", str(e))
async def login(session):
section("2. CUSTOMER LOGIN (2-step)")
token = None
try:
# Step 1
async with session.post(f"{API_BASE}/api/v1/customer/login",
json={"phone": TEST_PHONE},
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Login step 1 — phone", f"phone={TEST_PHONE}")
info(str(data))
else:
fail_test("Login step 1 — phone", f"status={r.status} {data}")
return None
# Step 2
async with session.post(f"{API_BASE}/api/v1/customer/verify-pin",
json={"phone": TEST_PHONE, "pin": TEST_PIN},
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
token = data.get("token") or data.get("jwt") or data.get("accesstoken")
if token:
pass_test("Login step 2 — PIN verify", "JWT token obtained")
info(f"Token: {token[:50]}...")
else:
fail_test("Login step 2 — PIN verify", f"No token in resp: {data}")
else:
fail_test("Login step 2 — PIN verify", f"status={r.status} {data}")
except Exception as e:
fail_test("Login", str(e))
return token
async def test_profile(session, token):
section("3. CUSTOMER PROFILE")
try:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(f"{API_BASE}/api/v1/customer/profile",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("GET /customer/profile", f"name={data.get('firstname','')} {data.get('lastname','')}")
else:
fail_test("GET /customer/profile", f"status={r.status} {data}")
except Exception as e:
fail_test("Customer profile", str(e))
async def test_pricing(session, token):
section("4. PRICING CHECK (Redis-backed DoormilePricing)")
try:
# Correct payload — zone directly, not pincodes
payload = {
"zone": "OtherState",
"service_type": "Normal",
"weight": 1.0,
"itemcategory": "Documents"
}
headers = {"Authorization": f"Bearer {token}"}
async with session.post(f"{API_BASE}/api/v1/pricing/check",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Pricing check — OtherState/Normal", f"resp={data}")
else:
fail_test("Pricing check", f"status={r.status} resp={data}")
# Also test Local pricing
payload2 = {"zone": "Local", "service_type": "Normal",
"weight": 0.5, "itemcategory": "Documents"}
async with session.post(f"{API_BASE}/api/v1/pricing/check",
json=payload2,
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Pricing check — Local/Normal", f"resp={data}")
else:
fail_test("Pricing check — Local/Normal", f"status={r.status} {data}")
except Exception as e:
fail_test("Pricing check", str(e))
async def test_city_gate(session, token):
section("5. CITY GATING — should REJECT Mumbai (400xxx)")
try:
payload = {
"pickupaddress": "MG Road, Mumbai",
"pickuppincode": "400001",
"pickuplatitude": 18.9388,
"pickuplongitude": 72.8354,
"deliveryaddress": "Hitech City, Hyderabad",
"deliverypincode": "500032",
"serviceoption": "Normal",
"parcels": [{"itemcategory": "Documents",
"itemdescription": "test", "weight": 0.5}]
}
headers = {"Authorization": f"Bearer {token}"}
async with session.post(f"{API_BASE}/api/v1/customer/bookings",
json=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 400:
pass_test("City gate — Mumbai rejected ✓", str(data))
else:
fail_test("City gate — Mumbai should be rejected",
f"got status={r.status} — city gate may not be active")
except Exception as e:
fail_test("City gate test", str(e))
async def create_booking(session, token):
section("6. CREATE BOOKING — Coimbatore → Hyderabad")
booking_id = None
try:
payload = {
"pickupaddress": "Gandhipuram, Coimbatore, Tamil Nadu",
"pickuppincode": "641001",
"pickuplatitude": 11.0168,
"pickuplongitude": 76.9558,
"deliveryaddress": "Hitech City, Hyderabad, Telangana",
"deliverypincode": "500032",
"serviceoption": "Normal",
"parcels": [
{
"itemcategory": "Documents",
"itemdescription": "Doormile E2E test parcel",
"weight": 0.5,
"length": 20,
"width": 15,
"height": 5
}
]
}
headers = {"Authorization": f"Bearer {token}"}
async with session.post(f"{API_BASE}/api/v1/customer/bookings",
json=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=15)) as r:
data = await r.json()
if r.status in [200, 201]:
# Try common field names for booking id
booking_id = (data.get("bookingid") or
data.get("booking_id") or
data.get("id") or
data.get("data", {}).get("bookingid"))
pass_test("Booking created", f"id={booking_id}")
info(f"Response: {json.dumps(data, indent=2)}")
else:
fail_test("Create booking", f"status={r.status} resp={data}")
except Exception as e:
fail_test("Create booking", str(e))
return booking_id
async def test_assignment(session, token, booking_id):
section("7. AUTO-ASSIGNMENT ENGINE (waiting 12s...)")
if not booking_id:
fail_test("Assignment check", "No booking_id from step 6")
return
info("Giving assignment engine 12 seconds to fire goroutine...")
for i in range(12, 0, -3):
info(f" {i}s remaining...")
await asyncio.sleep(3)
try:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(f"{API_BASE}/api/v1/customer/bookings/{booking_id}",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
status = (data.get("status") or
data.get("booking", {}).get("status", "unknown"))
miler = (data.get("assignedmileruserid") or
data.get("booking", {}).get("assignedmileruserid", "none"))
info(f"Booking status: {status}")
info(f"Assigned miler: {miler}")
if status in ["Miler_Assigned", "Pickup_Scheduled"]:
pass_test("Auto-assignment fired ✓", f"status={status} miler={miler}")
elif status == "Created":
fail_test("Auto-assignment", "Still 'Created' — check kubectl logs for assignment engine errors")
else:
pass_test("Booking updated", f"status={status}")
else:
fail_test("Check booking", f"status={r.status} {data}")
except Exception as e:
fail_test("Assignment check", str(e))
async def test_redis(booking_id):
section("8. REDIS — GEO index, booking cache, pricing cache")
try:
r = aioredis.Redis(host=REDIS_HOST, port=REDIS_PORT,
password=REDIS_PASSWORD, decode_responses=True)
await r.ping()
pass_test("Redis connection", f"{REDIS_HOST}:{REDIS_PORT}")
# Miler GEO index
count = await r.zcard("milers:locations")
if count > 0:
pass_test("milers:locations GEO index", f"{count} milers indexed")
# Find milers near Coimbatore
nearby = await r.georadius(
"milers:locations", 76.9558, 11.0168, 10, "km",
withcoord=True, count=5, sort="ASC"
)
if nearby:
pass_test("GEORADIUS near Coimbatore", f"found {len(nearby)} milers")
for m in nearby:
info(f" Miler: {m}")
else:
fail_test("GEORADIUS near Coimbatore", "No milers found in 10km radius")
else:
fail_test("milers:locations GEO index", "Empty — milers have no GPS in Redis")
# Booking cache
if booking_id:
data = await r.hgetall(f"bookings:{booking_id}")
if data:
pass_test("Booking Redis cache", f"keys={list(data.keys())}")
else:
fail_test("Booking Redis cache",
f"bookings:{booking_id} not in Redis — NATS worker may not be running")
# Pricing cache
keys = await r.keys("doormile:pricing:*")
if keys:
pass_test("Pricing Redis cache", f"{len(keys)} slabs cached")
else:
fail_test("Pricing Redis cache", "No pricing keys in Redis")
await r.aclose()
except Exception as e:
fail_test("Redis tests", str(e))
async def test_nats(booking_id):
section("9. NATS JETSTREAM — streams + publish test")
try:
nc = await nats.connect(
servers=[NATS_URL],
user=NATS_USER,
password=NATS_PASSWORD
)
js = nc.jetstream()
pass_test("NATS connected", NATS_URL)
streams = ["BOOKINGS", "TRACKING", "ASSIGNMENTS",
"NOTIFICATIONS", "CHAT", "STATUS"]
for stream in streams:
try:
info_obj = await js.stream_info(stream)
pass_test(f"Stream {stream}",
f"msgs={info_obj.state.messages}")
except Exception as e:
fail_test(f"Stream {stream}", str(e))
# Publish test GPS event
if booking_id:
evt = json.dumps({
"miler_id": "test_miler_001",
"booking_id": booking_id,
"lat": 11.0175,
"lon": 76.9565,
"timestamp": int(time.time())
})
ack = await js.publish("miler.location.updated", evt.encode())
pass_test("Publish miler.location.updated", f"seq={ack.seq}")
await nc.close()
except Exception as e:
fail_test("NATS tests", str(e))
async def test_internal_notify(session, booking_id):
section("10. INTERNAL NOTIFY API (/api/v1/internal/notify)")
if not booking_id:
fail_test("Internal notify", "No booking_id")
return
try:
payload = {
"booking_id": booking_id,
"target": "customer",
"title": "Doormile Test ✅",
"message": "E2E test notification — system is working!"
}
headers = {
"X-Internal-Key": INTERNAL_KEY,
"Content-Type": "application/json"
}
async with session.post(f"{API_BASE}/api/v1/internal/notify",
json=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Internal notify", f"resp={data}")
else:
fail_test("Internal notify", f"status={r.status} resp={data}")
except Exception as e:
fail_test("Internal notify", str(e))
async def test_reassign(session, booking_id):
section("11. INTERNAL REASSIGN API")
if not booking_id:
fail_test("Internal reassign", "No booking_id")
return
try:
headers = {
"X-Internal-Key": INTERNAL_KEY,
"Content-Type": "application/json"
}
async with session.post(
f"{API_BASE}/api/v1/internal/bookings/{booking_id}/reassign",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Internal reassign", f"resp={data}")
elif r.status == 400:
info(f"Reassign returned 400 — booking may not be in assignable state: {data}")
pass_test("Internal reassign endpoint reachable", f"status=400 (expected if not assigned yet)")
else:
fail_test("Internal reassign", f"status={r.status} resp={data}")
except Exception as e:
fail_test("Internal reassign", str(e))
async def test_websocket(booking_id):
section("12. WEBSOCKET LIVE TRACKING")
if not booking_id:
fail_test("WebSocket", "No booking_id")
return
try:
ws_url = f"wss://api.doormile.com/ws/bookings/{booking_id}/track"
info(f"Connecting to {ws_url}")
async with websockets.connect(ws_url, open_timeout=10) as ws:
pass_test("WebSocket connected", ws_url)
try:
msg = await asyncio.wait_for(ws.recv(), timeout=5)
data = json.loads(msg)
pass_test("WebSocket frame received", str(data))
except asyncio.TimeoutError:
info("No frame in 5s — miler GPS not streaming yet (normal for test)")
pass_test("WebSocket connection works", "no GPS frame yet — expected")
except Exception as e:
fail_test("WebSocket", str(e))
async def test_booking_cache_api(session, token, booking_id):
section("13. BOOKING CACHE API (Redis GET endpoint)")
if not booking_id:
fail_test("Booking cache API", "No booking_id")
return
try:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(
f"{API_BASE}/api/v1/bookings/cache/{booking_id}",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("GET /bookings/cache/:id", f"fields={list(data.keys()) if isinstance(data,dict) else data}")
else:
fail_test("GET /bookings/cache/:id", f"status={r.status} {data}")
except Exception as e:
fail_test("Booking cache API", str(e))
def print_summary():
section("FINAL TEST SUMMARY")
passed = [r for r in results if r[0] == "PASS"]
failed = [r for r in results if r[0] == "FAIL"]
for status, name in results:
icon = "" if status == "PASS" else ""
color = GREEN if status == "PASS" else RED
print(f" {color}{icon} {name}{RESET}")
pct = int(len(passed) / len(results) * 100) if results else 0
print(f"\n{BOLD}{'='*60}{RESET}")
print(f"{BOLD} {len(results)} tests | "
f"{GREEN}{len(passed)} passed{RESET}{BOLD} | "
f"{RED}{len(failed)} failed{RESET}{BOLD} | "
f"{YELLOW}{pct}% success{RESET}")
print(f"{BOLD}{'='*60}{RESET}\n")
if failed:
print(f"{RED}Failed tests to investigate:{RESET}")
for _, name in failed:
print(f"{name}")
print()
async def main():
print(f"\n{BOLD}{BLUE}")
print("╔══════════════════════════════════════════════════════════╗")
print("║ DOORMILE FULL SYSTEM TEST SUITE v2 ║")
print("║ Backend · NATS · Redis · Assignment · WebSocket · FCM ║")
print("╚══════════════════════════════════════════════════════════╝")
print(f"{RESET}\n")
async with aiohttp.ClientSession() as session:
await test_health(session)
token = await login(session)
if not token:
fail_test("STOPPING — no auth token")
print_summary()
return
await test_profile(session, token)
await test_pricing(session, token)
await test_city_gate(session, token)
booking_id = await create_booking(session, token)
await test_assignment(session, token, booking_id)
await test_redis(booking_id)
await test_nats(booking_id)
await test_internal_notify(session, booking_id)
await test_reassign(session, booking_id)
await test_websocket(booking_id)
await test_booking_cache_api(session, token, booking_id)
print_summary()
if __name__ == "__main__":
asyncio.run(main())

206
main.py Normal file
View File

@@ -0,0 +1,206 @@
"""
LogiFlow AI - Autonomous Logistics Agent System
=================================================
Usage:
python main.py --production # Start all agents (production mode)
python main.py --dashboard # Launch admin dashboard
python main.py --portal # Launch customer portal
python main.py --help # Show this help
"""
import asyncio
import sys
import signal
from datetime import datetime
from typing import Dict, Any
# Force UTF-8 on Windows so emoji in output doesn't crash
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
from dotenv import load_dotenv
load_dotenv() # must happen before any config import reads os.getenv()
from core.logger import logger
from core.agent import MasterAgent, Agent
from core.message_bus import message_bus
from core.http_client import close_session
from core.types import AgentTask, MessageType, Priority
from agents.order_agent import OrderAgent
from agents.dispatch_agent import DispatchAgent
from agents.fleet_agent import FleetAgent
from agents.hub_agent import HubAgent
from agents.customer_agent import CustomerAgent
from agents.exception_agent import ExceptionAgent
from agents.route_optimizer_agent import RouteOptimizerAgent
class LogiFlowAI:
"""Main orchestration class — manages all agents and system lifecycle."""
def __init__(self):
logger.info("=" * 60)
logger.info("LOGIFLOW AI - AUTONOMOUS LOGISTICS SYSTEM")
logger.info("=" * 60)
self.master_agent = MasterAgent()
self.agents: Dict[str, Agent] = {}
self.running = False
def initialize_agents(self):
logger.info("Initializing AI Agents...")
self.agents["ORDER_AGENT"] = OrderAgent()
self.agents["DISPATCH_AGENT"] = DispatchAgent()
self.agents["FLEET_AGENT"] = FleetAgent()
self.agents["HUB_AGENT"] = HubAgent()
self.agents["CUSTOMER_AGENT"] = CustomerAgent()
self.agents["EXCEPTION_AGENT"] = ExceptionAgent()
self.agents["ROUTE_OPTIMIZER"] = RouteOptimizerAgent()
for agent in self.agents.values():
self.master_agent.register_sub_agent(agent)
logger.info(f"{len(self.agents) + 1} agents initialized (JARVIS + {len(self.agents)} specialized)")
async def start_agents(self):
logger.info("Starting Agent System...")
self.running = True
tasks = [asyncio.create_task(self.master_agent.start())]
tasks += [asyncio.create_task(a.start()) for a in self.agents.values()]
logger.info("All agents running — waiting for work")
try:
await asyncio.gather(*tasks, return_exceptions=True)
except asyncio.CancelledError:
pass
finally:
self.running = False
async def stop(self):
self.running = False
await self.master_agent.stop()
for agent in self.agents.values():
await agent.stop()
def get_system_status(self) -> Dict[str, Any]:
status = {
"timestamp": datetime.now().isoformat(),
"system": "operational" if self.running else "stopped",
"agents": {},
}
for agent_id, agent in self.agents.items():
status["agents"][agent_id] = {
"status": agent.state.status,
"tasks_completed": agent.state.tasks_completed,
"current_task": agent.state.current_task,
}
status["agents"]["JARVIS"] = {
"status": self.master_agent.state.status,
"sub_agents": len(self.agents),
}
return status
# ------------------------------------------------------------------ #
# Run modes #
# ------------------------------------------------------------------ #
async def production_mode():
logger.info("PRODUCTION MODE — connecting to infrastructure")
try:
await message_bus.connect()
except Exception as e:
logger.warning(f"Could not connect to NATS: {e} — using local in-process fallback")
system = LogiFlowAI()
system.initialize_agents()
logger.info("Infrastructure connected. Press Ctrl+C to stop.")
loop = asyncio.get_running_loop()
def _shutdown(signum, frame):
logger.info("Shutdown signal received — stopping agents...")
loop.create_task(_graceful_shutdown(system))
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
try:
await system.start_agents()
finally:
await message_bus.disconnect()
await close_session()
logger.info("Clean shutdown complete")
async def _graceful_shutdown(system: LogiFlowAI):
await system.stop()
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for t in tasks:
t.cancel()
async def dashboard_mode():
import subprocess
logger.info("Launching Admin Dashboard at http://localhost:8501")
subprocess.run(["streamlit", "run", "dashboard/admin_dashboard.py"])
async def portal_mode():
import subprocess
logger.info("Launching Customer Portal at http://localhost:8502")
subprocess.run(["streamlit", "run", "customer_portal/portal.py", "--server.port", "8502"])
# ------------------------------------------------------------------ #
# Entry point #
# ------------------------------------------------------------------ #
def main():
if len(sys.argv) < 2 or sys.argv[1] == "--help":
print_help()
return
cmd = sys.argv[1]
if cmd == "--production":
asyncio.run(production_mode())
elif cmd == "--dashboard":
asyncio.run(dashboard_mode())
elif cmd == "--portal":
asyncio.run(portal_mode())
else:
logger.error(f"Unknown argument: {cmd}")
print_help()
sys.exit(1)
def print_help():
print("""
LogiFlow AI - Autonomous Logistics Agent System
===============================================
Usage:
python main.py --production Connect to NATS/Redis/Postgres and run forever
python main.py --dashboard Launch Streamlit admin dashboard
python main.py --portal Launch Streamlit customer portal
python main.py --help Show this help
Infrastructure (set in .env):
NATS nats://doormile@66.116.226.161:4223
Redis 66.116.226.255:6380
PG DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD
API GO_API_BASE_URL
""")
if __name__ == "__main__":
main()

30
requirements.txt Normal file
View File

@@ -0,0 +1,30 @@
# Core AI Agent Framework
openai>=1.0.0
anthropic>=0.21.0
# Real infrastructure
nats-py>=2.6.0
redis>=5.0.0
asyncpg>=0.29.0
aiohttp>=3.9.0
# Web Framework
fastapi>=0.110.0
uvicorn>=0.27.0
websockets>=0.14.0
# Frontend
streamlit>=1.32.0
# Database
sqlalchemy>=2.0.0
aiosqlite>=0.19.0
# Utilities
pydantic>=2.0.0
python-dateutil>=2.8.0
httpx>=0.27.0
python-dotenv>=1.0.0
# Monitoring
loguru>=0.7.0